From 497e01d537ae72a0e65fa58a0c1f574949ea11a0 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 28 Jul 2026 21:36:47 +0300 Subject: [PATCH 1/4] feat(selfhost-sema): check call argument types against param types --- _test_call_args/bux.toml | 4 ++ _test_call_args/src/Main.bux | 9 +++ src/sema.bux | 103 ++++++++++++++++++++++++++--------- 3 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 _test_call_args/bux.toml create mode 100644 _test_call_args/src/Main.bux diff --git a/_test_call_args/bux.toml b/_test_call_args/bux.toml new file mode 100644 index 0000000..21b12b8 --- /dev/null +++ b/_test_call_args/bux.toml @@ -0,0 +1,4 @@ +[Package] +Name = "call_args" +Version = "0.1.0" +Type = "bin" diff --git a/_test_call_args/src/Main.bux b/_test_call_args/src/Main.bux new file mode 100644 index 0000000..0b0d51e --- /dev/null +++ b/_test_call_args/src/Main.bux @@ -0,0 +1,9 @@ +extern func PrintInt(n: int); +extern func PrintLine(s: String); + +func Main() -> int { + PrintInt("hello"); + PrintLine(42); + PrintInt(true); + return 0; +} diff --git a/src/sema.bux b/src/sema.bux index cbcd71a..a0cdfff 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -210,6 +210,9 @@ module Sema { // Display-name helper for assignment diagnostics. func Sema_TypeNameForDiag(te: *TypeExpr, kind: int) -> String { if te != null as *TypeExpr { + // tekFunc TypeExprs have no typeName set (parser leaves it unset) — + // never touch te.typeName for them. + if te.kind == tekFunc { return "func"; } if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr { return String_Concat(te.pointerPointee.typeName, "*"); } @@ -225,6 +228,25 @@ module Sema { return "?"; } + // Assignability for diagnostics: strict numeric widening, bool family, + // exact pointer kind, named types by name (best-effort when refType available). + func Sema_CanAssignTo(sema: *Sema, fromKind: int, fromTe: *TypeExpr, toTe: *TypeExpr) -> bool { + if fromKind == tyUnknown { return true; } + if toTe == null as *TypeExpr { return true; } + let toKind: int = Sema_ResolveType(sema, toTe); + if toKind == tyUnknown { return true; } + if fromKind == tyNamed && toKind == tyNamed { + if fromTe != null as *TypeExpr && !String_Eq(fromTe.typeName, "") { + return String_Eq(fromTe.typeName, toTe.typeName); + } + return true; + } + if fromKind == toKind { return true; } + if Sema_IsStrictNumeric(fromKind) && Sema_IsStrictNumeric(toKind) { return true; } + if Sema_IsBool(fromKind) && Sema_IsBool(toKind) { return true; } + return false; + } + // --------------------------------------------------------------------------- // Block checking helper // --------------------------------------------------------------------------- @@ -1035,9 +1057,57 @@ module Sema { if kind == ekCall { let calleeType: int = Sema_CheckExpr(sema, expr.child1); Sema_ResolveCallArgs(sema, expr); + // Resolve callee decl for call-argument checking: direct calls to + // non-generic named functions only (inference handles generics). + var callDecl: *Decl = null as *Decl; + if expr.child1 != null as *Expr && expr.child1.kind == ekIdent { + let callSym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue); + if callSym.kind == skFunc && callSym.decl != null as *Decl { + if callSym.decl.typeParamCount == 0 { + callDecl = callSym.decl; + } + } + } + // Arity check (runs after Sema_ResolveCallArgs so defaults/named args align) + if callDecl != null as *Decl && expr.callArgCount != callDecl.paramCount { + Sema_EmitError(sema, expr.line, expr.column, + String_Concat("expected ", String_Concat(bux_int_to_str(callDecl.paramCount as int64), + String_Concat(" arguments, got ", bux_int_to_str(expr.callArgCount as int64))))); + callDecl = null as *Decl; + } + var argIdx: int = 0; var arg: *ExprList = expr.callArgs; while arg != null as *ExprList { - discard Sema_CheckExpr(sema, arg.expr); + let argKind: int = Sema_CheckExpr(sema, arg.expr); + // Per-argument type check against the parameter type + if callDecl != null as *Decl && argIdx < callDecl.paramCount { + let cp: *Param = Sema_DeclParam(callDecl, argIdx); + if cp != null as *Param && cp.refParamType != null as *TypeExpr { + // Skip conditions matching the bootstrap: unknown/named/typeparam + // arg kinds, and *char8 -> String (C string interop). + var argSkip: bool = false; + if argKind == tyUnknown || argKind == tyNamed || argKind == tyTypeParam { argSkip = true; } + // Function-typed params: selfhost cannot reliably type function + // values (e.g. '&Double' is tyPointer) — defer like the bootstrap. + if Sema_ResolveType(sema, cp.refParamType) == tyFunc { argSkip = true; } + if !argSkip && argKind == tyPointer && Sema_ResolveType(sema, cp.refParamType) == tyStr { + if arg.expr != null as *Expr && arg.expr.refType != null as *TypeExpr { + if arg.expr.refType.kind == tekPointer && arg.expr.refType.pointerPointee != null as *TypeExpr { + if String_Eq(arg.expr.refType.pointerPointee.typeName, "char8") { argSkip = true; } + } + } + } + if !argSkip && arg.expr != null as *Expr && !Sema_CanAssignTo(sema, argKind, arg.expr.refType, cp.refParamType) { + let wantName: String = Sema_TypeNameForDiag(cp.refParamType, Sema_ResolveType(sema, cp.refParamType)); + let gotName: String = Sema_TypeNameForDiag(arg.expr.refType, argKind); + Sema_EmitError(sema, arg.expr.line, arg.expr.column, + String_Concat("argument ", String_Concat(bux_int_to_str((argIdx + 1) as int64), + String_Concat(": expected ", String_Concat(wantName, + String_Concat(", got ", gotName)))))); + } + } + } + argIdx = argIdx + 1; arg = arg.next; } // Borrow check: reject double mutable borrow in @[Checked] functions @@ -1495,31 +1565,12 @@ module Sema { } // Assignment check: annotation vs initializer (skip when either side is unknown) if stmt.refStmtType != null as *TypeExpr && initType != tyUnknown { - let annotKind: int = Sema_ResolveType(sema, stmt.refStmtType); - if annotKind != tyUnknown { - var mismatch: bool = false; - if initType == tyNamed && annotKind == tyNamed { - // Both named: kinds are equal, compare type names (when available). - if stmt.child1 != null as *Expr && stmt.child1.refType != null as *TypeExpr { - if !String_Eq(stmt.child1.refType.typeName, "") && - !String_Eq(stmt.child1.refType.typeName, stmt.refStmtType.typeName) { - mismatch = true; - } - } - } else if initType != annotKind { - let numericOk: bool = Sema_IsStrictNumeric(initType) && Sema_IsStrictNumeric(annotKind); - let boolOk: bool = Sema_IsBool(initType) && Sema_IsBool(annotKind); - if !numericOk && !boolOk { - mismatch = true; - } - } - if mismatch { - let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType); - let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, annotKind); - let msg: String = String_Concat("cannot assign ", - String_Concat(gotName, String_Concat(" to ", wantName))); - Sema_EmitError(sema, stmt.line, stmt.column, msg); - } + if !Sema_CanAssignTo(sema, initType, stmt.child1.refType, stmt.refStmtType) { + let gotName: String = Sema_TypeNameForDiag(stmt.child1.refType, initType); + let wantName: String = Sema_TypeNameForDiag(stmt.refStmtType, Sema_ResolveType(sema, stmt.refStmtType)); + let msg: String = String_Concat("cannot assign ", + String_Concat(gotName, String_Concat(" to ", wantName))); + Sema_EmitError(sema, stmt.line, stmt.column, msg); } } sym.isMutable = stmt.boolValue; From 869fb9c7d3defc2d70d9a1371096c25b8f7280de Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 28 Jul 2026 22:01:49 +0300 Subject: [PATCH 2/4] =?UTF-8?q?feat(selfhost-cli):=20surface=20parser=20di?= =?UTF-8?q?agnostics=20=E2=80=94=20syntax=20errors=20no=20longer=20pass=20?= =?UTF-8?q?check=20silently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- _test_parse_recovery/bux.toml | 4 ++++ _test_parse_recovery/src/Main.bux | 18 +++++++++++++++ src/ast.bux | 12 ++++++++++ src/cli.bux | 34 +++++++++++++++++++++++++++ src/parser.bux | 38 +++---------------------------- 5 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 _test_parse_recovery/bux.toml create mode 100644 _test_parse_recovery/src/Main.bux diff --git a/_test_parse_recovery/bux.toml b/_test_parse_recovery/bux.toml new file mode 100644 index 0000000..7d66b12 --- /dev/null +++ b/_test_parse_recovery/bux.toml @@ -0,0 +1,4 @@ +[Package] +Name = "parse_recovery" +Version = "0.1.0" +Type = "bin" diff --git a/_test_parse_recovery/src/Main.bux b/_test_parse_recovery/src/Main.bux new file mode 100644 index 0000000..916ab77 --- /dev/null +++ b/_test_parse_recovery/src/Main.bux @@ -0,0 +1,18 @@ +func Good() -> int { + return 1; +} + +func Bad() -> int { + let x: int = ; + return 0; +} + +func AlsoBad() -> int { + let y: int = 5 + ; + let z: int = 6; + return y; +} + +func AlsoGood() -> int { + return 2; +} diff --git a/src/ast.bux b/src/ast.bux index 6d8dea8..524416f 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -373,6 +373,16 @@ module Ast { // Struct fields (up to 256) } + // --------------------------------------------------------------------------- + // Parser diagnostic (recoverable errors collected during parsing) + // --------------------------------------------------------------------------- + struct ParserDiag { + line: uint32, + column: uint32, + message: String, + severity: int, /* 0=error (fatal), 1=warning (recoverable) */ + } + // --------------------------------------------------------------------------- // Module — AST root // --------------------------------------------------------------------------- @@ -381,6 +391,8 @@ module Ast { path: String, // path segments joined itemCount: int, firstItem: *Decl, + diagCount: int, + diags: *ParserDiag, } // --------------------------------------------------------------------------- diff --git a/src/cli.bux b/src/cli.bux index 23abff2..523852e 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -358,6 +358,28 @@ module Cli { // Import the compiler pipeline // In self-hosting mode, these are compiled together from src/ +// --------------------------------------------------------------------------- +// Report parser diagnostics attached to a module (Rust-style, with snippets). +// Returns the number of diagnostics reported (0 = clean). +// --------------------------------------------------------------------------- + +func Cli_ReportParseDiags(mod: *Module, sourceName: String) -> int { + if mod == null as *Module { return 0; } + if mod.diagCount == 0 { return 0; } + var i: int = 0; + while i < mod.diagCount { + let diag: Diagnostic = Diagnostic { + message: mod.diags[i].message, + line: mod.diags[i].line, + column: mod.diags[i].column, + severity: 0, + }; + Diagnostic_Print(&diag, sourceName); + i = i + 1; + } + return mod.diagCount; +} + // --------------------------------------------------------------------------- // Compile a single .bux source file // --------------------------------------------------------------------------- @@ -389,6 +411,7 @@ func Cli_Compile(source: String, sourceName: String, targetTriple: String) -> St PrintLine("Parse failed"); return ""; } + if Cli_ReportParseDiags(mod, sourceName) > 0 { return ""; } PrintLine(" Parse done"); // Flatten module wrappers: find module decl and hoist its children @@ -551,6 +574,7 @@ func Cli_Check(srcPath: String) -> int { PrintLine("Parse failed"); return 1; } + if Cli_ReportParseDiags(mod, srcPath) > 0 { return 1; } PrintLine(" Parse done"); // Flatten module wrappers @@ -635,6 +659,7 @@ func Cli_CompileSource(source: String, sourceName: String) -> *HirModule { PrintLine(sourceName); return null as *HirModule; } + if Cli_ReportParseDiags(mod, sourceName) > 0 { return null as *HirModule; } // Phase 2b: macro expand let macEx3: *MacroExpander = MacroExpand_ExpandModule(mod); @@ -836,6 +861,10 @@ func Cli_MergeFileInto(target: *Module, path: String, skipNames: *String, skipCo if Lexer_DiagCount(lex) > 0 { return 0; } let mod: *Module = Parser_Parse(lex.tokens, lex.tokenCount); if mod == null as *Module { return 0; } + // Report parse diags but keep merging the recovered decls: this path + // handles stdlib/dependency files, and the parser currently emits + // recoverable diags for valid multi-line braced imports there. + discard Cli_ReportParseDiags(mod, path); // Tag every decl from this file for #line maps var stamp: *Decl = mod.firstItem; while stamp != null as *Decl { @@ -1928,6 +1957,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool, userMerged.path = ""; userMerged.itemCount = 0; userMerged.firstItem = null as *Decl; + userMerged.diagCount = 0; + userMerged.diags = null as *ParserDiag; // Parse each file and merge declarations into userMerged module var i: int = 0; @@ -1962,6 +1993,7 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool, PrintLine(path); return 1; } + if Cli_ReportParseDiags(mod, path) > 0 { return 1; } // Tag decls with this source path for multi-file #line var stampUser: *Decl = mod.firstItem; while stampUser != null as *Decl { @@ -2031,6 +2063,8 @@ func Cli_BuildProject(projectDir: String, targetTriple: String, isRelease: bool, merged.path = ""; merged.itemCount = 0; merged.firstItem = null as *Decl; + merged.diagCount = 0; + merged.diags = null as *ParserDiag; // Find and merge ALL stdlib declarations let stdlibDir: String = Cli_FindStdlibDir(projectDir); diff --git a/src/parser.bux b/src/parser.bux index 84a0ad4..b5158c6 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -30,13 +30,6 @@ module Parser { macroTemplateMode: bool, // allows $(…)* in macro! bodies } - struct ParserDiag { - line: uint32, - column: uint32, - message: String, - severity: int, /* 0=error (fatal), 1=warning (recoverable) */ - } - // --------------------------------------------------------------------------- // Token helpers // --------------------------------------------------------------------------- @@ -2827,34 +2820,9 @@ module Parser { } } - /* Print fatal parser diagnostics (severity == 0) or if nothing valid was parsed */ - if p.diagCount > 0 && mod.itemCount == 0 { - var di: int = 0; - while di < p.diagCount { - let d: ParserDiag = p.diags[di]; - Print("error: "); - PrintLine(d.message); - Print(" --> :"); - PrintInt(d.line as int64); - Print(":"); - PrintInt(d.column as int64); - PrintLine(""); - Print(" |"); - PrintLine(""); - Print(" "); - PrintInt(d.line as int64); - Print(" | "); - PrintLine(""); - Print(" | "); - var sp: uint32 = 0; - while sp < d.column - 1 && sp < 120 { - Print(" "); - sp = sp + 1; - } - PrintLine("^"); - di = di + 1; - } - } + // Attach parser diagnostics to the module; the CLI reports them + mod.diagCount = p.diagCount; + mod.diags = p.diags; return mod; } From a538e050419d8f4275b2c3dfbf49c05538996c31 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 28 Jul 2026 22:20:17 +0300 Subject: [PATCH 3/4] fix(selfhost-parser): accept comma-separated struct fields, multi-line import braces, glob imports - struct fields: ',' is a valid separator, incl. trailing comma before '}' - imports: stop path scan before '::*' (glob) and allow newlines before the closing '}' of braced multi-imports - interfaces: skip ';' after bodyless method signatures - null-guard all diag emitters (interp-fragment sub-parser has diags=null) --- src/parser.bux | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/parser.bux b/src/parser.bux index b5158c6..8a79311 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -113,7 +113,7 @@ module Parser { return parserAdvance(p); } let tok: LexToken = parserCurToken(p); - if p.diagCount < 256 { + if p.diagCount < 256 && p.diags != null as *ParserDiag { p.diags[p.diagCount] = ParserDiag { line: tok.line, column: tok.column, message: msg, severity: 1 }; @@ -123,7 +123,7 @@ module Parser { } func parserEmitDiag(p: *Parser, line: uint32, col: uint32, msg: String) { - if p.diagCount < 256 { + if p.diagCount < 256 && p.diags != null as *ParserDiag { p.diags[p.diagCount] = ParserDiag { line: line, column: col, message: msg, severity: 1 }; @@ -142,7 +142,7 @@ module Parser { if tok.kind == tkIdent || parserIsKeyword(tok.kind) { return parserAdvance(p); } - if p.diagCount < 256 { + if p.diagCount < 256 && p.diags != null as *ParserDiag { p.diags[p.diagCount] = ParserDiag { line: tok.line, column: tok.column, message: msg, severity: 1 }; @@ -2175,6 +2175,8 @@ module Parser { } if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; } if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; } + // Commas are also valid field separators (incl. trailing comma before '}') + if parserCheck(p, tkComma) { discard parserAdvance(p); continue; } let beforePos: int = p.pos; let fName: LexToken = parserExpectIdentOrKeyword(p, "expected field name"); if fieldCount >= 250 && fieldCount <= 256 { @@ -2274,9 +2276,10 @@ module Parser { d.isPublic = isPublic; // Parse path: Std::Io::PrintLine + // Stop before `::{` (multi-import) and `::*` (glob import) var pathStr: String = ""; var segCount: int = 0; - while parserCheck(p, tkIdent) || (segCount > 0 && parserCheck(p, tkColonColon) && parserPeek(p, 1) != tkLBrace) { + while parserCheck(p, tkIdent) || (segCount > 0 && parserCheck(p, tkColonColon) && parserPeek(p, 1) != tkLBrace && parserPeek(p, 1) != tkStar) { if segCount > 0 { discard parserAdvance(p); // :: if String_Len(pathStr) > 0 { @@ -2308,6 +2311,10 @@ module Parser { names = String_Concat(names, ","); if !parserMatch(p, tkComma) { break; } } + // Allow newlines before the closing brace (multi-line import lists) + while parserCheck(p, tkNewLine) { + discard parserAdvance(p); + } discard parserExpect(p, tkRBrace, "expected '}'"); d.useNames = names; d.useKind = 2; // ukMulti @@ -2366,6 +2373,8 @@ module Parser { var lastMethod: *Decl = null as *Decl; while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile { if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; } + // Bodyless method signatures end with ';' + if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; } if parserCheck(p, tkFunc) { let m: *Decl = parserParseFuncDecl(p, false, false, false); if methods == null as *Decl { From 4f583eaec1769d7cc2c605a0d8a989628c5cad4f Mon Sep 17 00:00:00 2001 From: dimgigov Date: Tue, 28 Jul 2026 22:32:20 +0300 Subject: [PATCH 4/4] feat(selfhost-diag): struct-literal refType, unknown named args, comma leniency, debug cleanup --- src/parser.bux | 16 ++++++++++------ src/sema.bux | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/parser.bux b/src/parser.bux index 8a79311..2e540b3 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -2168,6 +2168,7 @@ module Parser { parserParseTypeParams(p, d); discard parserExpect(p, tkLBrace, "expected '{'"); + var lastWasComma: bool = false; while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile { if fieldCount >= 256 { parserEmitDiag(p, line, col, "too many struct fields (max 255)"); @@ -2176,14 +2177,16 @@ module Parser { if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; } if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; } // Commas are also valid field separators (incl. trailing comma before '}') - if parserCheck(p, tkComma) { discard parserAdvance(p); continue; } + if parserCheck(p, tkComma) { + if fieldCount == 0 || lastWasComma { + parserEmitDiag(p, parserCurToken(p).line, parserCurToken(p).column, "expected field name"); + } + lastWasComma = true; + discard parserAdvance(p); + continue; + } let beforePos: int = p.pos; let fName: LexToken = parserExpectIdentOrKeyword(p, "expected field name"); - if fieldCount >= 250 && fieldCount <= 256 { - PrintLine(String_Concat("RAW fieldCount=", bux_int_to_str(fieldCount as int64))); - PrintLine(String_Concat("RAW fName=", fName.text)); - PrintLine(String_Concat("RAW pos=", bux_int_to_str(p.pos as int64))); - } discard parserExpect(p, tkColon, "expected ':' in struct field"); let fType: *TypeExpr = parserParseType(p); parserMatch(p, tkSemicolon); @@ -2196,6 +2199,7 @@ module Parser { d.fields[fieldCount].name = fName.text; d.fields[fieldCount].refFieldType = fType; fieldCount = fieldCount + 1; + lastWasComma = false; } d.fieldCount = fieldCount; discard parserExpect(p, tkRBrace, "expected '}'"); diff --git a/src/sema.bux b/src/sema.bux index a0cdfff..cb45548 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -360,6 +360,34 @@ module Sema { i = i + 1; } + // Report named args that match no parameter + var checkArg: *ExprList = expr.callArgs; + while checkArg != null as *ExprList { + if !String_Eq(checkArg.argName, "") { + var foundName: bool = false; + var j: int = 0; + while j < decl.paramCount { + var pj: *Param = null as *Param; + if j == 0 { pj = &decl.param0; } + else if j == 1 { pj = &decl.param1; } + else if j == 2 { pj = &decl.param2; } + else if j == 3 { pj = &decl.param3; } + else if j == 4 { pj = &decl.param4; } + else if j == 5 { pj = &decl.param5; } + else if j == 6 { pj = &decl.param6; } + else if j == 7 { pj = &decl.param7; } + else if j == 8 { pj = &decl.param8; } + if String_Eq(checkArg.argName, pj.name) { foundName = true; } + j = j + 1; + } + if !foundName { + Sema_EmitError(sema, expr.line, expr.column, + String_Concat("unknown argument name '", String_Concat(checkArg.argName, "'"))); + } + } + checkArg = checkArg.next; + } + expr.callArgs = newFirst; expr.callArgCount = newCount; } @@ -1227,6 +1255,10 @@ module Sema { // Struct init: TypeName { field: value, ... } if kind == ekStructInit { + let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te.kind = tekNamed; + te.typeName = expr.structName; + expr.refType = te; return tyNamed; }