diff --git a/Makefile b/Makefile index 329b813..6f287e8 100644 --- a/Makefile +++ b/Makefile @@ -3,9 +3,9 @@ SRC := bootstrap/main.nim OUT := buxc BUILD_DIR := build -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure -.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden selfhost-loop lsp +.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp all: build @@ -19,7 +19,7 @@ dev: debug: dev @echo "Debug binary: buxc_debug" -test: build test-examples +test: build test-examples test-errors @echo "Running lexer tests..." $(NIM) c -r tests/lexer_test.nim @echo "Running parser tests..." @@ -100,6 +100,11 @@ test-golden: build echo "Golden tests: $$passed passed, $$failed failed"; \ if [ $$failed -gt 0 ]; then exit 1; fi +test-errors: build + @echo "=== Error diagnostic golden tests ===" + @chmod +x tests/error_golden/run.sh + @tests/error_golden/run.sh ./$(OUT) + selfhost-loop: build @echo "=== Selfhost loop: bootstrap determinism check ===" @echo "Build A..." diff --git a/bootstrap/c_backend.nim b/bootstrap/c_backend.nim index 36af74a..98584b3 100644 --- a/bootstrap/c_backend.nim +++ b/bootstrap/c_backend.nim @@ -128,8 +128,31 @@ proc typeToC*(be: var CBackend, typ: Type): string = of "float64": return "double" of "bool": return "bool" else: return typ.name - of tkTuple: return "void*" # TODO: proper tuple struct - of tkFunc: return "void*" # TODO: function pointer + of tkTuple: + if typ.inner.len == 0: + return "Tuple_Empty" + var parts: seq[string] = @[] + for e in typ.inner: + var p = typeToC(be, e) + p = p.replace("const char*", "cstr").replace("unsigned int", "uint") + p = p.replace(" ", "_").replace("*", "Ptr").replace("(", "").replace(")", "").replace(",", "_") + parts.add(p) + let tname = "Tuple_" & parts.join("_") + # Ensure typedef is collected alongside slices + var already = false + for d in be.sliceTypeDefs: + if d.name == tname: + already = true + break + if not already: + # Reuse sliceTypeDefs as a generic "extra typedef" bag: elem holds field list markup + be.sliceTypeDefs.add((name: tname, elem: "/*tuple*/")) + return tname + of tkFunc: + if typ.inner.len == 0: return "void (*)(void)" + let params = typ.inner[0..^2].mapIt(typeToC(be, it)).join(", ") + let ret = typeToC(be, typ.inner[^1]) + return ret & " (*)(" & params & ")" else: when defined(release): return "void*" @@ -311,10 +334,11 @@ proc emitExpr(be: var CBackend, node: HirNode): string = return &"({base}).data[{idx}]" of hTupleInit: - var elems: seq[string] = @[] - for e in node.tupleInitElements: - elems.add(be.emitExpr(e)) - return &"{{{elems.join(\", \")}}}" + let typeName = typeToC(be, node.typ) + var fields: seq[string] = @[] + for i, e in node.tupleInitElements: + fields.add(&"._{i} = {be.emitExpr(e)}") + return &"(({typeName}){{{fields.join(\", \")}}})" of hCast: let operand = be.emitExpr(node.castOperand) diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index e4b1cb2..dbbd5f2 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -1,5 +1,6 @@ import std/[os, strutils, terminal, strformat, osproc, sets] import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend +import source_location type ColorMode* = enum @@ -89,6 +90,203 @@ proc printInfo(msg: string, useColor: bool) = else: echo("info: " & msg) +# --------------------------------------------------------------------------- +# Rust-style diagnostics (snippet + optional help hint) +# --------------------------------------------------------------------------- + +proc getSourceLine(path: string, lineNum: uint32): string = + ## Read a single 1-based line from path. Empty if unavailable. + if path.len == 0 or lineNum == 0 or not fileExists(path): + return "" + try: + let content = readFile(path) + var n: uint32 = 1 + for line in content.splitLines(): + if n == lineNum: + return line + inc n + except CatchableError: + discard + return "" + +proc extractQuotedName(msg: string): string = + ## Pull the first 'name' from messages like: undeclared identifier 'foo' + let a = msg.find('\'') + if a < 0: return "" + let b = msg.find('\'', a + 1) + if b <= a + 1: return "" + return msg[a + 1 .. b - 1] + +proc underlineLength(lineText: string, col: uint32, message: string): int = + ## Multi-character underline under the token at `col` (1-based). + ## Falls back to scanning a source token, or matching a quoted name in the message. + if lineText.len == 0: + return 1 + let start = if col > 0: int(col) - 1 else: 0 + if start < 0 or start >= lineText.len: + return 1 + + # Prefer highlighting the quoted identifier/token from the message when it + # appears on this line (e.g. undeclared identifier 'foo'). + let quoted = extractQuotedName(message) + if quoted.len > 0: + let idx = lineText.find(quoted) + if idx >= 0: + # If caret is on/near that token, use its full length + if abs(idx - start) <= quoted.len: + return quoted.len + + let c0 = lineText[start] + # String / char / backtick literals + if c0 == '"' or c0 == '\'' or c0 == '`': + let quote = c0 + var i = start + 1 + while i < lineText.len: + if lineText[i] == '\\' and i + 1 < lineText.len: + i += 2 + continue + if lineText[i] == quote: + return i - start + 1 + inc i + return max(1, lineText.len - start) + + # Identifier or keyword + if c0.isAlphaAscii or c0 == '_': + var i = start + while i < lineText.len and (lineText[i].isAlphaNumeric or lineText[i] == '_'): + inc i + return max(1, i - start) + + # Number literal + if c0.isDigit: + var i = start + while i < lineText.len and (lineText[i].isDigit or lineText[i] in {'.', 'x', 'X', 'b', 'B', 'o', 'O', 'a'..'f', 'A'..'F', '_'}): + inc i + # optional type suffix: 42i64, 1.0f + while i < lineText.len and lineText[i] in {'i', 'u', 'f', 'I', 'U', 'F', '0'..'9'}: + inc i + return max(1, i - start) + + # Multi-char operators starting at caret + const multiOps = ["<<=", ">>=", "**", "++", "--", "==", "!=", "<=", ">=", + "&&", "||", "<<", ">>", "+=", "-=", "*=", "/=", "%=", + "&=", "|=", "^=", "=>", "..", "->"] + for op in multiOps: + if start + op.len <= lineText.len and lineText[start .. start + op.len - 1] == op: + return op.len + + return 1 + +proc hintForMessage(msg: string): string = + ## Actionable help text for common compiler errors. + let m = msg.toLowerAscii() + if "cannot assign" in m: + return "ensure the right-hand side type matches the left-hand side" + if "undeclared identifier" in m: + return "check the spelling, or import the symbol from the right module" + if "too few arguments" in m or "too many arguments" in m: + return "compare the call with the function's parameter list" + if "missing argument for parameter" in m: + return "provide the missing argument (positional or named)" + if "use of moved value" in m: + return "the value was moved; clone it or restructure ownership" + if "shared reference" in m or "checked function" in m: + return "use '&mut T' for mutation, or drop @[Checked] for unchecked code" + if "double mutable borrow" in m or "already mutably borrowed" in m: + return "only one active '&mut' borrow is allowed at a time" + if "expected expression" in m: + return "the previous statement may be incomplete (missing value or ';')" + if "expected type" in m: + return "write a type name such as 'int', 'String', or 'Array'" + if "expected field name" in m: + return "after '.' use an identifier or a tuple index (.0, .1, ...)" + if "does not implement trait" in m: + return "add an 'extend Type for Trait { ... }' block, or pick another type" + if "duplicate symbol" in m: + return "rename one of the definitions or remove the duplicate" + if "unterminated" in m: + return "check for a missing closing quote, backtick, or comment delimiter" + return "" + +proc printDiagnostic*(severity: string, message: string, loc: SourceLocation, + useColor: bool, fallbackFile: string = "") = + ## Print a Rust-style diagnostic: + ## error: message + ## --> file:line:col + ## | + ## 42 | source line + ## | ^ + ## = help: hint + let isError = severity == "error" + if useColor: + stdout.setForegroundColor(if isError: fgRed else: fgYellow) + stdout.write(severity & ": ") + stdout.resetAttributes() + stdout.writeLine(message) + else: + let stream = if isError: stderr else: stdout + stream.writeLine(severity & ": " & message) + + let file = if loc.file.len > 0: loc.file else: fallbackFile + if loc.line > 0: + let locStr = if file.len > 0: + &"{file}:{loc.line}:{loc.column}" + else: + &"{loc.line}:{loc.column}" + if useColor: + stdout.setForegroundColor(fgCyan) + stdout.write(" --> ") + stdout.resetAttributes() + stdout.writeLine(locStr) + else: + stderr.writeLine(" --> " & locStr) + + let lineText = getSourceLine(file, loc.line) + if lineText.len > 0: + let gutter = $loc.line + let pad = " ".repeat(max(gutter.len, 3)) + # If message names a quoted token, prefer caret at that token's start + var col = loc.column + let quoted = extractQuotedName(message) + if quoted.len > 0: + let idx = lineText.find(quoted) + if idx >= 0: + col = uint32(idx + 1) + let ulen = underlineLength(lineText, col, message) + stdout.writeLine(pad & " |") + stdout.writeLine(" " & gutter & " | " & lineText) + # Multi-char underline under the token (1-based column) + var caretPad = "" + if col > 0: + caretPad = " ".repeat(int(col) - 1) + let marks = "^".repeat(max(1, ulen)) + stdout.writeLine(pad & " | " & caretPad & marks) + + let hint = hintForMessage(message) + if hint.len > 0: + if useColor: + stdout.setForegroundColor(fgGreen) + stdout.write(" = help: ") + stdout.resetAttributes() + stdout.writeLine(hint) + else: + stdout.writeLine(" = help: " & hint) + +proc printLexerDiags(diags: seq[LexerDiagnostic], useColor: bool, fallbackFile = "") = + for d in diags: + let sev = if d.severity == ldsError: "error" else: "warning" + printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile) + +proc printParserDiags(diags: seq[ParserDiagnostic], useColor: bool, fallbackFile = "") = + for d in diags: + let sev = if d.severity == pdsError: "error" else: "warning" + printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile) + +proc printSemaDiags(diags: seq[SemaDiagnostic], useColor: bool, fallbackFile = "") = + for d in diags: + let sev = if d.severity == sdsError: "error" else: "warning" + printDiagnostic(sev, d.message, d.loc, useColor, fallbackFile) + # --------------------------------------------------------------------------- # Commands # --------------------------------------------------------------------------- @@ -304,14 +502,12 @@ proc prepareProject(root: string, useColor: bool, opts: GlobalOptions): (Project let lexRes = tokenize(source, path) if lexRes.hasErrors: printError(&"lex errors in {path}", useColor) - for d in lexRes.diagnostics: - echo $d + printLexerDiags(lexRes.diagnostics, useColor, path) return (pctx, 1) let parseRes = parse(lexRes.tokens, path) if parseRes.diagnostics.len > 0: printError(&"parse errors in {path}", useColor) - for d in parseRes.diagnostics: - echo &"error: {d.message} at {d.loc}" + printParserDiags(parseRes.diagnostics, useColor, path) return (pctx, 1) for decl in parseRes.module.items: if decl.kind == dkModule: @@ -345,9 +541,7 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int = let semaRes = analyze(unifiedModule) if semaRes.hasErrors: printError("type errors in project", useColor) - for d in semaRes.diagnostics: - let sev = if d.severity == sdsError: "error" else: "warning" - echo &"{sev}: {d.message} at {d.loc}" + printSemaDiags(semaRes.diagnostics, useColor) return 1 if not opts.quiet: printInfo("check passed", useColor) @@ -448,9 +642,7 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = let (semaRes, semaCtx) = analyzeFull(unifiedModule) if semaRes.hasErrors: printError("type errors in project", useColor) - for d in semaRes.diagnostics: - let sev = if d.severity == sdsError: "error" else: "warning" - echo &"{sev}: {d.message} at {d.loc}" + printSemaDiags(semaRes.diagnostics, useColor) return 1 let hirMod = lowerModule(unifiedModule, semaCtx) diff --git a/bootstrap/hir.nim b/bootstrap/hir.nim index f55884e..e3fe86d 100644 --- a/bootstrap/hir.nim +++ b/bootstrap/hir.nim @@ -189,6 +189,10 @@ type consts*: seq[tuple[name: string, typ: Type, value: HirNode]] interfaces*: seq[tuple[name: string, hasAssocTypes: bool, methods: seq[tuple[name: string, params: seq[Type], ret: Type]]]] vtables*: seq[tuple[interfaceName: string, concreteType: string, methodNames: seq[string], hasAssocTypes: bool]] + ## Named functions used as values → need __adapt_ wrappers for fat-func ABI + funcAdapters*: seq[tuple[name: string, typ: Type]] + ## Extra func types seen in locals/closures that need BuxFn_* typedefs + seenFatTypes*: seq[Type] # Constructor helpers proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode = diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 7f62e41..6e567b6 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -1,4 +1,4 @@ -import std/[tables, sets, strutils] +import std/[tables, sets, strutils, strformat] import ast, types, token, source_location, hir, sema, scope type @@ -25,6 +25,11 @@ type closureDepth*: int currentClosureExpr*: Expr envInstanceName*: string + ## Named functions that must be wrapped as fat func values (multi-instance ABI) + funcAdapters*: HashSet[string] + funcAdapterSigs*: Table[string, Type] + ## All func types that need BuxFn_* typedefs (including locals) + seenFatTypes*: seq[Type] proc freshName(ctx: var LowerCtx): string = inc ctx.varCounter @@ -159,6 +164,50 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.generatedFuncInsts = initTable[string, bool]() result.extraFuncs = @[] result.varTypeExprs = initTable[string, TypeExpr]() + result.funcAdapters = initHashSet[string]() + result.funcAdapterSigs = initTable[string, Type]() + result.seenFatTypes = @[] + +proc sanitizeFatPart(s: string): string = + result = s.replace("const char*", "cstr").replace("unsigned int", "uint") + result = result.replace(" ", "_").replace("*", "Ptr").replace("(", "").replace(")", "").replace(",", "_").replace(".", "_") + +proc typeNameForFat(typ: Type): string +proc hirFuncFatTypeName*(typ: Type): string + +proc typeNameForFat(typ: Type): string = + ## Lightweight C-ish name for fat-func mangling (mirrors lir typeToCStr subset). + if typ == nil: return "void" + case typ.kind + of tkVoid: return "void" + of tkBool, tkBool8, tkBool16, tkBool32: return "bool" + of tkStr: return "cstr" + of tkInt, tkInt8, tkInt16, tkInt32, tkInt64: return "int" + of tkUInt, tkUInt8, tkUInt16, tkUInt32, tkUInt64: return "uint" + of tkFloat32: return "float" + of tkFloat64: return "double" + of tkPointer, tkRef, tkMutRef: + if typ.inner.len > 0: return sanitizeFatPart(typeNameForFat(typ.inner[0]) & "Ptr") + return "voidPtr" + of tkNamed: + case typ.name + of "String", "str": return "cstr" + else: return sanitizeFatPart(typ.name) + of tkFunc: + return hirFuncFatTypeName(typ) + else: + return "int" + +proc hirFuncFatTypeName*(typ: Type): string = + if typ == nil or typ.kind != tkFunc: return "BuxFn_void" + let ret = if typ.inner.len > 0: typeNameForFat(typ.inner[^1]) else: "void" + var parts: seq[string] = @[sanitizeFatPart(ret)] + if typ.inner.len > 1: + for p in typ.inner[0 ..^ 2]: + parts.add(sanitizeFatPart(typeNameForFat(p))) + else: + parts.add("void") + return "BuxFn_" & parts.join("_") proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type @@ -291,7 +340,14 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type = of tekOwn: return ctx.resolveTypeExpr(te.pointerPointee) of tekDynRef: return makeDynRef(te.dynInterface) of tekPointer: return makePointer(ctx.resolveTypeExpr(te.pointerPointee)) + of tekRef: return makeRef(ctx.resolveTypeExpr(te.pointerPointee)) + of tekMutRef: return makeMutRef(ctx.resolveTypeExpr(te.pointerPointee)) of tekSlice: return makeSlice(ctx.resolveTypeExpr(te.sliceElement)) + of tekTuple: + var elems: seq[Type] = @[] + for e in te.tupleElements: + elems.add(ctx.resolveTypeExpr(e)) + return makeTuple(elems) of tekFunc: var params: seq[Type] = @[] for p in te.funcParams: @@ -523,6 +579,18 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type = return makeVoid() of ekBorrow: return ctx.resolveExprType(expr.exprBorrowOperand) + of ekClosure: + var params: seq[Type] = @[] + for p in expr.exprClosureParams: + if p.ptype != nil: + params.add(ctx.resolveTypeExpr(p.ptype)) + else: + params.add(makeUnknown()) + let ret = if expr.exprClosureReturnType != nil: + ctx.resolveTypeExpr(expr.exprClosureReturnType) + else: + makeVoid() + return makeFunc(params, ret) else: return makeUnknown() proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] = @@ -742,9 +810,26 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = let capType = if idx < ctx.currentClosureExpr.captureTypeKinds.len: Type(kind: TypeKind(ctx.currentClosureExpr.captureTypeKinds[idx])) else: makeInt() let base = hirVar(ctx.envInstanceName, makeNamed(""), loc) return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc) + var resolvedName = name if ctx.importTable.hasKey(name): - return hirVar(ctx.importTable[name], typ, loc) - return hirVar(name, typ, loc) + resolvedName = ctx.importTable[name] + # Named function used as a value → fat function pointer via adapter + if typ != nil and typ.kind == tkFunc: + let sym = ctx.globalScope.lookup(name) + let sym2 = if sym == nil: ctx.globalScope.lookup(resolvedName) else: sym + if sym2 != nil and sym2.kind == skFunc: + let adaptName = "__adapt_" & resolvedName + ctx.funcAdapters.incl(resolvedName) + ctx.funcAdapterSigs[resolvedName] = typ + let fatName = hirFuncFatTypeName(typ) + let nullEnv = HirNode(kind: hCast, + castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc), + castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc) + return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[ + (name: "code", value: hirVar(adaptName, makePointer(makeVoid()), loc)), + (name: "env", value: nullEnv) + ], typ: typ, loc: loc) + return hirVar(resolvedName, typ, loc) of ekPath: # Handle enum variants: Color::Red → Color_Red @@ -756,6 +841,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = return hirSelf(typ, loc) of ekUnary: + # &NamedFunc used as func value → fat adapter (not a raw C function pointer) + if expr.exprUnaryOp == tkAmp and expr.exprUnaryOperand != nil and + expr.exprUnaryOperand.kind == ekIdent: + let fname = expr.exprUnaryOperand.exprIdent + var resolved = fname + if ctx.importTable.hasKey(fname): + resolved = ctx.importTable[fname] + let sym = ctx.globalScope.lookup(resolved) + if sym != nil and sym.kind == skFunc: + # Prefer declared func type on the symbol; fall back to expression type + var ftyp = if sym.typ != nil and sym.typ.kind == tkFunc: sym.typ else: typ + if ftyp == nil or ftyp.kind != tkFunc: + ftyp = ctx.resolveExprType(expr.exprUnaryOperand) + if ftyp != nil and ftyp.kind == tkFunc: + let adaptName = "__adapt_" & resolved + ctx.funcAdapters.incl(resolved) + ctx.funcAdapterSigs[resolved] = ftyp + ctx.seenFatTypes.add(ftyp) + let fatName = hirFuncFatTypeName(ftyp) + let nullEnv = HirNode(kind: hCast, + castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc), + castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc) + return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[ + (name: "code", value: hirVar(adaptName, makePointer(makeVoid()), loc)), + (name: "env", value: nullEnv) + ], typ: ftyp, loc: loc) let operand = ctx.lowerExpr(expr.exprUnaryOperand) return hirUnary(expr.exprUnaryOp, operand, typ, loc) @@ -883,6 +994,16 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = calleeName = expr.exprCallCallee.exprPath.join("_") let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) if calleeName != "": + # Named global function → direct call + let sym = ctx.globalScope.lookup(calleeName) + if sym != nil and sym.kind == skFunc: + return hirCall(calleeName, args, typ, loc) + # Variable holding a fat function pointer → indirect call + let ct = ctx.resolveExprType(expr.exprCallCallee) + if ct != nil and ct.kind == tkFunc: + let callee = hirVar(calleeName, ct, loc) + return HirNode(kind: hCallIndirect, callIndirectCallee: callee, + callIndirectArgs: args, typ: typ, loc: loc) return hirCall(calleeName, args, typ, loc) else: let callee = ctx.lowerExpr(expr.exprCallCallee) @@ -1236,7 +1357,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = of ekClosure: let f = ctx.lowerClosureFunc(expr) - return hirUnary(tkAmp, hirVar(f.name, makeFunc(@[], makeVoid()), loc), typ, loc) + if typ != nil and typ.kind == tkFunc: + ctx.seenFatTypes.add(typ) + let fatName = hirFuncFatTypeName(typ) + if expr.captureCount > 0 and f.envStructName.len > 0: + # Heap-allocate a fresh env so each closure value is independent + let envTmp = "__envp_" & $ctx.varCounter + inc ctx.varCounter + let fatTmp = "__fat_" & $ctx.varCounter + inc ctx.varCounter + var code = "" + code.add(&"{f.envStructName}* {envTmp} = ({f.envStructName}*)bux_alloc(sizeof({f.envStructName}));\n") + for i in 0 ..< expr.captureCount: + let capName = expr.captureNames[i] + code.add(&"{envTmp}->{capName} = {capName};\n") + code.add(&"{fatName} {fatTmp} = {{ .code = {f.name}, .env = {envTmp} }};") + ctx.pendingStmts.add(HirNode(kind: hEmit, emitCode: code, typ: makeVoid(), loc: loc)) + return hirVar(fatTmp, typ, loc) + else: + # Capture-less: fat pointer with NULL env + let nullEnv = HirNode(kind: hCast, + castOperand: hirLit(Token(kind: tkIntLiteral, text: "0", loc: loc), makeInt(), loc), + castType: makePointer(makeVoid()), typ: makePointer(makeVoid()), loc: loc) + return HirNode(kind: hStructInit, structInitName: fatName, structInitFields: @[ + (name: "code", value: hirVar(f.name, makePointer(makeVoid()), loc)), + (name: "env", value: nullEnv) + ], typ: typ, loc: loc) else: return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc), @@ -1255,28 +1401,14 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = if stmt.stmtLetInit != nil: initHir = ctx.lowerExpr(stmt.stmtLetInit) let allocaType = if stmt.stmtLetType != nil: - case stmt.stmtLetType.kind - of tekNamed: - ctx.resolveTypeExpr(stmt.stmtLetType) - of tekOwn: - ctx.resolveTypeExpr(stmt.stmtLetType.pointerPointee) - of tekPointer: - let pointeeType = ctx.resolveTypeExpr(stmt.stmtLetType.pointerPointee) - makePointer(pointeeType) - of tekSlice: - let elemType = ctx.resolveTypeExpr(stmt.stmtLetType.sliceElement) - makeSlice(elemType) - of tekFunc: - var params: seq[Type] = @[] - for p in stmt.stmtLetType.funcParams: - params.add(ctx.resolveTypeExpr(p)) - let ret = if stmt.stmtLetType.funcRet != nil: ctx.resolveTypeExpr(stmt.stmtLetType.funcRet) else: makeVoid() - makeFunc(params, ret) - else: makeUnknown() + # Full resolve covers named, pointer, slice, tuple, func, refs, etc. + ctx.resolveTypeExpr(stmt.stmtLetType) elif stmt.stmtLetInit != nil: ctx.resolveExprType(stmt.stmtLetInit) else: makeUnknown() + if allocaType != nil and allocaType.kind == tkFunc: + ctx.seenFatTypes.add(allocaType) let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc) let varNode = hirVar(stmt.stmtLetName, makePointer(allocaType), loc) @@ -1296,22 +1428,7 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = if initHir != nil: let store = hirStore(varNode, initHir, loc) stmts.add(store) - # If init is a closure with captures, emit capture assignments - if stmt.stmtLetInit != nil and stmt.stmtLetInit.kind == ekClosure and stmt.stmtLetInit.captureCount > 0: - let closureIdx = ctx.varCounter - 1 - let envInst = "__closure_env_instance_" & $closureIdx - var capStmts: seq[HirNode] = @[] - for i in 0 ..< stmt.stmtLetInit.captureCount: - let capName = stmt.stmtLetInit.captureNames[i] - let capType = if i < stmt.stmtLetInit.captureTypeKinds.len: Type(kind: TypeKind(stmt.stmtLetInit.captureTypeKinds[i])) else: makeInt() - let base = hirVar(envInst, makeNamed(""), loc) - let field = HirNode(kind: hFieldAccess, fieldAccessName: capName, fieldAccessBase: base, typ: capType, loc: loc) - let val = hirVar(capName, capType, loc) - capStmts.add(hirAssign(field, val, loc)) - # Prepend capture assignments before the let - var allStmts = capStmts - allStmts.add(stmts) - return hirBlock(allStmts, nil, makeVoid(), loc) + # Capture filling for closures is done at the ekClosure site (heap env). return hirBlock(stmts, nil, makeVoid(), loc) of skReturn: @@ -1723,6 +1840,8 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc = let name = "__closure_" & $ctx.varCounter inc ctx.varCounter var f = HirFunc(name: name, isPublic: false) + # Always take a leading env pointer (fat-func ABI); may be unused. + f.params.add((name: "__env", typ: makePointer(makeVoid()))) # Copy capture metadata if expr.captureCount > 0: f.captureNames = expr.captureNames @@ -1730,7 +1849,7 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc = f.captureTypes.add(Type(kind: TypeKind(tk))) f.envStructName = "__closure_env_" & $(ctx.varCounter - 1) f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1) - # Params + # User params for p in expr.exprClosureParams: f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown())) # Return type @@ -1935,4 +2054,8 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = if allFound: vtableInfos.add((ifaceName, typeName, methodNames, hasAssoc)) - result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts, interfaces: ifaceInfos, vtables: vtableInfos) + var adapters: seq[tuple[name: string, typ: Type]] = @[] + for name in ctx.funcAdapters: + let t = if ctx.funcAdapterSigs.hasKey(name): ctx.funcAdapterSigs[name] else: makeFunc(@[makeInt()], makeInt()) + adapters.add((name, t)) + result = HirModule(funcs: funcs, externFuncs: externFuncs, structs: structs, enums: enums, consts: consts, interfaces: ifaceInfos, vtables: vtableInfos, funcAdapters: adapters, seenFatTypes: ctx.seenFatTypes) diff --git a/bootstrap/lexer.nim b/bootstrap/lexer.nim index 727e0d4..8ba9b34 100644 --- a/bootstrap/lexer.nim +++ b/bootstrap/lexer.nim @@ -70,7 +70,8 @@ proc matchStr(lex: var Lexer, s: string): bool = return true proc currentLocation(lex: Lexer): SourceLocation = - result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos)) + result = SourceLocation(line: lex.line, column: lex.col, offset: uint32(lex.pos), + file: lex.sourceName) proc emitError(lex: var Lexer, loc: SourceLocation, message: string) = lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message)) diff --git a/bootstrap/lir_c_backend.nim b/bootstrap/lir_c_backend.nim index 127f3df..b60ce9d 100644 --- a/bootstrap/lir_c_backend.nim +++ b/bootstrap/lir_c_backend.nim @@ -149,14 +149,22 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) = be.emitLine(&"{v(instr.src)}({argsStr});") of lirCallIndirect: + ## Fat function pointer call: f.code(f.env, args...) var argsStr = "" for i, arg in instr.extra: if i > 0: argsStr.add(", ") argsStr.add(v(arg)) + let callee = v(instr.src) if instr.dst.kind != lvkVoid: - be.emitLine(&"{v(instr.dst)} = ({v(instr.src)})({argsStr});") + if argsStr.len > 0: + be.emitLine(&"{v(instr.dst)} = ({callee}.code)({callee}.env, {argsStr});") + else: + be.emitLine(&"{v(instr.dst)} = ({callee}.code)({callee}.env);") else: - be.emitLine(&"({v(instr.src)})({argsStr});") + if argsStr.len > 0: + be.emitLine(&"({callee}.code)({callee}.env, {argsStr});") + else: + be.emitLine(&"({callee}.code)({callee}.env);") # ── Return ── of lirRet: @@ -348,6 +356,21 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin # ── Struct/Enum emission (from HIR module) ── +proc sanitizeCTypeNamePart(s: string): string = + result = s + result = result.replace("const char*", "cstr") + result = result.replace("unsigned int", "uint") + result = result.replace(" ", "_") + result = result.replace("*", "Ptr") + result = result.replace("(", "") + result = result.replace(")", "") + result = result.replace(",", "_") + result = result.replace(".", "_") + +proc typeToCStr(typ: Type): string +proc funcFatTypeName(typ: Type): string +proc funcCodePtrType(typ: Type): string + proc typeToCStr(typ: Type): string = ## Duplicate from lir_lower for self-containedness if typ == nil: return "int" @@ -396,13 +419,39 @@ proc typeToCStr(typ: Type): string = of "float64": return "double" of "bool": return "bool" else: return typ.name + of tkTuple: + if typ.inner.len == 0: + return "Tuple_Empty" + var parts: seq[string] = @[] + for e in typ.inner: + parts.add(sanitizeCTypeNamePart(typeToCStr(e))) + return "Tuple_" & parts.join("_") of tkFunc: - if typ.inner.len == 0: return "void (*)(void)" - let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ") - let ret = typeToCStr(typ.inner[^1]) - return ret & " (*)(" & params & ")" + return funcFatTypeName(typ) else: return "int" +proc funcFatTypeName(typ: Type): string = + if typ == nil or typ.kind != tkFunc: + return "BuxFn_void" + let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void" + var parts: seq[string] = @[sanitizeCTypeNamePart(ret)] + if typ.inner.len > 1: + for p in typ.inner[0 ..^ 2]: + parts.add(sanitizeCTypeNamePart(typeToCStr(p))) + else: + parts.add("void") + return "BuxFn_" & parts.join("_") + +proc funcCodePtrType(typ: Type): string = + if typ == nil or typ.kind != tkFunc: + return "void (*)(void*)" + let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void" + var params: seq[string] = @["void* env"] + if typ.inner.len > 1: + for p in typ.inner[0 ..^ 2]: + params.add(typeToCStr(p)) + return ret & " (*)(" & params.join(", ") & ")" + proc emitStructDef(be: var LirCBackend, name: string, fields: seq[tuple[name: string, typ: Type]]) = be.emitLine(&"typedef struct {name} {{") be.indent += 1 @@ -481,11 +530,36 @@ proc collectValueDeps(typ: Type): seq[string] = return @[typ.name] of tkSlice: return @[typeToCStr(typ)] - of tkPointer, tkRef, tkMutRef, tkTuple, tkFunc: + of tkTuple: + var deps: seq[string] = @[] + for e in typ.inner: + for d in collectValueDeps(e): + if d notin deps: + deps.add(d) + if e != nil and e.kind == tkTuple: + let tn = typeToCStr(e) + if tn notin deps: + deps.add(tn) + return deps + of tkPointer, tkRef, tkMutRef, tkFunc: return @[] else: return @[] +proc emitTupleDef(be: var LirCBackend, typ: Type) = + ## typedef struct { T0 _0; T1 _1; ... } Tuple_...; + let name = typeToCStr(typ) + be.emitLine(&"typedef struct {name} {{") + be.indent += 1 + if typ.inner.len == 0: + be.emitLine("char _pad;") + else: + for i, e in typ.inner: + be.emitLine(&"{typeToCStr(e)} _{i};") + be.indent -= 1 + be.emitLine(&"}} {name};") + be.emitLine("") + proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) = be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};") @@ -663,6 +737,104 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s elif sliceMap.hasKey(name): be.emitSliceTypeDef(name, sliceMap[name]) + # Collect and emit tuple typedefs used in the module (and nested tuples first). + var tupleTypes: seq[Type] = @[] + var tupleNames: HashSet[string] + proc registerTuple(t: Type) = + if t == nil: return + case t.kind + of tkTuple: + for e in t.inner: + registerTuple(e) + let name = typeToCStr(t) + if not tupleNames.contains(name): + tupleNames.incl(name) + tupleTypes.add(t) + of tkPointer, tkRef, tkMutRef, tkSlice: + if t.inner.len > 0: + registerTuple(t.inner[0]) + of tkFunc: + for e in t.inner: + registerTuple(e) + else: + discard + + for f in module.funcs: + registerTuple(f.retType) + for p in f.params: + registerTuple(p.typ) + for ef in module.externFuncs: + registerTuple(ef.retType) + for p in ef.params: + registerTuple(p.typ) + for s in module.structs: + for f in s.fields: + registerTuple(f.typ) + for e in module.enums: + for v in e.variants: + for ft in v.fields: + registerTuple(ft) + for nf in v.namedFields: + registerTuple(nf.typ) + + if tupleTypes.len > 0: + be.emitLine("/* Tuple types */") + for tt in tupleTypes: + be.emitTupleDef(tt) + + # Fat function-pointer typedefs (BuxFn_*) — before forward decls that use them + var fatTypes: seq[Type] = @[] + var fatNames: HashSet[string] + proc registerFat(t: Type) = + if t == nil: return + case t.kind + of tkFunc: + for e in t.inner: registerFat(e) + let n = funcFatTypeName(t) + if not fatNames.contains(n): + fatNames.incl(n) + fatTypes.add(t) + of tkPointer, tkRef, tkMutRef, tkSlice: + if t.inner.len > 0: registerFat(t.inner[0]) + of tkTuple: + for e in t.inner: registerFat(e) + else: discard + for f in module.funcs: + registerFat(f.retType) + for p in f.params: registerFat(p.typ) + for ef in module.externFuncs: + registerFat(ef.retType) + for p in ef.params: registerFat(p.typ) + for a in module.funcAdapters: + registerFat(a.typ) + for t in module.seenFatTypes: + registerFat(t) + if fatTypes.len > 0: + be.emitLine("/* Fat function pointer types (code + env) */") + for ft in fatTypes: + let n = funcFatTypeName(ft) + let codeT = funcCodePtrType(ft) + be.emitLine(&"typedef struct {n} {{") + be.indent += 1 + be.emitLine(cParamDecl(codeT, "code") & ";") + be.emitLine("void* env;") + be.indent -= 1 + be.emitLine(&"}} {n};") + be.emitLine("") + + # Env structs for closures with captures (heap-allocated per value) + for f in module.funcs: + if f.captureNames.len > 0 and f.envStructName != "": + be.emitLine(&"typedef struct {f.envStructName} {{") + be.indent += 1 + for i in 0 ..< f.captureNames.len: + let capName = f.captureNames[i] + let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int" + be.emitLine(&"{capType} {capName};") + be.indent -= 1 + be.emitLine(&"}} {f.envStructName};") + be.emitLine("") + # Forward function declarations for f in module.funcs: let rt = typeToCStr(f.retType) @@ -707,18 +879,28 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s be.emitLine("};") be.emitLine("") - # Emit env structs for closures with captures - for f in module.funcs: - if f.captureNames.len > 0 and f.envStructName != "": - be.emitLine(&"struct {f.envStructName} {{") + # Adapters for named functions used as fat-func values (after forward decls) + if module.funcAdapters.len > 0: + be.emitLine("/* Fat-func adapters for named functions */") + for a in module.funcAdapters: + let ret = if a.typ.inner.len > 0: typeToCStr(a.typ.inner[^1]) else: "void" + var params: seq[string] = @["void* env"] + var argNames: seq[string] = @[] + if a.typ.inner.len > 1: + for i, p in a.typ.inner[0 ..^ 2]: + let pn = "a" & $i + params.add(typeToCStr(p) & " " & pn) + argNames.add(pn) + let argsStr = argNames.join(", ") + be.emitLine(&"static {ret} __adapt_{a.name}({params.join(\", \")}) {{") be.indent += 1 - for i in 0 ..< f.captureNames.len: - let capName = f.captureNames[i] - let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int" - be.emitLine(&"{capType} {capName};") + be.emitLine("(void)env;") + if ret == "void": + be.emitLine(&"{a.name}({argsStr});") + else: + be.emitLine(&"return {a.name}({argsStr});") be.indent -= 1 - be.emitLine("};") - be.emitLine(&"static struct {f.envStructName} {f.envInstanceName};") + be.emitLine("}") be.emitLine("") # Emit all LIR functions diff --git a/bootstrap/lir_lower.nim b/bootstrap/lir_lower.nim index 1a495d4..e6592fb 100644 --- a/bootstrap/lir_lower.nim +++ b/bootstrap/lir_lower.nim @@ -57,6 +57,22 @@ proc cEscape(s: string): string = of '\0': result.add("\\0") else: result.add(c) +proc sanitizeCTypeNamePart(s: string): string = + ## Make a C type string safe for use inside a typedef name. + result = s + result = result.replace("const char*", "cstr") + result = result.replace("unsigned int", "uint") + result = result.replace(" ", "_") + result = result.replace("*", "Ptr") + result = result.replace("(", "") + result = result.replace(")", "") + result = result.replace(",", "_") + result = result.replace(".", "_") + +proc typeToCStr(typ: Type): string +proc funcFatTypeName*(typ: Type): string +proc funcCodePtrType*(typ: Type): string + proc typeToCStr(typ: Type): string = ## Convert a Bux Type to a C type string. if typ == nil: return "int" @@ -105,13 +121,44 @@ proc typeToCStr(typ: Type): string = of "float64": return "double" of "bool": return "bool" else: return typ.name + of tkTuple: + ## (T, U) → typedef struct { T _0; U _1; } Tuple_T_U; + if typ.inner.len == 0: + return "Tuple_Empty" + var parts: seq[string] = @[] + for e in typ.inner: + parts.add(sanitizeCTypeNamePart(typeToCStr(e))) + return "Tuple_" & parts.join("_") of tkFunc: - if typ.inner.len == 0: return "void (*)(void)" - let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ") - let ret = typeToCStr(typ.inner[^1]) - return ret & " (*)(" & params & ")" + ## Fat function pointer: { code(env, args...), env } + ## Enables multi-instance closures with captures. + return funcFatTypeName(typ) else: return "int" +proc funcFatTypeName*(typ: Type): string = + ## BuxFn__ (sanitized) + if typ == nil or typ.kind != tkFunc: + return "BuxFn_void" + let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void" + var parts: seq[string] = @[sanitizeCTypeNamePart(ret)] + if typ.inner.len > 1: + for p in typ.inner[0 ..^ 2]: + parts.add(sanitizeCTypeNamePart(typeToCStr(p))) + else: + parts.add("void") + return "BuxFn_" & parts.join("_") + +proc funcCodePtrType*(typ: Type): string = + ## C type of the .code field: ret (*)(void* env, params...) + if typ == nil or typ.kind != tkFunc: + return "void (*)(void*)" + let ret = if typ.inner.len > 0: typeToCStr(typ.inner[^1]) else: "void" + var params: seq[string] = @["void* env"] + if typ.inner.len > 1: + for i, p in typ.inner[0 ..^ 2]: + params.add(typeToCStr(p)) + return ret & " (*)(" & params.join(", ") & ")" + proc hirTypeToC(ctx: var LowerToLirCtx, node: HirNode): string = if node == nil: return "int" result = typeToCStr(node.typ) @@ -498,7 +545,12 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue = for e in node.tupleInitElements: elems.add(lowerExpr(ctx, e)) let t = b.freshTemp() - b.emitRawC(&"/* tuple */ {t.strVal} = {{{elems.mapIt($it).join(\", \")}}};") + let typeName = typeToCStr(node.typ) + b.emitAlloca(t.strVal, typeName) + var fields: seq[string] = @[] + for i, e in elems: + fields.add(&"._{i} = {lirValToC(e)}") + b.emitRawC(&"{t.strVal} = ({typeName}){{{fields.join(\", \")}}};") return t # ── If expression (ternary) ── @@ -780,6 +832,13 @@ proc lowerModuleToLir*(hirMod: HirModule): LirBuilder = ctx.funcRetType = retCT ctx.builder.beginFunc(f.name, params, retCT, f.isPublic) + # Closure thunks: materialize env from fat-func env pointer (by-value copy) + if f.captureNames.len > 0 and f.envStructName.len > 0 and f.envInstanceName.len > 0: + ctx.builder.emitRawC(&"struct {f.envStructName} {f.envInstanceName} = *((struct {f.envStructName}*)__env);") + elif f.params.len > 0 and f.params[0].name == "__env": + # Capture-less closure thunk still receives env + ctx.builder.emitRawC("(void)__env;") + if f.body != nil: if f.body.kind == hBlock: for stmt in f.body.blockStmts: diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 650f922..6a08232 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -660,11 +660,15 @@ proc parsePostfix(p: var Parser): Expr = discard p.expect(tkRBracket, "expected ']' to close index") left = Expr(kind: ekIndex, loc: loc, exprIndexObj: left, exprIndexIdx: idx, exprIndexBoundsCheck: false) of tkDot: - # Field expression or .await + # Field expression, tuple index (.0, .1), or .await discard p.advance() if p.check(tkAwait): discard p.advance() left = Expr(kind: ekAwait, loc: loc, exprAwaitOperand: left) + elif p.check(tkIntLiteral): + # Tuple element access: t.0 → field "_0" + let idxText = p.advance().text + left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: "_" & idxText) else: let fieldName = p.expectIdentOrKeyword("expected field name after '.'").text left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: fieldName) diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 1473c73..7be7632 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -1249,6 +1249,20 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = # Auto-dereference pointer/reference types for field access if objType.kind in {tkPointer, tkRef, tkMutRef} and objType.inner.len > 0: objType = objType.inner[0] + if objType.kind == tkTuple: + # Tuple fields: .0 / .1 → stored as "_0" / "_1" + var idx = -1 + let fname = expr.exprFieldName + if fname.len > 0 and fname[0] == '_': + try: idx = parseInt(fname[1..^1]) + except ValueError: idx = -1 + else: + try: idx = parseInt(fname) + except ValueError: idx = -1 + if idx >= 0 and idx < objType.inner.len: + return objType.inner[idx] + sema.emitError(expr.loc, &"tuple has no element '{fname}' (tuple arity {objType.inner.len})") + return makeUnknown() if objType.kind == tkNamed: # Check if this is a _Data union field access if objType.name.endsWith("_Data"): @@ -1468,7 +1482,8 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = initType = sema.checkExpr(stmt.stmtLetInit, scope) let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType if stmt.stmtLetInit != nil and stmt.stmtLetType != nil and not initType.isAssignableTo(declaredType) and not (initType.kind in {TypeKind.tkUnknown, TypeKind.tkNamed, TypeKind.tkTypeParam}): - sema.emitError(stmt.loc, &"cannot assign {initType.toString} to {declaredType.toString}") + # Point at the initializer expression for a clearer caret + sema.emitError(stmt.stmtLetInit.loc, &"cannot assign {initType.toString} to {declaredType.toString}") if stmt.stmtLetInit == nil and stmt.stmtLetType == nil: sema.emitError(stmt.loc, "variable must have either type annotation or initializer") let isOwnVar = stmt.stmtLetType != nil and stmt.stmtLetType.kind == tekOwn diff --git a/bootstrap/source_location.nim b/bootstrap/source_location.nim index a31ea96..57f9849 100644 --- a/bootstrap/source_location.nim +++ b/bootstrap/source_location.nim @@ -3,6 +3,10 @@ type line*: uint32 ## 1-based column*: uint32 ## 1-based (UTF-8 byte offset in line) offset*: uint32 ## byte offset from start of file + file*: string ## source file path (empty if unknown) proc `$`*(loc: SourceLocation): string = - $loc.line & ":" & $loc.column + if loc.file.len > 0: + loc.file & ":" & $loc.line & ":" & $loc.column + else: + $loc.line & ":" & $loc.column diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index 6a0d8fd..a0922ef 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -105,10 +105,50 @@ f"Hello, {name}" // Interpolated string — expressions inside {} own T // Owned value (move semantics) T[] // Slice (unsized) T[N] // Fixed-size array -(T1, T2, T3) // Tuple -func(T1) -> T2 // Function type +(T1, T2, T3) // Tuple — access fields with .0, .1, .2 +func(T1) -> T2 // Function pointer type ``` +### Tuples +```bux +func Pair(a: int, b: int) -> (int, int) { + return (a, b); +} + +func Main() -> int { + let t: (int, int) = Pair(10, 20); + PrintInt(t.0); // 10 + PrintInt(t.1); // 20 + return 0; +} +``` + +### Function pointers and closures +```bux +func Apply(f: func(int) -> int, x: int) -> int { + return f(x); +} + +func Double(n: int) -> int { return n * 2; } + +func MakeAdder(base: int) -> func(int) -> int { + // Each call allocates its own capture environment + return |a: int| -> int { return a + base; }; +} + +func Main() -> int { + let g: func(int) -> int = Double; // named func → fat pointer + let a10 = MakeAdder(10); + let a20 = MakeAdder(20); + // a10 and a20 are independent instances + return Apply(g, 21) + a10(1) + a20(1); // 42 + 11 + 21 +} +``` + +`func(T) -> R` values are **fat pointers** `{ code, env }`: +- capturing closures store captures in a heap env +- capture-less closures and named functions use `env = null` + ### Structs ```bux struct Point { diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md new file mode 100644 index 0000000..668052a --- /dev/null +++ b/docs/QUALITY_PLAN.md @@ -0,0 +1,163 @@ +# Bux — План към „добър“ език (v0.5 → v1.0) + +> **Дата:** 2026-07-15 +> **Текущо:** v0.5.0 — selfhost loop, gradual ownership, green threads, 26+ examples ✅ +> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. + +--- + +## Диагноза (къде сме) + +| Слой | Състояние | Оценка | +|------|-----------|--------| +| Frontend (lex/parse) | Пълен Pratt parser, recovery | ★★★★☆ | +| Sema / generics | Monomorphization, trait bounds basic | ★★★★☆ | +| HIR → C | Работи; tuples/func-ptr half-baked в bootstrap | ★★★☆☆ | +| Selfhost (`src/`) | ~12k LOC, binary-identical loop | ★★★★★ | +| Gradual ownership | `@[Checked]`, `&`/`&mut`, move, Drop | ★★★☆☆ (basic) | +| Concurrency | M:N tasks + channels + async | ★★★★☆ | +| Stdlib | 25+ модула, но колекциите са минимални | ★★★☆☆ | +| Tooling | `new/build/run/test/fmt`, LSP prototype, VSCode | ★★☆☆☆ | +| Ecosystem / registry | path+git deps; няма централен registry | ★☆☆☆☆ | +| Документация | Има, но drift (PLAN vs README версии) | ★★★☆☆ | + +**Силна ниша:** gradual ownership (C-скорост на писане + opt-in Rust-safety). +**Слабо място:** ergonomics на stdlib + maturity на tooling + пълнота на borrow checker. + +--- + +## Какво значи „добър“ за Bux + +1. **Ежедневен DX** — колекции, string, assert, грешки, които разбираш за секунди. +2. **Предвидима безопасност** — `@[Checked]` да хваща 80% от UAF/double-borrow без lifetime hell. +3. **Selfhost като dogfood** — компилаторът и apps (`nexus`, `boko`) са proof. +4. **Инструменти** — fmt, test, LSP, package install без ръчна магия. +5. **Стабилна спецификация** — LanguageRef = реалното поведение. + +Не целим „по-добър Rust“. Целим **единствения език с gradual safety + Go-стил concurrency без GC**. + +--- + +## Фази + +### A — Ergonomics & Stdlib (P0, сега) 🔄 + +| # | Задача | Защо | Статус | +|---|--------|------|--------| +| A.1 | Array: Pop, Clear, IsEmpty, First, Last, Cap, Reserve | Без това колекциите са неудобни | ✅ (тази сесия) | +| A.2 | String: IsEmpty, ReplaceAll | Чести операции; само first-replace досега | ✅ (тази сесия) | +| A.3 | Os_Exit + Test_AssertEqString / richer asserts | Тестове и CLI без raw `bux_exit` | ✅ (тази сесия) | +| A.4 | Map_Remove / Set polish | Completeness на колекциите | ✅ (тази сесия) | +| A.5 | Iter: map/filter/fold върху closures | Higher-order без boilerplate | ⏳ | +| A.6 | Result helpers: Expect, UnwrapErr, Or | По-малко match boilerplate | ✅ (тази сесия) | + +### B — Compiler Correctness (P0) + +| # | Задача | Защо | Статус | +|---|--------|------|--------| +| B.1 | Proper tuple types в C backend | `(T,U)` → `Tuple_T_U` struct + `.0`/`.1` | ✅ | +| B.2 | Function pointer types | `func(T)->U` вече работи в LIR backend | ✅ | +| B.3 | Match expression до край в C (не `return "0"`) | Expression-context match | +| B.4 | Closures: multi-instance + loop/return в body | Реални higher-order callbacks | +| B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ | +| B.6 | Bootstrap ↔ selfhost feature parity | Operator overloading, string interp и в selfhost | + +### C — Gradual Ownership 2.0 (P1) + +| # | Задача | Защо | +|---|--------|------| +| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | +| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | +| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | +| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | + +### D — Tooling (P1) + +| # | Задача | Защо | +|---|--------|------| +| D.1 | LSP: hover, go-to-def, diagnostics (wire към sema) | IDE = adoption | +| D.2 | `bux fmt` стабилен + CI check | Единен style | +| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | +| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | +| D.5 | Golden tests за stdlib modules | Регресии без изненади | + +### E — Ecosystem & v1.0 (P2) + +| # | Задача | Защо | +|---|--------|------| +| E.1 | Package registry protocol (git/HTTP) | `bux add foo` без path hacks | +| E.2 | 3–5 production-quality apps в `apps/` | Showcase | +| E.3 | Language freeze + semver policy | Trust | +| E.4 | Debugger/DWARF basics | Systems audience | +| E.5 | Benchmarks vs C/Zig/Nim (micro + nexus) | Marketing + regression | + +--- + +## Препоръчан ред на работа + +``` +A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) + ↓ ↓ + D (tooling) ←────────── dogfood apps + ↓ + E (v1.0 ecosystem) +``` + +**Правило:** всяка сесия ship-ва нещо runnable (stdlib API, fix, example), не само docs. + +--- + +## Acceptance criteria за „добър v1.0“ + +- [ ] Всички examples + selfhost-loop + 3 apps минават на CI +- [ ] Array/Map/String/Test API покрива 90% от ежедневните нужди +- [ ] `@[Checked]` хваща use-after-move + double `&mut` в documented subset +- [ ] `bux test` + `bux fmt` + `bux check` са default developer loop +- [ ] LanguageRef синхронизиран с компилатора +- [ ] Поне един външен проект (не в monorepo) build-ва с git dep + +--- + +## Сесия 1 (stdlib ergonomics) + +1. `Array_Pop`, `Array_Clear`, `Array_IsEmpty`, `Array_First`, `Array_Last`, `Array_Cap`, `Array_Reserve` +2. `String_IsEmpty`, `String_ReplaceAll` +3. `Os_Exit` +4. `Test_AssertEqString`, `Test_AssertNeqInt`, `Test_AssertEqBool` +5. Example + docs update + +## Сесия 2 (collections + tuples) + +1. `Map_Remove` / `Map_Clear` / `Map_IsEmpty` (+ StringMap) +2. `Set_Remove` / `Set_Clear` / `Set_IsEmpty` +3. `Result_Expect` / `Result_UnwrapErr` / `Result_Or` +4. `Option_Expect` / `Option_Or` +5. **Tuples:** `(T, U)` → `typedef struct { T _0; U _1; } Tuple_T_U` + field access `.0`/`.1` +6. Examples: `tuples`, `func_ptr`, `map_remove` + +## Сесия 3 (diagnostics + collections) + +1. **Rust-style errors** in bootstrap CLI: `--> file:line:col`, source snippet, `^` caret, `= help:` hints +2. `SourceLocation.file` propagated from lexer +3. Better caret for type-mismatch on `let` (points at initializer) +4. `Array_Contains` / `Array_IndexOf` / `Array_Extend` +5. `Iter_AnyEq` / `Iter_AllEq` / `Iter_Collect` +6. Selfhost `Diagnostic_Hint` for common messages + +## Сесия 4 (diagnostics depth + LSP + strings) + +1. **Multi-char underlines** (`^^^^^^^` under tokens/strings/idents) +2. Quoted-name highlighting for `undeclared identifier 'x'` +3. **Golden error tests** (`tests/error_golden/`, `make test-errors`) +4. **LSP** runs `buxc check` and publishes real diagnostics +5. `String_IsBlank` / `String_Repeat` + +## Сесия 5 (multi-instance closures) + +1. **Fat function pointers** for all `func(...)` types: `BuxFn { code(env, args...), env }` +2. Capturing closures: heap-allocate env per creation site (independent instances) +3. Capture-less closures + named funcs: adapters with `env = NULL` +4. Calls through func values: `f.code(f.env, args...)` +5. Example `multi_closure.bux` — MakeAdder(10)/MakeAdder(20) yield 11 and 21 +6. **Selfhost parity:** same fat ABI in `src/hir_lower.bux` + `src/c_backend.bux` (makers, adapters) +``` diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 31bf006..783d612 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -153,7 +153,9 @@ Array_Filter(nums, |x| { return x > 10; }); 4. In thunk body: rewrite captured identifiers to `env_instance.x` via `hFieldAccess`. 5. C backend: emit env struct definition + global instance before thunk function. -**Limitations:** One global instance per closure AST node (no multiple instances). No loop/return support in closures yet. +**Status:** Multi-instance capturing closures work in **both** bootstrap and selfhost via fat +function pointers (`BuxFn { code, env }` + heap-allocated env per creation). Capture-less +closures and named functions use the same ABI (`env = NULL`, adapters for named funcs). **Complexity:** High — touches parser, sema, type system, HIR/LIR backend. diff --git a/docs/Stdlib.md b/docs/Stdlib.md index bc1e6d8..323f9d5 100644 --- a/docs/Stdlib.md +++ b/docs/Stdlib.md @@ -81,8 +81,19 @@ struct Array { |----------|-----------|-------------| | `Array_New` | `func Array_New(cap: uint) -> Array` | Create new array | | `Array_Push` | `func Array_Push(arr: *Array, value: T)` | Append element | +| `Array_Pop` | `func Array_Pop(arr: *Array) -> T` | Remove and return last element | +| `Array_Contains` | `func Array_Contains(arr: *Array, value: T) -> bool` | Linear search for value | +| `Array_IndexOf` | `func Array_IndexOf(arr: *Array, value: T) -> int` | First index or -1 | +| `Array_Extend` | `func Array_Extend(arr: *Array, other: *Array)` | Append all from other | | `Array_Get` | `func Array_Get(arr: *Array, index: uint) -> T` | Get element at index | +| `Array_Set` | `func Array_Set(arr: *Array, index: uint, value: T)` | Set element at index | +| `Array_First` | `func Array_First(arr: *Array) -> T` | First element (bounds-checked) | +| `Array_Last` | `func Array_Last(arr: *Array) -> T` | Last element (bounds-checked) | | `Array_Len` | `func Array_Len(arr: *Array) -> uint` | Get length | +| `Array_Cap` | `func Array_Cap(arr: *Array) -> uint` | Get capacity | +| `Array_IsEmpty` | `func Array_IsEmpty(arr: *Array) -> bool` | True if length is 0 | +| `Array_Clear` | `func Array_Clear(arr: *Array)` | Set length to 0 (keeps capacity) | +| `Array_Reserve` | `func Array_Reserve(arr: *Array, minCap: uint)` | Grow capacity if needed | | `Array_Free` | `func Array_Free(arr: *Array)` | Free memory | ### Example @@ -128,6 +139,9 @@ struct Iter { | `Iter_Count` | `func Iter_Count(it: *Iter) -> uint` | Count remaining elements | | `Iter_Skip` | `func Iter_Skip(it: *Iter, n: uint)` | Skip N elements | | `Iter_Take` | `func Iter_Take(it: *Iter, n: uint) -> Iter` | Take first N elements as new iterator | +| `Iter_AnyEq` | `func Iter_AnyEq(it: *Iter, value: T) -> bool` | True if any remaining element equals value | +| `Iter_AllEq` | `func Iter_AllEq(it: *Iter, value: T) -> bool` | True if all remaining equal value | +| `Iter_Collect` | `func Iter_Collect(it: *Iter) -> Array` | Collect remaining into a new Array | ### Example ```bux @@ -200,8 +214,12 @@ String manipulation utilities. | Function | Signature | Description | |----------|-----------|-------------| +| `String_IsEmpty` | `func String_IsEmpty(s: String) -> bool` | True if length is 0 | +| `String_IsBlank` | `func String_IsBlank(s: String) -> bool` | True if empty or only whitespace | +| `String_Repeat` | `func String_Repeat(s: String, count: uint) -> String` | Repeat string N times | | `String_Find` | `func String_Find(haystack: String, needle: String) -> String` | Find substring (returns pointer; 0 = not found) | | `String_Replace` | `func String_Replace(s: String, old: String, new: String) -> String` | Replace first occurrence | +| `String_ReplaceAll` | `func String_ReplaceAll(s: String, old: String, new: String) -> String` | Replace all non-overlapping occurrences | | `String_Format1` | `func String_Format1(pattern: String, a0: String) -> String` | Format with 1 arg (`{0}`) | | `String_Format2` | `func String_Format2(pattern: String, a0: String, a1: String) -> String` | Format with 2 args | | `String_Format3` | `func String_Format3(pattern: String, a0: String, a1: String, a2: String) -> String` | Format with 3 args | @@ -322,6 +340,9 @@ struct Set { | `Set_New` | `func Set_New(cap: uint) -> Set` | Create set | | `Set_Add` | `func Set_Add(s: *Set, value: T)` | Insert element (ignores duplicates) | | `Set_Has` | `func Set_Has(s: *Set, value: T) -> bool` | Check membership | +| `Set_Remove` | `func Set_Remove(s: *Set, value: T) -> bool` | Remove value | +| `Set_Clear` | `func Set_Clear(s: *Set)` | Clear all elements | +| `Set_IsEmpty` | `func Set_IsEmpty(s: *Set) -> bool` | True if empty | | `Set_Len` | `func Set_Len(s: *Set) -> uint` | Element count | | `Set_Free` | `func Set_Free(s: *Set)` | Free memory | @@ -371,6 +392,9 @@ struct Map { | `Map_Set` | `func Map_Set(m: *Map, key: K, value: V)` | Insert/update | | `Map_Get` | `func Map_Get(m: *Map, key: K) -> V` | Get value (zero if missing) | | `Map_Has` | `func Map_Has(m: *Map, key: K) -> bool` | Check key exists | +| `Map_Remove` | `func Map_Remove(m: *Map, key: K) -> bool` | Remove key (true if present) | +| `Map_Clear` | `func Map_Clear(m: *Map)` | Remove all entries (keeps capacity) | +| `Map_IsEmpty` | `func Map_IsEmpty(m: *Map) -> bool` | True if no entries | | `Map_Len` | `func Map_Len(m: *Map) -> uint` | Entry count | | `Map_Free` | `func Map_Free(m: *Map)` | Free memory | @@ -419,6 +443,9 @@ struct StringMap { | `StringMap_Set` | `func StringMap_Set(m: *StringMap, key: String, value: V)` | Insert/update | | `StringMap_Get` | `func StringMap_Get(m: *StringMap, key: String) -> V` | Get value | | `StringMap_Has` | `func StringMap_Has(m: *StringMap, key: String) -> bool` | Check key exists | +| `StringMap_Remove` | `func StringMap_Remove(m: *StringMap, key: String) -> bool` | Remove key | +| `StringMap_Clear` | `func StringMap_Clear(m: *StringMap)` | Clear all entries | +| `StringMap_IsEmpty` | `func StringMap_IsEmpty(m: *StringMap) -> bool` | True if empty | | `StringMap_Len` | `func StringMap_Len(m: *StringMap) -> uint` | Entry count | | `StringMap_Free` | `func StringMap_Free(m: *StringMap)` | Free memory | @@ -914,7 +941,7 @@ func Main() -> int { Operating system interface. ```bux -import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdir}; +import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdir, Os_Exit}; ``` | Function | Signature | Description | @@ -925,6 +952,28 @@ import Std::Os::{Os_ArgsCount, Os_Args, Os_GetEnv, Os_SetEnv, Os_GetCwd, Os_Chdi | `Os_SetEnv` | `func Os_SetEnv(name: String, value: String) -> bool` | Set environment variable | | `Os_GetCwd` | `func Os_GetCwd() -> String` | Get current working directory | | `Os_Chdir` | `func Os_Chdir(path: String) -> bool` | Change directory | +| `Os_Exit` | `func Os_Exit(code: int)` | Terminate process with exit code | + +--- + +## Std::Test + +Lightweight assertions for `bux test` and example programs. + +```bux +import Std::Test::*; +``` + +| Function | Signature | Description | +|----------|-----------|-------------| +| `Test_Assert` | `func Test_Assert(cond: bool)` | Panic if false | +| `Test_AssertTrue` / `Test_AssertFalse` | `func ...(cond: bool)` | Boolean asserts | +| `Test_AssertEqInt` | `func Test_AssertEqInt(a: int, b: int)` | Integer equality | +| `Test_AssertNeqInt` | `func Test_AssertNeqInt(a: int, b: int)` | Integer inequality | +| `Test_AssertEqString` | `func Test_AssertEqString(a: String, b: String)` | String equality | +| `Test_AssertEqBool` | `func Test_AssertEqBool(a: bool, b: bool)` | Boolean equality | +| `Test_Fail` / `Test_Pass` | `func ...(msg: String)` | Explicit fail / log pass | +| `Test_Exit` | `func Test_Exit(code: int)` | Exit with code | --- diff --git a/examples/array_iter_extra.bux b/examples/array_iter_extra.bux new file mode 100644 index 0000000..59d898a --- /dev/null +++ b/examples/array_iter_extra.bux @@ -0,0 +1,46 @@ +// Array_Contains / IndexOf / Extend + Iter_Collect / AnyEq +import Std::Io::{PrintLine, PrintInt}; +import Std::Array::{ + Array, Array_New, Array_Push, Array_Contains, Array_IndexOf, + Array_Extend, Array_Len, Array_Get, Array_Free +}; +import Std::Iter::{Array_Iter, Iter, Iter_Collect, Iter_AnyEq, Iter_AllEq, Iter_HasNext, Iter_Next}; +import Std::Test::{Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass}; + +func Main() -> int { + var a: Array = Array_New(4); + Array_Push(&a, 10); + Array_Push(&a, 20); + Array_Push(&a, 30); + + Test_AssertTrue(Array_Contains(&a, 20)); + Test_AssertFalse(Array_Contains(&a, 99)); + Test_AssertEqInt(Array_IndexOf(&a, 30), 2); + Test_AssertEqInt(Array_IndexOf(&a, 99), -1); + + var b: Array = Array_New(2); + Array_Push(&b, 40); + Array_Push(&b, 50); + Array_Extend(&a, &b); + Test_AssertEqInt(Array_Len(&a) as int, 5); + Test_AssertEqInt(Array_Get(&a, 4), 50); + + var it: Iter = Array_Iter(&a); + Test_AssertTrue(Iter_AnyEq(&it, 10)); + Test_AssertFalse(Iter_AllEq(&it, 10)); + + // Skip first two via advancing, collect rest + discard Iter_Next(&it); + discard Iter_Next(&it); + var rest: Array = Iter_Collect(&it); + Test_AssertEqInt(Array_Len(&rest) as int, 3); + Test_AssertEqInt(Array_Get(&rest, 0), 30); + + Array_Free(&a); + Array_Free(&b); + Array_Free(&rest); + + PrintLine("array_iter_extra: ok"); + Test_Pass("array + iter extras"); + return 0; +} diff --git a/examples/func_ptr.bux b/examples/func_ptr.bux new file mode 100644 index 0000000..ac258b6 --- /dev/null +++ b/examples/func_ptr.bux @@ -0,0 +1,27 @@ +// Function pointer types: func(T) -> U +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +func Apply(f: func(int) -> int, x: int) -> int { + return f(x); +} + +func Double(n: int) -> int { + return n * 2; +} + +func Inc(n: int) -> int { + return n + 1; +} + +func Main() -> int { + let g: func(int) -> int = Double; + Test_AssertEqInt(Apply(g, 21), 42); + Test_AssertEqInt(Apply(Double, 7), 14); + Test_AssertEqInt(Apply(Inc, 99), 100); + + PrintInt(Apply(Double, 5)); + PrintLine(""); + Test_Pass("func_ptr"); + return 0; +} diff --git a/examples/map_remove.bux b/examples/map_remove.bux new file mode 100644 index 0000000..7f87d1a --- /dev/null +++ b/examples/map_remove.bux @@ -0,0 +1,60 @@ +// Map_Remove / Map_Clear / Set_Remove +import Std::Io::{PrintLine, PrintInt}; +import Std::Map::{Map, Map_New, Map_Set, Map_Get, Map_Has, Map_Remove, Map_Clear, Map_Len, Map_IsEmpty, Map_Free}; +import Std::Set::{Set, Set_New, Set_Add, Set_Has, Set_Remove, Set_Len, Set_IsEmpty, Set_Free}; +import Std::Test::{Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_Pass}; +import Std::Result::{Result, Result_NewOk, Result_NewErr, Result_IsOk, Result_IsErr, Result_UnwrapOr, Result_Or, Result_UnwrapErr}; +import Std::Option::{Option, Option_NewSome, Option_NewNone, Option_IsSome, Option_Or, Option_UnwrapOr}; +import Std::String::{String_Eq}; + +func Main() -> int { + // --- Map --- + var m: Map = Map_New(16); + Map_Set(&m, 1, 100); + Map_Set(&m, 2, 200); + Map_Set(&m, 3, 300); + Test_AssertEqInt(Map_Len(&m) as int, 3); + Test_AssertTrue(Map_Has(&m, 2)); + Test_AssertTrue(Map_Remove(&m, 2)); + Test_AssertFalse(Map_Has(&m, 2)); + Test_AssertEqInt(Map_Len(&m) as int, 2); + Test_AssertEqInt(Map_Get(&m, 1), 100); + Test_AssertEqInt(Map_Get(&m, 3), 300); + Test_AssertFalse(Map_Remove(&m, 99)); + Map_Clear(&m); + Test_AssertTrue(Map_IsEmpty(&m)); + Map_Free(&m); + + // --- Set --- + var s: Set = Set_New(16); + Set_Add(&s, 10); + Set_Add(&s, 20); + Set_Add(&s, 30); + Test_AssertTrue(Set_Remove(&s, 20)); + Test_AssertFalse(Set_Has(&s, 20)); + Test_AssertTrue(Set_Has(&s, 10)); + Test_AssertEqInt(Set_Len(&s) as int, 2); + Test_AssertFalse(Set_IsEmpty(&s)); + Set_Free(&s); + + // --- Result helpers --- + let ok: Result = Result_NewOk(42); + let err: Result = Result_NewErr("boom"); + Test_AssertTrue(Result_IsOk(ok)); + Test_AssertTrue(Result_IsErr(err)); + Test_AssertEqInt(Result_UnwrapOr(err, -1), -1); + let recovered: Result = Result_Or(err, Result_NewOk(7)); + Test_AssertEqInt(Result_UnwrapOr(recovered, 0), 7); + Test_AssertTrue(String_Eq(Result_UnwrapErr(err), "boom")); + + // --- Option helpers --- + let some: Option = Option_NewSome(5); + let none: Option = Option_NewNone(); + Test_AssertTrue(Option_IsSome(some)); + let o2: Option = Option_Or(none, some); + Test_AssertEqInt(Option_UnwrapOr(o2, 0), 5); + + PrintLine("map_remove: ok"); + Test_Pass("map_remove + result helpers"); + return 0; +} diff --git a/examples/multi_closure.bux b/examples/multi_closure.bux new file mode 100644 index 0000000..617fcdf --- /dev/null +++ b/examples/multi_closure.bux @@ -0,0 +1,37 @@ +// Multi-instance closures: each capturing closure gets its own heap env +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +func MakeAdder(base: int) -> func(int) -> int { + return |a: int| -> int { + return a + base; + }; +} + +func Apply(f: func(int) -> int, x: int) -> int { + return f(x); +} + +func Main() -> int { + let a10: func(int) -> int = MakeAdder(10); + let a20: func(int) -> int = MakeAdder(20); + + // Independent instances — not a single global env + Test_AssertEqInt(a10(1), 11); + Test_AssertEqInt(a20(1), 21); + Test_AssertEqInt(a10(5), 15); + Test_AssertEqInt(Apply(a20, 3), 23); + + // Capture-less still works + let add: func(int, int) -> int = |x: int, y: int| -> int { + return x + y; + }; + Test_AssertEqInt(add(2, 3), 5); + + PrintInt(a10(1)); + PrintLine(""); + PrintInt(a20(1)); + PrintLine(""); + Test_Pass("multi_closure"); + return 0; +} diff --git a/examples/stdlib_ergonomics.bux b/examples/stdlib_ergonomics.bux new file mode 100644 index 0000000..3c63abf --- /dev/null +++ b/examples/stdlib_ergonomics.bux @@ -0,0 +1,55 @@ +// Stdlib ergonomics demo: Array helpers, String_ReplaceAll, Test asserts +import Std::Io::{PrintLine, PrintInt}; +import Std::Array::{ + Array, Array_New, Array_Push, Array_Pop, Array_Clear, Array_IsEmpty, + Array_First, Array_Last, Array_Cap, Array_Reserve, Array_Len, Array_Get, Array_Free +}; +import Std::String::{String_IsEmpty, String_ReplaceAll, String_Eq, String_Len}; +import Std::Test::{ + Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, + Test_AssertNeqInt, Test_AssertEqBool, Test_Pass +}; + +func Main() -> int { + // --- Array: Reserve / Push / First / Last / Pop / Clear --- + var arr: Array = Array_New(2); + Array_Reserve(&arr, 8); + Test_AssertTrue(Array_Cap(&arr) >= 8); + Test_AssertTrue(Array_IsEmpty(&arr)); + + Array_Push(&arr, 10); + Array_Push(&arr, 20); + Array_Push(&arr, 30); + + Test_AssertFalse(Array_IsEmpty(&arr)); + Test_AssertEqInt(Array_Len(&arr) as int, 3); + Test_AssertEqInt(Array_First(&arr), 10); + Test_AssertEqInt(Array_Last(&arr), 30); + + let popped: int = Array_Pop(&arr); + Test_AssertEqInt(popped, 30); + Test_AssertEqInt(Array_Len(&arr) as int, 2); + Test_AssertEqInt(Array_Get(&arr, 1), 20); + + Array_Clear(&arr); + Test_AssertTrue(Array_IsEmpty(&arr)); + Test_AssertTrue(Array_Cap(&arr) >= 8); // capacity retained + Array_Free(&arr); + + // --- String: IsEmpty / ReplaceAll --- + Test_AssertTrue(String_IsEmpty("")); + Test_AssertFalse(String_IsEmpty("x")); + + let multi: String = String_ReplaceAll("a-b-a-b-a", "a", "X"); + Test_AssertEqString(multi, "X-b-X-b-X"); + Test_AssertNeqInt(String_Len(multi) as int, 0); + + // Safe when replacement contains the needle (no infinite loop) + let safe: String = String_ReplaceAll("..", ".", "x."); + Test_AssertEqString(safe, "x.x."); + + Test_AssertEqBool(true, true); + Test_Pass("stdlib ergonomics"); + PrintLine("stdlib_ergonomics: all checks passed"); + return 0; +} diff --git a/examples/string_extra.bux b/examples/string_extra.bux new file mode 100644 index 0000000..1d11fe3 --- /dev/null +++ b/examples/string_extra.bux @@ -0,0 +1,21 @@ +// String_IsBlank / String_Repeat +import Std::Io::{PrintLine}; +import Std::String::{String_IsBlank, String_IsEmpty, String_Repeat, String_Eq, String_Len}; +import Std::Test::{Test_AssertTrue, Test_AssertFalse, Test_AssertEqInt, Test_AssertEqString, Test_Pass}; + +func Main() -> int { + Test_AssertTrue(String_IsEmpty("")); + Test_AssertTrue(String_IsBlank("")); + Test_AssertTrue(String_IsBlank(" \t\n")); + Test_AssertFalse(String_IsBlank(" x ")); + + let dots: String = String_Repeat(".", 5); + Test_AssertEqString(dots, "....."); + Test_AssertEqInt(String_Len(String_Repeat("ab", 3)) as int, 6); + Test_AssertEqString(String_Repeat("x", 0), ""); + Test_AssertEqString(String_Repeat("ok", 1), "ok"); + + PrintLine(String_Repeat("Bux ", 2)); + Test_Pass("string_extra"); + return 0; +} diff --git a/examples/tuples.bux b/examples/tuples.bux new file mode 100644 index 0000000..1583040 --- /dev/null +++ b/examples/tuples.bux @@ -0,0 +1,32 @@ +// Tuple types: (T, U), return, field access via .0 / .1 +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +func MakePair(a: int, b: int) -> (int, int) { + return (a, b); +} + +func Swap(t: (int, int)) -> (int, int) { + return (t.1, t.0); +} + +func Main() -> int { + let t: (int, int) = MakePair(10, 20); + Test_AssertEqInt(t.0, 10); + Test_AssertEqInt(t.1, 20); + + let s: (int, int) = Swap(t); + Test_AssertEqInt(s.0, 20); + Test_AssertEqInt(s.1, 10); + + let lit: (int, int) = (7, 8); + Test_AssertEqInt(lit.0, 7); + Test_AssertEqInt(lit.1, 8); + + PrintInt(t.0); + PrintLine(""); + PrintInt(t.1); + PrintLine(""); + Test_Pass("tuples"); + return 0; +} diff --git a/lib/Array.bux b/lib/Array.bux index 9ed355c..fd55ab1 100644 --- a/lib/Array.bux +++ b/lib/Array.bux @@ -58,4 +58,78 @@ func Array_operator_index_set(self: *Array, idx: uint, value: T) { Array_Set(self, idx, value); } +/* True if the array has no elements */ +func Array_IsEmpty(self: *Array) -> bool { + return self.len == 0; +} + +/* Current capacity (not length) */ +func Array_Cap(self: *Array) -> uint { + return self.cap; +} + +/* Drop length to zero; keeps allocated capacity */ +func Array_Clear(self: *Array) { + self.len = 0; +} + +/* Ensure capacity is at least minCap (does not shrink) */ +func Array_Reserve(self: *Array, minCap: uint) { + if minCap <= self.cap { + return; + } + self.cap = minCap; + self.data = bux_realloc(self.data as *void, self.cap * sizeof(T)) as *T; +} + +/* First element (panics if empty via bounds check) */ +func Array_First(self: *Array) -> T { + return Array_Get(self, 0); +} + +/* Last element (panics if empty via bounds check) */ +func Array_Last(self: *Array) -> T { + return Array_Get(self, self.len - 1); +} + +/* Remove and return the last element (panics if empty) */ +func Array_Pop(self: *Array) -> T { + bux_bounds_check(0, self.len); + self.len = self.len - 1; + return self.data[self.len]; +} + +/* Linear search: true if value is present (uses ==) */ +func Array_Contains(self: *Array, value: T) -> bool { + var i: uint = 0; + while i < self.len { + if self.data[i] == value { + return true; + } + i = i + 1; + } + return false; +} + +/* Index of first equal element, or -1 if not found */ +func Array_IndexOf(self: *Array, value: T) -> int { + var i: uint = 0; + while i < self.len { + if self.data[i] == value { + return i as int; + } + i = i + 1; + } + return -1; +} + +/* Append all elements of other onto self */ +func Array_Extend(self: *Array, other: *Array) { + var i: uint = 0; + while i < other.len { + Array_Push(self, other.data[i]); + i = i + 1; + } +} + } diff --git a/lib/Iter.bux b/lib/Iter.bux index 8973cd9..8247c0f 100644 --- a/lib/Iter.bux +++ b/lib/Iter.bux @@ -67,4 +67,44 @@ func Iter_Take(it: *Iter, n: uint) -> Iter { return Iter { data: it.data, len: endPos, pos: it.pos }; } +/* True if any remaining element equals value */ +func Iter_AnyEq(it: *Iter, value: T) -> bool { + var i: uint = it.pos; + while i < it.len { + if it.data[i] == value { + return true; + } + i = i + 1; + } + return false; +} + +/* True if every remaining element equals value (true if empty) */ +func Iter_AllEq(it: *Iter, value: T) -> bool { + var i: uint = it.pos; + while i < it.len { + if it.data[i] != value { + return false; + } + i = i + 1; + } + return true; +} + +/* Collect remaining elements into a new Array */ +func Iter_Collect(it: *Iter) -> Array { + let remaining: uint = it.len - it.pos; + var cap: uint = remaining; + if cap == 0 { + cap = 1; + } + var arr: Array = Array_New(cap); + var i: uint = it.pos; + while i < it.len { + Array_Push(&arr, it.data[i]); + i = i + 1; + } + return arr; +} + } diff --git a/lib/Map.bux b/lib/Map.bux index ddbbb28..9d3d558 100644 --- a/lib/Map.bux +++ b/lib/Map.bux @@ -80,6 +80,41 @@ func Map_Len(m: *Map) -> uint { return m.len; } +func Map_IsEmpty(m: *Map) -> bool { + return m.len == 0; +} + +/* Remove key if present. Rebuilds the table to keep open-addressing correct. */ +func Map_Remove(m: *Map, key: K) -> bool { + if !Map_Has(m, key) { + return false; + } + var fresh: Map = Map_New(m.cap); + var i: uint = 0; + while i < m.cap { + if m.entries[i].occupied { + if m.entries[i].key != key { + Map_Set(&fresh, m.entries[i].key, m.entries[i].value); + } + } + i = i + 1; + } + bux_free(m.entries as *void); + m.entries = fresh.entries; + m.cap = fresh.cap; + m.len = fresh.len; + return true; +} + +func Map_Clear(m: *Map) { + var i: uint = 0; + while i < m.cap { + m.entries[i].occupied = false; + i = i + 1; + } + m.len = 0; +} + func Map_Free(m: *Map) { bux_free(m.entries as *void); m.entries = null as *MapEntry; @@ -163,6 +198,40 @@ func StringMap_Len(m: *StringMap) -> uint { return m.len; } +func StringMap_IsEmpty(m: *StringMap) -> bool { + return m.len == 0; +} + +func StringMap_Remove(m: *StringMap, key: String) -> bool { + if !StringMap_Has(m, key) { + return false; + } + var fresh: StringMap = StringMap_New(m.cap); + var i: uint = 0; + while i < m.cap { + if m.entries[i].occupied { + if !String_Eq(m.entries[i].key, key) { + StringMap_Set(&fresh, m.entries[i].key, m.entries[i].value); + } + } + i = i + 1; + } + bux_free(m.entries as *void); + m.entries = fresh.entries; + m.cap = fresh.cap; + m.len = fresh.len; + return true; +} + +func StringMap_Clear(m: *StringMap) { + var i: uint = 0; + while i < m.cap { + m.entries[i].occupied = false; + i = i + 1; + } + m.len = 0; +} + func StringMap_Free(m: *StringMap) { bux_free(m.entries as *void); m.entries = null as *StringMapEntry; diff --git a/lib/Option.bux b/lib/Option.bux index a50ad2b..b2a7a99 100644 --- a/lib/Option.bux +++ b/lib/Option.bux @@ -1,6 +1,8 @@ module Std::Option { import Std::Io::{PrintLine}; +extern func bux_exit(code: int); + enum Option { Some(int), None, @@ -39,4 +41,21 @@ func Option_UnwrapOr(o: Option, fallback: int) -> int { return fallback; } +/* Unwrap Some or panic with a custom message */ +func Option_Expect(o: Option, msg: String) -> int { + if o.tag != Option_Some { + PrintLine(msg); + bux_exit(1); + } + return o.data.Some_0; +} + +/* If o is Some return it, otherwise return other */ +func Option_Or(o: Option, other: Option) -> Option { + if o.tag == Option_Some { + return o; + } + return other; +} + } diff --git a/lib/Os.bux b/lib/Os.bux index edfe9fe..540c7fc 100644 --- a/lib/Os.bux +++ b/lib/Os.bux @@ -6,6 +6,7 @@ extern func bux_getenv(name: String) -> String; extern func bux_setenv(name: String, value: String) -> int; extern func bux_getcwd() -> String; extern func bux_chdir(path: String) -> int; +extern func bux_exit(code: int); func Os_ArgsCount() -> int { return bux_argc(); @@ -31,4 +32,9 @@ func Os_Chdir(path: String) -> bool { return bux_chdir(path) == 0; } +/* Terminate the process with the given exit code */ +func Os_Exit(code: int) { + bux_exit(code); +} + } diff --git a/lib/Result.bux b/lib/Result.bux index 1bc2cb3..fefe0db 100644 --- a/lib/Result.bux +++ b/lib/Result.bux @@ -1,6 +1,8 @@ module Std::Result { import Std::Io::{PrintLine}; +extern func bux_exit(code: int); + enum Result { Ok(int), Err(String), @@ -41,4 +43,30 @@ func Result_UnwrapOr(r: Result, fallback: int) -> int { return fallback; } +/* Unwrap Ok or panic with a custom message */ +func Result_Expect(r: Result, msg: String) -> int { + if r.tag != Result_Ok { + PrintLine(msg); + bux_exit(1); + } + return r.data.Ok_0; +} + +/* Extract Err payload (panics if Ok) */ +func Result_UnwrapErr(r: Result) -> String { + if r.tag != Result_Err { + PrintLine("panic: unwrap_err on Ok"); + return ""; + } + return r.data.Err_0; +} + +/* If r is Ok return it, otherwise return other */ +func Result_Or(r: Result, other: Result) -> Result { + if r.tag == Result_Ok { + return r; + } + return other; +} + } diff --git a/lib/Set.bux b/lib/Set.bux index cf57fe2..644f299 100644 --- a/lib/Set.bux +++ b/lib/Set.bux @@ -61,6 +61,43 @@ func Set_Len(s: *Set) -> uint { return s.len; } +func Set_IsEmpty(s: *Set) -> bool { + return s.len == 0; +} + +/* Remove value if present. Rebuilds the table to keep open-addressing correct. */ +func Set_Remove(s: *Set, value: T) -> bool { + if !Set_Has(s, value) { + return false; + } + var fresh: Set = Set_New(s.cap); + var i: uint = 0; + while i < s.cap { + if s.entries[i].occupied { + var entryPtr: *T = &s.entries[i].value; + var valuePtr: *T = &value; + if bux_mem_eq(entryPtr as *void, valuePtr as *void, sizeof(T)) == 0 { + Set_Add(&fresh, s.entries[i].value); + } + } + i = i + 1; + } + bux_free(s.entries as *void); + s.entries = fresh.entries; + s.cap = fresh.cap; + s.len = fresh.len; + return true; +} + +func Set_Clear(s: *Set) { + var i: uint = 0; + while i < s.cap { + s.entries[i].occupied = false; + i = i + 1; + } + s.len = 0; +} + func Set_Free(s: *Set) { bux_free(s.entries as *void); s.entries = null as *SetEntry; diff --git a/lib/String.bux b/lib/String.bux index f7787f3..2a69af7 100644 --- a/lib/String.bux +++ b/lib/String.bux @@ -34,6 +34,10 @@ func String_Len(s: String) -> uint { return bux_strlen(s); } +func String_IsEmpty(s: String) -> bool { + return bux_strlen(s) == 0; +} + func String_IsNull(s: String) -> bool { return bux_str_is_null(s) != 0; } @@ -147,6 +151,39 @@ func StringBuilder_Free(sb: *StringBuilder) { bux_sb_free(sb.handle); } +/* True if empty or only whitespace (space, tab, CR, LF) */ +func String_IsBlank(s: String) -> bool { + let n: uint = bux_strlen(s); + var i: uint = 0; + while i < n { + let ch: String = bux_str_slice(s, i, 1); + if !(String_Eq(ch, " ") || String_Eq(ch, "\t") || String_Eq(ch, "\n") || String_Eq(ch, "\r")) { + return false; + } + i = i + 1; + } + return true; +} + +/* Repeat s, count times (count==0 → empty string) */ +func String_Repeat(s: String, count: uint) -> String { + if count == 0 { + return ""; + } + if count == 1 { + return s; + } + var sb: StringBuilder = StringBuilder_New(); + var i: uint = 0; + while i < count { + StringBuilder_Append(&sb, s); + i = i + 1; + } + let result: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return result; +} + // --------------------------------------------------------------------------- // String split/join // --------------------------------------------------------------------------- @@ -194,6 +231,33 @@ func String_Replace(s: String, old: String, new: String) -> String { return result; } +/* Replace every non-overlapping occurrence of old with new. + Empty old is a no-op (returns s unchanged). Safe if new contains old. */ +func String_ReplaceAll(s: String, old: String, new: String) -> String { + let oldLen: uint = bux_strlen(old); + if oldLen == 0 { + return s; + } + var sb: StringBuilder = StringBuilder_New(); + var remaining: String = s; + while true { + let pos: String = bux_strstr(remaining, old); + if String_IsNull(pos) { + StringBuilder_Append(&sb, remaining); + break; + } + let prefixLen: uint = String_Offset(pos, remaining); + let prefix: String = bux_str_slice(remaining, 0, prefixLen); + StringBuilder_Append(&sb, prefix); + StringBuilder_Append(&sb, new); + let remLen: uint = bux_strlen(remaining); + remaining = bux_str_slice(remaining, prefixLen + oldLen, remLen - prefixLen - oldLen); + } + let result: String = StringBuilder_Build(&sb); + StringBuilder_Free(&sb); + return result; +} + extern func bux_str_to_float(s: String) -> float64; func String_ToFloat(s: String) -> float64 { diff --git a/lib/Test.bux b/lib/Test.bux index c67167c..9c23fdc 100644 --- a/lib/Test.bux +++ b/lib/Test.bux @@ -1,5 +1,6 @@ module Std::Test { import Std::Io::{PrintLine, PrintInt}; +import Std::String::{String_Eq}; extern func bux_exit(code: int); extern func bux_assert(cond: int, file: String, line: int, expr: String); @@ -14,7 +15,7 @@ func Test_Assert(cond: bool) { func Test_AssertEqInt(a: int, b: int) { if a != b { - PrintLine("ASSERT_EQ FAILED:"); + PrintLine("ASSERT_EQ_INT FAILED:"); PrintInt(a); PrintLine(" != "); PrintInt(b); @@ -22,6 +23,31 @@ func Test_AssertEqInt(a: int, b: int) { } } +func Test_AssertNeqInt(a: int, b: int) { + if a == b { + PrintLine("ASSERT_NEQ_INT FAILED: both are"); + PrintInt(a); + bux_exit(1); + } +} + +func Test_AssertEqString(a: String, b: String) { + if !String_Eq(a, b) { + PrintLine("ASSERT_EQ_STRING FAILED:"); + PrintLine(a); + PrintLine(" != "); + PrintLine(b); + bux_exit(1); + } +} + +func Test_AssertEqBool(a: bool, b: bool) { + if a != b { + PrintLine("ASSERT_EQ_BOOL FAILED"); + bux_exit(1); + } +} + func Test_AssertTrue(cond: bool) { if !cond { PrintLine("ASSERT_TRUE FAILED"); @@ -42,4 +68,9 @@ func Test_Fail(msg: String) { bux_exit(1); } +func Test_Pass(msg: String) { + PrintLine("PASS:"); + PrintLine(msg); +} + } diff --git a/rt/runtime.c b/rt/runtime.c index a141781..f142c5c 100644 --- a/rt/runtime.c +++ b/rt/runtime.c @@ -143,6 +143,64 @@ int64_t bux_mod_i64(int64_t a, int64_t b) { return a % b; } +/* Integer overflow checks (debug mode) */ +int64_t bux_add_i64_checked(int64_t a, int64_t b) { +#if defined(__GNUC__) || defined(__clang__) + int64_t result; + if (__builtin_add_overflow(a, b, &result)) { + bux_panic("integer overflow in addition"); + } + return result; +#else + if ((b > 0 && a > INT64_MAX - b) || (b < 0 && a < INT64_MIN - b)) { + bux_panic("integer overflow in addition"); + } + return a + b; +#endif +} + +int64_t bux_sub_i64_checked(int64_t a, int64_t b) { +#if defined(__GNUC__) || defined(__clang__) + int64_t result; + if (__builtin_sub_overflow(a, b, &result)) { + bux_panic("integer overflow in subtraction"); + } + return result; +#else + if ((b > 0 && a < INT64_MIN + b) || (b < 0 && a > INT64_MAX + b)) { + bux_panic("integer overflow in subtraction"); + } + return a - b; +#endif +} + +int64_t bux_mul_i64_checked(int64_t a, int64_t b) { +#if defined(__GNUC__) || defined(__clang__) + int64_t result; + if (__builtin_mul_overflow(a, b, &result)) { + bux_panic("integer overflow in multiplication"); + } + return result; +#else + if (a != 0 && b != 0) { + if ((a > 0 && b > 0 && a > INT64_MAX / b) || + (a > 0 && b < 0 && b < INT64_MIN / a) || + (a < 0 && b > 0 && a < INT64_MIN / b) || + (a < 0 && b < 0 && a < INT64_MAX / b)) { + bux_panic("integer overflow in multiplication"); + } + } + return a * b; +#endif +} + +int64_t bux_neg_i64_checked(int64_t a) { + if (a == INT64_MIN) { + bux_panic("integer overflow in negation"); + } + return -a; +} + /* String operations */ typedef struct { const char* data; @@ -189,6 +247,15 @@ void bux_bounds_check(size_t index, size_t len) { } } +/* Bounds check that returns the index (usable in expressions) */ +size_t bux_index_check(size_t index, size_t len) { + if (index >= len) { + fprintf(stderr, "bux panic: index out of bounds (index %zu, len %zu)\n", index, len); + abort(); + } + return index; +} + /* String wrappers with Bux-compatible signatures */ unsigned int bux_strlen(const char* s) { return (unsigned int)strlen(s); diff --git a/src/c_backend.bux b/src/c_backend.bux index 05752bb..1ea1e84 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -8,30 +8,9 @@ module CBackend { // --------------------------------------------------------------------------- func CBackend_TypeToC(kind: int) -> String { - if kind == tyVoid { return "void"; } - if kind == tyBool { return "bool"; } - if kind == tyBool8 { return "bool"; } - if kind == tyBool16 { return "bool"; } - if kind == tyBool32 { return "bool"; } - if kind == tyChar8 { return "char"; } - if kind == tyChar16 { return "uint16"; } - if kind == tyChar32 { return "uint32"; } - if kind == tyStr { return "String"; } - if kind == tyInt8 { return "int8"; } - if kind == tyInt16 { return "int16"; } - if kind == tyInt32 { return "int32"; } - if kind == tyInt64 { return "int64"; } - if kind == tyInt{ return "int"; } - if kind == tyUInt8 { return "uint8"; } - if kind == tyUInt16 { return "uint16"; } - if kind == tyUInt32 { return "uint32"; } - if kind == tyUInt64 { return "uint64"; } - if kind == tyUInt { return "uint"; } - if kind == tyFloat32 { return "float32"; } - if kind == tyFloat64 { return "float64"; } - if kind == tyPointer { return "void*"; } + let cName: String = Type_ToCName(kind); + if !String_Eq(cName, "") { return cName; } if kind == tyNamed { return "int"; } - if kind == tyFunc { return "void (*)(void)"; } return "int"; } @@ -338,31 +317,27 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { return; } - // Indirect call through function pointer + // Indirect call through fat function pointer: f.code(f.env, args...) if kind == hCallIndirect { - CBE_EmitExpr(cbe, node.child1); StringBuilder_Append(&cbe.sb, "("); - var needsComma: bool = false; + CBE_EmitExpr(cbe, node.child1); + StringBuilder_Append(&cbe.sb, ".code)("); + CBE_EmitExpr(cbe, node.child1); + StringBuilder_Append(&cbe.sb, ".env"); if node.child2 != null as *HirNode { + StringBuilder_Append(&cbe.sb, ", "); CBE_EmitExpr(cbe, node.child2); - needsComma = true; } if node.child3 != null as *HirNode { - if needsComma { - StringBuilder_Append(&cbe.sb, ", "); - } + StringBuilder_Append(&cbe.sb, ", "); CBE_EmitExpr(cbe, node.child3); - needsComma = true; } // Emit extra args from linked list var ai: int = 0; var curExtra: *HirArgList = node.extraData as *HirArgList; while ai < node.extraCount { - if needsComma { - StringBuilder_Append(&cbe.sb, ", "); - } + StringBuilder_Append(&cbe.sb, ", "); CBE_EmitExpr(cbe, curExtra.node); - needsComma = true; curExtra = curExtra.next; ai = ai + 1; } @@ -590,11 +565,27 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { } // Index: arr[idx] — emit as arr[idx] + // For Array desugar pattern (fieldPtr "data"), emit bounds-checked access if kind == hIndexPtr { - CBE_EmitExpr(cbe, node.child1); - StringBuilder_Append(&cbe.sb, "["); - CBE_EmitExpr(cbe, node.child2); - StringBuilder_Append(&cbe.sb, "]"); + var isArrayAccess: bool = false; + if node.child1 != null as *HirNode { + if node.child1.kind == hFieldPtr && String_Eq(node.child1.strValue, "data") { + isArrayAccess = true; + } + } + if isArrayAccess { + CBE_EmitExpr(cbe, node.child1.child1); + StringBuilder_Append(&cbe.sb, ".data[bux_index_check("); + CBE_EmitExpr(cbe, node.child2); + StringBuilder_Append(&cbe.sb, ", "); + CBE_EmitExpr(cbe, node.child1.child1); + StringBuilder_Append(&cbe.sb, ".len)]"); + } else { + CBE_EmitExpr(cbe, node.child1); + StringBuilder_Append(&cbe.sb, "["); + CBE_EmitExpr(cbe, node.child2); + StringBuilder_Append(&cbe.sb, "]"); + } return; } @@ -629,19 +620,28 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { StringBuilder_Append(&cbe.sb, ptrNode.strValue); return; } - // arrow field: load(arrow_field(base, field)) → base->field - if ptrKind == hArrowField { - CBE_EmitExpr(cbe, ptrNode.child1); - StringBuilder_Append(&cbe.sb, "->"); - StringBuilder_Append(&cbe.sb, ptrNode.strValue); - return; - } // index: load(index_ptr(base, idx)) → base[idx] + // For Array desugar pattern, emit bounds-checked access if ptrKind == hIndexPtr { - CBE_EmitExpr(cbe, ptrNode.child1); - StringBuilder_Append(&cbe.sb, "["); - CBE_EmitExpr(cbe, ptrNode.child2); - StringBuilder_Append(&cbe.sb, "]"); + var isArrayAccess: bool = false; + if ptrNode.child1 != null as *HirNode { + if ptrNode.child1.kind == hFieldPtr && String_Eq(ptrNode.child1.strValue, "data") { + isArrayAccess = true; + } + } + if isArrayAccess { + CBE_EmitExpr(cbe, ptrNode.child1.child1); + StringBuilder_Append(&cbe.sb, ".data[bux_index_check("); + CBE_EmitExpr(cbe, ptrNode.child2); + StringBuilder_Append(&cbe.sb, ", "); + CBE_EmitExpr(cbe, ptrNode.child1.child1); + StringBuilder_Append(&cbe.sb, ".len)]"); + } else { + CBE_EmitExpr(cbe, ptrNode.child1); + StringBuilder_Append(&cbe.sb, "["); + CBE_EmitExpr(cbe, ptrNode.child2); + StringBuilder_Append(&cbe.sb, "]"); + } return; } } @@ -727,6 +727,312 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { // Emit function declaration // --------------------------------------------------------------------------- +// Infer C type for a BuxFn mangled part (int, cstr, uint, void, ...) +func CBE_FatPartToC(part: String) -> String { + if String_Eq(part, "cstr") { return "const char*"; } + if String_Eq(part, "void") { return "void"; } + if String_Eq(part, "bool") { return "bool"; } + if String_Eq(part, "uint") { return "unsigned int"; } + if String_Eq(part, "float") { return "float"; } + if String_Eq(part, "double") || String_Eq(part, "float64") { return "double"; } + if String_EndsWith(part, "Ptr") { + let base: String = String_Slice(part, 0, String_Len(part) - 3); + return String_Concat(CBE_FatPartToC(base), "*"); + } + return part; // int, etc. +} + +// Emit typedefs for common BuxFn_* shapes (fat function pointers) +func CBE_EmitFatFuncTypedefs(cbe: *CEmitter, mod: *HirModule) { + StringBuilder_Append(&cbe.sb, "/* Fat function pointer types (code + env) */\n"); + // (int)->int + StringBuilder_Append(&cbe.sb, "typedef struct BuxFn_int_int {\n"); + StringBuilder_Append(&cbe.sb, " int (*code)(void* env, int a0);\n"); + StringBuilder_Append(&cbe.sb, " void* env;\n"); + StringBuilder_Append(&cbe.sb, "} BuxFn_int_int;\n"); + // (int,int)->int + StringBuilder_Append(&cbe.sb, "typedef struct BuxFn_int_int_int {\n"); + StringBuilder_Append(&cbe.sb, " int (*code)(void* env, int a0, int a1);\n"); + StringBuilder_Append(&cbe.sb, " void* env;\n"); + StringBuilder_Append(&cbe.sb, "} BuxFn_int_int_int;\n"); + // ()->void + StringBuilder_Append(&cbe.sb, "typedef struct BuxFn_void_void {\n"); + StringBuilder_Append(&cbe.sb, " void (*code)(void* env);\n"); + StringBuilder_Append(&cbe.sb, " void* env;\n"); + StringBuilder_Append(&cbe.sb, "} BuxFn_void_void;\n"); + // ()->int + StringBuilder_Append(&cbe.sb, "typedef struct BuxFn_int_void {\n"); + StringBuilder_Append(&cbe.sb, " int (*code)(void* env);\n"); + StringBuilder_Append(&cbe.sb, " void* env;\n"); + StringBuilder_Append(&cbe.sb, "} BuxFn_int_void;\n"); + // Scan module for any other BuxFn_* names + var i: int = 0; + while i < mod.funcCount { + CBE_MaybeEmitExtraFat(cbe, mod.funcs[i].retTypeName); + var p: int = 0; + while p < mod.funcs[i].paramCount { + var ptype: String = ""; + if p == 0 { ptype = mod.funcs[i].param0.typeName; } + else if p == 1 { ptype = mod.funcs[i].param1.typeName; } + else if p == 2 { ptype = mod.funcs[i].param2.typeName; } + else if p == 3 { ptype = mod.funcs[i].param3.typeName; } + else if p == 4 { ptype = mod.funcs[i].param4.typeName; } + else if p == 5 { ptype = mod.funcs[i].param5.typeName; } + else if p == 6 { ptype = mod.funcs[i].param6.typeName; } + else if p == 7 { ptype = mod.funcs[i].param7.typeName; } + else if p == 8 { ptype = mod.funcs[i].param8.typeName; } + CBE_MaybeEmitExtraFat(cbe, ptype); + p = p + 1; + } + i = i + 1; + } + StringBuilder_Append(&cbe.sb, "\n"); +} + +func CBE_MaybeEmitExtraFat(cbe: *CEmitter, name: String) { + if String_Eq(name, "") { return; } + if !String_StartsWith(name, "BuxFn_") { return; } + // Skip ones we already emit as built-ins + if String_Eq(name, "BuxFn_int_int") { return; } + if String_Eq(name, "BuxFn_int_int_int") { return; } + if String_Eq(name, "BuxFn_void_void") { return; } + if String_Eq(name, "BuxFn_int_void") { return; } + CBE_EmitOneFatTypedef(cbe, name); +} + +func CBE_CollectBuxFn(names: *String, count: *int, name: String) { + if String_Eq(name, "") { return; } + if !String_StartsWith(name, "BuxFn_") { return; } + if *count >= 64 { return; } + var i: int = 0; + while i < *count { + if String_Eq(names[i], name) { return; } + i = i + 1; + } + names[*count] = name; + *count = *count + 1; +} + +// BuxFn_ret_p0_p1 → typedef with code pointer +func CBE_EmitOneFatTypedef(cbe: *CEmitter, fatName: String) { + // Split fatName after "BuxFn_" into parts by '_' + let prefixLen: uint = 6; // "BuxFn_" + let rest: String = String_Slice(fatName, prefixLen, String_Len(fatName) - prefixLen); + // Parse parts: first = ret, rest = params (use simple split) + let partCount: uint = String_SplitCount(rest, "_"); + if partCount == 0 { return; } + let retPart: String = String_SplitPart(rest, "_", 0); + let retC: String = CBE_FatPartToC(retPart); + + StringBuilder_Append(&cbe.sb, "typedef struct "); + StringBuilder_Append(&cbe.sb, fatName); + StringBuilder_Append(&cbe.sb, " {\n "); + // code field: ret (*code)(void* env, params...) + StringBuilder_Append(&cbe.sb, retC); + StringBuilder_Append(&cbe.sb, " (*code)(void* env"); + var pi: uint = 1; + while pi < partCount { + let pPart: String = String_SplitPart(rest, "_", pi); + if !(pi == 1 && String_Eq(pPart, "void") && partCount == 2) { + StringBuilder_Append(&cbe.sb, ", "); + StringBuilder_Append(&cbe.sb, CBE_FatPartToC(pPart)); + } + pi = pi + 1; + } + StringBuilder_Append(&cbe.sb, ");\n void* env;\n} "); + StringBuilder_Append(&cbe.sb, fatName); + StringBuilder_Append(&cbe.sb, ";\n"); +} + +func CBE_EmitMakerDecl(cbe: *CEmitter, f: *HirFunc) { + // Infer fat type from thunk: skip __env, use user params + ret + var fatName: String = "BuxFn_"; + var retC: String = f.retTypeName; + if String_Eq(retC, "") { retC = "int"; } + fatName = String_Concat(fatName, Lcx_SanitizeFatPart(retC)); + var pi: int = 1; // skip __env + while pi < f.paramCount { + var ptype: String = "int"; + if pi == 1 { ptype = f.param1.typeName; } + else if pi == 2 { ptype = f.param2.typeName; } + else if pi == 3 { ptype = f.param3.typeName; } + else if pi == 4 { ptype = f.param4.typeName; } + else if pi == 5 { ptype = f.param5.typeName; } + else if pi == 6 { ptype = f.param6.typeName; } + else if pi == 7 { ptype = f.param7.typeName; } + else if pi == 8 { ptype = f.param8.typeName; } + if String_Eq(ptype, "") { ptype = "int"; } + fatName = String_Concat(fatName, "_"); + fatName = String_Concat(fatName, Lcx_SanitizeFatPart(ptype)); + pi = pi + 1; + } + if f.paramCount <= 1 { + fatName = String_Concat(fatName, "_void"); + } + StringBuilder_Append(&cbe.sb, fatName); + StringBuilder_Append(&cbe.sb, " __make_"); + StringBuilder_Append(&cbe.sb, f.name); + StringBuilder_Append(&cbe.sb, "("); + var ci: int = 0; + while ci < f.captureCount { + if ci > 0 { StringBuilder_Append(&cbe.sb, ", "); } + var capType: String = "int"; + var capName: String = "c"; + if ci == 0 { capName = f.captureName0; capType = CBackend_TypeToC(f.captureType0); } + else if ci == 1 { capName = f.captureName1; capType = CBackend_TypeToC(f.captureType1); } + else if ci == 2 { capName = f.captureName2; capType = CBackend_TypeToC(f.captureType2); } + else if ci == 3 { capName = f.captureName3; capType = CBackend_TypeToC(f.captureType3); } + else if ci == 4 { capName = f.captureName4; capType = CBackend_TypeToC(f.captureType4); } + else if ci == 5 { capName = f.captureName5; capType = CBackend_TypeToC(f.captureType5); } + else if ci == 6 { capName = f.captureName6; capType = CBackend_TypeToC(f.captureType6); } + else if ci == 7 { capName = f.captureName7; capType = CBackend_TypeToC(f.captureType7); } + StringBuilder_Append(&cbe.sb, capType); + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, capName); + ci = ci + 1; + } + StringBuilder_Append(&cbe.sb, ")"); +} + +func CBE_EmitMakerFunc(cbe: *CEmitter, f: *HirFunc) { + CBE_EmitMakerDecl(cbe, f); + StringBuilder_Append(&cbe.sb, " {\n"); + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, f.envStructName); + StringBuilder_Append(&cbe.sb, "* __e = ("); + StringBuilder_Append(&cbe.sb, f.envStructName); + StringBuilder_Append(&cbe.sb, "*)bux_alloc(sizeof("); + StringBuilder_Append(&cbe.sb, f.envStructName); + StringBuilder_Append(&cbe.sb, "));\n"); + var ci: int = 0; + while ci < f.captureCount { + var capName: String = ""; + if ci == 0 { capName = f.captureName0; } + else if ci == 1 { capName = f.captureName1; } + else if ci == 2 { capName = f.captureName2; } + else if ci == 3 { capName = f.captureName3; } + else if ci == 4 { capName = f.captureName4; } + else if ci == 5 { capName = f.captureName5; } + else if ci == 6 { capName = f.captureName6; } + else if ci == 7 { capName = f.captureName7; } + StringBuilder_Append(&cbe.sb, " __e->"); + StringBuilder_Append(&cbe.sb, capName); + StringBuilder_Append(&cbe.sb, " = "); + StringBuilder_Append(&cbe.sb, capName); + StringBuilder_Append(&cbe.sb, ";\n"); + ci = ci + 1; + } + // Build fat return type name same as decl + var fatName: String = "BuxFn_"; + var retC: String = f.retTypeName; + if String_Eq(retC, "") { retC = "int"; } + fatName = String_Concat(fatName, Lcx_SanitizeFatPart(retC)); + var pi: int = 1; + while pi < f.paramCount { + var ptype: String = "int"; + if pi == 1 { ptype = f.param1.typeName; } + else if pi == 2 { ptype = f.param2.typeName; } + else if pi == 3 { ptype = f.param3.typeName; } + else if pi == 4 { ptype = f.param4.typeName; } + else if pi == 5 { ptype = f.param5.typeName; } + else if pi == 6 { ptype = f.param6.typeName; } + else if pi == 7 { ptype = f.param7.typeName; } + else if pi == 8 { ptype = f.param8.typeName; } + if String_Eq(ptype, "") { ptype = "int"; } + fatName = String_Concat(fatName, "_"); + fatName = String_Concat(fatName, Lcx_SanitizeFatPart(ptype)); + pi = pi + 1; + } + if f.paramCount <= 1 { + fatName = String_Concat(fatName, "_void"); + } + StringBuilder_Append(&cbe.sb, " return ("); + StringBuilder_Append(&cbe.sb, fatName); + StringBuilder_Append(&cbe.sb, "){ .code = "); + StringBuilder_Append(&cbe.sb, f.name); + StringBuilder_Append(&cbe.sb, ", .env = __e };\n}\n\n"); +} + +// Emit adapters for any non-closure function (used when taken as value) +func CBE_EmitAllAdapters(cbe: *CEmitter, mod: *HirModule) { + StringBuilder_Append(&cbe.sb, "/* Fat-func adapters for named functions */\n"); + var i: int = 0; + while i < mod.funcCount { + let fname: String = mod.funcs[i].name; + // Skip closures, makers, adapters themselves + if String_StartsWith(fname, "__closure_") || String_StartsWith(fname, "__make_") || String_StartsWith(fname, "__adapt_") { + i = i + 1; + continue; + } + if CBE_FuncHasGeneric(&mod.funcs[i]) { + i = i + 1; + continue; + } + // Only emit adapter if function has body + if mod.funcs[i].body == null as *HirNode { + i = i + 1; + continue; + } + // Adapter signature: ret __adapt_F(void* env, params...) { return F(params); } + var retC: String = mod.funcs[i].retTypeName; + if String_Eq(retC, "") { retC = "void"; } + StringBuilder_Append(&cbe.sb, "static "); + StringBuilder_Append(&cbe.sb, retC); + StringBuilder_Append(&cbe.sb, " __adapt_"); + StringBuilder_Append(&cbe.sb, fname); + StringBuilder_Append(&cbe.sb, "(void* env"); + var p: int = 0; + while p < mod.funcs[i].paramCount { + // Skip if first param is already __env (shouldn't for named funcs) + var pname: String = ""; + var ptype: String = "int"; + if p == 0 { pname = mod.funcs[i].param0.name; ptype = mod.funcs[i].param0.typeName; } + else if p == 1 { pname = mod.funcs[i].param1.name; ptype = mod.funcs[i].param1.typeName; } + else if p == 2 { pname = mod.funcs[i].param2.name; ptype = mod.funcs[i].param2.typeName; } + else if p == 3 { pname = mod.funcs[i].param3.name; ptype = mod.funcs[i].param3.typeName; } + else if p == 4 { pname = mod.funcs[i].param4.name; ptype = mod.funcs[i].param4.typeName; } + else if p == 5 { pname = mod.funcs[i].param5.name; ptype = mod.funcs[i].param5.typeName; } + else if p == 6 { pname = mod.funcs[i].param6.name; ptype = mod.funcs[i].param6.typeName; } + else if p == 7 { pname = mod.funcs[i].param7.name; ptype = mod.funcs[i].param7.typeName; } + else if p == 8 { pname = mod.funcs[i].param8.name; ptype = mod.funcs[i].param8.typeName; } + if String_Eq(ptype, "") { ptype = "int"; } + StringBuilder_Append(&cbe.sb, ", "); + StringBuilder_Append(&cbe.sb, ptype); + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, pname); + p = p + 1; + } + StringBuilder_Append(&cbe.sb, ") {\n (void)env;\n"); + if String_Eq(retC, "void") { + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, fname); + StringBuilder_Append(&cbe.sb, "("); + } else { + StringBuilder_Append(&cbe.sb, " return "); + StringBuilder_Append(&cbe.sb, fname); + StringBuilder_Append(&cbe.sb, "("); + } + p = 0; + while p < mod.funcs[i].paramCount { + if p > 0 { StringBuilder_Append(&cbe.sb, ", "); } + var pname: String = ""; + if p == 0 { pname = mod.funcs[i].param0.name; } + else if p == 1 { pname = mod.funcs[i].param1.name; } + else if p == 2 { pname = mod.funcs[i].param2.name; } + else if p == 3 { pname = mod.funcs[i].param3.name; } + else if p == 4 { pname = mod.funcs[i].param4.name; } + else if p == 5 { pname = mod.funcs[i].param5.name; } + else if p == 6 { pname = mod.funcs[i].param6.name; } + else if p == 7 { pname = mod.funcs[i].param7.name; } + else if p == 8 { pname = mod.funcs[i].param8.name; } + StringBuilder_Append(&cbe.sb, pname); + p = p + 1; + } + StringBuilder_Append(&cbe.sb, ");\n}\n\n"); + i = i + 1; + } +} + func CBE_EmitFuncDecl(cbe: *CEmitter, f: *HirFunc) { // Return type if String_Eq(f.retTypeName, "") || String_Eq(f.retTypeName, "void") { @@ -961,7 +1267,12 @@ func CBackend_Generate(mod: *HirModule) -> String { StringBuilder_Append(&cbe.sb, "typedef char char8;\n\n"); // Runtime declarations StringBuilder_Append(&cbe.sb, "void* bux_alloc(unsigned int size);\n"); - StringBuilder_Append(&cbe.sb, "void bux_free(void* ptr);\n\n"); + StringBuilder_Append(&cbe.sb, "void bux_free(void* ptr);\n"); + StringBuilder_Append(&cbe.sb, "unsigned long long bux_index_check(unsigned long long index, unsigned long long len);\n"); + StringBuilder_Append(&cbe.sb, "int64 bux_add_i64_checked(int64 a, int64 b);\n"); + StringBuilder_Append(&cbe.sb, "int64 bux_sub_i64_checked(int64 a, int64 b);\n"); + StringBuilder_Append(&cbe.sb, "int64 bux_mul_i64_checked(int64 a, int64 b);\n"); + StringBuilder_Append(&cbe.sb, "int64 bux_neg_i64_checked(int64 a);\n\n"); // Forward declare all struct types (skip empty names) var si: int = 0; @@ -1121,12 +1432,53 @@ func CBackend_Generate(mod: *HirModule) -> String { si = si + 1; } + // Fat function-pointer typedefs (BuxFn_*) — before forward decls + CBE_EmitFatFuncTypedefs(cbe, mod); + + // Env structs for capturing closures (no static instance — heap per value) + var ei2: int = 0; + while ei2 < mod.funcCount { + if mod.funcs[ei2].captureCount > 0 && !String_Eq(mod.funcs[ei2].envStructName, "") { + StringBuilder_Append(&cbe.sb, "typedef struct "); + StringBuilder_Append(&cbe.sb, mod.funcs[ei2].envStructName); + StringBuilder_Append(&cbe.sb, " {\n"); + var ci2: int = 0; + while ci2 < mod.funcs[ei2].captureCount { + var capName: String = ""; + var capType: String = "int"; + if ci2 == 0 { capName = mod.funcs[ei2].captureName0; capType = CBackend_TypeToC(mod.funcs[ei2].captureType0); } + else if ci2 == 1 { capName = mod.funcs[ei2].captureName1; capType = CBackend_TypeToC(mod.funcs[ei2].captureType1); } + else if ci2 == 2 { capName = mod.funcs[ei2].captureName2; capType = CBackend_TypeToC(mod.funcs[ei2].captureType2); } + else if ci2 == 3 { capName = mod.funcs[ei2].captureName3; capType = CBackend_TypeToC(mod.funcs[ei2].captureType3); } + else if ci2 == 4 { capName = mod.funcs[ei2].captureName4; capType = CBackend_TypeToC(mod.funcs[ei2].captureType4); } + else if ci2 == 5 { capName = mod.funcs[ei2].captureName5; capType = CBackend_TypeToC(mod.funcs[ei2].captureType5); } + else if ci2 == 6 { capName = mod.funcs[ei2].captureName6; capType = CBackend_TypeToC(mod.funcs[ei2].captureType6); } + else if ci2 == 7 { capName = mod.funcs[ei2].captureName7; capType = CBackend_TypeToC(mod.funcs[ei2].captureType7); } + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, capType); + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, capName); + StringBuilder_Append(&cbe.sb, ";\n"); + ci2 = ci2 + 1; + } + StringBuilder_Append(&cbe.sb, "} "); + StringBuilder_Append(&cbe.sb, mod.funcs[ei2].envStructName); + StringBuilder_Append(&cbe.sb, ";\n\n"); + } + ei2 = ei2 + 1; + } + // Forward declarations for all functions (skip generics) var i: int = 0; while i < mod.funcCount { if !CBE_FuncHasGeneric(&mod.funcs[i]) { CBE_EmitFuncDecl(cbe, &mod.funcs[i]); StringBuilder_Append(&cbe.sb, ";\n"); + // Maker for capturing closures + if mod.funcs[i].captureCount > 0 { + CBE_EmitMakerDecl(cbe, &mod.funcs[i]); + StringBuilder_Append(&cbe.sb, ";\n"); + } } i = i + 1; } @@ -1141,6 +1493,9 @@ func CBackend_Generate(mod: *HirModule) -> String { } StringBuilder_Append(&cbe.sb, "\n"); + // Adapters before function bodies (bodies may take funcs as values) + CBE_EmitAllAdapters(cbe, mod); + // Function definitions (skip generics) var hasMain: bool = false; i = 0; @@ -1157,45 +1512,23 @@ func CBackend_Generate(mod: *HirModule) -> String { i = i + 1; continue; } - // Emit env struct and global instance for closures with captures - if mod.funcs[i].captureCount > 0 { - let envName: String = mod.funcs[i].envStructName; - let instName: String = mod.funcs[i].envInstanceName; - if !String_Eq(envName, "") { - // Emit struct definition - StringBuilder_Append(&cbe.sb, "struct "); - StringBuilder_Append(&cbe.sb, envName); - StringBuilder_Append(&cbe.sb, " {\n"); - var ci: int = 0; - while ci < mod.funcs[i].captureCount { - var capName: String = ""; - var capType: String = "int"; - if ci == 0 { capName = mod.funcs[i].captureName0; capType = CBackend_TypeToC(mod.funcs[i].captureType0); } - else if ci == 1 { capName = mod.funcs[i].captureName1; capType = CBackend_TypeToC(mod.funcs[i].captureType1); } - else if ci == 2 { capName = mod.funcs[i].captureName2; capType = CBackend_TypeToC(mod.funcs[i].captureType2); } - else if ci == 3 { capName = mod.funcs[i].captureName3; capType = CBackend_TypeToC(mod.funcs[i].captureType3); } - else if ci == 4 { capName = mod.funcs[i].captureName4; capType = CBackend_TypeToC(mod.funcs[i].captureType4); } - else if ci == 5 { capName = mod.funcs[i].captureName5; capType = CBackend_TypeToC(mod.funcs[i].captureType5); } - else if ci == 6 { capName = mod.funcs[i].captureName6; capType = CBackend_TypeToC(mod.funcs[i].captureType6); } - else if ci == 7 { capName = mod.funcs[i].captureName7; capType = CBackend_TypeToC(mod.funcs[i].captureType7); } - StringBuilder_Append(&cbe.sb, " "); - StringBuilder_Append(&cbe.sb, capType); - StringBuilder_Append(&cbe.sb, " "); - StringBuilder_Append(&cbe.sb, capName); - StringBuilder_Append(&cbe.sb, ";\n"); - ci = ci + 1; - } - StringBuilder_Append(&cbe.sb, "};\n"); - // Emit global instance - StringBuilder_Append(&cbe.sb, "static struct "); - StringBuilder_Append(&cbe.sb, envName); - StringBuilder_Append(&cbe.sb, " "); - StringBuilder_Append(&cbe.sb, instName); - StringBuilder_Append(&cbe.sb, ";\n\n"); - } - } CBE_EmitFuncDecl(cbe, &mod.funcs[i]); StringBuilder_Append(&cbe.sb, " {\n"); + // Capturing closure thunk: materialize env from fat-func env pointer + if mod.funcs[i].captureCount > 0 && !String_Eq(mod.funcs[i].envStructName, "") && !String_Eq(mod.funcs[i].envInstanceName, "") { + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, mod.funcs[i].envStructName); + StringBuilder_Append(&cbe.sb, " "); + StringBuilder_Append(&cbe.sb, mod.funcs[i].envInstanceName); + StringBuilder_Append(&cbe.sb, " = *(("); + StringBuilder_Append(&cbe.sb, mod.funcs[i].envStructName); + StringBuilder_Append(&cbe.sb, "*)__env);\n"); + } else if mod.funcs[i].paramCount > 0 { + // Capture-less closure still has __env + if String_Eq(mod.funcs[i].param0.name, "__env") { + StringBuilder_Append(&cbe.sb, " (void)__env;\n"); + } + } // Body cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.deferCount = 0; @@ -1216,6 +1549,10 @@ func CBackend_Generate(mod: *HirModule) -> String { StringBuilder_Append(&cbe.sb, " return 0;\n"); } StringBuilder_Append(&cbe.sb, "\n}\n\n"); + // After capturing closure thunk, emit heap-env maker + if mod.funcs[i].captureCount > 0 { + CBE_EmitMakerFunc(cbe, &mod.funcs[i]); + } i = i + 1; } diff --git a/src/cli.bux b/src/cli.bux index 6a0ba55..452ccf6 100644 --- a/src/cli.bux +++ b/src/cli.bux @@ -55,12 +55,44 @@ func Diagnostic_GetLine(path: String, lineNum: uint32) -> String { return bux_str_split_part(content, "\n", lineNum - 1); } +/* Simple substring check for help hints */ +func Diagnostic_MsgContains(msg: String, needle: String) -> bool { + return bux_str_contains(msg, needle) != 0; +} + +/* Actionable help for common error messages */ +func Diagnostic_Hint(msg: String) -> String { + if Diagnostic_MsgContains(msg, "cannot assign") { + return "ensure the right-hand side type matches the left-hand side"; + } + if Diagnostic_MsgContains(msg, "undeclared identifier") { + return "check the spelling, or import the symbol from the right module"; + } + if Diagnostic_MsgContains(msg, "too few arguments") { + return "compare the call with the function's parameter list"; + } + if Diagnostic_MsgContains(msg, "too many arguments") { + return "compare the call with the function's parameter list"; + } + if Diagnostic_MsgContains(msg, "use of moved value") { + return "the value was moved; clone it or restructure ownership"; + } + if Diagnostic_MsgContains(msg, "expected expression") { + return "the previous statement may be incomplete (missing value or ';')"; + } + if Diagnostic_MsgContains(msg, "duplicate symbol") { + return "rename one of the definitions or remove the duplicate"; + } + return ""; +} + /* Print a diagnostic in Rust-style format: * error: * --> :: * | * 42 | * | ^ + * = help: */ func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) { /* Severity prefix */ @@ -94,14 +126,57 @@ func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) { Print(" | "); PrintLine(lineText); - /* Underline */ + /* Underline (multi-char for identifiers/string tokens) */ Print(" | "); var i: uint32 = 0; while i < diag.column - 1 && i < 120 { Print(" "); i = i + 1; } - PrintLine("^"); + /* Estimate token length from the source line */ + var ulen: uint = 1; + let col0: uint = diag.column - 1; + let lineLen: uint = String_Len(lineText); + if col0 < lineLen { + let first: String = String_Chars(lineText, col0); + if String_Eq(first, "\"") || String_Eq(first, "`") || String_Eq(first, "'") { + var j: uint = col0 + 1; + while j < lineLen { + let cj: String = String_Chars(lineText, j); + if String_Eq(cj, first) { + ulen = j - col0 + 1; + break; + } + j = j + 1; + } + } else { + var j: uint = col0; + while j < lineLen { + let cj: String = String_Chars(lineText, j); + if String_Contains("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_", cj) { + j = j + 1; + } else { + break; + } + } + if j > col0 { + ulen = j - col0; + } + } + } + var k: uint = 0; + while k < ulen { + Print("^"); + k = k + 1; + } + PrintLine(""); + } + + /* Helpful hint when we recognize the error */ + let hint: String = Diagnostic_Hint(diag.message); + if !String_Eq(hint, "") { + Print(" = help: "); + PrintLine(hint); } } diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 64e3bd4..c6e4db3 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -43,30 +43,7 @@ struct LowerCtx { // --------------------------------------------------------------------------- func Lcx_ResolveTypeKindFromName(name: String) -> int { - if String_Eq(name, "void") { return tyVoid; } - if String_Eq(name, "bool") { return tyBool; } - if String_Eq(name, "bool8") { return tyBool8; } - if String_Eq(name, "bool16") { return tyBool16; } - if String_Eq(name, "bool32") { return tyBool32; } - if String_Eq(name, "char8") { return tyChar8; } - if String_Eq(name, "char16") { return tyChar16; } - if String_Eq(name, "char32") { return tyChar32; } - if String_Eq(name, "String") { return tyStr; } - if String_Eq(name, "str") { return tyStr; } - if String_Eq(name, "int8") { return tyInt8; } - if String_Eq(name, "int16") { return tyInt16; } - if String_Eq(name, "int32") { return tyInt32; } - if String_Eq(name, "int64") { return tyInt64; } - if String_Eq(name, "int") { return tyInt; } - if String_Eq(name, "uint8") { return tyUInt8; } - if String_Eq(name, "uint16") { return tyUInt16; } - if String_Eq(name, "uint32") { return tyUInt32; } - if String_Eq(name, "uint64") { return tyUInt64; } - if String_Eq(name, "uint") { return tyUInt; } - if String_Eq(name, "float32") { return tyFloat32; } - if String_Eq(name, "float64") { return tyFloat64; } - if String_Eq(name, "float") { return tyFloat64; } - return tyNamed; + return Type_FromName(name); } func Lcx_TypeKindToName(kind: int) -> String { @@ -178,38 +155,58 @@ func Lcx_SubstituteType(ctx: *LowerCtx, te: *TypeExpr) -> *TypeExpr { return te; } -// Build C function-pointer type string from a tekFunc TypeExpr, e.g. "int (*)(int)" -func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String { - if te == null as *TypeExpr || te.kind != tekFunc { return "void (*)(void)"; } - var retName: String = "void"; - if te.funcRet != null as *TypeExpr { - if te.funcRet.kind == tekPointer && te.funcRet.pointerPointee != null as *TypeExpr { - retName = String_Concat(te.funcRet.pointerPointee.typeName, "*"); - } else { - retName = te.funcRet.typeName; - } - if String_Eq(retName, "") { retName = "int"; } +// Sanitize a C type fragment for use inside BuxFn_* mangled names +func Lcx_SanitizeFatPart(s: String) -> String { + var r: String = s; + if String_Eq(r, "String") || String_Eq(r, "str") || String_Eq(r, "const char*") { + return "cstr"; } - var result: String = retName; - result = String_Concat(result, " (*)("); + if String_Eq(r, "unsigned int") { return "uint"; } + // crude replacements for * and spaces + r = String_ReplaceAll(r, "*", "Ptr"); + r = String_ReplaceAll(r, " ", "_"); + r = String_ReplaceAll(r, "(", ""); + r = String_ReplaceAll(r, ")", ""); + r = String_ReplaceAll(r, ",", "_"); + if String_Eq(r, "") { return "int"; } + return r; +} + +func Lcx_TypeExprFatPart(te: *TypeExpr) -> String { + if te == null as *TypeExpr { return "void"; } + if te.kind == tekPointer && te.pointerPointee != null as *TypeExpr { + return Lcx_SanitizeFatPart(String_Concat(te.pointerPointee.typeName, "Ptr")); + } + if te.kind == tekFunc { + return Lcx_SanitizeFatPart(Lcx_BuildFuncTypeName(te)); + } + var n: String = te.typeName; + if String_Eq(n, "") { n = "int"; } + return Lcx_SanitizeFatPart(n); +} + +// Fat function-pointer type name: BuxFn___... +// Enables multi-instance closures (code + env). +func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String { + if te == null as *TypeExpr || te.kind != tekFunc { + return "BuxFn_void_void"; + } + var retPart: String = "void"; + if te.funcRet != null as *TypeExpr { + retPart = Lcx_TypeExprFatPart(te.funcRet); + } + var result: String = String_Concat("BuxFn_", retPart); var cur: *TypeExprList = te.funcParams; - var first: bool = true; + var anyParam: bool = false; while cur != null as *TypeExprList { - if !first { - result = String_Concat(result, ", "); - } - var pName: String = "int"; - if cur.te.kind == tekPointer && cur.te.pointerPointee != null as *TypeExpr { - pName = String_Concat(cur.te.pointerPointee.typeName, "*"); - } else { - pName = cur.te.typeName; - } - if String_Eq(pName, "") { pName = "int"; } - result = String_Concat(result, pName); - first = false; + result = String_Concat(result, "_"); + result = String_Concat(result, Lcx_TypeExprFatPart(cur.te)); + anyParam = true; cur = cur.next; } - result = String_Concat(result, ")"); + if !anyParam { + result = String_Concat(result, "_void"); + } return result; } @@ -469,9 +466,44 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { return n; } } + let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue); + // Named function used as a value → fat pointer via __adapt_ wrapper + if sym.kind == skFunc { + var fatName: String = "BuxFn_int_int"; + if sym.refType != null as *TypeExpr && sym.refType.kind == tekFunc { + fatName = Lcx_BuildFuncTypeName(sym.refType); + } else if !String_Eq(sym.typeName, "") && String_StartsWith(sym.typeName, "BuxFn_") { + fatName = sym.typeName; + } + n.kind = hStructInit; + n.strValue = fatName; + n.typeKind = tyFunc; + n.typeName = fatName; + let codeField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + codeField.kind = hBlock; + codeField.strValue = "code"; + let codeVal: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + codeVal.kind = hVar; + codeVal.strValue = String_Concat("__adapt_", expr.strValue); + codeField.child1 = codeVal; + let envField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + envField.kind = hBlock; + envField.strValue = "env"; + let nullEnv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + nullEnv.kind = hCast; + nullEnv.typeName = "void*"; + let zero: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + zero.kind = hLit; + zero.intValue = tkIntLiteral; + zero.strValue = "0"; + nullEnv.child1 = zero; + envField.child1 = nullEnv; + codeField.child3 = envField; + n.child1 = codeField; + return n; + } n.kind = hVar; n.strValue = expr.strValue; - let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue); n.typeKind = sym.typeKind; if expr.refType != null as *TypeExpr { @@ -566,6 +598,30 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { } } + // Overflow checking: in @[Checked] mode, lower +, -, * on signed integers to checked calls + if ctx.checkedFunc && !ctx.releaseFunc { + var opKind: int = expr.intValue; + var isArithOp: bool = opKind == tkPlus || opKind == tkMinus || opKind == tkStar; + var isSignedInt: bool = false; + if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr { + let lhsKind: int = Lcx_ResolveTypeKind(expr.child1.refType); + isSignedInt = Type_IsSigned(lhsKind); + } + if isArithOp && isSignedInt { + var checkedFunc: String = ""; + if opKind == tkPlus { checkedFunc = "bux_add_i64_checked"; } + else if opKind == tkMinus { checkedFunc = "bux_sub_i64_checked"; } + else if opKind == tkStar { checkedFunc = "bux_mul_i64_checked"; } + if !String_Eq(checkedFunc, "") { + n.kind = hCall; + n.strValue = checkedFunc; + n.child1 = Lcx_LowerExpr(ctx, expr.child1); + n.child2 = Lcx_LowerExpr(ctx, expr.child2); + return n; + } + } + } + n.kind = hBinary; n.intValue = expr.intValue; // operator n.child1 = Lcx_LowerExpr(ctx, expr.child1); @@ -575,6 +631,20 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { // Unary if kind == ekUnary { + // Overflow checking: in @[Checked] mode, lower negation on signed integers to checked call + if ctx.checkedFunc && !ctx.releaseFunc && expr.intValue == tkMinus { + var isSignedInt: bool = false; + if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr { + let operandKind: int = Lcx_ResolveTypeKind(expr.child1.refType); + isSignedInt = Type_IsSigned(operandKind); + } + if isSignedInt { + n.kind = hCall; + n.strValue = "bux_neg_i64_checked"; + n.child1 = Lcx_LowerExpr(ctx, expr.child1); + return n; + } + } n.kind = hUnary; n.intValue = expr.intValue; n.child1 = Lcx_LowerExpr(ctx, expr.child1); @@ -1132,20 +1202,85 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { return n; } - // Closure: generate function and return address-of + // Closure: fat function pointer (multi-instance via heap env + maker) if kind == ekClosure { let f: *HirFunc = Lcx_LowerClosureFunc(ctx, expr); - n.kind = hUnary; - n.intValue = tkAmp; - let varNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - varNode.kind = hVar; - varNode.strValue = f.name; - varNode.typeKind = tyFunc; - n.child1 = varNode; - n.typeKind = tyFunc; - if expr.refType != null as *TypeExpr { - n.typeName = Lcx_BuildFuncTypeName(expr.refType); + var fatName: String = "BuxFn_int_int"; + if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc { + fatName = Lcx_BuildFuncTypeName(expr.refType); + } else if f.paramCount >= 2 { + // thunk has __env + user params; approximate from ret + user arity + fatName = "BuxFn_int_int"; + if f.paramCount == 3 { fatName = "BuxFn_int_int_int"; } + if f.paramCount == 1 { fatName = "BuxFn_int_void"; } } + if f.captureCount > 0 { + // Call __make_(captures...) which heap-allocs env + n.kind = hCall; + n.strValue = String_Concat("__make_", f.name); + n.typeKind = tyFunc; + n.typeName = fatName; + var ci: int = 0; + while ci < f.captureCount { + var capName: String = ""; + if ci == 0 { capName = f.captureName0; } + else if ci == 1 { capName = f.captureName1; } + else if ci == 2 { capName = f.captureName2; } + else if ci == 3 { capName = f.captureName3; } + else if ci == 4 { capName = f.captureName4; } + else if ci == 5 { capName = f.captureName5; } + else if ci == 6 { capName = f.captureName6; } + else if ci == 7 { capName = f.captureName7; } + let capVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + capVar.kind = hVar; + capVar.strValue = capName; + if ci == 0 { n.child1 = capVar; } + else if ci == 1 { n.child2 = capVar; } + else if ci == 2 { + let firstExtra: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList; + firstExtra.node = capVar; + firstExtra.next = null as *HirArgList; + n.extraData = firstExtra as *void; + n.extraCount = 1; + } else { + var cur: *HirArgList = n.extraData as *HirArgList; + while cur.next != null as *HirArgList { cur = cur.next; } + let newNode: *HirArgList = bux_alloc(sizeof(HirArgList)) as *HirArgList; + newNode.node = capVar; + newNode.next = null as *HirArgList; + cur.next = newNode; + n.extraCount = n.extraCount + 1; + } + ci = ci + 1; + } + return n; + } + // Capture-less: compound literal fat pointer with NULL env + n.kind = hStructInit; + n.strValue = fatName; + n.typeKind = tyFunc; + n.typeName = fatName; + let codeField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + codeField.kind = hBlock; + codeField.strValue = "code"; + let codeVal: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + codeVal.kind = hVar; + codeVal.strValue = f.name; + codeField.child1 = codeVal; + let envField: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + envField.kind = hBlock; + envField.strValue = "env"; + let nullEnv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + nullEnv.kind = hCast; + nullEnv.typeName = "void*"; + let zero: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + zero.kind = hLit; + zero.intValue = tkIntLiteral; + zero.strValue = "0"; + nullEnv.child1 = zero; + envField.child1 = nullEnv; + codeField.child3 = envField; + n.child1 = codeField; return n; } @@ -1227,6 +1362,25 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { return n; } + // Block expression (boolValue = true means unsafe block) + if kind == ekBlock { + if expr.refBlock != null as *Block { + if expr.boolValue { + let oldChecked: bool = ctx.checkedFunc; + let oldRelease: bool = ctx.releaseFunc; + ctx.checkedFunc = false; + ctx.releaseFunc = false; + let blockNode: *HirNode = Lcx_LowerBlock(ctx, expr.refBlock, -1); + ctx.checkedFunc = oldChecked; + ctx.releaseFunc = oldRelease; + return blockNode; + } else { + return Lcx_LowerBlock(ctx, expr.refBlock, -1); + } + } + return n; + } + return n; } @@ -1461,71 +1615,8 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode { } } - // If init is a closure with captures, emit capture assignments before the let - if stmt.child1 != null as *Expr && stmt.child1.kind == ekClosure && stmt.child1.captureCount > 0 { - let closureIdx: int = ctx.funcCount - 1; - let envInst: String = String_Concat("__closure_env_instance_", String_FromInt(closureIdx)); - // Build a block: capture assignments + let store - let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - blockNode.kind = hBlock; - blockNode.line = line; - blockNode.column = col; - var firstStmt: *HirNode = null as *HirNode; - var lastStmt: *HirNode = null as *HirNode; - var ci: int = 0; - while ci < stmt.child1.captureCount { - var capName: String = ""; - if ci == 0 { capName = stmt.child1.captureName0; } - else if ci == 1 { capName = stmt.child1.captureName1; } - else if ci == 2 { capName = stmt.child1.captureName2; } - else if ci == 3 { capName = stmt.child1.captureName3; } - else if ci == 4 { capName = stmt.child1.captureName4; } - else if ci == 5 { capName = stmt.child1.captureName5; } - else if ci == 6 { capName = stmt.child1.captureName6; } - else if ci == 7 { capName = stmt.child1.captureName7; } - // Build: envInst.capName = capName; - let assignNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - assignNode.kind = hAssign; - assignNode.line = line; - assignNode.column = col; - let fieldNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - fieldNode.kind = hFieldAccess; - fieldNode.strValue = capName; - let baseNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - baseNode.kind = hVar; - baseNode.strValue = envInst; - fieldNode.child1 = baseNode; - let valNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - valNode.kind = hVar; - valNode.strValue = capName; - assignNode.child1 = fieldNode; - assignNode.child2 = valNode; - if firstStmt == null as *HirNode { - firstStmt = assignNode; - lastStmt = assignNode; - } else { - lastStmt.child3 = assignNode; - lastStmt = assignNode; - } - ci = ci + 1; - } - // Append the let store - if firstStmt == null as *HirNode { - firstStmt = storeNode; - lastStmt = storeNode; - } else { - lastStmt.child3 = storeNode; - lastStmt = storeNode; - } - // Append auto-Drop defer if present - if deferNode != null as *HirNode { - lastStmt.child3 = deferNode; - lastStmt = deferNode; - } - blockNode.child1 = firstStmt; - return blockNode; - } - // Non-closure: wrap with defer if present + // Capturing closures allocate env via __make_* at ekClosure site. + // Wrap with defer if present if deferNode != null as *HirNode { storeNode.child3 = deferNode; let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; @@ -2244,8 +2335,16 @@ func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc { let retTe: *TypeExpr = Lcx_SubstituteType(ctx, decl.retType); if retTe != null as *TypeExpr { - f.retTypeName = retTe.typeName; f.retTypeKind = Lcx_ResolveTypeKind(retTe); + if retTe.kind == tekFunc { + f.retTypeName = Lcx_BuildFuncTypeName(retTe); + } else if !String_Eq(retTe.typeName, "") { + f.retTypeName = retTe.typeName; + } else if retTe.kind == tekPointer && retTe.pointerPointee != null as *TypeExpr { + f.retTypeName = String_Concat(retTe.pointerPointee.typeName, "*"); + } else { + f.retTypeName = ""; + } } else { f.retTypeName = ""; f.retTypeKind = 0; @@ -2320,25 +2419,36 @@ func Lcx_LowerClosureFunc(ctx: *LowerCtx, expr: *Expr) -> *HirFunc { f.isPublic = false; let params: *Decl = expr.closureParams; - if params != null as *Decl { - f.paramCount = params.paramCount; - if params.paramCount > 0 { f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param0, ¶ms.param0, ctx); } - if params.paramCount > 1 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, ¶ms.param1, ctx); } - if params.paramCount > 2 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, ¶ms.param2, ctx); } - if params.paramCount > 3 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, ¶ms.param3, ctx); } - if params.paramCount > 4 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, ¶ms.param4, ctx); } - if params.paramCount > 5 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, ¶ms.param5, ctx); } - if params.paramCount > 6 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, ¶ms.param6, ctx); } - if params.paramCount > 7 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, ¶ms.param7, ctx); } - if params.paramCount > 8 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, ¶ms.param8, ctx); } - } + // Fat-func ABI: leading void* __env, then user params + var userCount: int = 0; + if params != null as *Decl { userCount = params.paramCount; } + f.paramCount = userCount + 1; + f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; + f.param0.name = "__env"; + f.param0.typeKind = tyPointer; + f.param0.typeName = "void*"; + if userCount > 0 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, ¶ms.param0, ctx); } + if userCount > 1 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, ¶ms.param1, ctx); } + if userCount > 2 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, ¶ms.param2, ctx); } + if userCount > 3 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, ¶ms.param3, ctx); } + if userCount > 4 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, ¶ms.param4, ctx); } + if userCount > 5 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, ¶ms.param5, ctx); } + if userCount > 6 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, ¶ms.param6, ctx); } + if userCount > 7 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, ¶ms.param7, ctx); } - if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc && expr.refType.funcRet != null as *TypeExpr { - f.retTypeName = expr.refType.funcRet.typeName; - f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet); + if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc { + // Return type of the *thunk* is the closure's return type (not the fat type) + if expr.refType.funcRet != null as *TypeExpr { + f.retTypeName = expr.refType.funcRet.typeName; + if String_Eq(f.retTypeName, "") { f.retTypeName = "int"; } + f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet); + } else { + f.retTypeName = "void"; + f.retTypeKind = tyVoid; + } } else { - f.retTypeName = ""; - f.retTypeKind = 0; + f.retTypeName = "int"; + f.retTypeKind = tyInt; } // Copy capture metadata from AST diff --git a/src/lexer.bux b/src/lexer.bux index 3e3663d..883eeb6 100644 --- a/src/lexer.bux +++ b/src/lexer.bux @@ -277,6 +277,7 @@ func lexKeywordKind(text: String) -> int { if String_Eq(text, "switch") { return tkSwitch; } if String_Eq(text, "case") { return tkCase; } if String_Eq(text, "default") { return tkDefault; } + if String_Eq(text, "unsafe") { return tkUnsafe; } if String_Eq(text, "async") { return tkAsync; } if String_Eq(text, "await") { return tkAwait; } if String_Eq(text, "spawn") { return tkSpawn; } diff --git a/src/parser.bux b/src/parser.bux index 269927a..cd8b524 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -399,6 +399,15 @@ func parserParsePrimary(p: *Parser) -> *Expr { return parserParseClosure(p); } + // unsafe { ... } — unsafe block expression + if kind == tkUnsafe { + discard parserAdvance(p); + let e: *Expr = parserMakeExpr(ekBlock, line, col); + e.boolValue = true; // marks this block as unsafe + e.refBlock = parserParseBlock(p); + return e; + } + parserEmitDiag(p, line, col, "expected expression"); return parserMakeExpr(ekLiteral, line, col); } diff --git a/src/sema.bux b/src/sema.bux index 73ae7fb..98abfd2 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -136,7 +136,6 @@ func Sema_BuildFuncTypeExprFromDecl(decl: *Decl) -> *TypeExpr { func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int { if te == null as *TypeExpr { return tyUnknown; } - let name: String = te.typeName; if te.kind == tekPointer { return tyPointer; @@ -146,33 +145,7 @@ func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int { return tyFunc; } - if String_Eq(name, "void") { return tyVoid; } - if String_Eq(name, "bool") { return tyBool; } - if String_Eq(name, "bool8") { return tyBool8; } - if String_Eq(name, "bool16") { return tyBool16; } - if String_Eq(name, "bool32") { return tyBool32; } - if String_Eq(name, "char8") { return tyChar8; } - if String_Eq(name, "char16") { return tyChar16; } - if String_Eq(name, "char32") { return tyChar32; } - if String_Eq(name, "String") { return tyStr; } - if String_Eq(name, "str") { return tyStr; } - if String_Eq(name, "int8") { return tyInt8; } - if String_Eq(name, "int16") { return tyInt16; } - if String_Eq(name, "int32") { return tyInt32; } - if String_Eq(name, "int64") { return tyInt64; } - if String_Eq(name, "int") { return tyInt; } - if String_Eq(name, "uint8") { return tyUInt8; } - if String_Eq(name, "uint16") { return tyUInt16; } - if String_Eq(name, "uint32") { return tyUInt32; } - if String_Eq(name, "uint64") { return tyUInt64; } - if String_Eq(name, "uint") { return tyUInt; } - if String_Eq(name, "float32") { return tyFloat32; } - if String_Eq(name, "float64") { return tyFloat64; } - if String_Eq(name, "float") { return tyFloat64; } - - // Type resolution uses scope-based lookup via Sema_CollectGlobals - // TODO: add StringMap-based type table for faster named-type validation - return tyNamed; + return Type_FromName(te.typeName); } // --------------------------------------------------------------------------- @@ -758,10 +731,17 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { return tyUnknown; } - // Block expression + // Block expression (boolValue = true means unsafe block) if kind == ekBlock { if expr.refBlock != null as *Block { - Sema_CheckBlock(sema, expr.refBlock); + 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); + } } return tyVoid; } diff --git a/src/token.bux b/src/token.bux index e76c30d..5dc3cc7 100644 --- a/src/token.bux +++ b/src/token.bux @@ -142,6 +142,7 @@ const tkDefer: int = 106; const tkSwitch: int = 107; const tkCase: int = 108; const tkDefault: int = 109; +const tkUnsafe: int = 110; // --------------------------------------------------------------------------- // Token struct @@ -165,6 +166,7 @@ func Token_IsKeyword(kind: int) -> bool { if kind >= tkFunc && kind <= tkExtern { return true; } if kind >= tkAs && kind <= tkSuper { return true; } if kind == tkSizeOf { return true; } + if kind >= tkDefer && kind <= tkUnsafe { return true; } if kind >= tkAsync && kind <= tkSpawn { return true; } return false; } @@ -215,6 +217,15 @@ func Token_KeywordKind(text: String) -> int { if String_Eq(text, "self") { return tkSelf; } if String_Eq(text, "super") { return tkSuper; } if String_Eq(text, "sizeof") { return tkSizeOf; } + if String_Eq(text, "defer") { return tkDefer; } + if String_Eq(text, "switch") { return tkSwitch; } + if String_Eq(text, "case") { return tkCase; } + if String_Eq(text, "default") { return tkDefault; } + if String_Eq(text, "unsafe") { return tkUnsafe; } + if String_Eq(text, "discard") { return tkDiscard; } + 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, "true") { return tkBoolLiteral; } if String_Eq(text, "false") { return tkBoolLiteral; } return tkIdent; @@ -259,6 +270,15 @@ func Token_KindName(kind: int) -> String { if kind == tkNull { return "null"; } if kind == tkSelf { return "self"; } if kind == tkSuper { return "super"; } + if kind == tkUnsafe { return "unsafe"; } + if kind == tkDefer { return "defer"; } + if kind == tkSwitch { return "switch"; } + if kind == tkCase { return "case"; } + if kind == tkDefault { return "default"; } + if kind == tkAsync { return "async"; } + if kind == tkAwait { return "await"; } + if kind == tkSpawn { return "spawn"; } + if kind == tkDiscard { return "discard"; } if kind == tkLParen { return "("; } if kind == tkRParen { return ")"; } if kind == tkLBrace { return "{"; } diff --git a/src/types.bux b/src/types.bux index 281fdc7..197db0f 100644 --- a/src/types.bux +++ b/src/types.bux @@ -186,4 +186,94 @@ func Type_ToString(t: Type) -> String { if t.kind == tyFunc { return t.name; } return "?"; } + +// --------------------------------------------------------------------------- +// Type_FromName — central type-name → kind mapping (used by sema, hir_lower) +// --------------------------------------------------------------------------- + +func Type_FromName(name: String) -> int { + if String_Eq(name, "void") { return tyVoid; } + if String_Eq(name, "bool") { return tyBool; } + if String_Eq(name, "bool8") { return tyBool8; } + if String_Eq(name, "bool16") { return tyBool16; } + if String_Eq(name, "bool32") { return tyBool32; } + if String_Eq(name, "char8") { return tyChar8; } + if String_Eq(name, "char16") { return tyChar16; } + if String_Eq(name, "char32") { return tyChar32; } + if String_Eq(name, "String") { return tyStr; } + if String_Eq(name, "str") { return tyStr; } + if String_Eq(name, "int8") { return tyInt8; } + if String_Eq(name, "int16") { return tyInt16; } + if String_Eq(name, "int32") { return tyInt32; } + if String_Eq(name, "int64") { return tyInt64; } + if String_Eq(name, "int") { return tyInt; } + if String_Eq(name, "uint8") { return tyUInt8; } + if String_Eq(name, "uint16") { return tyUInt16; } + if String_Eq(name, "uint32") { return tyUInt32; } + if String_Eq(name, "uint64") { return tyUInt64; } + if String_Eq(name, "uint") { return tyUInt; } + if String_Eq(name, "float32") { return tyFloat32; } + if String_Eq(name, "float64") { return tyFloat64; } + if String_Eq(name, "float") { return tyFloat64; } + return tyNamed; +} + +// --------------------------------------------------------------------------- +// Type_ToCName — type kind → C type name (used by C backend) +// --------------------------------------------------------------------------- + +func Type_ToCName(kind: int) -> String { + if kind == tyVoid { return "void"; } + if kind == tyBool || kind == tyBool8 || kind == tyBool16 || kind == tyBool32 { return "bool"; } + if kind == tyChar8 { return "char"; } + if kind == tyChar16 { return "uint16"; } + if kind == tyChar32 { return "uint32"; } + if kind == tyStr { return "String"; } + if kind == tyInt8 { return "int8"; } + if kind == tyInt16 { return "int16"; } + if kind == tyInt32 { return "int32"; } + if kind == tyInt64 { return "int64"; } + if kind == tyInt { return "int"; } + if kind == tyUInt8 { return "uint8"; } + if kind == tyUInt16 { return "uint16"; } + if kind == tyUInt32 { return "uint32"; } + if kind == tyUInt64 { return "uint64"; } + if kind == tyUInt { return "uint"; } + if kind == tyFloat32 { return "float32"; } + if kind == tyFloat64 { return "float64"; } + if kind == tyPointer { return "void*"; } + // Fat function pointer — concrete BuxFn_* name comes from typeName field + if kind == tyFunc { return "BuxFn"; } + return ""; +} + +// --------------------------------------------------------------------------- +// Signed / Unsigned / Float predicates +// --------------------------------------------------------------------------- + +func Type_IsSigned(kind: int) -> bool { + return kind == tyInt8 || kind == tyInt16 || kind == tyInt32 || kind == tyInt64 || kind == tyInt; +} + +func Type_IsUnsigned(kind: int) -> bool { + return kind == tyUInt8 || kind == tyUInt16 || kind == tyUInt32 || kind == tyUInt64 || kind == tyUInt; +} + +func Type_IsFloat(kind: int) -> bool { + return kind == tyFloat32 || kind == tyFloat64; +} + +// --------------------------------------------------------------------------- +// Type_SizeOf — byte size of a primitive type (0 for non-primitive) +// --------------------------------------------------------------------------- + +func Type_SizeOf(kind: int) -> int { + if kind == tyBool || kind == tyBool8 || kind == tyChar8 || kind == tyInt8 || kind == tyUInt8 { return 1; } + if kind == tyBool16 || kind == tyChar16 || kind == tyInt16 || kind == tyUInt16 { return 2; } + if kind == tyBool32 || kind == tyChar32 || kind == tyInt32 || kind == tyUInt32 || kind == tyFloat32 { return 4; } + if kind == tyInt64 || kind == tyUInt64 || kind == tyFloat64 { return 8; } + if kind == tyInt || kind == tyUInt { return 8; } + if kind == tyPointer { return 8; } + return 0; +} } diff --git a/tests/error_golden/run.sh b/tests/error_golden/run.sh new file mode 100755 index 0000000..32dcdb3 --- /dev/null +++ b/tests/error_golden/run.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Golden tests for Rust-style compiler diagnostics. +# Usage: from repo root: tests/error_golden/run.sh [path/to/buxc] +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUXC="${1:-$ROOT/buxc}" +DIR="$(cd "$(dirname "$0")" && pwd)" + +if [[ ! -x "$BUXC" ]]; then + echo "error: buxc not found at $BUXC (run make build first)" + exit 1 +fi + +passed=0 +failed=0 + +normalize() { + # Replace absolute path prefix with FILE, drop trailing blank lines + sed -E \ + -e "s|$DIR/[^:]+:|FILE:|g" \ + -e "s|$ROOT/[^:]+:|FILE:|g" \ + -e "s|//+|/|g" \ + | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' +} + +for case_dir in "$DIR"/*/; do + name="$(basename "$case_dir")" + [[ -f "$case_dir/expected.err" ]] || continue + [[ -f "$case_dir/bux.toml" ]] || continue + + out="$("$BUXC" build "$case_dir" --color off 2>&1 || true)" + got="$(printf '%s\n' "$out" | normalize)" + exp="$(cat "$case_dir/expected.err")" + + # Compare ignoring full absolute path differences already normalized + if [[ "$got" == "$exp" ]]; then + echo " PASS $name" + passed=$((passed + 1)) + else + echo " FAIL $name" + echo "---- expected ----" + printf '%s\n' "$exp" + echo "---- got ----" + printf '%s\n' "$got" + echo "--------------" + failed=$((failed + 1)) + fi +done + +echo "Error golden tests: $passed passed, $failed failed" +if [[ $failed -gt 0 ]]; then + exit 1 +fi diff --git a/tests/error_golden/type_mismatch/bux.toml b/tests/error_golden/type_mismatch/bux.toml new file mode 100644 index 0000000..d17cffd --- /dev/null +++ b/tests/error_golden/type_mismatch/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "type_mismatch" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/tests/error_golden/type_mismatch/expected.err b/tests/error_golden/type_mismatch/expected.err new file mode 100644 index 0000000..b19bff0 --- /dev/null +++ b/tests/error_golden/type_mismatch/expected.err @@ -0,0 +1,7 @@ +error: type errors in project +error: cannot assign String to int + --> FILE:4:18 + | + 4 | let x: int = "hello"; + | ^^^^^^^ + = help: ensure the right-hand side type matches the left-hand side diff --git a/tests/error_golden/type_mismatch/src/Main.bux b/tests/error_golden/type_mismatch/src/Main.bux new file mode 100644 index 0000000..1a23028 --- /dev/null +++ b/tests/error_golden/type_mismatch/src/Main.bux @@ -0,0 +1,6 @@ +import Std::Io::PrintLine; + +func Main() -> int { + let x: int = "hello"; + return 0; +} diff --git a/tests/error_golden/undeclared/bux.toml b/tests/error_golden/undeclared/bux.toml new file mode 100644 index 0000000..560030d --- /dev/null +++ b/tests/error_golden/undeclared/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "undeclared" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/tests/error_golden/undeclared/expected.err b/tests/error_golden/undeclared/expected.err new file mode 100644 index 0000000..437e731 --- /dev/null +++ b/tests/error_golden/undeclared/expected.err @@ -0,0 +1,7 @@ +error: type errors in project +error: undeclared identifier 'noSuchVar' + --> FILE:4:15 + | + 4 | PrintLine(noSuchVar); + | ^^^^^^^^^ + = help: check the spelling, or import the symbol from the right module diff --git a/tests/error_golden/undeclared/src/Main.bux b/tests/error_golden/undeclared/src/Main.bux new file mode 100644 index 0000000..9335245 --- /dev/null +++ b/tests/error_golden/undeclared/src/Main.bux @@ -0,0 +1,6 @@ +import Std::Io::PrintLine; + +func Main() -> int { + PrintLine(noSuchVar); + return 0; +} diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index e161577..41e175e 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -4,7 +4,7 @@ # Usage: bux-lsp # The editor spawns this binary and communicates via stdin/stdout. -import std/[json, os, strutils, streams, tables] +import std/[json, os, strutils, streams, tables, osproc] # --------------------------------------------------------------------------- # JSON-RPC Transport @@ -175,19 +175,150 @@ proc analyzeFile(path: string, content: string): DocumentState = i += 1 # --------------------------------------------------------------------------- -# Diagnostics (placeholder — emits empty diagnostics) +# Diagnostics — run `buxc check` when available and parse Rust-style errors # --------------------------------------------------------------------------- -proc publishDiagnostics(stream: FileStream, uri: string) = +type + LspDiag = object + line: int ## 0-based for LSP + col: int ## 0-based + endCol: int ## 0-based exclusive + severity: int ## 1=error, 2=warning + message: string + +proc findBuxc(): string = + ## Prefer buxc next to the LSP binary, then PATH. + let beside = getAppDir() / "buxc" + if fileExists(beside): return beside + let beside2 = getCurrentDir() / "buxc" + if fileExists(beside2): return beside2 + result = findExe("buxc") + +proc parseBuxcDiagnostics(output, sourcePath: string): seq[LspDiag] = + ## Parse lines like: + ## error: cannot assign String to int + ## --> /path/Main.bux:4:18 + result = @[] + let lines = output.splitLines() + var i = 0 + while i < lines.len: + let line = lines[i] + var sev = 0 + var msg = "" + if line.startsWith("error: "): + sev = 1 + msg = line[7..^1] + elif line.startsWith("warning: "): + sev = 2 + msg = line[9..^1] + else: + inc i + continue + + # Skip aggregate headers like "type errors in project" + if msg.startsWith("type errors") or msg.startsWith("parse errors") or + msg.startsWith("lex errors"): + inc i + continue + + var fileLine = 1 + var fileCol = 1 + if i + 1 < lines.len and lines[i + 1].strip().startsWith("-->"): + let locPart = lines[i + 1].strip()[3..^1].strip() + # path:line:col + let parts = locPart.rsplit(':', maxsplit = 2) + if parts.len >= 3: + try: + fileLine = parseInt(parts[^2]) + fileCol = parseInt(parts[^1]) + except: discard + # Optionally filter to the open document + let pathPart = if parts.len >= 3: parts[0] else: "" + if sourcePath.len > 0 and pathPart.len > 0: + if not pathPart.endsWith(sourcePath.extractFilename) and + pathPart != sourcePath: + i += 1 + continue + # Estimate end column from message quote or single caret width + var endCol = fileCol + let q = msg.find('\'') + if q >= 0: + let q2 = msg.find('\'', q + 1) + if q2 > q + 1: + endCol = fileCol + (q2 - q - 1) + if endCol <= fileCol: + endCol = fileCol + 1 + + result.add(LspDiag( + line: max(0, fileLine - 1), + col: max(0, fileCol - 1), + endCol: max(0, endCol - 1), + severity: sev, + message: msg + )) + inc i + +proc runBuxcDiagnostics(sourcePath, content: string): seq[LspDiag] = + result = @[] + let buxc = findBuxc() + if buxc.len == 0: + return + + # Prefer package root if this file lives under src/ + var projectDir = sourcePath.parentDir + if projectDir.endsWith("src"): + projectDir = projectDir.parentDir + let toml = projectDir / "bux.toml" + + var cmd: string + var workDir: string + if fileExists(toml): + workDir = projectDir + cmd = buxc & " check --color off" + else: + # Temp package for free-standing buffers + let tmp = getTempDir() / "bux-lsp-" & $getCurrentProcessId() + createDir(tmp / "src") + writeFile(tmp / "bux.toml", """[Package] +Name = "lsp_tmp" +Version = "0.0.0" +Type = "bin" +[Build] +Output = "Bin" +""") + writeFile(tmp / "src" / "Main.bux", content) + workDir = tmp + cmd = buxc & " check --color off" + + try: + let (output, _) = execCmdEx(cmd, workingDir = workDir) + result = parseBuxcDiagnostics(output, sourcePath) + except CatchableError: + discard + +proc publishDiagnostics(stream: FileStream, uri: string, diags: seq[LspDiag] = @[]) = + var arr = newJArray() + for d in diags: + arr.add(%*{ + "range": { + "start": {"line": d.line, "character": d.col}, + "end": {"line": d.line, "character": d.endCol} + }, + "severity": d.severity, + "source": "buxc", + "message": d.message + }) sendNotification(stream, "textDocument/publishDiagnostics", %*{ "uri": uri, - "diagnostics": [] + "diagnostics": arr }) proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) = - let updated = analyzeFile(uriToPath(doc.uri), doc.content) + let path = uriToPath(doc.uri) + let updated = analyzeFile(path, doc.content) doc.symbols = updated.symbols - publishDiagnostics(stream, doc.uri) + let diags = runBuxcDiagnostics(path, doc.content) + publishDiagnostics(stream, doc.uri, diags) # --------------------------------------------------------------------------- # Completion