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
+3
View File
@@ -127,6 +127,7 @@ module Ast {
const ekAwait: int = 25;
const ekStringInterp: int = 26;
const ekClosure: int = 27;
const ekMacroCall: int = 28; // name!(args) — expanded before sema
struct ExprList {
expr: *Expr,
@@ -218,6 +219,7 @@ module Ast {
const skDecl: int = 11;
const skDefer: int = 12;
const skSwitch: int = 13;
const skMacroRep: int = 14; // $( stmts… )* in macro templates
struct ElseIf {
line: uint32;
@@ -265,6 +267,7 @@ module Ast {
const dkTypeAlias: int = 9;
const dkExternFunc: int = 10;
const dkExternVar: int = 11;
const dkMacro: int = 12; // macro! name { rules }; rules in childDecl1
struct Param {
line: uint32;
+63 -11
View File
@@ -113,25 +113,64 @@ module CBackend {
return name;
}
/// Mark droppable locals moved by-value (struct fields / nested).
/// True when a C type name owns heap / has auto-Drop (not primitives).
func CBE_IsDroppableTypeName(tn: String) -> bool {
if tn == null as String || String_Eq(tn, "") { return false; }
if String_Eq(tn, "void") || String_Eq(tn, "int") || String_Eq(tn, "bool") {
return false;
}
if String_Eq(tn, "int64") || String_Eq(tn, "uint") || String_Eq(tn, "uint64") {
return false;
}
if String_Eq(tn, "float") || String_Eq(tn, "float64") || String_Eq(tn, "char8") {
return false;
}
if String_Eq(tn, "String") || String_Eq(tn, "cstr") { return false; }
// Array_int, Map_*, user @[Drop] structs (Bag, Token, …)
return true;
}
/// Mark droppable locals moved by-value (struct fields / nested / partial field).
/// `valueTypeHint`: when non-empty (e.g. function return type), used to decide
/// whether a field access is an ownership move (`return bag.items` vs `return bag.tag`).
func CBE_MarkMovedFromNode(cbe: *CEmitter, node: *HirNode) {
CBE_MarkMovedFromNodeHint(cbe, node, "");
}
func CBE_MarkMovedFromNodeHint(cbe: *CEmitter, node: *HirNode, valueTypeHint: String) {
if node == null as *HirNode { return; }
if node.kind == hVar {
CBE_AddMoved(cbe, node.strValue);
return;
}
// Partial field move: only when the *value* type is droppable
if node.kind == hFieldPtr || node.kind == hFieldAccess {
var vty: String = valueTypeHint;
if String_Eq(vty, "") {
vty = node.typeName;
}
// Field HIR often stores the *base* struct typeName — prefer hint
if CBE_IsDroppableTypeName(vty) {
// Walk to base local (hVar / load / nested)
CBE_MarkMovedFromNodeHint(cbe, node.child1, "");
}
return;
}
if node.kind == hLoad {
CBE_MarkMovedFromNodeHint(cbe, node.child1, valueTypeHint);
return;
}
if node.kind == hStructInit {
var field: *HirNode = node.child1;
while field != null as *HirNode {
CBE_MarkMovedFromNode(cbe, field.child1);
CBE_MarkMovedFromNodeHint(cbe, field.child1, "");
field = field.child3;
}
return;
}
if node.kind == hTupleInit {
// child1/child2 + linked extras if any
CBE_MarkMovedFromNode(cbe, node.child1);
CBE_MarkMovedFromNode(cbe, node.child2);
CBE_MarkMovedFromNodeHint(cbe, node.child1, "");
CBE_MarkMovedFromNodeHint(cbe, node.child2, "");
return;
}
}
@@ -375,13 +414,16 @@ module CBackend {
return;
}
// Binary
// Binary — always parenthesize so C precedence cannot rewrite the AST.
// Without parens, Mul(Add(a,b), c) emits `a + b * c` (= a+(b*c)) instead of (a+b)*c.
if kind == hBinary {
StringBuilder_Append(&cbe.sb, "(");
CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, " ");
StringBuilder_Append(&cbe.sb, CBackend_OpToC(node.intValue));
StringBuilder_Append(&cbe.sb, " ");
CBE_EmitExpr(cbe, node.child2);
StringBuilder_Append(&cbe.sb, ")");
return;
}
@@ -494,9 +536,15 @@ module CBackend {
// (Emitting Drop before the value used to use-after-drop on `return a.id`.)
if kind == hReturn {
CBE_EmitDebugLine(cbe, node);
// Track moved variables via return / field-move into returned struct
// Track moved variables via return / field-move into returned struct.
// Pass currentRetType so `return bag.items` (Array) marks bag, but
// `return bag.tag` (int) does not.
if node.child1 != null as *HirNode {
CBE_MarkMovedFromNode(cbe, node.child1);
var retHint: String = "";
if cbe.currentRetType != null as String {
retHint = cbe.currentRetType;
}
CBE_MarkMovedFromNodeHint(cbe, node.child1, retHint);
}
if node.child1 != null as *HirNode && cbe.deferCount > 0 {
// Materialize into a temp so Drop cannot clobber the returned value.
@@ -554,9 +602,13 @@ module CBackend {
// Store: combine alloca + value into single declaration
if kind == hStore {
// Track moved variables via assignment/let
if node.child2 != null as *HirNode && node.child2.kind == hVar {
CBE_AddMoved(cbe, node.child2.strValue);
// Track moved variables via assignment/let (incl. partial field rhs)
if node.child2 != null as *HirNode {
if node.child2.kind == hVar {
CBE_AddMoved(cbe, node.child2.strValue);
} else {
CBE_MarkMovedFromNode(cbe, node.child2);
}
}
// Reinitialization removes moved status
if node.child1 != null as *HirNode && node.child1.kind == hVar {
+77 -4
View File
@@ -17,6 +17,7 @@ module Cli {
extern func bux_system(cmd: String) -> int;
extern func bux_getenv(name: String) -> String;
extern func bux_setenv(name: String, value: String) -> int;
extern func bux_cc_ld_stable() -> String;
extern func bux_strlen(s: String) -> uint;
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
@@ -226,6 +227,25 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St
decl = decl.childDecl2;
}
// Phase 2b: declarative macro! / quote! expansion
PrintLine(" Macro expand...");
let macEx: *MacroExpander = MacroExpand_ExpandModule(mod);
if MacroExpand_DiagCount(macEx) > 0 {
var mi: int = 0;
while mi < MacroExpand_DiagCount(macEx) {
let md: MacroDiag = MacroExpand_GetDiag(macEx, mi);
let diag: Diagnostic = Diagnostic {
message: md.message,
line: md.line,
column: md.column,
severity: 0,
};
Diagnostic_Print(&diag, sourceName);
mi = mi + 1;
}
return "";
}
// Phase 3: Semantic analysis
PrintLine(" Sema...");
let sema: *Sema = Sema_Analyze(mod);
@@ -340,13 +360,17 @@ func Cli_Build(srcPath: String, outPath: String, targetTriple: String, isRelease
if !String_Eq(targetTriple, "") {
StringBuilder_Append(&cmdBuf, "clang ");
StringBuilder_Append(&cmdBuf, optFlags);
StringBuilder_Append(&cmdBuf, " -pthread -Wl,--build-id=none -target ");
StringBuilder_Append(&cmdBuf, " -pthread");
StringBuilder_Append(&cmdBuf, bux_cc_ld_stable());
StringBuilder_Append(&cmdBuf, " -target ");
StringBuilder_Append(&cmdBuf, targetTriple);
StringBuilder_Append(&cmdBuf, " ");
} else {
StringBuilder_Append(&cmdBuf, "cc ");
StringBuilder_Append(&cmdBuf, optFlags);
StringBuilder_Append(&cmdBuf, " -pthread -Wl,--build-id=none ");
StringBuilder_Append(&cmdBuf, " -pthread");
StringBuilder_Append(&cmdBuf, bux_cc_ld_stable());
StringBuilder_Append(&cmdBuf, " ");
}
StringBuilder_Append(&cmdBuf, "-o ");
StringBuilder_Append(&cmdBuf, outPath);
@@ -424,6 +448,24 @@ func Cli_Check(srcPath: String) -> int {
decl2 = decl2.childDecl2;
}
// Phase 2b: macro expand
let macEx2: *MacroExpander = MacroExpand_ExpandModule(mod);
if MacroExpand_DiagCount(macEx2) > 0 {
var mi2: int = 0;
while mi2 < MacroExpand_DiagCount(macEx2) {
let md2: MacroDiag = MacroExpand_GetDiag(macEx2, mi2);
let diag: Diagnostic = Diagnostic {
message: md2.message,
line: md2.line,
column: md2.column,
severity: 0,
};
Diagnostic_Print(&diag, srcPath);
mi2 = mi2 + 1;
}
return 1;
}
// Phase 3: Sema
let sema: *Sema = Sema_Analyze(mod);
if Sema_HasError(sema) {
@@ -479,6 +521,14 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule {
return null as *HirModule;
}
// Phase 2b: macro expand
let macEx3: *MacroExpander = MacroExpand_ExpandModule(mod);
if MacroExpand_DiagCount(macEx3) > 0 {
Print("Macro errors in ");
PrintLine(sourceName);
return null as *HirModule;
}
// Phase 3: Semantic analysis
let sema: *Sema = Sema_Analyze(mod);
if Sema_HasError(sema) {
@@ -1623,6 +1673,25 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
PrintInt(merged.itemCount);
PrintLine(" declarations");
// Declarative macro! / quote! expansion (before type-check)
PrintLine("Expanding macros...");
let macEx: *MacroExpander = MacroExpand_ExpandModule(merged);
if MacroExpand_DiagCount(macEx) > 0 {
var mi: int = 0;
while mi < MacroExpand_DiagCount(macEx) {
let md: MacroDiag = MacroExpand_GetDiag(macEx, mi);
let diag: Diagnostic = Diagnostic {
message: md.message,
line: md.line,
column: md.column,
severity: 0,
};
Diagnostic_Print(&diag, "<macro>");
mi = mi + 1;
}
return 1;
}
// Semantic analysis
PrintLine("Running sema...");
let sema: *Sema = Sema_Analyze(merged);
@@ -1705,13 +1774,17 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool)
if !String_Eq(targetTriple, "") {
StringBuilder_Append(&ccBuf, "clang ");
StringBuilder_Append(&ccBuf, optFlags2);
StringBuilder_Append(&ccBuf, " -pthread -Wl,--build-id=none -target ");
StringBuilder_Append(&ccBuf, " -pthread");
StringBuilder_Append(&ccBuf, bux_cc_ld_stable());
StringBuilder_Append(&ccBuf, " -target ");
StringBuilder_Append(&ccBuf, targetTriple);
StringBuilder_Append(&ccBuf, " ");
} else {
StringBuilder_Append(&ccBuf, "cc ");
StringBuilder_Append(&ccBuf, optFlags2);
StringBuilder_Append(&ccBuf, " -pthread -Wl,--build-id=none ");
StringBuilder_Append(&ccBuf, " -pthread");
StringBuilder_Append(&ccBuf, bux_cc_ld_stable());
StringBuilder_Append(&ccBuf, " ");
}
StringBuilder_Append(&ccBuf, "-o ");
StringBuilder_Append(&ccBuf, outBin);
+14
View File
@@ -282,6 +282,7 @@ module Lexer {
if String_Eq(text, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; }
if String_Eq(text, "macro") { return tkMacro; }
return tkIdent;
}
@@ -666,6 +667,19 @@ module Lexer {
lexEmitToken(lex, tkHash); return;
}
// Macro fragment $name, or bare $ for $(…)*
if c == 36 { // '$'
if Lex_IsIdentStart(lexPeek(lex, 0)) {
while !lexIsAtEnd(lex) && Lex_IsIdentChar(lexPeek(lex, 0)) {
discard lexAdvance(lex);
}
lexEmitToken(lex, tkIdent);
return;
}
lexEmitToken(lex, tkDollar);
return;
}
lexEmitDiag(lex, "unexpected character");
lexEmitToken(lex, tkUnknown);
}
+1105
View File
File diff suppressed because it is too large Load Diff
+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;
+28 -8
View File
@@ -1202,16 +1202,34 @@ module Sema {
}
// Block expression (boolValue = true means unsafe block)
// Value is the last skExpr (macro! templates and `{ e }` as expr).
// Check stmts inside a child scope and return last expr type WITHOUT
// re-checking outside that scope (locals must stay visible).
if kind == ekBlock {
if expr.refBlock != null as *Block {
let prevChecked: bool = sema.checkedFunc;
if expr.boolValue {
let prevChecked: bool = sema.checkedFunc;
sema.checkedFunc = false;
Sema_CheckBlock(sema, expr.refBlock);
sema.checkedFunc = prevChecked;
} else {
Sema_CheckBlock(sema, expr.refBlock);
}
var blockScope: Scope = Scope_NewChild(sema.scope);
let prevScope: *Scope = sema.scope;
sema.scope = &blockScope;
var lastType: int = tyVoid;
var blkWalk: *Stmt = expr.refBlock.firstStmt;
while blkWalk != null as *Stmt {
Sema_CheckStmt(sema, blkWalk);
if blkWalk.kind == skExpr && blkWalk.child1 != null as *Expr {
// CheckStmt already typed the expr; re-read via CheckExpr in-scope
lastType = Sema_CheckExpr(sema, blkWalk.child1);
if blkWalk.child1.refType != null as *TypeExpr {
expr.refType = blkWalk.child1.refType;
}
}
blkWalk = blkWalk.nextStmt;
}
sema.scope = prevScope;
sema.checkedFunc = prevChecked;
return lastType;
}
return tyVoid;
}
@@ -1424,6 +1442,8 @@ module Sema {
sym.refType = null as *TypeExpr;
if stmt.refStmtType != null as *TypeExpr {
sym.refType = stmt.refStmtType;
// Prefer annotation typeKind (block inits previously left tyVoid)
sym.typeKind = Sema_ResolveType(sema, stmt.refStmtType);
if stmt.refStmtType.kind == tekPointer && stmt.refStmtType.pointerPointee != null as *TypeExpr {
sym.typeName = String_Concat(stmt.refStmtType.pointerPointee.typeName, "*");
} else {
@@ -2329,13 +2349,13 @@ module Sema {
s.currentRetType = tyVoid;
}
// Enable borrow checking for @[Checked] functions
// @[Checked] enables borrow checks; @[Release] forces zero-cost (C.4)
let wasChecked: bool = s.checkedFunc;
s.checkedFunc = decl.isChecked != 0;
let wasRelease: bool = s.releaseFunc;
s.releaseFunc = decl.isRelease != 0;
s.checkedFunc = (decl.isChecked != 0) && !s.releaseFunc;
s.movedCount = 0;
// C.1: lifetime elision before walking the body
// C.1: lifetime elision before walking the body (no-op if not checked)
Sema_ApplyLifetimeElision(s, decl);
// Check body statements
+7
View File
@@ -147,6 +147,10 @@ module Token {
// Lifetime parameter token: 'a, 'b, ... (not a char literal)
const tkLifetime: int = 111;
// Declarative macros (session 60 — selfhost parity)
const tkMacro: int = 112;
const tkDollar: int = 113; // bare $ for $(…)*
// ---------------------------------------------------------------------------
// Token struct
// ---------------------------------------------------------------------------
@@ -229,6 +233,7 @@ module Token {
if String_Eq(text, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; }
if String_Eq(text, "macro") { return tkMacro; }
if String_Eq(text, "true") { return tkBoolLiteral; }
if String_Eq(text, "false") { return tkBoolLiteral; }
return tkIdent;
@@ -318,6 +323,8 @@ module Token {
if kind == tkAmpAmp { return "&&"; }
if kind == tkPipePipe { return "||"; }
if kind == tkBang { return "!"; }
if kind == tkMacro { return "macro"; }
if kind == tkDollar { return "$"; }
if kind == tkEq { return "=="; }
if kind == tkNe { return "!="; }
if kind == tkLt { return "<"; }