rewrite API
The rewrite package applies a rewrite rule at one position of a term and
reports exactly what happened: the term before and after, the rule’s name
and the path from the root to the rewritten position. Bounded
normalization and traces are built by repeating such single steps. Every
function has a Term[T] version and, where noted, a version for any
@syntax.BindingSyntax AST.
import {
"Luna-Flow/type_theory/syntax",
"Luna-Flow/type_theory/rewrite",
}
A rule is a function (Term[T]) -> Term[T]? that returns Some(result)
when it applies to a term as a whole and None otherwise. The traversal
functions decide where the rule is tried. The theory is in the
rewrite design.
Rule names
RuleName
RuleName is a non-empty identifier for a reduction rule.
pub struct RuleName {
value : String
} derive(Compare, Eq, Hash, @debug.Debug)
Rule names label steps in results and traces. They compare, order and hash
by their text; the order is that of String: shorter texts first, then by
UTF-16 code units.
RuleName::new
RuleName::new validates and creates a rule name.
pub fn RuleName::new(String) -> Result[Self, RuleNameError]
Returns Err(RuleNameError::Empty) for the empty string and Ok(name)
otherwise.
RuleName::unsafe_new
RuleName::unsafe_new creates a rule name from a string known to be
non-empty.
pub fn RuleName::unsafe_new(String) -> Self
Aborts on the empty string. Use it for literals such as "beta", where the
invariant is evident; use RuleName::new for names that come from input.
RuleName::value
RuleName::value returns the text of a rule name.
pub fn RuleName::value(Self) -> String
RuleName::equal, RuleName::compare, RuleName::hash
These methods are the promoted Eq, Compare and Hash implementations.
pub fn RuleName::equal(Self, Self) -> Bool
pub fn RuleName::compare(Self, Self) -> Int
pub fn RuleName::hash(Self) -> Int
test "rule names" {
assert_eq(@rewrite.RuleName::new(""), Err(@rewrite.RuleNameError::Empty))
match @rewrite.RuleName::new("beta") {
Ok(name) => inspect(name.value(), content="beta")
Err(_) => fail("non-empty name rejected")
}
// shortlex order: shorter names first
let eta = @rewrite.RuleName::unsafe_new("eta")
assert_true(eta < @rewrite.RuleName::unsafe_new("beta"))
}
RuleNameError
RuleNameError lists the reasons a rule name is invalid.
pub(all) enum RuleNameError {
Empty
} derive(Eq, @debug.Debug)
pub fn RuleNameError::equal(Self, Self) -> Bool
Empty is the only case: rule names must not be empty.
Positions
ReductionFrame
ReductionFrame is one step from a node to one of its children.
pub(all) enum ReductionFrame {
BinderBody
ApplyHead
ApplyArgument(Int)
} derive(Eq, @debug.Debug)
pub fn ReductionFrame::equal(Self, Self) -> Bool
BinderBody enters the body of a Bind, ApplyHead the head of an
Apply, and ApplyArgument(i) its argument number i, counting from 0.
ReductionPath
ReductionPath is the sequence of frames from the root to a position.
pub struct ReductionPath {
frames : Array[ReductionFrame]
} derive(Eq, @debug.Debug)
The empty path is the root.
ReductionPath::root, ReductionPath::prepend, ReductionPath::to_array
These functions build and read paths.
pub fn ReductionPath::root() -> Self
pub fn ReductionPath::prepend(Self, ReductionFrame) -> Self
pub fn ReductionPath::to_array(Self) -> Array[ReductionFrame]
prepend(frame) adds a frame at the root end, which is how a traversal
lifts the path of a step in a child to the parent. to_array returns the
frames root first.
ReductionPath::equal
ReductionPath::equal compares two paths frame by frame.
pub fn ReductionPath::equal(Self, Self) -> Bool
test "paths are built from the redex up" {
let path = @rewrite.ReductionPath::root()
.prepend(@rewrite.ApplyArgument(1))
.prepend(@rewrite.BinderBody)
assert_eq(path.to_array(), [@rewrite.BinderBody, @rewrite.ApplyArgument(1)])
assert_eq(@rewrite.ReductionPath::root().to_array(), [])
}
Single steps on Term
StepResult
StepResult[T] is the outcome of one reduction attempt.
pub(all) enum StepResult[T] {
NoStep
Reduced(before~ : @syntax.Term[T], after~ : @syntax.Term[T], rule~ : RuleName, path~ : ReductionPath)
} derive(Eq, @debug.Debug)
pub fn[T : Eq] StepResult::equal(Self[T], Self[T]) -> Bool
NoStep means the traversal found no position where the rule applies.
Reduced records the whole term before, the whole term after, the rule
and the path of the rewritten position. Exactly one position was rewritten:
the subterm of before at path is a redex of the rule, and after is
before with that subterm replaced by the rule’s result.
top_down_once
top_down_once rewrites the first position, in pre-order, at which the rule
applies.
pub fn[T] top_down_once(@syntax.Term[T], RuleName, (@syntax.Term[T]) -> @syntax.Term[T]?) -> StepResult[T]
The rule is tried at a node before its children; children are visited head
first, then arguments from left to right, and binder bodies are entered. The
chosen position is the leftmost-outermost redex. NoStep means that the rule
applies nowhere in the term.
bottom_up_once
bottom_up_once rewrites the first position, in post-order, at which the
rule applies.
pub fn[T] bottom_up_once(@syntax.Term[T], RuleName, (@syntax.Term[T]) -> @syntax.Term[T]?) -> StepResult[T]
Children are tried before their parent, in the same left-to-right order. The
chosen position is the leftmost-innermost redex. NoStep again means that
the rule applies nowhere.
fn drop_zero(t : @syntax.Term[String]) -> @syntax.Term[String]? {
match t {
Apply(Value("+"), [Value("0"), other]) => Some(other)
_ => None
}
}
test "outermost versus innermost" {
let rule = @rewrite.RuleName::unsafe_new("drop_zero")
let inner : @syntax.Term[String] = Apply(Value("+"), [Value("0"), Value("1")])
let outer : @syntax.Term[String] = Apply(Value("+"), [Value("0"), inner])
match @rewrite.top_down_once(outer, rule, drop_zero) {
Reduced(after~, path~, ..) => {
assert_eq(after, inner)
assert_eq(path.to_array(), [])
}
NoStep => fail("expected a step")
}
match @rewrite.bottom_up_once(outer, rule, drop_zero) {
Reduced(after~, path~, ..) => {
assert_eq(after, Apply(Value("+"), [Value("0"), Value("1")]))
assert_eq(path.to_array(), [@rewrite.ApplyArgument(1)])
}
NoStep => fail("expected a step")
}
}
Repeating steps on Term
NormalizationResult
NormalizationResult[T] is the outcome of bounded repetition of a step
function.
pub(all) enum NormalizationResult[T] {
NormalForm(term~ : @syntax.Term[T], steps~ : Int)
StepLimitReached(term~ : @syntax.Term[T], steps~ : Int)
} derive(Eq, @debug.Debug)
pub fn[T : Eq] NormalizationResult::equal(Self[T], Self[T]) -> Bool
NormalForm(term, steps): the step function returns NoStep on term,
which was reached after steps steps. StepLimitReached(term, steps): the
limit of steps steps was used up and term can still be reduced.
normalize
normalize applies a step function until it returns NoStep or a step limit
is reached.
pub fn[T] normalize(@syntax.Term[T], (@syntax.Term[T]) -> StepResult[T], Int) -> NormalizationResult[T]
normalize(term, step, max_steps) performs at most max_steps successful
steps and calls step at most max_steps + 1 times: after the last allowed
step it calls step once more to decide between NormalForm and
StepLimitReached. With max_steps <= 0 it performs no step and only
classifies term. The step function decides the strategy; see
eval for ready-made ones.
ReductionTrace
ReductionTrace[T] records a bounded run: the initial term, every successful
step and the final result.
pub struct ReductionTrace[T] {
initial : @syntax.Term[T]
steps : Array[StepResult[T]]
result : NormalizationResult[T]
} derive(Eq, @debug.Debug)
pub fn[T] ReductionTrace::initial(Self[T]) -> @syntax.Term[T]
pub fn[T] ReductionTrace::steps(Self[T]) -> Array[StepResult[T]]
pub fn[T] ReductionTrace::result(Self[T]) -> NormalizationResult[T]
pub fn[T : Eq] ReductionTrace::equal(Self[T], Self[T]) -> Bool
Every element of steps is a Reduced value; consecutive steps chain, the
after of one being the before of the next. steps().length() equals the
steps count of result. steps() returns a copy.
trace
trace runs the same loop as normalize and records every step.
pub fn[T] trace(@syntax.Term[T], (@syntax.Term[T]) -> StepResult[T], Int) -> ReductionTrace[T]
fn decrement(t : @syntax.Term[Int]) -> @syntax.Term[Int]? {
match t {
Value(n) if n > 0 => Some(Value(n - 1))
_ => None
}
}
test "normalize and trace" {
let rule = @rewrite.RuleName::unsafe_new("decrement")
let step = (t : @syntax.Term[Int]) => @rewrite.top_down_once(t, rule, decrement)
assert_eq(
@rewrite.normalize(Value(3), step, 10),
NormalForm(term=Value(0), steps=3),
)
let run = @rewrite.trace(Value(3), step, 2)
assert_eq(run.steps().length(), 2)
assert_eq(run.result(), StepLimitReached(term=Value(1), steps=2))
assert_eq(run.initial(), Value(3))
}
Binding-aware ASTs
GenericStepResult
GenericStepResult[N] is StepResult for a downstream AST N.
pub(all) enum GenericStepResult[N] {
NoStep
Reduced(before~ : N, after~ : N, rule~ : RuleName, path~ : ReductionPath)
} derive(Eq, @debug.Debug)
pub fn[N : Eq] GenericStepResult::equal(Self[N], Self[N]) -> Bool
generic_top_down_once
generic_top_down_once is top_down_once for any BindingSyntax AST.
pub fn[N : @syntax.BindingSyntax] generic_top_down_once(N, RuleName, (N) -> N?) -> GenericStepResult[N]
The traversal uses BindingSyntax::project to find children and the trait
constructors to rebuild the parents of the rewritten node. Opaque and
variable nodes have no children; the rule is still tried on them.
GenericNormalizationResult
GenericNormalizationResult[N] is NormalizationResult for a downstream
AST.
pub(all) enum GenericNormalizationResult[N] {
NormalForm(term~ : N, steps~ : Int)
StepLimitReached(term~ : N, steps~ : Int)
} derive(Eq, @debug.Debug)
pub fn[N : Eq] GenericNormalizationResult::equal(Self[N], Self[N]) -> Bool
generic_normalize
generic_normalize repeats generic_top_down_once with one rule until no
step applies or the step limit is reached.
pub fn[N : @syntax.BindingSyntax] generic_normalize(N, RuleName, (N) -> N?, Int) -> GenericNormalizationResult[N]
It has the same step-count contract as normalize, with the top-down
strategy fixed.
test "generic rewriting on Term" {
let rule = @rewrite.RuleName::unsafe_new("drop_zero")
let inner : @syntax.Term[String] = Apply(Value("+"), [Value("0"), Value("1")])
let outer : @syntax.Term[String] = Apply(Value("+"), [Value("0"), inner])
assert_eq(
@rewrite.generic_normalize(outer, rule, drop_zero, 10),
NormalForm(term=Value("1"), steps=2),
)
}
For a downstream AST see the adapter tutorial.