Skip to content

RewriteDsl.kt

src/jvmMain/kotlin/afield/runtime/macros/RewriteDsl.kt · 249 lines

@file:afield.runtime.completion.CompletionCategory("Workspace")

package afield.runtime.macros

The rule-authoring surface for the pattern-rewrite layer (see PatternRewrite).

These declarations are erased: a rewrite { … } block is collected and deleted from the source by PatternRewrite.expand before the K2 REPL ever compiles it, so none of these functions is ever actually called at run time. They exist only so a rule authored in a live box - parses, - is importable (import afield.runtime.macros.*), and - reads sensibly (the names rewrite / from / to / Expr resolve to something).

The whole point (see PatternRewrite) is that a rule body never needs to type-check — only parse. PSI parsing runs no name resolution, so from { max(x, y) } parses into a valid tree even though max, x, y resolve to nothing. If one of these functions is somehow reached at run time it means expansion did not happen, which is a bug — hence the throw.

Surface:

rewrite { x: Expr, y: Expr ->
    from { max(x, y) }
    to   { if (x >= y) x else y }
}
The lambda's value parameters (x: Expr, y: Expr) are the metavariable declarations: inside from/to, a name reference is a hole iff its name is in that parameter set, and its declared type names the MetaKind. No unquote/$/, escape is needed — the known metavar set is the escape.

sealed interface MetaKind

The kind of syntax a metavariable is allowed to bind. Used for early rejection + readability.

object Expr : MetaKind

Any expression (KtExpression). The common case.

object Ident : MetaKind

A simple name reference (KtNameReferenceExpression).

object Stmt : MetaKind

A statement-position element. Treated like Expr in v1.

object TypeRef : MetaKind

A type reference (KtTypeReference).

object Vararg : MetaKind

A variadic hole: binds a run of call arguments (zero or more). Only meaningful inside an argument list. In from it consumes the matching arguments; in to it expands to them, comma-separated. Example:

rewrite { first: Expr, rest: Vararg ->
    from { log(first, rest) }
    to { logger.info(first, rest) }
}
log(tag, a, b, c)   →   logger.info(tag, a, b, c)

private fun erased(what: String): Nothing =
    throw MacroRegistry.MacroExpansionProblem(
        "`$what` is expanded at macro time and must never be called directly — " +
                "a live call means the surrounding `rewrite {}` block was not collected/deleted before compile"
    )

fun rewrite(rule: () -> Unit): Unit = erased("rewrite")

Declare a rewrite rule. Collected and deleted before compilation; see PatternRewrite.

The lambda's parameters declare the metavariables; its body must contain exactly one from call and one to call. The parameter types (Expr/Ident/Stmt/TypeRef) are read textually by the collector, so exact functional-type conformance of this signature is irrelevant — the block parses and is stripped regardless.

fun rule(name: String, rule: () -> Unit): Unit = erased("rule")

Declare a named rewrite rule, so a strategy block can refer to it by name. Otherwise identical to rewrite — same from/to/where/provide/ai body, same erase-before-compile treatment. The name is read textually by the collector (a string literal), so — like the metavar types — it never needs to type-check.

rule("negate") { x: Expr -> from { neg(x) }; to { -x } }
strategy { repeat("negate") }

fun strategy(block: () -> Unit): Unit = erased("strategy")

A controllable term-rewriting strategy over the box's named rules (see PatternRewrite.Strat).

With NO strategy {} block the engine keeps its historical behaviour: a global fixpoint that fires every rule at every match, everywhere, until nothing changes. A strategy {} block replaces that with an explicit control program — only rules it names fire, in the order and at the sites it dictates. The body is a single strategy expression built from the combinators below; it is erased and blanked before compilation, so it only needs to parse.

rule("unwrap") { x: Expr -> from { f(x) }; to { g(x) } }
strategy { bottomup("unwrap") }     // rewrite the innermost f(...) only
Fail-closed: an unknown rule name or a malformed program rewrites NOTHING and logs a diagnostic.

class Strategy internal constructor()

Opaque result of a strategy combinator. Erased — a strategy block never actually runs.

private fun erasedStrategy(what: String): Strategy = erased(what)

fun once(name: String): Strategy = erasedStrategy("once")

Fire rule name at the single first (outermost) match — at most once.

fun topdown(name: String): Strategy = erasedStrategy("topdown")

Fire rule name at the single outermost (shallowest) match site.

fun bottomup(name: String): Strategy = erasedStrategy("bottomup")

Fire rule name at the single innermost (deepest) match site — differs from topdown on nested matches (given f(f(a)), topdown rewrites the outer call, bottomup the inner one).

fun attempt(s: Any?): Strategy = erasedStrategy("attempt")

Run s once; if it does not apply, fall back to identity. Never fails. (Spelled attempt because try is a Kotlin hard keyword and cannot be a call name.)

fun repeat(s: Any?, n: Int): Strategy = erasedStrategy("repeat")

Iterate s to a fixpoint (or at most n times). Never fails.

fun repeat(s: Any?): Strategy = erasedStrategy("repeat")

Iterate s to a fixpoint (unbounded within the engine's iteration cap). Never fails.

fun seq(vararg s: Any?): Strategy = erasedStrategy("seq")

Run each strategy in order, threading the result; fails as soon as one fails. See also then.

fun choice(vararg s: Any?): Strategy = erasedStrategy("choice")

Run the first alternative that succeeds; fails only if none apply.

infix fun Any?.then(other: Any?): Strategy = erasedStrategy("then")

Infix sequencing: a then b runs a, then b on its result (fail-fast).

fun from(pattern: () -> Unit): Unit = erased("from")

The pattern (left-hand side) of a rewrite rule. Its body is quasiquoted, never evaluated.

fun to(replacement: () -> Unit): Unit = erased("to")

The replacement (right-hand side) of a rewrite rule. Its body is quasiquoted, never evaluated.

fun <T> unquote(block: () -> T): T = erased("unquote")

Procedural unquote: inside a to {} body, unquote { … } runs Kotlin at expansion time and splices the block's toString() result into the emitted source as Kotlin source code. Each metavariable is available inside the block as a String holding its captured source text, so captures can be transformed as strings. The block runs in the box's REPL context, so it can call box-scoped helpers.

IMPORTANT: the result is spliced verbatim as code, so produce valid Kotlin. To emit a string value, produce a quoted literal — e.g. reversing an identifier into a string literal:

rewrite { x: Ident ->
    from { rev(x) }
    to { unquote { "\"" + x.reversed() + "\"" } }
}
rev(hello)   →   "olleh"        // a String literal, not a bare identifier
(Emitting unquote { x.reversed() } would splice the bare token olleh, an unresolved reference.)

Evaluation is fail-soft: on the execute path a failing block becomes a compiling error(...) stand-in with a logged diagnostic; in editor-preview expansions the call becomes TODO() (a well-typed placeholder) since user code is not run there.

fun provide(preamble: () -> Unit): Unit = erased("provide")

An optional preamble contribution. When the rule fires, the declarations in this block are metavar-substituted (no hygiene rename — these are meant to define stable box-level names) and merged into the box preamble under a stable, content-derived key, so repeated runs deduplicate and the segment only re-executes when its content changes (see the /*prefix*/ "stack mark" idempotency mechanism). Example:

rewrite { n: Ident ->
    from { counter(n) }
    provide { var Box.n by Counting() }
    to { n.bump() }
}

fun ai(prompt: String): Unit = erased("ai")

An optional cached AI request. When the rule fires, the (metavar-substituted) prompt is resolved once against an AI model and the generated code is cached into the box attic under a stable key; subsequent runs with the same prompt reuse the cached block with no model call. The to {} side forwards to whatever the generated code defines. Example:

rewrite { x: Expr ->
    from { explain(x) }
    ai("write fun __explain(v: Any?): String that summarizes ${x}")
    to { __explain(x) }
}

fun where(guard: () -> Boolean): Unit = erased("where")

An optional guard: the rule fires only if the (structural) predicate over the captured metavariables holds. The body is a single boolean expression built from the predicates below, combined with &&, ||, !, and parentheses — for example:

rewrite { x: Expr, y: Expr ->
    from { swap(x, y) }
    where { distinct(x, y) && !isLiteral(x) }
    to { swap(y, x) }
}
Guards are evaluated structurally at macro time (no type-checking / no code execution), so only the fixed predicate vocabulary below is understood.

private fun erasedBool(what: String): Boolean = erased(what)

fun distinct(vararg metavars: Any?): Boolean = erasedBool("distinct")

True iff all listed metavariable captures are pairwise structurally distinct.

fun identical(a: Any?, b: Any?): Boolean = erasedBool("identical")

True iff the two captures are structurally identical.

fun isLiteral(metavar: Any?): Boolean = erasedBool("isLiteral")

True iff the capture is a literal (numeric/boolean/char constant or a string template).

fun isIdentifier(metavar: Any?): Boolean = erasedBool("isIdentifier")

True iff the capture is a bare identifier / simple name reference.

fun isCall(metavar: Any?): Boolean = erasedBool("isCall")

True iff the capture is a function or constructor call.

fun isCallTo(metavar: Any?, calleeName: String): Boolean = erasedBool("isCallTo")

True iff the capture is a call whose callee has the given calleeName. Handy for the type-directed coercion idiom — wrap every f(x) in a runtime adapter but guard against re-wrapping the adapter you just introduced, so the fixpoint terminates:

rewrite { x: Expr ->
    from { f(x) }
    where { !isCallTo(x, "coerce") }
    to { f(coerce(::f, SomeType::class, x)) }
}