This note captures the next cleanup pass for the macro language after the
recent %def / %redef split and the introduction of verbatim blocks.
The goal is not to make the language more powerful. The goal is to make it more locally predictable for both humans and coding agents.
Main objective
The language is now much more legible than before, but a few semantic pressure points still weaken local reasoning.
The next phase should tighten invariants around:
-
stable vs rebindable names
-
effectful argument evaluation
-
silent-empty behavior
-
operational builtins that do not fit the conceptual core
-
namespace/import behavior that is still more magical than ideal
Priority order
1. Make %def truly constant
This should become a hard semantic promise, not only a documentation claim.
Target invariant:
-
%def(name, …)introduces an immutable macro binding in the current frame -
%redef(name, …)introduces or updates a rebindable binding in the current frame -
%redefmay not replace a%defbinding -
duplicate
%defin the same frame is an error
Why:
-
this restores genuine name stability
-
it makes
%redefthe only visually marked rebinding path -
it improves prose reliability and agent reasoning
Concrete work:
-
evaluator enforcement
-
tests for
%def/%redefreplacement rules -
docs/examples kept aligned
2. Remove %set from argument position
The biggest remaining semantic hazard is eager argument evaluation in caller
scope combined with %set.
Problem shape:
-
arguments are effectful programs, not just values
-
%setin argument position mutates caller state before the callee frame exists -
harmless-looking calls can hide mutations
Implemented direction:
-
keep
%set -
do not redesign the whole evaluation model immediately
-
reject
%setin argument position withInvalidUsage
Why this is the right first cut:
-
it removes the worst hidden caller-scope mutation
-
it keeps eager argument evaluation for ordinary value-producing expressions
-
it tightens the language without adding new syntax or mode flags
3. Add a strictness surface for silent-empty behavior
The language still collapses:
-
intentionally absent
-
missing by mistake
into the same empty-string result too often.
Highest-value cases:
-
undefined variable lookup
%(name) -
unbound formal parameters
Recommended direction:
-
strict vars by default
-
strict params by default
-
explicit opt-outs:
--no-strict-vars/--no-strict-params
Helpful companion builtins:
-
%defined(name)or%is_set(name) -
%default(x, fallback) -
maybe
%require(x, msg)
The point is not types. The point is making intentional emptiness explicit.
4. Demote %here from the conceptual core
%here is operationally useful, but it does not belong comfortably in the
core expression model.
Why it is different:
-
rewrites source instead of producing ordinary output
-
one-file preflight invariant: multiple live
%herecalls are rejected -
sets global early-exit state
-
mutates future semantics by escaping itself
Near-term work:
-
move it further out of the conceptual core in docs
-
done: hard-error on multiple live
%herecalls in the same file
Possible longer-term direction:
-
treat it as a dedicated workflow primitive instead of a normal builtin
5. Remove %importas
%importas was added speculatively and did not pay for its complexity.
Reasons to remove it:
-
no real project workflow currently depends on it
-
its semantics were more magical than the value justified
-
it invited namespace expectations it did not cleanly satisfy
Direction:
-
keep
%includeand%import -
keep
%aliasas the explicit composition tool -
if a real namespace need appears later, design it from that concrete use case
6. Keep the block model explicit
The distinction is good, but easy to misread if not reinforced:
-
%{ … %}= quoted argument block, still macro-active -
%[ … %]= verbatim block, opaque to macro parsing
Near-term work:
-
done: keep both names visible in docs
-
done: use examples that show the contrast directly
-
continue avoiding prose that treats both simply as generic “blocks”
Coding-model implications
For agent-facing guidance, the preferred profile should become:
Preferred:
-
%deffor stable names -
%redefonly for deliberate phase-oriented rebinding -
%[ … %]for literal embedded regions -
%{ … %}only when you need a single macro-active argument -
%alias(…, k=v)as the only capture mechanism
Acceptable but advanced:
-
%pydef -
%export
Red flags:
-
%setin argument position -
relying on undefined variables becoming empty
-
relying on missing parameters becoming empty
-
%herein generated code
Review questions
Each tightening step should be judged against a small set of blunt questions:
-
Did this improve local reasoning?
-
Did this make names more stable?
-
Did this reduce hidden side effects?
-
Did this reduce ambiguous “empty means anything” behavior?
-
Did this keep the language small?
If the answer is no, the change is probably moving in the wrong direction.