Files
bux-lang/src/macroexpand.bux
T
dimgigov ec5984762b
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled
feat: try/unwrap payload types, LSP format, macro paste, freestanding runtime
- Type `?`/`!` as Result/Option Ok payload (not always int); fix unwrap C types
- LSP 0.18 document formatting (bux fmt) + VS Code format-on-save
- Macro `:type` generics (Array_New<$t>) and operators-only tt paste
- Ship runtime_freestanding.c + BUX_RUNTIME=freestanding + smokes/examples
2026-07-28 16:56:35 +03:00

1574 lines
66 KiB
Plaintext

// macroexpand.bux — declarative macro! expansion (selfhost parity, session 60)
// Expands name!(args) using macro! rules; grafts call-site locations (quote hygiene).
module MacroExpand {
extern func bux_alloc(size: uint) -> *void;
extern func bux_strlen(s: String) -> uint;
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
extern func bux_str_contains(haystack: String, needle: String) -> int;
extern func PrintLine(s: String);
extern func Print(s: String);
extern func PrintInt(n: int);
// ---------------------------------------------------------------------------
// Fragment environment (up to 9 $frags singles + 2 rep lists)
// ---------------------------------------------------------------------------
struct MacroEnv {
count: int;
n0: String; n1: String; n2: String; n3: String; n4: String;
n5: String; n6: String; n7: String; n8: String;
a0: *Expr; a1: *Expr; a2: *Expr; a3: *Expr; a4: *Expr;
a5: *Expr; a6: *Expr; a7: *Expr; a8: *Expr;
// Up to 2 named rep lists (multi-rep + compound zip)
listName0: String;
listCount0: int;
l0: *Expr; l1: *Expr; l2: *Expr; l3: *Expr; l4: *Expr;
l5: *Expr; l6: *Expr; l7: *Expr; l8: *Expr; l9: *Expr;
l10: *Expr; l11: *Expr; l12: *Expr; l13: *Expr; l14: *Expr; l15: *Expr;
listName1: String;
listCount1: int;
m0: *Expr; m1: *Expr; m2: *Expr; m3: *Expr; m4: *Expr;
m5: *Expr; m6: *Expr; m7: *Expr; m8: *Expr; m9: *Expr;
m10: *Expr; m11: *Expr; m12: *Expr; m13: *Expr; m14: *Expr; m15: *Expr;
// Unhygienic call-site binders from `var $name` / `for $i` (skip gensym)
unhyCount: int;
u0: String; u1: String; u2: String; u3: String;
u4: String; u5: String; u6: String; u7: String;
}
func Env_New() -> MacroEnv {
return MacroEnv {
count: 0,
n0: "", n1: "", n2: "", n3: "", n4: "", n5: "", n6: "", n7: "", n8: "",
a0: null as *Expr, a1: null as *Expr, a2: null as *Expr,
a3: null as *Expr, a4: null as *Expr, a5: null as *Expr,
a6: null as *Expr, a7: null as *Expr, a8: null as *Expr,
listName0: "", listCount0: 0,
l0: null as *Expr, l1: null as *Expr, l2: null as *Expr, l3: null as *Expr,
l4: null as *Expr, l5: null as *Expr, l6: null as *Expr, l7: null as *Expr,
l8: null as *Expr, l9: null as *Expr, l10: null as *Expr, l11: null as *Expr,
l12: null as *Expr, l13: null as *Expr, l14: null as *Expr, l15: null as *Expr,
listName1: "", listCount1: 0,
m0: null as *Expr, m1: null as *Expr, m2: null as *Expr, m3: null as *Expr,
m4: null as *Expr, m5: null as *Expr, m6: null as *Expr, m7: null as *Expr,
m8: null as *Expr, m9: null as *Expr, m10: null as *Expr, m11: null as *Expr,
m12: null as *Expr, m13: null as *Expr, m14: null as *Expr, m15: null as *Expr,
unhyCount: 0,
u0: "", u1: "", u2: "", u3: "", u4: "", u5: "", u6: "", u7: ""
};
}
func Env_IsUnhy(env: *MacroEnv, name: String) -> bool {
if String_Eq(name, "") { return false; }
if env.unhyCount > 0 && String_Eq(env.u0, name) { return true; }
if env.unhyCount > 1 && String_Eq(env.u1, name) { return true; }
if env.unhyCount > 2 && String_Eq(env.u2, name) { return true; }
if env.unhyCount > 3 && String_Eq(env.u3, name) { return true; }
if env.unhyCount > 4 && String_Eq(env.u4, name) { return true; }
if env.unhyCount > 5 && String_Eq(env.u5, name) { return true; }
if env.unhyCount > 6 && String_Eq(env.u6, name) { return true; }
if env.unhyCount > 7 && String_Eq(env.u7, name) { return true; }
return false;
}
func Env_AddUnhy(env: *MacroEnv, name: String) {
if String_Eq(name, "") { return; }
if Env_IsUnhy(env, name) { return; }
if env.unhyCount >= 8 { return; }
if env.unhyCount == 0 { env.u0 = name; }
else if env.unhyCount == 1 { env.u1 = name; }
else if env.unhyCount == 2 { env.u2 = name; }
else if env.unhyCount == 3 { env.u3 = name; }
else if env.unhyCount == 4 { env.u4 = name; }
else if env.unhyCount == 5 { env.u5 = name; }
else if env.unhyCount == 6 { env.u6 = name; }
else { env.u7 = name; }
env.unhyCount = env.unhyCount + 1;
}
func Env_ListSet(env: *MacroEnv, which: int, idx: int, e: *Expr) {
if which == 0 {
if idx == 0 { env.l0 = e; }
else if idx == 1 { env.l1 = e; }
else if idx == 2 { env.l2 = e; }
else if idx == 3 { env.l3 = e; }
else if idx == 4 { env.l4 = e; }
else if idx == 5 { env.l5 = e; }
else if idx == 6 { env.l6 = e; }
else if idx == 7 { env.l7 = e; }
else if idx == 8 { env.l8 = e; }
else if idx == 9 { env.l9 = e; }
else if idx == 10 { env.l10 = e; }
else if idx == 11 { env.l11 = e; }
else if idx == 12 { env.l12 = e; }
else if idx == 13 { env.l13 = e; }
else if idx == 14 { env.l14 = e; }
else if idx == 15 { env.l15 = e; }
} else {
if idx == 0 { env.m0 = e; }
else if idx == 1 { env.m1 = e; }
else if idx == 2 { env.m2 = e; }
else if idx == 3 { env.m3 = e; }
else if idx == 4 { env.m4 = e; }
else if idx == 5 { env.m5 = e; }
else if idx == 6 { env.m6 = e; }
else if idx == 7 { env.m7 = e; }
else if idx == 8 { env.m8 = e; }
else if idx == 9 { env.m9 = e; }
else if idx == 10 { env.m10 = e; }
else if idx == 11 { env.m11 = e; }
else if idx == 12 { env.m12 = e; }
else if idx == 13 { env.m13 = e; }
else if idx == 14 { env.m14 = e; }
else if idx == 15 { env.m15 = e; }
}
}
func Env_ListGet(env: *MacroEnv, which: int, idx: int) -> *Expr {
if which == 0 {
if idx == 0 { return env.l0; }
if idx == 1 { return env.l1; }
if idx == 2 { return env.l2; }
if idx == 3 { return env.l3; }
if idx == 4 { return env.l4; }
if idx == 5 { return env.l5; }
if idx == 6 { return env.l6; }
if idx == 7 { return env.l7; }
if idx == 8 { return env.l8; }
if idx == 9 { return env.l9; }
if idx == 10 { return env.l10; }
if idx == 11 { return env.l11; }
if idx == 12 { return env.l12; }
if idx == 13 { return env.l13; }
if idx == 14 { return env.l14; }
if idx == 15 { return env.l15; }
} else {
if idx == 0 { return env.m0; }
if idx == 1 { return env.m1; }
if idx == 2 { return env.m2; }
if idx == 3 { return env.m3; }
if idx == 4 { return env.m4; }
if idx == 5 { return env.m5; }
if idx == 6 { return env.m6; }
if idx == 7 { return env.m7; }
if idx == 8 { return env.m8; }
if idx == 9 { return env.m9; }
if idx == 10 { return env.m10; }
if idx == 11 { return env.m11; }
if idx == 12 { return env.m12; }
if idx == 13 { return env.m13; }
if idx == 14 { return env.m14; }
if idx == 15 { return env.m15; }
}
return null as *Expr;
}
func Env_ListWhich(env: *MacroEnv, name: String) -> int {
if !String_Eq(env.listName0, "") && String_Eq(env.listName0, name) { return 0; }
if !String_Eq(env.listName1, "") && String_Eq(env.listName1, name) { return 1; }
return -1;
}
func Env_ListCountOf(env: *MacroEnv, which: int) -> int {
if which == 0 { return env.listCount0; }
if which == 1 { return env.listCount1; }
return 0;
}
func Env_CopySingles(dst: *MacroEnv, src: *MacroEnv) {
dst.n0 = src.n0; dst.a0 = src.a0;
dst.n1 = src.n1; dst.a1 = src.a1;
dst.n2 = src.n2; dst.a2 = src.a2;
dst.n3 = src.n3; dst.a3 = src.a3;
dst.n4 = src.n4; dst.a4 = src.a4;
dst.n5 = src.n5; dst.a5 = src.a5;
dst.n6 = src.n6; dst.a6 = src.a6;
dst.n7 = src.n7; dst.a7 = src.a7;
dst.n8 = src.n8; dst.a8 = src.a8;
dst.count = src.count;
}
func Env_Set(env: *MacroEnv, idx: int, name: String, arg: *Expr) {
if idx == 0 { env.n0 = name; env.a0 = arg; }
else if idx == 1 { env.n1 = name; env.a1 = arg; }
else if idx == 2 { env.n2 = name; env.a2 = arg; }
else if idx == 3 { env.n3 = name; env.a3 = arg; }
else if idx == 4 { env.n4 = name; env.a4 = arg; }
else if idx == 5 { env.n5 = name; env.a5 = arg; }
else if idx == 6 { env.n6 = name; env.a6 = arg; }
else if idx == 7 { env.n7 = name; env.a7 = arg; }
else if idx == 8 { env.n8 = name; env.a8 = arg; }
if idx + 1 > env.count { env.count = idx + 1; }
}
func Env_Lookup(env: *MacroEnv, name: String) -> *Expr {
if String_Eq(env.n0, name) { return env.a0; }
if String_Eq(env.n1, name) { return env.a1; }
if String_Eq(env.n2, name) { return env.a2; }
if String_Eq(env.n3, name) { return env.a3; }
if String_Eq(env.n4, name) { return env.a4; }
if String_Eq(env.n5, name) { return env.a5; }
if String_Eq(env.n6, name) { return env.a6; }
if String_Eq(env.n7, name) { return env.a7; }
if String_Eq(env.n8, name) { return env.a8; }
return null as *Expr;
}
// If binder name is a $frag bound to ekIdent, return that ident text
func Env_BinderFromFrag(env: *MacroEnv, name: String) -> String {
let bound: *Expr = Env_Lookup(env, name);
if bound == null as *Expr { return ""; }
if bound.kind != ekIdent { return ""; }
return bound.strValue;
}
// Find free single slot (prefer 0..7, then 8)
func Env_SetNamed(env: *MacroEnv, name: String, arg: *Expr) {
var i: int = 0;
while i < 9 {
var existing: String = "";
if i == 0 { existing = env.n0; }
else if i == 1 { existing = env.n1; }
else if i == 2 { existing = env.n2; }
else if i == 3 { existing = env.n3; }
else if i == 4 { existing = env.n4; }
else if i == 5 { existing = env.n5; }
else if i == 6 { existing = env.n6; }
else if i == 7 { existing = env.n7; }
else { existing = env.n8; }
if String_Eq(existing, "") || String_Eq(existing, name) {
Env_Set(env, i, name, arg);
return;
}
i = i + 1;
}
Env_Set(env, 8, name, arg);
}
func Rule_FragName(rule: *Decl, idx: int) -> String {
if idx == 0 { return rule.param0.name; }
if idx == 1 { return rule.param1.name; }
if idx == 2 { return rule.param2.name; }
if idx == 3 { return rule.param3.name; }
if idx == 4 { return rule.param4.name; }
if idx == 5 { return rule.param5.name; }
if idx == 6 { return rule.param6.name; }
if idx == 7 { return rule.param7.name; }
if idx == 8 { return rule.param8.name; }
return "";
}
func Macro_RenameIdentsInExpr(e: *Expr, oldN: String, newN: String) {
if e == null as *Expr { return; }
if e.kind == ekIdent && String_Eq(e.strValue, oldN) {
e.strValue = newN;
}
Macro_RenameIdentsInExpr(e.child1, oldN, newN);
Macro_RenameIdentsInExpr(e.child2, oldN, newN);
Macro_RenameIdentsInExpr(e.child3, oldN, newN);
if e.refBlock != null as *Block {
Macro_GensymBlockApply(e.refBlock, oldN, newN);
}
var args: *ExprList = e.callArgs;
while args != null as *ExprList {
Macro_RenameIdentsInExpr(args.expr, oldN, newN);
args = args.next;
}
}
func Macro_GensymBlockApply(b: *Block, oldN: String, newN: String) {
if b == null as *Block { return; }
var s: *Stmt = b.firstStmt;
while s != null as *Stmt {
// let/var and for-loop binder
if (s.kind == skLet || s.kind == skFor) && String_Eq(s.strValue, oldN) {
s.strValue = newN;
}
Macro_RenameIdentsInExpr(s.child1, oldN, newN);
Macro_RenameIdentsInExpr(s.child2, oldN, newN);
Macro_RenameIdentsInExpr(s.child3, oldN, newN);
if s.refStmtBlock != null as *Block {
Macro_GensymBlockApply(s.refStmtBlock, oldN, newN);
}
if s.refStmtElse != null as *Block {
Macro_GensymBlockApply(s.refStmtElse, oldN, newN);
}
s = s.nextStmt;
}
}
// Collect template-introduced binders (let/var + for) then rename whole body.
// Skip unhygienic call-site binders from `var $name` (tracked on env).
func Macro_GensymBlock(ex: *MacroExpander, b: *Block, env: *MacroEnv) {
if b == null as *Block || ex == null as *MacroExpander { return; }
var s: *Stmt = b.firstStmt;
while s != null as *Stmt {
if (s.kind == skLet || s.kind == skFor) && !String_Eq(s.strValue, "") {
var skip: bool = false;
if env != null as *MacroEnv && Env_IsUnhy(env, s.strValue) {
skip = true;
}
if !skip {
ex.gensymCounter = ex.gensymCounter + 1;
let neu: String = String_Concat(String_Concat("__m", String_FromInt(ex.gensymCounter)), String_Concat("_", s.strValue));
let oldN: String = s.strValue;
Macro_GensymBlockApply(b, oldN, neu);
}
}
if s.refStmtBlock != null as *Block {
Macro_GensymBlock(ex, s.refStmtBlock, env);
}
if s.refStmtElse != null as *Block {
Macro_GensymBlock(ex, s.refStmtElse, env);
}
if s.child1 != null as *Expr && s.child1.kind == ekBlock {
Macro_GensymBlock(ex, s.child1.refBlock, env);
}
s = s.nextStmt;
}
}
// Flatten ekIdent / ekField(::) chain to "A::B::C" (selfhost path style)
func Macro_PathFromExpr(e: *Expr) -> String {
if e == null as *Expr { return ""; }
if e.kind == ekIdent { return e.strValue; }
if e.kind == ekPath { return e.strValue; }
if e.kind == ekField {
let base: String = Macro_PathFromExpr(e.child1);
if String_Eq(base, "") { return e.strValue; }
return String_Concat(base, String_Concat("::", e.strValue));
}
return "";
}
// Convert call-site expr → pattern for `$p:pat` (ident/lit/path/call/field)
func Macro_ExprToPattern(aexp: *Expr) -> *Pattern {
if aexp == null as *Expr { return null as *Pattern; }
if aexp.kind == ekMacroPat { return Ast_ClonePattern(aexp.macroPat); }
if aexp.kind == ekIdent {
if String_Eq(aexp.strValue, "_") {
let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern;
p.kind = pkWildcard;
p.line = aexp.line;
p.column = aexp.column;
return p;
}
let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern;
p.kind = pkIdent;
p.line = aexp.line;
p.column = aexp.column;
p.patIdent = aexp.strValue;
return p;
}
if aexp.kind == ekLiteral {
let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern;
p.kind = pkLiteral;
p.line = aexp.line;
p.column = aexp.column;
p.patLitKind = aexp.tokKind;
p.patLitText = aexp.tokText;
return p;
}
if aexp.kind == ekPath || aexp.kind == ekField {
let path: String = Macro_PathFromExpr(aexp);
if String_Eq(path, "") { return null as *Pattern; }
let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern;
p.kind = pkEnum;
p.line = aexp.line;
p.column = aexp.column;
p.patEnumPath = path;
return p;
}
if aexp.kind == ekCall {
// Opt::Some(v) — callee is ekField chain
var path: String = Macro_PathFromExpr(aexp.child1);
if String_Eq(path, "") { return null as *Pattern; }
let p: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern;
p.kind = pkEnum;
p.line = aexp.line;
p.column = aexp.column;
p.patEnumPath = path;
var last: *Pattern = null as *Pattern;
var arg: *ExprList = aexp.callArgs;
while arg != null as *ExprList {
let ap: *Pattern = Macro_ExprToPattern(arg.expr);
if ap == null as *Pattern { return null as *Pattern; }
if last == null as *Pattern {
p.patArgs = ap;
last = ap;
} else {
last.patNext = ap;
last = ap;
}
arg = arg.next;
}
return p;
}
return null as *Pattern;
}
// Coerce arg for kind; returns normalized expr or null on mismatch
func Macro_CoerceArg(kindStr: String, aexp: *Expr) -> *Expr {
if aexp == null as *Expr { return null as *Expr; }
if String_Eq(kindStr, "ident") {
if aexp.kind != ekIdent { return null as *Expr; }
return aexp;
}
if String_Eq(kindStr, "literal") {
if aexp.kind != ekLiteral { return null as *Expr; }
return aexp;
}
if String_Eq(kindStr, "block") {
if aexp.kind != ekBlock { return null as *Expr; }
return aexp;
}
if String_Eq(kindStr, "stmt") {
if aexp.kind == ekMacroStmt { return aexp; }
if aexp.kind == ekMacroPat { return null as *Expr; }
// Wrap expression as expression-statement
let st: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
st.kind = skExpr;
st.line = aexp.line;
st.column = aexp.column;
st.child1 = aexp;
let e: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
e.kind = ekMacroStmt;
e.line = aexp.line;
e.column = aexp.column;
e.macroStmt = st;
return e;
}
if String_Eq(kindStr, "pat") {
let pat: *Pattern = Macro_ExprToPattern(aexp);
if pat == null as *Pattern { return null as *Expr; }
let e: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
e.kind = ekMacroPat;
e.line = aexp.line;
e.column = aexp.column;
e.macroPat = pat;
return e;
}
// expr rejects stmt/pat wrappers
if String_Eq(kindStr, "expr") {
if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; }
return aexp;
}
// tt — any single call-site AST fragment (session 76/84/85).
// Delimiter-balanced multi-element groups (tuple `(a,b)` / slice `[a,b]`)
// flatten when spliced as sole call arg: `$f($args)` → `f(a, b)`.
if String_Eq(kindStr, "tt") {
if aexp.kind == ekMacroTt { return aexp; }
let wrap: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
wrap.kind = ekMacroTt;
wrap.line = aexp.line;
wrap.column = aexp.column;
wrap.sourceFile = aexp.sourceFile;
wrap.child1 = aexp;
// group flag: tuple or non-empty slice lit
wrap.boolValue = aexp.kind == ekTuple ||
(aexp.kind == ekSlice && aexp.callArgCount > 0);
return wrap;
}
// type — session 87+: named / pointer / generic Array<int> from call-site
if String_Eq(kindStr, "type") {
if aexp.kind == ekMacroType { return aexp; }
var te: *TypeExpr = null as *TypeExpr;
if aexp.kind == ekIdent || aexp.kind == ekGenericCall {
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.line = aexp.line;
te.column = aexp.column;
if aexp.kind == ekGenericCall && !String_Eq(aexp.genericCallee, "") {
te.typeName = aexp.genericCallee;
} else {
te.typeName = aexp.strValue;
}
// Generic type args: Array<int> / Map<K,V> (selfhost: up to 2 string args)
var gcount: int = aexp.genericTypeArgCount;
if gcount > 0 {
te.typeArgName0 = aexp.genericTypeArg0;
te.typeArgCount = 1;
if gcount > 1 {
te.typeArgName1 = aexp.genericTypeArg1;
te.typeArgCount = 2;
}
}
} else if aexp.kind == ekUnary && aexp.intValue == tkStar && aexp.child1 != null as *Expr {
// *T from unary star
let inner: *Expr = Macro_CoerceArg("type", aexp.child1);
if inner == null as *Expr || inner.refType == null as *TypeExpr {
return null as *Expr;
}
te = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekPointer;
te.line = aexp.line;
te.column = aexp.column;
te.pointerPointee = inner.refType;
if inner.refType != null as *TypeExpr {
te.typeName = String_Concat(inner.refType.typeName, "*");
}
} else {
return null as *Expr;
}
let tw: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
tw.kind = ekMacroType;
tw.line = aexp.line;
tw.column = aexp.column;
tw.refType = te;
return tw;
}
// default: treat as expr
if aexp.kind == ekMacroStmt || aexp.kind == ekMacroPat { return null as *Expr; }
return aexp;
}
// Substitute `$t:type` in TypeExpr (named type starting with $)
func Macro_SubstType(te: *TypeExpr, env: *MacroEnv) -> *TypeExpr {
if te == null as *TypeExpr { return null as *TypeExpr; }
if te.kind == tekNamed && String_StartsWith(te.typeName, "$") {
let bound: *Expr = Env_Lookup(env, te.typeName);
if bound != null as *Expr && bound.kind == ekMacroType && bound.refType != null as *TypeExpr {
return bound.refType;
}
}
// Array<$t> / Map<$k,$v> — substitute type-arg name slots
if te.kind == tekNamed && te.typeArgCount > 0 {
if String_StartsWith(te.typeArgName0, "$") {
let b0: *Expr = Env_Lookup(env, te.typeArgName0);
if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr {
// Flatten simple named type arg (int, String, …)
if b0.refType.kind == tekNamed && b0.refType.typeArgCount == 0 {
te.typeArgName0 = b0.refType.typeName;
} else if b0.refType.kind == tekNamed {
// Nested generic: store mangled-ish "Array_int" for mono
te.typeArgName0 = b0.refType.typeName;
if b0.refType.typeArgCount > 0 {
te.typeArgName0 = String_Concat(te.typeArgName0, "_");
te.typeArgName0 = String_Concat(te.typeArgName0, b0.refType.typeArgName0);
}
} else if b0.refType.kind == tekPointer && b0.refType.pointerPointee != null as *TypeExpr {
te.typeArgName0 = String_Concat(b0.refType.pointerPointee.typeName, "p");
}
}
}
if te.typeArgCount > 1 && String_StartsWith(te.typeArgName1, "$") {
let b1: *Expr = Env_Lookup(env, te.typeArgName1);
if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr {
if b1.refType.kind == tekNamed {
te.typeArgName1 = b1.refType.typeName;
}
}
}
}
if te.pointerPointee != null as *TypeExpr {
te.pointerPointee = Macro_SubstType(te.pointerPointee, env);
if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr {
te.typeName = String_Concat(te.pointerPointee.typeName, "*");
}
}
if te.sliceElement != null as *TypeExpr {
te.sliceElement = Macro_SubstType(te.sliceElement, env);
}
if te.funcRet != null as *TypeExpr {
te.funcRet = Macro_SubstType(te.funcRet, env);
}
return te;
}
// Substitute `$t` in Expr generic type-arg slots (Array_New<$t>)
func Macro_SubstGenericTypeArgs(e: *Expr, env: *MacroEnv) {
if e == null as *Expr { return; }
if e.genericTypeArgCount > 0 && String_StartsWith(e.genericTypeArg0, "$") {
let b0: *Expr = Env_Lookup(env, e.genericTypeArg0);
if b0 != null as *Expr && b0.kind == ekMacroType && b0.refType != null as *TypeExpr {
if b0.refType.kind == tekNamed {
e.genericTypeArg0 = b0.refType.typeName;
}
}
}
if e.genericTypeArgCount > 1 && String_StartsWith(e.genericTypeArg1, "$") {
let b1: *Expr = Env_Lookup(env, e.genericTypeArg1);
if b1 != null as *Expr && b1.kind == ekMacroType && b1.refType != null as *TypeExpr {
if b1.refType.kind == tekNamed {
e.genericTypeArg1 = b1.refType.typeName;
}
}
}
}
// Fragment kind check: "ident" | "literal" | "block" | "stmt" | "pat" | "expr" | "tt"
func Macro_FragMatches(kindStr: String, aexp: *Expr) -> bool {
return Macro_CoerceArg(kindStr, aexp) != null as *Expr;
}
// kinds encoded as "expr;ident;rep:expr," — return fi-th segment
func Macro_KindAt(kinds: String, fi: int) -> String {
if kinds == null as String || String_Eq(kinds, "") { return "expr"; }
var start: int = 0;
var idx: int = 0;
let n: int = bux_strlen(kinds) as int;
var i: int = 0;
while i <= n {
if i == n || kinds[i] == 59 as char8 { // ';'
if idx == fi {
let len: int = i - start;
if len <= 0 { return "expr"; }
return bux_str_slice(kinds, start as uint, len as uint);
}
idx = idx + 1;
start = i + 1;
}
i = i + 1;
}
return "expr";
}
// Does body (or nested MacroRep) reference list name as an ident?
func Macro_BodyUsesListName(b: *Block, name: String) -> bool {
if b == null as *Block || String_Eq(name, "") { return false; }
var s: *Stmt = b.firstStmt;
while s != null as *Stmt {
if Macro_ExprUsesListName(s.child1, name) { return true; }
if Macro_ExprUsesListName(s.child2, name) { return true; }
if Macro_ExprUsesListName(s.child3, name) { return true; }
if s.refStmtBlock != null as *Block {
if Macro_BodyUsesListName(s.refStmtBlock, name) { return true; }
}
if s.refStmtElse != null as *Block {
if Macro_BodyUsesListName(s.refStmtElse, name) { return true; }
}
s = s.nextStmt;
}
return false;
}
func Macro_ExprUsesListName(e: *Expr, name: String) -> bool {
if e == null as *Expr { return false; }
if e.kind == ekIdent && String_Eq(e.strValue, name) { return true; }
if Macro_ExprUsesListName(e.child1, name) { return true; }
if Macro_ExprUsesListName(e.child2, name) { return true; }
if Macro_ExprUsesListName(e.child3, name) { return true; }
if e.refBlock != null as *Block {
if Macro_BodyUsesListName(e.refBlock, name) { return true; }
}
var args: *ExprList = e.callArgs;
while args != null as *ExprList {
if Macro_ExprUsesListName(args.expr, name) { return true; }
args = args.next;
}
return false;
}
func Macro_AppendBlockStmts(n: *Block, part: *Block) {
if part == null as *Block { return; }
var ps: *Stmt = part.firstStmt;
while ps != null as *Stmt {
let nxt: *Stmt = ps.nextStmt;
ps.nextStmt = null as *Stmt;
if n.firstStmt == null as *Stmt {
n.firstStmt = ps;
n.lastStmt = ps;
} else {
n.lastStmt.nextStmt = ps;
n.lastStmt = ps;
}
n.stmtCount = n.stmtCount + 1;
ps = nxt;
}
}
// Flatten $(…)* while substituting — zip multi-lists; expand once when no lists left
func Subst_Block_Flat(b: *Block, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Block {
if b == null as *Block { return null as *Block; }
let n: *Block = bux_alloc(sizeof(Block)) as *Block;
n.line = line;
n.column = col;
n.sourceFile = file;
n.stmtCount = 0;
n.firstStmt = null as *Stmt;
n.lastStmt = null as *Stmt;
var s: *Stmt = b.firstStmt;
while s != null as *Stmt {
if s.kind == skMacroRep && s.refStmtBlock != null as *Block {
let use0: bool = Macro_BodyUsesListName(s.refStmtBlock, env.listName0);
let use1: bool = Macro_BodyUsesListName(s.refStmtBlock, env.listName1);
// Nested same-list after outer bound items as singles: expand once
if !use0 && !use1 {
let partOnce: *Block = Subst_Block(s.refStmtBlock, env, file, line, col);
Macro_AppendBlockStmts(n, partOnce);
} else {
var nRep: int = 0;
if use0 && env.listCount0 > nRep { nRep = env.listCount0; }
if use1 && env.listCount1 > nRep { nRep = env.listCount1; }
var li: int = 0;
while li < nRep {
var inner: MacroEnv = Env_New();
Env_CopySingles(&inner, env);
// Keep unreferenced lists for nested MacroRep siblings
if !use0 && !String_Eq(env.listName0, "") {
inner.listName0 = env.listName0;
inner.listCount0 = env.listCount0;
var ci: int = 0;
while ci < env.listCount0 {
Env_ListSet(&inner, 0, ci, Env_ListGet(env, 0, ci));
ci = ci + 1;
}
}
if !use1 && !String_Eq(env.listName1, "") {
inner.listName1 = env.listName1;
inner.listCount1 = env.listCount1;
var cj: int = 0;
while cj < env.listCount1 {
Env_ListSet(&inner, 1, cj, Env_ListGet(env, 1, cj));
cj = cj + 1;
}
}
if use0 && li < env.listCount0 {
Env_SetNamed(&inner, env.listName0, Env_ListGet(env, 0, li));
}
if use1 && li < env.listCount1 {
Env_SetNamed(&inner, env.listName1, Env_ListGet(env, 1, li));
}
let part: *Block = Subst_Block(s.refStmtBlock, &inner, file, line, col);
Macro_AppendBlockStmts(n, part);
li = li + 1;
}
}
} else if s.kind == skExpr && s.child1 != null as *Expr && s.child1.kind == ekIdent {
// Splice `$s:stmt` bound to ekMacroStmt as a real statement
let bound: *Expr = Env_Lookup(env, s.child1.strValue);
if bound != null as *Expr && bound.kind == ekMacroStmt && bound.macroStmt != null as *Stmt {
let one: *Stmt = Subst_Stmt(bound.macroStmt, env, file, line, col);
if one != null as *Stmt {
one.nextStmt = null as *Stmt;
if n.firstStmt == null as *Stmt {
n.firstStmt = one;
n.lastStmt = one;
} else {
n.lastStmt.nextStmt = one;
n.lastStmt = one;
}
n.stmtCount = n.stmtCount + 1;
}
} else {
let one2: *Stmt = Subst_Stmt(s, env, file, line, col);
if one2 != null as *Stmt {
one2.nextStmt = null as *Stmt;
if n.firstStmt == null as *Stmt {
n.firstStmt = one2;
n.lastStmt = one2;
} else {
n.lastStmt.nextStmt = one2;
n.lastStmt = one2;
}
n.stmtCount = n.stmtCount + 1;
}
}
} else {
let one: *Stmt = Subst_Stmt(s, env, file, line, col);
if one != null as *Stmt {
one.nextStmt = null as *Stmt;
if n.firstStmt == null as *Stmt {
n.firstStmt = one;
n.lastStmt = one;
} else {
n.lastStmt.nextStmt = one;
n.lastStmt = one;
}
n.stmtCount = n.stmtCount + 1;
}
}
s = s.nextStmt;
}
return n;
}
// ---------------------------------------------------------------------------
// Diagnostics
// ---------------------------------------------------------------------------
const maxMacroDiags: int = 64;
struct MacroDiag {
line: uint32;
column: uint32;
message: String;
}
struct MacroEntry {
name: String;
decl: *Decl;
}
struct MacroExpander {
diagCount: int;
diags: *MacroDiag;
// Flat macro table — max 64 macros
macroCount: int;
entries: *MacroEntry;
gensymCounter: int;
}
func MacroExpand_New() -> *MacroExpander {
let ex: *MacroExpander = bux_alloc(sizeof(MacroExpander)) as *MacroExpander;
ex.diagCount = 0;
ex.diags = bux_alloc((maxMacroDiags * sizeof(MacroDiag)) as uint) as *MacroDiag;
ex.macroCount = 0;
ex.entries = bux_alloc((64 * sizeof(MacroEntry)) as uint) as *MacroEntry;
ex.gensymCounter = 0;
return ex;
}
func MacroExpand_Err(ex: *MacroExpander, line: uint32, col: uint32, msg: String) {
if ex.diagCount < maxMacroDiags {
ex.diags[ex.diagCount] = MacroDiag { line: line, column: col, message: msg };
ex.diagCount = ex.diagCount + 1;
}
}
func MacroExpand_Register(ex: *MacroExpander, name: String, d: *Decl) {
if ex.macroCount >= 64 { return; }
ex.entries[ex.macroCount] = MacroEntry { name: name, decl: d };
ex.macroCount = ex.macroCount + 1;
}
func MacroExpand_Lookup(ex: *MacroExpander, name: String) -> *Decl {
var i: int = 0;
while i < ex.macroCount {
if String_Eq(ex.entries[i].name, name) {
return ex.entries[i].decl;
}
i = i + 1;
}
return null as *Decl;
}
// ---------------------------------------------------------------------------
// Call-site graft: force line/col + sourceFile on tree
// ---------------------------------------------------------------------------
func Graft_Expr(e: *Expr, file: String, line: uint32, col: uint32) {
if e == null as *Expr { return; }
e.line = line;
e.column = col;
if file != null as String && !String_Eq(file, "") {
e.sourceFile = file;
}
Graft_Expr(e.child1, file, line, col);
Graft_Expr(e.child2, file, line, col);
Graft_Expr(e.child3, file, line, col);
if e.refBlock != null as *Block {
Graft_Block(e.refBlock, file, line, col);
}
var args: *ExprList = e.callArgs;
while args != null as *ExprList {
Graft_Expr(args.expr, file, line, col);
args = args.next;
}
var arm: *MatchArm = e.matchArms;
while arm != null as *MatchArm {
Graft_Expr(arm.body, file, line, col);
arm = arm.next;
}
}
func Graft_Stmt(s: *Stmt, file: String, line: uint32, col: uint32) {
if s == null as *Stmt { return; }
s.line = line;
s.column = col;
if file != null as String && !String_Eq(file, "") {
s.sourceFile = file;
}
Graft_Expr(s.child1, file, line, col);
Graft_Expr(s.child2, file, line, col);
Graft_Expr(s.child3, file, line, col);
if s.refStmtBlock != null as *Block {
Graft_Block(s.refStmtBlock, file, line, col);
}
if s.refStmtElse != null as *Block {
Graft_Block(s.refStmtElse, file, line, col);
}
Graft_Stmt(s.nextStmt, file, line, col);
}
func Graft_Block(b: *Block, file: String, line: uint32, col: uint32) {
if b == null as *Block { return; }
b.line = line;
b.column = col;
if file != null as String && !String_Eq(file, "") {
b.sourceFile = file;
}
Graft_Stmt(b.firstStmt, file, line, col);
}
// ---------------------------------------------------------------------------
// Substitute $frags in a cloned tree
// ---------------------------------------------------------------------------
// Build callArgs for a call/macro-call, flattening MacroRep and MacroTt groups.
func Macro_SubstCallArgs(oldArgs: *ExprList, env: *MacroEnv, file: String, line: uint32, col: uint32,
outCount: *int) -> *ExprList {
var first: *ExprList = null as *ExprList;
var last: *ExprList = null as *ExprList;
var count: int = 0;
var args: *ExprList = oldArgs;
while args != null as *ExprList {
let a: *Expr = args.expr;
var didFlat: bool = false;
// Expression-level `$( body ),*`
if a != null as *Expr && a.kind == ekMacroRep && a.child1 != null as *Expr {
let body: *Expr = a.child1;
let use0: bool = Macro_ExprUsesListName(body, env.listName0);
let use1: bool = Macro_ExprUsesListName(body, env.listName1);
if !use0 && !use1 {
let node: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node.expr = Subst_Expr(body, env, file, line, col);
node.next = null as *ExprList;
node.argName = "";
if first == null as *ExprList { first = node; last = node; }
else { last.next = node; last = node; }
count = count + 1;
} else {
var nRep: int = 0;
if use0 && env.listCount0 > nRep { nRep = env.listCount0; }
if use1 && env.listCount1 > nRep { nRep = env.listCount1; }
var li: int = 0;
while li < nRep {
var inner: MacroEnv = Env_New();
Env_CopySingles(&inner, env);
if use0 && li < env.listCount0 {
Env_SetNamed(&inner, env.listName0, Env_ListGet(env, 0, li));
}
if use1 && li < env.listCount1 {
Env_SetNamed(&inner, env.listName1, Env_ListGet(env, 1, li));
}
let node2: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node2.expr = Subst_Expr(body, &inner, file, line, col);
node2.next = null as *ExprList;
node2.argName = "";
if first == null as *ExprList { first = node2; last = node2; }
else { last.next = node2; last = node2; }
count = count + 1;
li = li + 1;
}
}
didFlat = true;
} else if a != null as *Expr && a.kind == ekIdent {
let bound: *Expr = Env_Lookup(env, a.strValue);
// Bare `$args:tt` group → flatten tuple/slice elements as call args
if bound != null as *Expr && bound.kind == ekMacroTt && bound.boolValue &&
bound.child1 != null as *Expr &&
(bound.child1.kind == ekTuple || bound.child1.kind == ekSlice) {
var tel: *ExprList = bound.child1.callArgs;
while tel != null as *ExprList {
let ce: *Expr = Ast_CloneExpr(tel.expr);
Graft_Expr(ce, file, line, col);
let node3: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node3.expr = ce;
node3.next = null as *ExprList;
node3.argName = "";
if first == null as *ExprList { first = node3; last = node3; }
else { last.next = node3; last = node3; }
count = count + 1;
tel = tel.next;
}
didFlat = true;
}
}
if !didFlat {
let node4: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node4.expr = Subst_Expr(a, env, file, line, col);
node4.next = null as *ExprList;
node4.argName = "";
if first == null as *ExprList { first = node4; last = node4; }
else { last.next = node4; last = node4; }
count = count + 1;
}
args = args.next;
}
*outCount = count;
return first;
}
func Subst_Expr(e: *Expr, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Expr {
if e == null as *Expr { return null as *Expr; }
if e.kind == ekIdent {
let bound: *Expr = Env_Lookup(env, e.strValue);
if bound != null as *Expr {
// Value position: unwrap MacroTt wrapper
if bound.kind == ekMacroTt && bound.child1 != null as *Expr {
let n: *Expr = Ast_CloneExpr(bound.child1);
Graft_Expr(n, file, line, col);
return n;
}
// Type fragments are not values — leave a zero literal
if bound.kind == ekMacroType {
let z: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
z.kind = ekLiteral;
z.line = line;
z.column = col;
z.tokKind = 0;
z.tokText = "0";
z.intValue = 0;
return z;
}
let n2: *Expr = Ast_CloneExpr(bound);
Graft_Expr(n2, file, line, col);
return n2;
}
}
// In-place subst on clone of e
let c: *Expr = Ast_CloneExpr(e);
if c == null as *Expr { return null as *Expr; }
c.child1 = Subst_Expr(c.child1, env, file, line, col);
c.child2 = Subst_Expr(c.child2, env, file, line, col);
c.child3 = Subst_Expr(c.child3, env, file, line, col);
// Array_New<$t> / Foo<$t,$u>
Macro_SubstGenericTypeArgs(c, env);
// sizeof / cast / is type annotations
if c.refType != null as *TypeExpr {
c.refType = Macro_SubstType(c.refType, env);
}
if c.refBlock != null as *Block {
c.refBlock = Subst_Block_Flat(c.refBlock, env, file, line, col);
}
// callArgs: flatten MacroRep + MacroTt groups for calls
if c.kind == ekCall || c.kind == ekMacroCall {
var nCount: int = 0;
c.callArgs = Macro_SubstCallArgs(c.callArgs, env, file, line, col, &nCount);
c.callArgCount = nCount;
} else {
var args2: *ExprList = c.callArgs;
while args2 != null as *ExprList {
args2.expr = Subst_Expr(args2.expr, env, file, line, col);
args2 = args2.next;
}
}
var arm: *MatchArm = c.matchArms;
while arm != null as *MatchArm {
arm.pattern = Subst_Pattern(arm.pattern, env, file, line, col);
arm.body = Subst_Expr(arm.body, env, file, line, col);
arm = arm.next;
}
c.line = line;
c.column = col;
if file != null as String && !String_Eq(file, "") {
c.sourceFile = file;
}
return c;
}
func Subst_Pattern(p: *Pattern, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Pattern {
if p == null as *Pattern { return null as *Pattern; }
// `$p:pat` as whole pattern (pkIdent name `$p`)
if p.kind == pkIdent && !String_Eq(p.patIdent, "") {
let bound: *Expr = Env_Lookup(env, p.patIdent);
if bound != null as *Expr && bound.kind == ekMacroPat && bound.macroPat != null as *Pattern {
let np: *Pattern = Ast_ClonePattern(bound.macroPat);
if np != null as *Pattern {
np.line = line;
np.column = col;
}
return np;
}
}
// MVP: other patterns kept as cloned (no nested `$p` rewrite)
let c: *Pattern = Ast_ClonePattern(p);
if c != null as *Pattern {
c.line = line;
c.column = col;
}
return c;
}
func Subst_Stmt(s: *Stmt, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Stmt {
if s == null as *Stmt { return null as *Stmt; }
let c: *Stmt = Ast_CloneStmt(s);
// Ast_CloneStmt clones nextStmt chain — break to single and rebuild
c.nextStmt = null as *Stmt;
// Unhygienic binder: `var $name` / `for $i` with $frag:ident → call-site name
if (c.kind == skLet || c.kind == skFor) && !String_Eq(c.strValue, "") {
let bn: String = Env_BinderFromFrag(env, c.strValue);
if !String_Eq(bn, "") {
c.strValue = bn;
Env_AddUnhy(env, bn);
}
}
c.child1 = Subst_Expr(c.child1, env, file, line, col);
c.child2 = Subst_Expr(c.child2, env, file, line, col);
c.child3 = Subst_Expr(c.child3, env, file, line, col);
// let x: $t = …
if c.refStmtType != null as *TypeExpr {
c.refStmtType = Macro_SubstType(c.refStmtType, env);
}
if c.refStmtBlock != null as *Block {
c.refStmtBlock = Subst_Block(c.refStmtBlock, env, file, line, col);
}
if c.refStmtElse != null as *Block {
c.refStmtElse = Subst_Block(c.refStmtElse, env, file, line, col);
}
c.line = line;
c.column = col;
if file != null as String && !String_Eq(file, "") {
c.sourceFile = file;
}
// Do NOT walk nextStmt — Subst_Block_Flat iterates the chain itself
c.nextStmt = null as *Stmt;
return c;
}
func Subst_Block(b: *Block, env: *MacroEnv, file: String, line: uint32, col: uint32) -> *Block {
// Always flatten $(…)* so nested expression-blocks expand correctly
return Subst_Block_Flat(b, env, file, line, col);
}
// ---------------------------------------------------------------------------
// Expand trees
// ---------------------------------------------------------------------------
func Expand_Expr(ex: *MacroExpander, e: *Expr, depth: int) -> *Expr {
if e == null as *Expr { return null as *Expr; }
if depth > 32 {
MacroExpand_Err(ex, e.line, e.column, "macro expansion depth exceeded");
return e;
}
if e.kind == ekMacroCall {
return Expand_OneCall(ex, e, depth);
}
e.child1 = Expand_Expr(ex, e.child1, depth);
e.child2 = Expand_Expr(ex, e.child2, depth);
e.child3 = Expand_Expr(ex, e.child3, depth);
if e.refBlock != null as *Block {
e.refBlock = Expand_Block(ex, e.refBlock, depth);
}
var args: *ExprList = e.callArgs;
while args != null as *ExprList {
args.expr = Expand_Expr(ex, args.expr, depth);
args = args.next;
}
var arm: *MatchArm = e.matchArms;
while arm != null as *MatchArm {
arm.body = Expand_Expr(ex, arm.body, depth);
arm = arm.next;
}
return e;
}
func Expand_OneCall(ex: *MacroExpander, call: *Expr, depth: int) -> *Expr {
let name: String = call.strValue;
let siteLine: uint32 = call.line;
let siteCol: uint32 = call.column;
let siteFile: String = call.sourceFile;
// Built-in quote!(e)
if String_Eq(name, "quote") {
if call.callArgCount != 1 || call.callArgs == null as *ExprList {
MacroExpand_Err(ex, siteLine, siteCol, "quote! expects exactly 1 argument");
return call;
}
let arg: *Expr = Expand_Expr(ex, call.callArgs.expr, depth + 1);
let n: *Expr = Ast_CloneExpr(arg);
Graft_Expr(n, siteFile, siteLine, siteCol);
return n;
}
let mdecl: *Decl = MacroExpand_Lookup(ex, name);
if mdecl == null as *Decl {
MacroExpand_Err(ex, siteLine, siteCol, String_Concat("unknown macro '", String_Concat(name, "'")));
return call;
}
// Match rule: fixed, trailing rep, multi-rep groups (;), compound zip (chunk>1).
// Group lengths encoded on call.genericCallee as "n0;n1;…" (empty = one flat group).
var rule: *Decl = mdecl.childDecl1;
var matched: *Decl = null as *Decl;
var env: MacroEnv = Env_New();
// Pre-expand all args once
var expArgs: *ExprList = null as *ExprList;
var expTail: *ExprList = null as *ExprList;
var nargs: int = 0;
var rawArg: *ExprList = call.callArgs;
while rawArg != null as *ExprList {
let aexp: *Expr = Expand_Expr(ex, rawArg.expr, depth + 1);
let node: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node.expr = aexp;
node.next = null as *ExprList;
node.argName = "";
if expArgs == null as *ExprList {
expArgs = node;
expTail = node;
} else {
expTail.next = node;
expTail = node;
}
nargs = nargs + 1;
rawArg = rawArg.next;
}
// Parse group lengths from genericCallee ("2;3") — empty means one group of all args
var g0: int = nargs;
var g1: int = 0;
var nGroups: int = 1;
let glens: String = call.genericCallee;
if glens != null as String && !String_Eq(glens, "") {
var gstart: int = 0;
var gidx: int = 0;
let glen: int = bux_strlen(glens) as int;
var gi: int = 0;
while gi <= glen {
if gi == glen || glens[gi] == 59 as char8 {
let gpart: String = bux_str_slice(glens, gstart as uint, (gi - gstart) as uint);
let gv: int = String_ToInt(gpart) as int;
if gidx == 0 { g0 = gv; }
else if gidx == 1 { g1 = gv; nGroups = 2; }
gidx = gidx + 1;
gstart = gi + 1;
}
gi = gi + 1;
}
if gidx > 0 { nGroups = gidx; }
}
while rule != null as *Decl {
let kinds: String = rule.useNames;
var nSeg: int = 0;
let klen: int = bux_strlen(kinds) as int;
if klen == 0 {
nSeg = 0;
} else {
nSeg = 1;
var ci: int = 0;
while ci < klen {
if kinds[ci] == 59 as char8 { nSeg = nSeg + 1; }
ci = ci + 1;
}
}
var nReps: int = 0;
var sgi: int = 0;
while sgi < nSeg {
let ks0: String = Macro_KindAt(kinds, sgi);
if String_StartsWith(ks0, "rep:") { nReps = nReps + 1; }
sgi = sgi + 1;
}
let useGroups: bool = nReps > 1 && nGroups > 1;
// Session 86 — juxta free-form: single arg F(a,b) → $f:ident + $args:tt
var ruleArgs: *ExprList = expArgs;
var ruleNargs: int = nargs;
if !useGroups && nReps == 0 && nSeg == 2 && nargs == 1 && expArgs != null as *ExprList {
let k0: String = Macro_KindAt(kinds, 0);
let k1: String = Macro_KindAt(kinds, 1);
let only: *Expr = expArgs.expr;
if String_Eq(k0, "ident") && String_Eq(k1, "tt") &&
only != null as *Expr && only.kind == ekCall &&
only.child1 != null as *Expr && only.child1.kind == ekIdent {
let callee: *Expr = only.child1;
// Build MacroTt group from call args (tuple of elements)
let inner: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
inner.kind = ekTuple;
inner.line = only.line;
inner.column = only.column;
inner.callArgs = only.callArgs;
inner.callArgCount = only.callArgCount;
let group: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
group.kind = ekMacroTt;
group.line = only.line;
group.column = only.column;
group.child1 = inner;
group.boolValue = true;
let n0: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
n0.expr = callee;
n0.next = null as *ExprList;
n0.argName = "";
let n1: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
n1.expr = group;
n1.next = null as *ExprList;
n1.argName = "";
n0.next = n1;
ruleArgs = n0;
ruleNargs = 2;
}
}
env = Env_New();
var ok: bool = true;
var argList: *ExprList = ruleArgs;
var flatLeft: int = ruleNargs;
var gIdx: int = 0;
var gOff: int = 0; // offset within current group when useGroups
var paramIdx: int = 0;
var listSlot: int = 0; // next free list slot (0 or 1)
var seg: int = 0;
while seg < nSeg && ok {
var kindStr: String = Macro_KindAt(kinds, seg);
if String_StartsWith(kindStr, "rep:") {
var rest: String = bux_str_slice(kindStr, 4, bux_strlen(kindStr) - 4);
var chunk: int = 1;
var atPos: int = -1;
var ri: int = 0;
let rlen: int = bux_strlen(rest) as int;
while ri < rlen {
if rest[ri] == 64 as char8 { atPos = ri; break; }
ri = ri + 1;
}
if atPos >= 0 {
let numStr: String = bux_str_slice(rest, (atPos + 1) as uint, (rlen - atPos - 1) as uint);
chunk = String_ToInt(numStr) as int;
if chunk < 1 { chunk = 1; }
}
// Determine how many args this rep consumes
var take: int = 0;
if useGroups {
if gIdx >= nGroups {
take = 0; // empty trailing rep
} else {
if gIdx == 0 { take = g0; }
else { take = g1; }
gIdx = gIdx + 1;
gOff = 0;
}
} else {
take = flatLeft;
}
if take % chunk != 0 { ok = false; break; }
// Compound zip: de-interleave into parallel lists
if chunk == 1 {
if listSlot > 1 { ok = false; break; }
let nm: String = Rule_FragName(rule, paramIdx);
if listSlot == 0 {
env.listName0 = nm;
env.listCount0 = 0;
} else {
env.listName1 = nm;
env.listCount1 = 0;
}
var ti: int = 0;
while ti < take && argList != null as *ExprList {
Env_ListSet(&env, listSlot, Env_ListCountOf(&env, listSlot), argList.expr);
if listSlot == 0 { env.listCount0 = env.listCount0 + 1; }
else { env.listCount1 = env.listCount1 + 1; }
argList = argList.next;
flatLeft = flatLeft - 1;
ti = ti + 1;
}
if ti != take { ok = false; break; }
listSlot = listSlot + 1;
paramIdx = paramIdx + 1;
} else if chunk == 2 {
// Parallel lists for $a and $b
if listSlot > 0 { ok = false; break; } // need both free slots
let nm0: String = Rule_FragName(rule, paramIdx);
let nm1: String = Rule_FragName(rule, paramIdx + 1);
env.listName0 = nm0;
env.listName1 = nm1;
env.listCount0 = 0;
env.listCount1 = 0;
var ti2: int = 0;
while ti2 < take && argList != null as *ExprList {
// even → list0, odd → list1
if ti2 % 2 == 0 {
Env_ListSet(&env, 0, env.listCount0, argList.expr);
env.listCount0 = env.listCount0 + 1;
} else {
Env_ListSet(&env, 1, env.listCount1, argList.expr);
env.listCount1 = env.listCount1 + 1;
}
argList = argList.next;
flatLeft = flatLeft - 1;
ti2 = ti2 + 1;
}
if ti2 != take { ok = false; break; }
listSlot = 2;
paramIdx = paramIdx + 2;
} else {
ok = false;
break;
}
} else {
// Fixed fragment
if useGroups {
// advance group if exhausted
var gAvail: int = 0;
if gIdx == 0 { gAvail = g0 - gOff; }
else if gIdx == 1 { gAvail = g1 - gOff; }
else { gAvail = 0; }
if gAvail <= 0 {
gIdx = gIdx + 1;
gOff = 0;
if gIdx == 0 { gAvail = g0; }
else if gIdx == 1 { gAvail = g1; }
else { gAvail = 0; }
}
if gAvail <= 0 || argList == null as *ExprList { ok = false; break; }
gOff = gOff + 1;
} else {
if argList == null as *ExprList { ok = false; break; }
}
let aexp: *Expr = argList.expr;
let coerced: *Expr = Macro_CoerceArg(kindStr, aexp);
if coerced == null as *Expr { ok = false; break; }
Env_Set(&env, paramIdx, Rule_FragName(rule, paramIdx), coerced);
argList = argList.next;
flatLeft = flatLeft - 1;
paramIdx = paramIdx + 1;
}
seg = seg + 1;
}
if ok {
if useGroups {
if gIdx < nGroups { ok = false; }
// remaining empty groups ok only if trailing empty reps handled
} else {
if argList != null as *ExprList || flatLeft != 0 { ok = false; }
}
}
if ok {
matched = rule;
break;
}
rule = rule.childDecl2;
}
if matched == null as *Decl {
MacroExpand_Err(ex, siteLine, siteCol, String_Concat("macro has no matching rule for '", String_Concat(name, "'")));
return call;
}
if matched.refBody == null as *Block {
MacroExpand_Err(ex, siteLine, siteCol, "macro rule has empty body");
return call;
}
let body: *Block = Subst_Block_Flat(matched.refBody, &env, siteFile, siteLine, siteCol);
// Hygiene: unique let names per expansion (CBE is function-scoped)
Macro_GensymBlock(ex, body, &env);
let blk: *Expr = bux_alloc(sizeof(Expr)) as *Expr;
// zero via MakeExpr-like
let made: Expr = Ast_MakeExpr(ekBlock, siteLine, siteCol);
blk.kind = made.kind;
blk.line = siteLine;
blk.column = siteCol;
blk.sourceFile = siteFile;
blk.strValue = "";
blk.intValue = 0;
blk.boolValue = false;
blk.tokKind = 0;
blk.tokText = "";
blk.child1 = null as *Expr;
blk.child2 = null as *Expr;
blk.child3 = null as *Expr;
blk.refType = null as *TypeExpr;
blk.refBlock = body;
blk.callArgs = null as *ExprList;
blk.callArgCount = 0;
blk.matchArms = null as *MatchArm;
blk.matchArmCount = 0;
blk.genericCallee = "";
blk.genericTypeArgCount = 0;
blk.structName = "";
blk.structFieldCount = 0;
blk.closureParams = null as *Decl;
blk.captureCount = 0;
// Nested macros inside expansion
return Expand_Expr(ex, blk, depth + 1);
}
func Expand_Block(ex: *MacroExpander, b: *Block, depth: int) -> *Block {
if b == null as *Block { return null as *Block; }
var s: *Stmt = b.firstStmt;
while s != null as *Stmt {
Expand_Stmt(ex, s, depth);
s = s.nextStmt;
}
return b;
}
func Expand_Stmt(ex: *MacroExpander, s: *Stmt, depth: int) {
if s == null as *Stmt { return; }
s.child1 = Expand_Expr(ex, s.child1, depth);
s.child2 = Expand_Expr(ex, s.child2, depth);
s.child3 = Expand_Expr(ex, s.child3, depth);
if s.refStmtBlock != null as *Block {
Expand_Block(ex, s.refStmtBlock, depth);
}
if s.refStmtElse != null as *Block {
Expand_Block(ex, s.refStmtElse, depth);
}
if s.refStmtDecl != null as *Decl {
Expand_Decl(ex, s.refStmtDecl, depth);
}
}
func Expand_Decl(ex: *MacroExpander, d: *Decl, depth: int) {
if d == null as *Decl { return; }
if d.kind == dkFunc {
if d.refBody != null as *Block {
Expand_Block(ex, d.refBody, depth);
}
} else if d.kind == dkImpl {
var m: *Decl = d.childDecl1;
while m != null as *Decl {
Expand_Decl(ex, m, depth);
m = m.childDecl2;
}
} else if d.kind == dkModule {
var it: *Decl = d.childDecl1;
while it != null as *Decl {
Expand_Decl(ex, it, depth);
it = it.childDecl2;
}
} else if d.kind == dkConst {
d.constValue = Expand_Expr(ex, d.constValue, depth);
}
// do not expand into nested macros' templates (rules stay as templates)
}
func Collect_Macros(ex: *MacroExpander, d: *Decl) {
if d == null as *Decl { return; }
if d.kind == dkMacro {
// Only top-level macros have non-empty name; rules have empty strValue
if !String_Eq(d.strValue, "") {
MacroExpand_Register(ex, d.strValue, d);
}
} else if d.kind == dkModule {
var it: *Decl = d.childDecl1;
while it != null as *Decl {
Collect_Macros(ex, it);
it = it.childDecl2;
}
}
}
/// Expand all macro! calls in module. Returns number of errors.
func MacroExpand_ExpandModule(mod: *Module) -> *MacroExpander {
let ex: *MacroExpander = MacroExpand_New();
if mod == null as *Module { return ex; }
var d: *Decl = mod.firstItem;
while d != null as *Decl {
Collect_Macros(ex, d);
d = d.childDecl2;
}
d = mod.firstItem;
while d != null as *Decl {
Expand_Decl(ex, d, 0);
d = d.childDecl2;
}
return ex;
}
func MacroExpand_DiagCount(ex: *MacroExpander) -> int {
if ex == null as *MacroExpander { return 0; }
return ex.diagCount;
}
func MacroExpand_GetDiag(ex: *MacroExpander, i: int) -> MacroDiag {
return ex.diags[i];
}
}