feat: macros (multi-rep, hygiene), Drop field-move, lean multi-OS CI
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

Sessions 56–69: declarative macro! with rep/zip/literal/block and
unhygienic var $name binders; partial field-move skip Drop; @[Release]
polish; LSP type hierarchy; CI Nim cache + lean macOS + Windows smoke.
This commit is contained in:
2026-07-20 17:19:46 +03:00
parent 6f2a3b1d88
commit fe3b1e8b6a
41 changed files with 5281 additions and 141 deletions
+294 -7
View File
@@ -27,6 +27,7 @@ module Parser {
diagCount: int,
diags: *ParserDiag,
structInitAllowed: bool,
macroTemplateMode: bool, // allows $(…)* in macro! bodies
}
struct ParserDiag {
@@ -1252,11 +1253,81 @@ module Parser {
continue;
}
// ! (unwrap operator)
// ! — macro call name!(args) or unwrap
if kind == tkBang {
discard parserAdvance(p);
let line: uint32 = parserCurToken(p).line;
let col: uint32 = parserCurToken(p).column;
if left.kind == ekIdent && parserCheck(p, tkLParen) {
discard parserAdvance(p); // (
let e: *Expr = parserMakeExpr(ekMacroCall, line, col);
e.strValue = left.strValue;
var argCount: int = 0;
var firstArg: *ExprList = null as *ExprList;
var lastArg: *ExprList = null as *ExprList;
// Multi-rep groups: m!(a,b; c,d) — lengths encoded in genericCallee
var groupLens: String = "";
var curGroup: int = 0;
var nGroups: int = 0;
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRParen) { break; }
// `;` starts a new arg group
if parserMatch(p, tkSemicolon) {
if String_Eq(groupLens, "") {
groupLens = String_FromInt(curGroup as int64);
} else {
groupLens = String_Concat(groupLens, String_Concat(";", String_FromInt(curGroup as int64)));
}
nGroups = nGroups + 1;
curGroup = 0;
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
continue;
}
let argExpr: *Expr = parserParseExpr(p);
let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
argNode.expr = argExpr;
argNode.next = null as *ExprList;
argNode.argName = "";
if firstArg == null as *ExprList {
firstArg = argNode;
lastArg = argNode;
} else {
lastArg.next = argNode;
lastArg = argNode;
}
argCount = argCount + 1;
curGroup = curGroup + 1;
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserMatch(p, tkComma) {
continue;
}
// allow `;` at loop top; otherwise end of args
if !parserCheck(p, tkSemicolon) {
break;
}
}
// finalize last group
if curGroup > 0 || nGroups == 0 {
if String_Eq(groupLens, "") {
groupLens = String_FromInt(curGroup as int64);
} else {
groupLens = String_Concat(groupLens, String_Concat(";", String_FromInt(curGroup as int64)));
}
nGroups = nGroups + 1;
}
// single group → empty genericCallee (flat match)
if nGroups <= 1 {
e.genericCallee = "";
} else {
e.genericCallee = groupLens;
}
e.callArgs = firstArg;
e.callArgCount = argCount;
discard parserExpect(p, tkRParen, "expected ')' to close macro arguments");
left = e;
continue;
}
let e: *Expr = parserMakeExpr(ekUnwrap, line, col);
e.child1 = left;
left = e;
@@ -1466,11 +1537,50 @@ module Parser {
// ---------------------------------------------------------------------------
func parserParseStmt(p: *Parser) -> *Stmt {
while parserCheck(p, tkNewLine) {
discard parserAdvance(p);
}
let tok: LexToken = parserCurToken(p);
let line: uint32 = tok.line;
let col: uint32 = tok.column;
let kind: int = tok.kind;
// Macro template: $( stmts… )*
if p.macroTemplateMode && kind == tkDollar && parserPeek(p, 1) == tkLParen {
discard parserAdvance(p); // $
discard parserAdvance(p); // (
let body: *Block = bux_alloc(sizeof(Block)) as *Block;
body.line = line;
body.column = col;
body.sourceFile = "";
body.stmtCount = 0;
body.firstStmt = null as *Stmt;
body.lastStmt = null as *Stmt;
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRParen) { break; }
let inner: *Stmt = parserParseStmt(p);
if body.firstStmt == null as *Stmt {
body.firstStmt = inner;
body.lastStmt = inner;
} else {
body.lastStmt.nextStmt = inner;
body.lastStmt = inner;
}
body.stmtCount = body.stmtCount + 1;
}
discard parserExpect(p, tkRParen, "expected ')' to close macro repetition");
discard parserExpect(p, tkStar, "expected '*' after macro repetition");
parserMatch(p, tkSemicolon);
let s: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt;
s.kind = skMacroRep;
s.line = line;
s.column = col;
s.refStmtBlock = body;
s.nextStmt = null as *Stmt;
return s;
}
// let / var
if kind == tkLet || kind == tkVar {
let isVar: bool = (kind == tkVar);
@@ -2197,6 +2307,178 @@ module Parser {
}
// ---------------------------------------------------------------------------
// macro! name { ($x:expr, …) => { template } … }
// ---------------------------------------------------------------------------
func parserSetMacroFragName(d: *Decl, idx: int, name: String) {
if idx == 0 { d.param0.name = name; }
else if idx == 1 { d.param1.name = name; }
else if idx == 2 { d.param2.name = name; }
else if idx == 3 { d.param3.name = name; }
else if idx == 4 { d.param4.name = name; }
else if idx == 5 { d.param5.name = name; }
else if idx == 6 { d.param6.name = name; }
else if idx == 7 { d.param7.name = name; }
else if idx == 8 { d.param8.name = name; }
}
func parserParseMacroDecl(p: *Parser, isPublic: bool) -> *Decl {
let line: uint32 = parserCurToken(p).line;
let col: uint32 = parserCurToken(p).column;
discard parserExpect(p, tkMacro, "expected 'macro'");
discard parserExpect(p, tkBang, "expected '!' after macro");
let nameTok: LexToken = parserExpect(p, tkIdent, "expected macro name");
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
discard parserExpect(p, tkLBrace, "expected '{' to start macro body");
let d: *Decl = bux_alloc(sizeof(Decl)) as *Decl;
d.kind = dkMacro;
d.line = line;
d.column = col;
d.isPublic = isPublic;
d.strValue = nameTok.text;
d.childDecl1 = null as *Decl;
d.childDecl2 = null as *Decl;
var firstRule: *Decl = null as *Decl;
var lastRule: *Decl = null as *Decl;
var ruleCount: int = 0;
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
if parserCheck(p, tkRBrace) { break; }
let rline: uint32 = parserCurToken(p).line;
let rcol: uint32 = parserCurToken(p).column;
discard parserExpect(p, tkLParen, "expected '(' to start macro pattern");
let rule: *Decl = bux_alloc(sizeof(Decl)) as *Decl;
rule.kind = dkMacro;
rule.line = rline;
rule.column = rcol;
rule.strValue = "";
rule.paramCount = 0;
rule.childDecl2 = null as *Decl;
// useNames encodes: "expr" | "ident" | "tt" | "rep:expr," | "rep:expr+expr," (compound)
// Multiple pattern elements joined by `;` → multi-rep groups at call site
var kindsEnc: String = "";
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRParen) { break; }
if parserMatch(p, tkSemicolon) { continue; }
// $( $a:kind , $b:kind ),*
if parserCheck(p, tkDollar) && parserPeek(p, 1) == tkLParen {
discard parserAdvance(p); // $
discard parserAdvance(p); // (
var repNames: String = "";
var repKinds: String = "";
var nIn: int = 0;
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRParen) { break; }
let fragTok: LexToken = parserExpect(p, tkIdent, "expected $name fragment");
if !String_StartsWith(fragTok.text, "$") {
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
}
discard parserExpect(p, tkColon, "expected ':' after fragment name");
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text;
if String_Eq(kname, "lit") { kname = "literal"; }
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|| String_Eq(kname, "literal") || String_Eq(kname, "block")) {
kname = "expr";
}
if nIn == 0 {
repNames = fragTok.text;
repKinds = kname;
} else {
repNames = String_Concat(repNames, String_Concat("+", fragTok.text));
repKinds = String_Concat(repKinds, String_Concat("+", kname));
}
if rule.paramCount < 9 {
parserSetMacroFragName(rule, rule.paramCount, fragTok.text);
rule.paramCount = rule.paramCount + 1;
}
nIn = nIn + 1;
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if !parserMatch(p, tkComma) { break; }
}
discard parserExpect(p, tkRParen, "expected ')' after repeated fragment");
var sep: String = "";
if parserMatch(p, tkComma) { sep = ","; }
discard parserExpect(p, tkStar, "expected '*' after macro repetition");
let enc: String = String_Concat("rep:", String_Concat(repKinds, sep));
// mark compound count via leading digit in typeParam0 of rule (hack: use isDrop)
if rule.paramCount > 0 {
// store chunk size on last param via isVariadic false; use methodCount as chunk
// Encode: kindsEnc entry includes chunk after @
let enc2: String = String_Concat(enc, String_Concat("@", String_FromInt(nIn)));
if String_Eq(kindsEnc, "") { kindsEnc = enc2; }
else { kindsEnc = String_Concat(kindsEnc, String_Concat(";", enc2)); }
}
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserMatch(p, tkSemicolon) { continue; }
if parserMatch(p, tkComma) { continue; }
break;
} else {
let fragTok: LexToken = parserExpect(p, tkIdent, "expected $name fragment");
if !String_StartsWith(fragTok.text, "$") {
parserEmitDiag(p, fragTok.line, fragTok.column, "macro fragment must start with '$'");
}
discard parserExpect(p, tkColon, "expected ':' after fragment name");
let kindTok: LexToken = parserExpect(p, tkIdent, "expected fragment kind");
var kname: String = kindTok.text;
if String_Eq(kname, "lit") { kname = "literal"; }
if !(String_Eq(kname, "expr") || String_Eq(kname, "ident") || String_Eq(kname, "tt")
|| String_Eq(kname, "literal") || String_Eq(kname, "block")) {
kname = "expr";
}
if rule.paramCount < 9 {
parserSetMacroFragName(rule, rule.paramCount, fragTok.text);
rule.paramCount = rule.paramCount + 1;
}
if String_Eq(kindsEnc, "") { kindsEnc = kname; }
else { kindsEnc = String_Concat(kindsEnc, String_Concat(";", kname)); }
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserMatch(p, tkComma) { continue; }
if parserMatch(p, tkSemicolon) { continue; }
break;
}
}
rule.useNames = kindsEnc;
discard parserExpect(p, tkRParen, "expected ')' to close macro pattern");
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
discard parserExpect(p, tkFatArrow, "expected '=>' after macro pattern");
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
let savedTpl: bool = p.macroTemplateMode;
p.macroTemplateMode = true;
rule.refBody = parserParseBlock(p);
p.macroTemplateMode = savedTpl;
if firstRule == null as *Decl {
firstRule = rule;
lastRule = rule;
} else {
lastRule.childDecl2 = rule;
lastRule = rule;
}
ruleCount = ruleCount + 1;
while parserCheck(p, tkNewLine) || parserCheck(p, tkComma) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
}
discard parserExpect(p, tkRBrace, "expected '}' to close macro");
d.childDecl1 = firstRule;
d.methodCount = ruleCount;
if ruleCount == 0 {
parserEmitDiag(p, line, col, "macro has no rules");
}
return d;
}
// Top-level declaration
// ---------------------------------------------------------------------------
@@ -2207,11 +2489,15 @@ module Parser {
}
let isPublic: bool = parserMatch(p, tkPub);
// Parse @[Checked] / @[Drop] / @[Release] attribute
// Parse stacked @[Checked] / @[Drop] / @[Release] attributes
var isChecked: int = 0;
var isDrop: int = 0;
var isRelease: int = 0;
if parserCheck(p, tkAt) {
while true {
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
if !parserCheck(p, tkAt) { break; }
discard parserAdvance(p); // @
if parserCheck(p, tkLBracket) {
discard parserAdvance(p); // [
@@ -2232,10 +2518,9 @@ module Parser {
discard parserAdvance(p); // ]
}
}
// Skip newlines after attribute before the declaration
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
}
while parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) {
discard parserAdvance(p);
}
let kind: int = parserPeek(p, 0);
@@ -2270,6 +2555,7 @@ module Parser {
if kind == tkImport { return parserParseImportDecl(p, isPublic); }
if kind == tkExtern { return parserParseExternDecl(p, isPublic); }
if kind == tkInterface { return parserParseInterfaceDecl(p, isPublic); }
if kind == tkMacro { return parserParseMacroDecl(p, isPublic); }
if kind == tkExtend {
discard parserAdvance(p);
@@ -2396,6 +2682,7 @@ module Parser {
p.tokenCount = tokenCount;
p.pos = 0;
p.structInitAllowed = true;
p.macroTemplateMode = false;
let diagBuf: *ParserDiag = bux_alloc(256 as uint * sizeof(ParserDiag)) as *ParserDiag;
p.diags = diagBuf;
p.diagCount = 0;