Concatenative languages lend themselves to rewriting because they do not bind variables and thus do not incur any confusion with renaming/scope (compositional rather than applicative).
Manfred von Thun has already written on this: there are algebraic laws for concatenative programming like the algebraic laws one has for e.g. groups (\(a \cdot 1 = a\)).
Some simple ones that do not need quotation:
dip(f) dip(g) = dip(f g)
dip(+) + = + +
(holds because \(+\) is associative; works on all other associative operations)0 drop = id
and so on for constantsswap swap = id
dup and = id
dup or = id
dup xor = False
swap * = *
(holds because \( \cdot \) is commutative; works on all other commutative operations)swap > = <
(and so on for other operations that have reflect, such as \( > \)).> not = <=
(and so on)
I implemented the above lints in my own Kempe compiler. A linting pass is easily implemented with pattern matching one has in ML-style functional languages:
lintAtoms :: [Atom b b] -> Maybe (Warning b)
lintAtoms [] = Nothing
lintAtoms (a@(Dip l _):a'@Dip{}:_) = Just (DoubleDip l a a')
lintAtoms (a@(IntLit l _):(AtBuiltin _ Drop):_) = Just (PushDrop l a)
...