feat: multi-instance closures, richer stdlib, and Rust-style diagnostics

Introduce fat function pointers (BuxFn {code, env}) so capturing closures
are heap-allocated per value in both bootstrap and selfhost. Expand
Array/Map/Set/String/Test/Result APIs, add proper tuple codegen and
error snippets with multi-char underlines, golden diagnostic tests, and
LSP diagnostics via buxc check.
This commit is contained in:
2026-07-15 16:00:21 +03:00
parent 94e6806dda
commit 61ac06ab5f
48 changed files with 2789 additions and 362 deletions
+8 -3
View File
@@ -3,9 +3,9 @@ SRC := bootstrap/main.nim
OUT := buxc OUT := buxc
BUILD_DIR := build 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 all: build
@@ -19,7 +19,7 @@ dev:
debug: dev debug: dev
@echo "Debug binary: buxc_debug" @echo "Debug binary: buxc_debug"
test: build test-examples test: build test-examples test-errors
@echo "Running lexer tests..." @echo "Running lexer tests..."
$(NIM) c -r tests/lexer_test.nim $(NIM) c -r tests/lexer_test.nim
@echo "Running parser tests..." @echo "Running parser tests..."
@@ -100,6 +100,11 @@ test-golden: build
echo "Golden tests: $$passed passed, $$failed failed"; \ echo "Golden tests: $$passed passed, $$failed failed"; \
if [ $$failed -gt 0 ]; then exit 1; fi 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 selfhost-loop: build
@echo "=== Selfhost loop: bootstrap determinism check ===" @echo "=== Selfhost loop: bootstrap determinism check ==="
@echo "Build A..." @echo "Build A..."
+30 -6
View File
@@ -128,8 +128,31 @@ proc typeToC*(be: var CBackend, typ: Type): string =
of "float64": return "double" of "float64": return "double"
of "bool": return "bool" of "bool": return "bool"
else: return typ.name else: return typ.name
of tkTuple: return "void*" # TODO: proper tuple struct of tkTuple:
of tkFunc: return "void*" # TODO: function pointer 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: else:
when defined(release): when defined(release):
return "void*" return "void*"
@@ -311,10 +334,11 @@ proc emitExpr(be: var CBackend, node: HirNode): string =
return &"({base}).data[{idx}]" return &"({base}).data[{idx}]"
of hTupleInit: of hTupleInit:
var elems: seq[string] = @[] let typeName = typeToC(be, node.typ)
for e in node.tupleInitElements: var fields: seq[string] = @[]
elems.add(be.emitExpr(e)) for i, e in node.tupleInitElements:
return &"{{{elems.join(\", \")}}}" fields.add(&"._{i} = {be.emitExpr(e)}")
return &"(({typeName}){{{fields.join(\", \")}}})"
of hCast: of hCast:
let operand = be.emitExpr(node.castOperand) let operand = be.emitExpr(node.castOperand)
+202 -10
View File
@@ -1,5 +1,6 @@
import std/[os, strutils, terminal, strformat, osproc, sets] import std/[os, strutils, terminal, strformat, osproc, sets]
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
import source_location
type type
ColorMode* = enum ColorMode* = enum
@@ -89,6 +90,203 @@ proc printInfo(msg: string, useColor: bool) =
else: else:
echo("info: " & msg) 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<int>'"
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 # Commands
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -304,14 +502,12 @@ proc prepareProject(root: string, useColor: bool, opts: GlobalOptions): (Project
let lexRes = tokenize(source, path) let lexRes = tokenize(source, path)
if lexRes.hasErrors: if lexRes.hasErrors:
printError(&"lex errors in {path}", useColor) printError(&"lex errors in {path}", useColor)
for d in lexRes.diagnostics: printLexerDiags(lexRes.diagnostics, useColor, path)
echo $d
return (pctx, 1) return (pctx, 1)
let parseRes = parse(lexRes.tokens, path) let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0: if parseRes.diagnostics.len > 0:
printError(&"parse errors in {path}", useColor) printError(&"parse errors in {path}", useColor)
for d in parseRes.diagnostics: printParserDiags(parseRes.diagnostics, useColor, path)
echo &"error: {d.message} at {d.loc}"
return (pctx, 1) return (pctx, 1)
for decl in parseRes.module.items: for decl in parseRes.module.items:
if decl.kind == dkModule: if decl.kind == dkModule:
@@ -345,9 +541,7 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
let semaRes = analyze(unifiedModule) let semaRes = analyze(unifiedModule)
if semaRes.hasErrors: if semaRes.hasErrors:
printError("type errors in project", useColor) printError("type errors in project", useColor)
for d in semaRes.diagnostics: printSemaDiags(semaRes.diagnostics, useColor)
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
return 1 return 1
if not opts.quiet: if not opts.quiet:
printInfo("check passed", useColor) printInfo("check passed", useColor)
@@ -448,9 +642,7 @@ proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
let (semaRes, semaCtx) = analyzeFull(unifiedModule) let (semaRes, semaCtx) = analyzeFull(unifiedModule)
if semaRes.hasErrors: if semaRes.hasErrors:
printError("type errors in project", useColor) printError("type errors in project", useColor)
for d in semaRes.diagnostics: printSemaDiags(semaRes.diagnostics, useColor)
let sev = if d.severity == sdsError: "error" else: "warning"
echo &"{sev}: {d.message} at {d.loc}"
return 1 return 1
let hirMod = lowerModule(unifiedModule, semaCtx) let hirMod = lowerModule(unifiedModule, semaCtx)
+4
View File
@@ -189,6 +189,10 @@ type
consts*: seq[tuple[name: string, typ: Type, value: HirNode]] 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]]]] 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]] 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 # Constructor helpers
proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode = proc hirLit*(tok: Token, typ: Type, loc: SourceLocation): HirNode =
+162 -39
View File
@@ -1,4 +1,4 @@
import std/[tables, sets, strutils] import std/[tables, sets, strutils, strformat]
import ast, types, token, source_location, hir, sema, scope import ast, types, token, source_location, hir, sema, scope
type type
@@ -25,6 +25,11 @@ type
closureDepth*: int closureDepth*: int
currentClosureExpr*: Expr currentClosureExpr*: Expr
envInstanceName*: string 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 = proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter inc ctx.varCounter
@@ -159,6 +164,50 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.generatedFuncInsts = initTable[string, bool]() result.generatedFuncInsts = initTable[string, bool]()
result.extraFuncs = @[] result.extraFuncs = @[]
result.varTypeExprs = initTable[string, TypeExpr]() 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 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 tekOwn: return ctx.resolveTypeExpr(te.pointerPointee)
of tekDynRef: return makeDynRef(te.dynInterface) of tekDynRef: return makeDynRef(te.dynInterface)
of tekPointer: return makePointer(ctx.resolveTypeExpr(te.pointerPointee)) 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 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: of tekFunc:
var params: seq[Type] = @[] var params: seq[Type] = @[]
for p in te.funcParams: for p in te.funcParams:
@@ -523,6 +579,18 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
return makeVoid() return makeVoid()
of ekBorrow: of ekBorrow:
return ctx.resolveExprType(expr.exprBorrowOperand) 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() else: return makeUnknown()
proc extractGenericStructInfo(ctx: LowerCtx, te: TypeExpr): tuple[baseName: string, typeArgs: seq[TypeExpr]] = 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 capType = if idx < ctx.currentClosureExpr.captureTypeKinds.len: Type(kind: TypeKind(ctx.currentClosureExpr.captureTypeKinds[idx])) else: makeInt()
let base = hirVar(ctx.envInstanceName, makeNamed(""), loc) let base = hirVar(ctx.envInstanceName, makeNamed(""), loc)
return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc) return HirNode(kind: hFieldAccess, fieldAccessName: name, fieldAccessBase: base, typ: capType, loc: loc)
var resolvedName = name
if ctx.importTable.hasKey(name): if ctx.importTable.hasKey(name):
return hirVar(ctx.importTable[name], typ, loc) resolvedName = ctx.importTable[name]
return hirVar(name, typ, loc) # 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: of ekPath:
# Handle enum variants: Color::Red → Color_Red # Handle enum variants: Color::Red → Color_Red
@@ -756,6 +841,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
return hirSelf(typ, loc) return hirSelf(typ, loc)
of ekUnary: 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) let operand = ctx.lowerExpr(expr.exprUnaryOperand)
return hirUnary(expr.exprUnaryOp, operand, typ, loc) return hirUnary(expr.exprUnaryOp, operand, typ, loc)
@@ -883,6 +994,16 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
calleeName = expr.exprCallCallee.exprPath.join("_") calleeName = expr.exprCallCallee.exprPath.join("_")
let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs) let args = ctx.lowerCallArgs(expr.exprCallCallee, expr.exprCallArgs)
if calleeName != "": 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) return hirCall(calleeName, args, typ, loc)
else: else:
let callee = ctx.lowerExpr(expr.exprCallCallee) let callee = ctx.lowerExpr(expr.exprCallCallee)
@@ -1236,7 +1357,32 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekClosure: of ekClosure:
let f = ctx.lowerClosureFunc(expr) 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: else:
return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc), 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: if stmt.stmtLetInit != nil:
initHir = ctx.lowerExpr(stmt.stmtLetInit) initHir = ctx.lowerExpr(stmt.stmtLetInit)
let allocaType = if stmt.stmtLetType != nil: let allocaType = if stmt.stmtLetType != nil:
case stmt.stmtLetType.kind # Full resolve covers named, pointer, slice, tuple, func, refs, etc.
of tekNamed:
ctx.resolveTypeExpr(stmt.stmtLetType) 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()
elif stmt.stmtLetInit != nil: elif stmt.stmtLetInit != nil:
ctx.resolveExprType(stmt.stmtLetInit) ctx.resolveExprType(stmt.stmtLetInit)
else: else:
makeUnknown() makeUnknown()
if allocaType != nil and allocaType.kind == tkFunc:
ctx.seenFatTypes.add(allocaType)
let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc) let alloca = hirAlloca(stmt.stmtLetName, allocaType, loc)
let varNode = hirVar(stmt.stmtLetName, makePointer(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: if initHir != nil:
let store = hirStore(varNode, initHir, loc) let store = hirStore(varNode, initHir, loc)
stmts.add(store) stmts.add(store)
# If init is a closure with captures, emit capture assignments # Capture filling for closures is done at the ekClosure site (heap env).
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)
return hirBlock(stmts, nil, makeVoid(), loc) return hirBlock(stmts, nil, makeVoid(), loc)
of skReturn: of skReturn:
@@ -1723,6 +1840,8 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
let name = "__closure_" & $ctx.varCounter let name = "__closure_" & $ctx.varCounter
inc ctx.varCounter inc ctx.varCounter
var f = HirFunc(name: name, isPublic: false) 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 # Copy capture metadata
if expr.captureCount > 0: if expr.captureCount > 0:
f.captureNames = expr.captureNames f.captureNames = expr.captureNames
@@ -1730,7 +1849,7 @@ proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
f.captureTypes.add(Type(kind: TypeKind(tk))) f.captureTypes.add(Type(kind: TypeKind(tk)))
f.envStructName = "__closure_env_" & $(ctx.varCounter - 1) f.envStructName = "__closure_env_" & $(ctx.varCounter - 1)
f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1) f.envInstanceName = "__closure_env_instance_" & $(ctx.varCounter - 1)
# Params # User params
for p in expr.exprClosureParams: for p in expr.exprClosureParams:
f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown())) f.params.add((name: p.name, typ: if p.ptype != nil: ctx.resolveTypeExpr(p.ptype) else: makeUnknown()))
# Return type # Return type
@@ -1935,4 +2054,8 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
if allFound: if allFound:
vtableInfos.add((ifaceName, typeName, methodNames, hasAssoc)) 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)
+2 -1
View File
@@ -70,7 +70,8 @@ proc matchStr(lex: var Lexer, s: string): bool =
return true return true
proc currentLocation(lex: Lexer): SourceLocation = 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) = proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message)) lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
+199 -17
View File
@@ -149,14 +149,22 @@ proc emitInstr(be: var LirCBackend, instr: LirInstr) =
be.emitLine(&"{v(instr.src)}({argsStr});") be.emitLine(&"{v(instr.src)}({argsStr});")
of lirCallIndirect: of lirCallIndirect:
## Fat function pointer call: f.code(f.env, args...)
var argsStr = "" var argsStr = ""
for i, arg in instr.extra: for i, arg in instr.extra:
if i > 0: argsStr.add(", ") if i > 0: argsStr.add(", ")
argsStr.add(v(arg)) argsStr.add(v(arg))
let callee = v(instr.src)
if instr.dst.kind != lvkVoid: 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: else:
be.emitLine(&"({v(instr.src)})({argsStr});") be.emitLine(&"{v(instr.dst)} = ({callee}.code)({callee}.env);")
else:
if argsStr.len > 0:
be.emitLine(&"({callee}.code)({callee}.env, {argsStr});")
else:
be.emitLine(&"({callee}.code)({callee}.env);")
# ── Return ── # ── Return ──
of lirRet: of lirRet:
@@ -348,6 +356,21 @@ proc emitFunc(be: var LirCBackend, f: LirFunc, funcRetTypes: Table[string, strin
# ── Struct/Enum emission (from HIR module) ── # ── 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 = proc typeToCStr(typ: Type): string =
## Duplicate from lir_lower for self-containedness ## Duplicate from lir_lower for self-containedness
if typ == nil: return "int" if typ == nil: return "int"
@@ -396,13 +419,39 @@ proc typeToCStr(typ: Type): string =
of "float64": return "double" of "float64": return "double"
of "bool": return "bool" of "bool": return "bool"
else: return typ.name 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: of tkFunc:
if typ.inner.len == 0: return "void (*)(void)" return funcFatTypeName(typ)
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
let ret = typeToCStr(typ.inner[^1])
return ret & " (*)(" & params & ")"
else: return "int" 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]]) = proc emitStructDef(be: var LirCBackend, name: string, fields: seq[tuple[name: string, typ: Type]]) =
be.emitLine(&"typedef struct {name} {{") be.emitLine(&"typedef struct {name} {{")
be.indent += 1 be.indent += 1
@@ -481,11 +530,36 @@ proc collectValueDeps(typ: Type): seq[string] =
return @[typ.name] return @[typ.name]
of tkSlice: of tkSlice:
return @[typeToCStr(typ)] 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 @[] return @[]
else: else:
return @[] 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) = proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) =
be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};") 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): elif sliceMap.hasKey(name):
be.emitSliceTypeDef(name, sliceMap[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 # Forward function declarations
for f in module.funcs: for f in module.funcs:
let rt = typeToCStr(f.retType) let rt = typeToCStr(f.retType)
@@ -707,18 +879,28 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
be.emitLine("};") be.emitLine("};")
be.emitLine("") be.emitLine("")
# Emit env structs for closures with captures # Adapters for named functions used as fat-func values (after forward decls)
for f in module.funcs: if module.funcAdapters.len > 0:
if f.captureNames.len > 0 and f.envStructName != "": be.emitLine("/* Fat-func adapters for named functions */")
be.emitLine(&"struct {f.envStructName} {{") 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 be.indent += 1
for i in 0 ..< f.captureNames.len: be.emitLine("(void)env;")
let capName = f.captureNames[i] if ret == "void":
let capType = if i < f.captureTypes.len: typeToCStr(f.captureTypes[i]) else: "int" be.emitLine(&"{a.name}({argsStr});")
be.emitLine(&"{capType} {capName};") else:
be.emitLine(&"return {a.name}({argsStr});")
be.indent -= 1 be.indent -= 1
be.emitLine("};") be.emitLine("}")
be.emitLine(&"static struct {f.envStructName} {f.envInstanceName};")
be.emitLine("") be.emitLine("")
# Emit all LIR functions # Emit all LIR functions
+64 -5
View File
@@ -57,6 +57,22 @@ proc cEscape(s: string): string =
of '\0': result.add("\\0") of '\0': result.add("\\0")
else: result.add(c) 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 = proc typeToCStr(typ: Type): string =
## Convert a Bux Type to a C type string. ## Convert a Bux Type to a C type string.
if typ == nil: return "int" if typ == nil: return "int"
@@ -105,13 +121,44 @@ proc typeToCStr(typ: Type): string =
of "float64": return "double" of "float64": return "double"
of "bool": return "bool" of "bool": return "bool"
else: return typ.name 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: of tkFunc:
if typ.inner.len == 0: return "void (*)(void)" ## Fat function pointer: { code(env, args...), env }
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ") ## Enables multi-instance closures with captures.
let ret = typeToCStr(typ.inner[^1]) return funcFatTypeName(typ)
return ret & " (*)(" & params & ")"
else: return "int" else: return "int"
proc funcFatTypeName*(typ: Type): string =
## BuxFn_<ret>_<params...> (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 = proc hirTypeToC(ctx: var LowerToLirCtx, node: HirNode): string =
if node == nil: return "int" if node == nil: return "int"
result = typeToCStr(node.typ) result = typeToCStr(node.typ)
@@ -498,7 +545,12 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
for e in node.tupleInitElements: for e in node.tupleInitElements:
elems.add(lowerExpr(ctx, e)) elems.add(lowerExpr(ctx, e))
let t = b.freshTemp() 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 return t
# ── If expression (ternary) ── # ── If expression (ternary) ──
@@ -780,6 +832,13 @@ proc lowerModuleToLir*(hirMod: HirModule): LirBuilder =
ctx.funcRetType = retCT ctx.funcRetType = retCT
ctx.builder.beginFunc(f.name, params, retCT, f.isPublic) 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 != nil:
if f.body.kind == hBlock: if f.body.kind == hBlock:
for stmt in f.body.blockStmts: for stmt in f.body.blockStmts:
+5 -1
View File
@@ -660,11 +660,15 @@ proc parsePostfix(p: var Parser): Expr =
discard p.expect(tkRBracket, "expected ']' to close index") discard p.expect(tkRBracket, "expected ']' to close index")
left = Expr(kind: ekIndex, loc: loc, exprIndexObj: left, exprIndexIdx: idx, exprIndexBoundsCheck: false) left = Expr(kind: ekIndex, loc: loc, exprIndexObj: left, exprIndexIdx: idx, exprIndexBoundsCheck: false)
of tkDot: of tkDot:
# Field expression or .await # Field expression, tuple index (.0, .1), or .await
discard p.advance() discard p.advance()
if p.check(tkAwait): if p.check(tkAwait):
discard p.advance() discard p.advance()
left = Expr(kind: ekAwait, loc: loc, exprAwaitOperand: left) 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: else:
let fieldName = p.expectIdentOrKeyword("expected field name after '.'").text let fieldName = p.expectIdentOrKeyword("expected field name after '.'").text
left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: fieldName) left = Expr(kind: ekField, loc: loc, exprFieldObj: left, exprFieldName: fieldName)
+16 -1
View File
@@ -1249,6 +1249,20 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
# Auto-dereference pointer/reference types for field access # Auto-dereference pointer/reference types for field access
if objType.kind in {tkPointer, tkRef, tkMutRef} and objType.inner.len > 0: if objType.kind in {tkPointer, tkRef, tkMutRef} and objType.inner.len > 0:
objType = objType.inner[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: if objType.kind == tkNamed:
# Check if this is a _Data union field access # Check if this is a _Data union field access
if objType.name.endsWith("_Data"): 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) initType = sema.checkExpr(stmt.stmtLetInit, scope)
let declaredType = if stmt.stmtLetType != nil: sema.resolveType(stmt.stmtLetType) else: initType 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}): 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: if stmt.stmtLetInit == nil and stmt.stmtLetType == nil:
sema.emitError(stmt.loc, "variable must have either type annotation or initializer") sema.emitError(stmt.loc, "variable must have either type annotation or initializer")
let isOwnVar = stmt.stmtLetType != nil and stmt.stmtLetType.kind == tekOwn let isOwnVar = stmt.stmtLetType != nil and stmt.stmtLetType.kind == tekOwn
+4
View File
@@ -3,6 +3,10 @@ type
line*: uint32 ## 1-based line*: uint32 ## 1-based
column*: uint32 ## 1-based (UTF-8 byte offset in line) column*: uint32 ## 1-based (UTF-8 byte offset in line)
offset*: uint32 ## byte offset from start of file offset*: uint32 ## byte offset from start of file
file*: string ## source file path (empty if unknown)
proc `$`*(loc: SourceLocation): string = proc `$`*(loc: SourceLocation): string =
if loc.file.len > 0:
loc.file & ":" & $loc.line & ":" & $loc.column
else:
$loc.line & ":" & $loc.column $loc.line & ":" & $loc.column
+42 -2
View File
@@ -105,10 +105,50 @@ f"Hello, {name}" // Interpolated string — expressions inside {}
own T // Owned value (move semantics) own T // Owned value (move semantics)
T[] // Slice (unsized) T[] // Slice (unsized)
T[N] // Fixed-size array T[N] // Fixed-size array
(T1, T2, T3) // Tuple (T1, T2, T3) // Tuple — access fields with .0, .1, .2
func(T1) -> T2 // Function type 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 ### Structs
```bux ```bux
struct Point { struct Point {
+163
View File
@@ -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 | 35 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)
```
+3 -1
View File
@@ -153,7 +153,9 @@ Array_Filter(nums, |x| { return x > 10; });
4. In thunk body: rewrite captured identifiers to `env_instance.x` via `hFieldAccess`. 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. 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. **Complexity:** High — touches parser, sema, type system, HIR/LIR backend.
+50 -1
View File
@@ -81,8 +81,19 @@ struct Array<T> {
|----------|-----------|-------------| |----------|-----------|-------------|
| `Array_New<T>` | `func Array_New<T>(cap: uint) -> Array<T>` | Create new array | | `Array_New<T>` | `func Array_New<T>(cap: uint) -> Array<T>` | Create new array |
| `Array_Push<T>` | `func Array_Push<T>(arr: *Array<T>, value: T)` | Append element | | `Array_Push<T>` | `func Array_Push<T>(arr: *Array<T>, value: T)` | Append element |
| `Array_Pop<T>` | `func Array_Pop<T>(arr: *Array<T>) -> T` | Remove and return last element |
| `Array_Contains<T>` | `func Array_Contains<T>(arr: *Array<T>, value: T) -> bool` | Linear search for value |
| `Array_IndexOf<T>` | `func Array_IndexOf<T>(arr: *Array<T>, value: T) -> int` | First index or -1 |
| `Array_Extend<T>` | `func Array_Extend<T>(arr: *Array<T>, other: *Array<T>)` | Append all from other |
| `Array_Get<T>` | `func Array_Get<T>(arr: *Array<T>, index: uint) -> T` | Get element at index | | `Array_Get<T>` | `func Array_Get<T>(arr: *Array<T>, index: uint) -> T` | Get element at index |
| `Array_Set<T>` | `func Array_Set<T>(arr: *Array<T>, index: uint, value: T)` | Set element at index |
| `Array_First<T>` | `func Array_First<T>(arr: *Array<T>) -> T` | First element (bounds-checked) |
| `Array_Last<T>` | `func Array_Last<T>(arr: *Array<T>) -> T` | Last element (bounds-checked) |
| `Array_Len<T>` | `func Array_Len<T>(arr: *Array<T>) -> uint` | Get length | | `Array_Len<T>` | `func Array_Len<T>(arr: *Array<T>) -> uint` | Get length |
| `Array_Cap<T>` | `func Array_Cap<T>(arr: *Array<T>) -> uint` | Get capacity |
| `Array_IsEmpty<T>` | `func Array_IsEmpty<T>(arr: *Array<T>) -> bool` | True if length is 0 |
| `Array_Clear<T>` | `func Array_Clear<T>(arr: *Array<T>)` | Set length to 0 (keeps capacity) |
| `Array_Reserve<T>` | `func Array_Reserve<T>(arr: *Array<T>, minCap: uint)` | Grow capacity if needed |
| `Array_Free<T>` | `func Array_Free<T>(arr: *Array<T>)` | Free memory | | `Array_Free<T>` | `func Array_Free<T>(arr: *Array<T>)` | Free memory |
### Example ### Example
@@ -128,6 +139,9 @@ struct Iter<T> {
| `Iter_Count<T>` | `func Iter_Count<T>(it: *Iter<T>) -> uint` | Count remaining elements | | `Iter_Count<T>` | `func Iter_Count<T>(it: *Iter<T>) -> uint` | Count remaining elements |
| `Iter_Skip<T>` | `func Iter_Skip<T>(it: *Iter<T>, n: uint)` | Skip N elements | | `Iter_Skip<T>` | `func Iter_Skip<T>(it: *Iter<T>, n: uint)` | Skip N elements |
| `Iter_Take<T>` | `func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T>` | Take first N elements as new iterator | | `Iter_Take<T>` | `func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T>` | Take first N elements as new iterator |
| `Iter_AnyEq<T>` | `func Iter_AnyEq<T>(it: *Iter<T>, value: T) -> bool` | True if any remaining element equals value |
| `Iter_AllEq<T>` | `func Iter_AllEq<T>(it: *Iter<T>, value: T) -> bool` | True if all remaining equal value |
| `Iter_Collect<T>` | `func Iter_Collect<T>(it: *Iter<T>) -> Array<T>` | Collect remaining into a new Array |
### Example ### Example
```bux ```bux
@@ -200,8 +214,12 @@ String manipulation utilities.
| Function | Signature | Description | | 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_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_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_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_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 | | `String_Format3` | `func String_Format3(pattern: String, a0: String, a1: String, a2: String) -> String` | Format with 3 args |
@@ -322,6 +340,9 @@ struct Set<T> {
| `Set_New<T>` | `func Set_New<T>(cap: uint) -> Set<T>` | Create set | | `Set_New<T>` | `func Set_New<T>(cap: uint) -> Set<T>` | Create set |
| `Set_Add<T>` | `func Set_Add<T>(s: *Set<T>, value: T)` | Insert element (ignores duplicates) | | `Set_Add<T>` | `func Set_Add<T>(s: *Set<T>, value: T)` | Insert element (ignores duplicates) |
| `Set_Has<T>` | `func Set_Has<T>(s: *Set<T>, value: T) -> bool` | Check membership | | `Set_Has<T>` | `func Set_Has<T>(s: *Set<T>, value: T) -> bool` | Check membership |
| `Set_Remove<T>` | `func Set_Remove<T>(s: *Set<T>, value: T) -> bool` | Remove value |
| `Set_Clear<T>` | `func Set_Clear<T>(s: *Set<T>)` | Clear all elements |
| `Set_IsEmpty<T>` | `func Set_IsEmpty<T>(s: *Set<T>) -> bool` | True if empty |
| `Set_Len<T>` | `func Set_Len<T>(s: *Set<T>) -> uint` | Element count | | `Set_Len<T>` | `func Set_Len<T>(s: *Set<T>) -> uint` | Element count |
| `Set_Free<T>` | `func Set_Free<T>(s: *Set<T>)` | Free memory | | `Set_Free<T>` | `func Set_Free<T>(s: *Set<T>)` | Free memory |
@@ -371,6 +392,9 @@ struct Map<K, V> {
| `Map_Set<K,V>` | `func Map_Set<K,V>(m: *Map<K,V>, key: K, value: V)` | Insert/update | | `Map_Set<K,V>` | `func Map_Set<K,V>(m: *Map<K,V>, key: K, value: V)` | Insert/update |
| `Map_Get<K,V>` | `func Map_Get<K,V>(m: *Map<K,V>, key: K) -> V` | Get value (zero if missing) | | `Map_Get<K,V>` | `func Map_Get<K,V>(m: *Map<K,V>, key: K) -> V` | Get value (zero if missing) |
| `Map_Has<K,V>` | `func Map_Has<K,V>(m: *Map<K,V>, key: K) -> bool` | Check key exists | | `Map_Has<K,V>` | `func Map_Has<K,V>(m: *Map<K,V>, key: K) -> bool` | Check key exists |
| `Map_Remove<K,V>` | `func Map_Remove<K,V>(m: *Map<K,V>, key: K) -> bool` | Remove key (true if present) |
| `Map_Clear<K,V>` | `func Map_Clear<K,V>(m: *Map<K,V>)` | Remove all entries (keeps capacity) |
| `Map_IsEmpty<K,V>` | `func Map_IsEmpty<K,V>(m: *Map<K,V>) -> bool` | True if no entries |
| `Map_Len<K,V>` | `func Map_Len<K,V>(m: *Map<K,V>) -> uint` | Entry count | | `Map_Len<K,V>` | `func Map_Len<K,V>(m: *Map<K,V>) -> uint` | Entry count |
| `Map_Free<K,V>` | `func Map_Free<K,V>(m: *Map<K,V>)` | Free memory | | `Map_Free<K,V>` | `func Map_Free<K,V>(m: *Map<K,V>)` | Free memory |
@@ -419,6 +443,9 @@ struct StringMap<V> {
| `StringMap_Set<V>` | `func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V)` | Insert/update | | `StringMap_Set<V>` | `func StringMap_Set<V>(m: *StringMap<V>, key: String, value: V)` | Insert/update |
| `StringMap_Get<V>` | `func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V` | Get value | | `StringMap_Get<V>` | `func StringMap_Get<V>(m: *StringMap<V>, key: String) -> V` | Get value |
| `StringMap_Has<V>` | `func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool` | Check key exists | | `StringMap_Has<V>` | `func StringMap_Has<V>(m: *StringMap<V>, key: String) -> bool` | Check key exists |
| `StringMap_Remove<V>` | `func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool` | Remove key |
| `StringMap_Clear<V>` | `func StringMap_Clear<V>(m: *StringMap<V>)` | Clear all entries |
| `StringMap_IsEmpty<V>` | `func StringMap_IsEmpty<V>(m: *StringMap<V>) -> bool` | True if empty |
| `StringMap_Len<V>` | `func StringMap_Len<V>(m: *StringMap<V>) -> uint` | Entry count | | `StringMap_Len<V>` | `func StringMap_Len<V>(m: *StringMap<V>) -> uint` | Entry count |
| `StringMap_Free<V>` | `func StringMap_Free<V>(m: *StringMap<V>)` | Free memory | | `StringMap_Free<V>` | `func StringMap_Free<V>(m: *StringMap<V>)` | Free memory |
@@ -914,7 +941,7 @@ func Main() -> int {
Operating system interface. Operating system interface.
```bux ```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 | | 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_SetEnv` | `func Os_SetEnv(name: String, value: String) -> bool` | Set environment variable |
| `Os_GetCwd` | `func Os_GetCwd() -> String` | Get current working directory | | `Os_GetCwd` | `func Os_GetCwd() -> String` | Get current working directory |
| `Os_Chdir` | `func Os_Chdir(path: String) -> bool` | Change 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 |
--- ---
+46
View File
@@ -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<int> = Array_New<int>(4);
Array_Push<int>(&a, 10);
Array_Push<int>(&a, 20);
Array_Push<int>(&a, 30);
Test_AssertTrue(Array_Contains<int>(&a, 20));
Test_AssertFalse(Array_Contains<int>(&a, 99));
Test_AssertEqInt(Array_IndexOf<int>(&a, 30), 2);
Test_AssertEqInt(Array_IndexOf<int>(&a, 99), -1);
var b: Array<int> = Array_New<int>(2);
Array_Push<int>(&b, 40);
Array_Push<int>(&b, 50);
Array_Extend<int>(&a, &b);
Test_AssertEqInt(Array_Len<int>(&a) as int, 5);
Test_AssertEqInt(Array_Get<int>(&a, 4), 50);
var it: Iter<int> = Array_Iter<int>(&a);
Test_AssertTrue(Iter_AnyEq<int>(&it, 10));
Test_AssertFalse(Iter_AllEq<int>(&it, 10));
// Skip first two via advancing, collect rest
discard Iter_Next<int>(&it);
discard Iter_Next<int>(&it);
var rest: Array<int> = Iter_Collect<int>(&it);
Test_AssertEqInt(Array_Len<int>(&rest) as int, 3);
Test_AssertEqInt(Array_Get<int>(&rest, 0), 30);
Array_Free<int>(&a);
Array_Free<int>(&b);
Array_Free<int>(&rest);
PrintLine("array_iter_extra: ok");
Test_Pass("array + iter extras");
return 0;
}
+27
View File
@@ -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;
}
+60
View File
@@ -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<int, int> = Map_New<int, int>(16);
Map_Set<int, int>(&m, 1, 100);
Map_Set<int, int>(&m, 2, 200);
Map_Set<int, int>(&m, 3, 300);
Test_AssertEqInt(Map_Len<int, int>(&m) as int, 3);
Test_AssertTrue(Map_Has<int, int>(&m, 2));
Test_AssertTrue(Map_Remove<int, int>(&m, 2));
Test_AssertFalse(Map_Has<int, int>(&m, 2));
Test_AssertEqInt(Map_Len<int, int>(&m) as int, 2);
Test_AssertEqInt(Map_Get<int, int>(&m, 1), 100);
Test_AssertEqInt(Map_Get<int, int>(&m, 3), 300);
Test_AssertFalse(Map_Remove<int, int>(&m, 99));
Map_Clear<int, int>(&m);
Test_AssertTrue(Map_IsEmpty<int, int>(&m));
Map_Free<int, int>(&m);
// --- Set ---
var s: Set<int> = Set_New<int>(16);
Set_Add<int>(&s, 10);
Set_Add<int>(&s, 20);
Set_Add<int>(&s, 30);
Test_AssertTrue(Set_Remove<int>(&s, 20));
Test_AssertFalse(Set_Has<int>(&s, 20));
Test_AssertTrue(Set_Has<int>(&s, 10));
Test_AssertEqInt(Set_Len<int>(&s) as int, 2);
Test_AssertFalse(Set_IsEmpty<int>(&s));
Set_Free<int>(&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;
}
+37
View File
@@ -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;
}
+55
View File
@@ -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<int> = Array_New<int>(2);
Array_Reserve<int>(&arr, 8);
Test_AssertTrue(Array_Cap<int>(&arr) >= 8);
Test_AssertTrue(Array_IsEmpty<int>(&arr));
Array_Push<int>(&arr, 10);
Array_Push<int>(&arr, 20);
Array_Push<int>(&arr, 30);
Test_AssertFalse(Array_IsEmpty<int>(&arr));
Test_AssertEqInt(Array_Len<int>(&arr) as int, 3);
Test_AssertEqInt(Array_First<int>(&arr), 10);
Test_AssertEqInt(Array_Last<int>(&arr), 30);
let popped: int = Array_Pop<int>(&arr);
Test_AssertEqInt(popped, 30);
Test_AssertEqInt(Array_Len<int>(&arr) as int, 2);
Test_AssertEqInt(Array_Get<int>(&arr, 1), 20);
Array_Clear<int>(&arr);
Test_AssertTrue(Array_IsEmpty<int>(&arr));
Test_AssertTrue(Array_Cap<int>(&arr) >= 8); // capacity retained
Array_Free<int>(&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;
}
+21
View File
@@ -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;
}
+32
View File
@@ -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;
}
+74
View File
@@ -58,4 +58,78 @@ func Array_operator_index_set<T>(self: *Array<T>, idx: uint, value: T) {
Array_Set<T>(self, idx, value); Array_Set<T>(self, idx, value);
} }
/* True if the array has no elements */
func Array_IsEmpty<T>(self: *Array<T>) -> bool {
return self.len == 0;
}
/* Current capacity (not length) */
func Array_Cap<T>(self: *Array<T>) -> uint {
return self.cap;
}
/* Drop length to zero; keeps allocated capacity */
func Array_Clear<T>(self: *Array<T>) {
self.len = 0;
}
/* Ensure capacity is at least minCap (does not shrink) */
func Array_Reserve<T>(self: *Array<T>, 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<T>(self: *Array<T>) -> T {
return Array_Get<T>(self, 0);
}
/* Last element (panics if empty via bounds check) */
func Array_Last<T>(self: *Array<T>) -> T {
return Array_Get<T>(self, self.len - 1);
}
/* Remove and return the last element (panics if empty) */
func Array_Pop<T>(self: *Array<T>) -> 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<T>(self: *Array<T>, 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<T>(self: *Array<T>, 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<T>(self: *Array<T>, other: *Array<T>) {
var i: uint = 0;
while i < other.len {
Array_Push<T>(self, other.data[i]);
i = i + 1;
}
}
} }
+40
View File
@@ -67,4 +67,44 @@ func Iter_Take<T>(it: *Iter<T>, n: uint) -> Iter<T> {
return Iter<T> { data: it.data, len: endPos, pos: it.pos }; return Iter<T> { data: it.data, len: endPos, pos: it.pos };
} }
/* True if any remaining element equals value */
func Iter_AnyEq<T>(it: *Iter<T>, 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<T>(it: *Iter<T>, 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<T>(it: *Iter<T>) -> Array<T> {
let remaining: uint = it.len - it.pos;
var cap: uint = remaining;
if cap == 0 {
cap = 1;
}
var arr: Array<T> = Array_New<T>(cap);
var i: uint = it.pos;
while i < it.len {
Array_Push<T>(&arr, it.data[i]);
i = i + 1;
}
return arr;
}
} }
+69
View File
@@ -80,6 +80,41 @@ func Map_Len<K, V>(m: *Map<K, V>) -> uint {
return m.len; return m.len;
} }
func Map_IsEmpty<K, V>(m: *Map<K, V>) -> bool {
return m.len == 0;
}
/* Remove key if present. Rebuilds the table to keep open-addressing correct. */
func Map_Remove<K, V>(m: *Map<K, V>, key: K) -> bool {
if !Map_Has<K, V>(m, key) {
return false;
}
var fresh: Map<K, V> = Map_New<K, V>(m.cap);
var i: uint = 0;
while i < m.cap {
if m.entries[i].occupied {
if m.entries[i].key != key {
Map_Set<K, V>(&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<K, V>(m: *Map<K, V>) {
var i: uint = 0;
while i < m.cap {
m.entries[i].occupied = false;
i = i + 1;
}
m.len = 0;
}
func Map_Free<K, V>(m: *Map<K, V>) { func Map_Free<K, V>(m: *Map<K, V>) {
bux_free(m.entries as *void); bux_free(m.entries as *void);
m.entries = null as *MapEntry<K, V>; m.entries = null as *MapEntry<K, V>;
@@ -163,6 +198,40 @@ func StringMap_Len<V>(m: *StringMap<V>) -> uint {
return m.len; return m.len;
} }
func StringMap_IsEmpty<V>(m: *StringMap<V>) -> bool {
return m.len == 0;
}
func StringMap_Remove<V>(m: *StringMap<V>, key: String) -> bool {
if !StringMap_Has<V>(m, key) {
return false;
}
var fresh: StringMap<V> = StringMap_New<V>(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<V>(&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<V>(m: *StringMap<V>) {
var i: uint = 0;
while i < m.cap {
m.entries[i].occupied = false;
i = i + 1;
}
m.len = 0;
}
func StringMap_Free<V>(m: *StringMap<V>) { func StringMap_Free<V>(m: *StringMap<V>) {
bux_free(m.entries as *void); bux_free(m.entries as *void);
m.entries = null as *StringMapEntry<V>; m.entries = null as *StringMapEntry<V>;
+19
View File
@@ -1,6 +1,8 @@
module Std::Option { module Std::Option {
import Std::Io::{PrintLine}; import Std::Io::{PrintLine};
extern func bux_exit(code: int);
enum Option { enum Option {
Some(int), Some(int),
None, None,
@@ -39,4 +41,21 @@ func Option_UnwrapOr(o: Option, fallback: int) -> int {
return fallback; 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;
}
} }
+6
View File
@@ -6,6 +6,7 @@ extern func bux_getenv(name: String) -> String;
extern func bux_setenv(name: String, value: String) -> int; extern func bux_setenv(name: String, value: String) -> int;
extern func bux_getcwd() -> String; extern func bux_getcwd() -> String;
extern func bux_chdir(path: String) -> int; extern func bux_chdir(path: String) -> int;
extern func bux_exit(code: int);
func Os_ArgsCount() -> int { func Os_ArgsCount() -> int {
return bux_argc(); return bux_argc();
@@ -31,4 +32,9 @@ func Os_Chdir(path: String) -> bool {
return bux_chdir(path) == 0; return bux_chdir(path) == 0;
} }
/* Terminate the process with the given exit code */
func Os_Exit(code: int) {
bux_exit(code);
}
} }
+28
View File
@@ -1,6 +1,8 @@
module Std::Result { module Std::Result {
import Std::Io::{PrintLine}; import Std::Io::{PrintLine};
extern func bux_exit(code: int);
enum Result { enum Result {
Ok(int), Ok(int),
Err(String), Err(String),
@@ -41,4 +43,30 @@ func Result_UnwrapOr(r: Result, fallback: int) -> int {
return fallback; 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;
}
} }
+37
View File
@@ -61,6 +61,43 @@ func Set_Len<T>(s: *Set<T>) -> uint {
return s.len; return s.len;
} }
func Set_IsEmpty<T>(s: *Set<T>) -> bool {
return s.len == 0;
}
/* Remove value if present. Rebuilds the table to keep open-addressing correct. */
func Set_Remove<T>(s: *Set<T>, value: T) -> bool {
if !Set_Has<T>(s, value) {
return false;
}
var fresh: Set<T> = Set_New<T>(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<T>(&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<T>(s: *Set<T>) {
var i: uint = 0;
while i < s.cap {
s.entries[i].occupied = false;
i = i + 1;
}
s.len = 0;
}
func Set_Free<T>(s: *Set<T>) { func Set_Free<T>(s: *Set<T>) {
bux_free(s.entries as *void); bux_free(s.entries as *void);
s.entries = null as *SetEntry<T>; s.entries = null as *SetEntry<T>;
+64
View File
@@ -34,6 +34,10 @@ func String_Len(s: String) -> uint {
return bux_strlen(s); return bux_strlen(s);
} }
func String_IsEmpty(s: String) -> bool {
return bux_strlen(s) == 0;
}
func String_IsNull(s: String) -> bool { func String_IsNull(s: String) -> bool {
return bux_str_is_null(s) != 0; return bux_str_is_null(s) != 0;
} }
@@ -147,6 +151,39 @@ func StringBuilder_Free(sb: *StringBuilder) {
bux_sb_free(sb.handle); 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 // String split/join
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -194,6 +231,33 @@ func String_Replace(s: String, old: String, new: String) -> String {
return result; 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; extern func bux_str_to_float(s: String) -> float64;
func String_ToFloat(s: String) -> float64 { func String_ToFloat(s: String) -> float64 {
+32 -1
View File
@@ -1,5 +1,6 @@
module Std::Test { module Std::Test {
import Std::Io::{PrintLine, PrintInt}; import Std::Io::{PrintLine, PrintInt};
import Std::String::{String_Eq};
extern func bux_exit(code: int); extern func bux_exit(code: int);
extern func bux_assert(cond: int, file: String, line: int, expr: String); 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) { func Test_AssertEqInt(a: int, b: int) {
if a != b { if a != b {
PrintLine("ASSERT_EQ FAILED:"); PrintLine("ASSERT_EQ_INT FAILED:");
PrintInt(a); PrintInt(a);
PrintLine(" != "); PrintLine(" != ");
PrintInt(b); 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) { func Test_AssertTrue(cond: bool) {
if !cond { if !cond {
PrintLine("ASSERT_TRUE FAILED"); PrintLine("ASSERT_TRUE FAILED");
@@ -42,4 +68,9 @@ func Test_Fail(msg: String) {
bux_exit(1); bux_exit(1);
} }
func Test_Pass(msg: String) {
PrintLine("PASS:");
PrintLine(msg);
}
} }
+67
View File
@@ -143,6 +143,64 @@ int64_t bux_mod_i64(int64_t a, int64_t b) {
return a % 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 */ /* String operations */
typedef struct { typedef struct {
const char* data; 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 */ /* String wrappers with Bux-compatible signatures */
unsigned int bux_strlen(const char* s) { unsigned int bux_strlen(const char* s) {
return (unsigned int)strlen(s); return (unsigned int)strlen(s);
+415 -78
View File
@@ -8,30 +8,9 @@ module CBackend {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func CBackend_TypeToC(kind: int) -> String { func CBackend_TypeToC(kind: int) -> String {
if kind == tyVoid { return "void"; } let cName: String = Type_ToCName(kind);
if kind == tyBool { return "bool"; } if !String_Eq(cName, "") { return cName; }
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*"; }
if kind == tyNamed { return "int"; } if kind == tyNamed { return "int"; }
if kind == tyFunc { return "void (*)(void)"; }
return "int"; return "int";
} }
@@ -338,31 +317,27 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return; return;
} }
// Indirect call through function pointer // Indirect call through fat function pointer: f.code(f.env, args...)
if kind == hCallIndirect { if kind == hCallIndirect {
CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, "("); 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 { if node.child2 != null as *HirNode {
StringBuilder_Append(&cbe.sb, ", ");
CBE_EmitExpr(cbe, node.child2); CBE_EmitExpr(cbe, node.child2);
needsComma = true;
} }
if node.child3 != null as *HirNode { if node.child3 != null as *HirNode {
if needsComma {
StringBuilder_Append(&cbe.sb, ", "); StringBuilder_Append(&cbe.sb, ", ");
}
CBE_EmitExpr(cbe, node.child3); CBE_EmitExpr(cbe, node.child3);
needsComma = true;
} }
// Emit extra args from linked list // Emit extra args from linked list
var ai: int = 0; var ai: int = 0;
var curExtra: *HirArgList = node.extraData as *HirArgList; var curExtra: *HirArgList = node.extraData as *HirArgList;
while ai < node.extraCount { while ai < node.extraCount {
if needsComma {
StringBuilder_Append(&cbe.sb, ", "); StringBuilder_Append(&cbe.sb, ", ");
}
CBE_EmitExpr(cbe, curExtra.node); CBE_EmitExpr(cbe, curExtra.node);
needsComma = true;
curExtra = curExtra.next; curExtra = curExtra.next;
ai = ai + 1; ai = ai + 1;
} }
@@ -590,11 +565,27 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
} }
// Index: arr[idx] — emit as arr[idx] // Index: arr[idx] — emit as arr[idx]
// For Array<T> desugar pattern (fieldPtr "data"), emit bounds-checked access
if kind == hIndexPtr { if kind == hIndexPtr {
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); CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, "["); StringBuilder_Append(&cbe.sb, "[");
CBE_EmitExpr(cbe, node.child2); CBE_EmitExpr(cbe, node.child2);
StringBuilder_Append(&cbe.sb, "]"); StringBuilder_Append(&cbe.sb, "]");
}
return; return;
} }
@@ -629,19 +620,28 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
StringBuilder_Append(&cbe.sb, ptrNode.strValue); StringBuilder_Append(&cbe.sb, ptrNode.strValue);
return; 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] // index: load(index_ptr(base, idx)) → base[idx]
// For Array<T> desugar pattern, emit bounds-checked access
if ptrKind == hIndexPtr { if ptrKind == hIndexPtr {
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); CBE_EmitExpr(cbe, ptrNode.child1);
StringBuilder_Append(&cbe.sb, "["); StringBuilder_Append(&cbe.sb, "[");
CBE_EmitExpr(cbe, ptrNode.child2); CBE_EmitExpr(cbe, ptrNode.child2);
StringBuilder_Append(&cbe.sb, "]"); StringBuilder_Append(&cbe.sb, "]");
}
return; return;
} }
} }
@@ -727,6 +727,312 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
// Emit function declaration // 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) { func CBE_EmitFuncDecl(cbe: *CEmitter, f: *HirFunc) {
// Return type // Return type
if String_Eq(f.retTypeName, "") || String_Eq(f.retTypeName, "void") { 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"); StringBuilder_Append(&cbe.sb, "typedef char char8;\n\n");
// Runtime declarations // Runtime declarations
StringBuilder_Append(&cbe.sb, "void* bux_alloc(unsigned int size);\n"); 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) // Forward declare all struct types (skip empty names)
var si: int = 0; var si: int = 0;
@@ -1121,12 +1432,53 @@ func CBackend_Generate(mod: *HirModule) -> String {
si = si + 1; 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) // Forward declarations for all functions (skip generics)
var i: int = 0; var i: int = 0;
while i < mod.funcCount { while i < mod.funcCount {
if !CBE_FuncHasGeneric(&mod.funcs[i]) { if !CBE_FuncHasGeneric(&mod.funcs[i]) {
CBE_EmitFuncDecl(cbe, &mod.funcs[i]); CBE_EmitFuncDecl(cbe, &mod.funcs[i]);
StringBuilder_Append(&cbe.sb, ";\n"); 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; i = i + 1;
} }
@@ -1141,6 +1493,9 @@ func CBackend_Generate(mod: *HirModule) -> String {
} }
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
// Adapters before function bodies (bodies may take funcs as values)
CBE_EmitAllAdapters(cbe, mod);
// Function definitions (skip generics) // Function definitions (skip generics)
var hasMain: bool = false; var hasMain: bool = false;
i = 0; i = 0;
@@ -1157,45 +1512,23 @@ func CBackend_Generate(mod: *HirModule) -> String {
i = i + 1; i = i + 1;
continue; 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]); CBE_EmitFuncDecl(cbe, &mod.funcs[i]);
StringBuilder_Append(&cbe.sb, " {\n"); 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 // Body
cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.checkedFunc = mod.funcs[i].checkedFunc;
cbe.deferCount = 0; cbe.deferCount = 0;
@@ -1216,6 +1549,10 @@ func CBackend_Generate(mod: *HirModule) -> String {
StringBuilder_Append(&cbe.sb, " return 0;\n"); StringBuilder_Append(&cbe.sb, " return 0;\n");
} }
StringBuilder_Append(&cbe.sb, "\n}\n\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; i = i + 1;
} }
+77 -2
View File
@@ -55,12 +55,44 @@ func Diagnostic_GetLine(path: String, lineNum: uint32) -> String {
return bux_str_split_part(content, "\n", lineNum - 1); 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: /* Print a diagnostic in Rust-style format:
* error: <message> * error: <message>
* --> <path>:<line>:<col> * --> <path>:<line>:<col>
* | * |
* 42 | <source_line> * 42 | <source_line>
* | <spaces>^ * | <spaces>^
* = help: <hint>
*/ */
func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) { func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) {
/* Severity prefix */ /* Severity prefix */
@@ -94,14 +126,57 @@ func Diagnostic_Print(diag: *Diagnostic, sourcePath: String) {
Print(" | "); Print(" | ");
PrintLine(lineText); PrintLine(lineText);
/* Underline */ /* Underline (multi-char for identifiers/string tokens) */
Print(" | "); Print(" | ");
var i: uint32 = 0; var i: uint32 = 0;
while i < diag.column - 1 && i < 120 { while i < diag.column - 1 && i < 120 {
Print(" "); Print(" ");
i = i + 1; 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);
} }
} }
+252 -142
View File
@@ -43,30 +43,7 @@ struct LowerCtx {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func Lcx_ResolveTypeKindFromName(name: String) -> int { func Lcx_ResolveTypeKindFromName(name: String) -> int {
if String_Eq(name, "void") { return tyVoid; } return Type_FromName(name);
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;
} }
func Lcx_TypeKindToName(kind: int) -> String { func Lcx_TypeKindToName(kind: int) -> String {
@@ -178,38 +155,58 @@ func Lcx_SubstituteType(ctx: *LowerCtx, te: *TypeExpr) -> *TypeExpr {
return te; return te;
} }
// Build C function-pointer type string from a tekFunc TypeExpr, e.g. "int (*)(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";
}
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_<ret>_<p0>_<p1>...
// Enables multi-instance closures (code + env).
func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String { func Lcx_BuildFuncTypeName(te: *TypeExpr) -> String {
if te == null as *TypeExpr || te.kind != tekFunc { return "void (*)(void)"; } if te == null as *TypeExpr || te.kind != tekFunc {
var retName: String = "void"; return "BuxFn_void_void";
}
var retPart: String = "void";
if te.funcRet != null as *TypeExpr { if te.funcRet != null as *TypeExpr {
if te.funcRet.kind == tekPointer && te.funcRet.pointerPointee != null as *TypeExpr { retPart = Lcx_TypeExprFatPart(te.funcRet);
retName = String_Concat(te.funcRet.pointerPointee.typeName, "*");
} else {
retName = te.funcRet.typeName;
} }
if String_Eq(retName, "") { retName = "int"; } var result: String = String_Concat("BuxFn_", retPart);
}
var result: String = retName;
result = String_Concat(result, " (*)(");
var cur: *TypeExprList = te.funcParams; var cur: *TypeExprList = te.funcParams;
var first: bool = true; var anyParam: bool = false;
while cur != null as *TypeExprList { while cur != null as *TypeExprList {
if !first { result = String_Concat(result, "_");
result = String_Concat(result, ", "); result = String_Concat(result, Lcx_TypeExprFatPart(cur.te));
} anyParam = true;
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;
cur = cur.next; cur = cur.next;
} }
result = String_Concat(result, ")"); if !anyParam {
result = String_Concat(result, "_void");
}
return result; return result;
} }
@@ -469,9 +466,44 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
return n; 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.kind = hVar;
n.strValue = expr.strValue; n.strValue = expr.strValue;
let sym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
n.typeKind = sym.typeKind; n.typeKind = sym.typeKind;
if expr.refType != null as *TypeExpr { 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.kind = hBinary;
n.intValue = expr.intValue; // operator n.intValue = expr.intValue; // operator
n.child1 = Lcx_LowerExpr(ctx, expr.child1); n.child1 = Lcx_LowerExpr(ctx, expr.child1);
@@ -575,6 +631,20 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
// Unary // Unary
if kind == ekUnary { 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.kind = hUnary;
n.intValue = expr.intValue; n.intValue = expr.intValue;
n.child1 = Lcx_LowerExpr(ctx, expr.child1); n.child1 = Lcx_LowerExpr(ctx, expr.child1);
@@ -1132,20 +1202,85 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
return n; return n;
} }
// Closure: generate function and return address-of // Closure: fat function pointer (multi-instance via heap env + maker)
if kind == ekClosure { if kind == ekClosure {
let f: *HirFunc = Lcx_LowerClosureFunc(ctx, expr); let f: *HirFunc = Lcx_LowerClosureFunc(ctx, expr);
n.kind = hUnary; var fatName: String = "BuxFn_int_int";
n.intValue = tkAmp; if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc {
let varNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; fatName = Lcx_BuildFuncTypeName(expr.refType);
varNode.kind = hVar; } else if f.paramCount >= 2 {
varNode.strValue = f.name; // thunk has __env + user params; approximate from ret + user arity
varNode.typeKind = tyFunc; fatName = "BuxFn_int_int";
n.child1 = varNode; if f.paramCount == 3 { fatName = "BuxFn_int_int_int"; }
n.typeKind = tyFunc; if f.paramCount == 1 { fatName = "BuxFn_int_void"; }
if expr.refType != null as *TypeExpr {
n.typeName = Lcx_BuildFuncTypeName(expr.refType);
} }
if f.captureCount > 0 {
// Call __make_<closure>(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; return n;
} }
@@ -1227,6 +1362,25 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
return n; 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; 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 // Capturing closures allocate env via __make_* at ekClosure site.
if stmt.child1 != null as *Expr && stmt.child1.kind == ekClosure && stmt.child1.captureCount > 0 { // Wrap with defer if present
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
if deferNode != null as *HirNode { if deferNode != null as *HirNode {
storeNode.child3 = deferNode; storeNode.child3 = deferNode;
let blockNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; 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); let retTe: *TypeExpr = Lcx_SubstituteType(ctx, decl.retType);
if retTe != null as *TypeExpr { if retTe != null as *TypeExpr {
f.retTypeName = retTe.typeName;
f.retTypeKind = Lcx_ResolveTypeKind(retTe); 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 { } else {
f.retTypeName = ""; f.retTypeName = "";
f.retTypeKind = 0; f.retTypeKind = 0;
@@ -2320,25 +2419,36 @@ func Lcx_LowerClosureFunc(ctx: *LowerCtx, expr: *Expr) -> *HirFunc {
f.isPublic = false; f.isPublic = false;
let params: *Decl = expr.closureParams; let params: *Decl = expr.closureParams;
if params != null as *Decl { // Fat-func ABI: leading void* __env, then user params
f.paramCount = params.paramCount; var userCount: int = 0;
if params.paramCount > 0 { f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param0, &params.param0, ctx); } if params != null as *Decl { userCount = params.paramCount; }
if params.paramCount > 1 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, &params.param1, ctx); } f.paramCount = userCount + 1;
if params.paramCount > 2 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, &params.param2, ctx); } f.param0 = bux_alloc(sizeof(HirParam)) as *HirParam;
if params.paramCount > 3 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, &params.param3, ctx); } f.param0.name = "__env";
if params.paramCount > 4 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, &params.param4, ctx); } f.param0.typeKind = tyPointer;
if params.paramCount > 5 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, &params.param5, ctx); } f.param0.typeName = "void*";
if params.paramCount > 6 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, &params.param6, ctx); } if userCount > 0 { f.param1 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param1, &params.param0, ctx); }
if params.paramCount > 7 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, &params.param7, ctx); } if userCount > 1 { f.param2 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param2, &params.param1, ctx); }
if params.paramCount > 8 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, &params.param8, ctx); } if userCount > 2 { f.param3 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param3, &params.param2, ctx); }
} if userCount > 3 { f.param4 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param4, &params.param3, ctx); }
if userCount > 4 { f.param5 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param5, &params.param4, ctx); }
if userCount > 5 { f.param6 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param6, &params.param5, ctx); }
if userCount > 6 { f.param7 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param7, &params.param6, ctx); }
if userCount > 7 { f.param8 = bux_alloc(sizeof(HirParam)) as *HirParam; Lcx_LowerParam(f.param8, &params.param7, ctx); }
if expr.refType != null as *TypeExpr && expr.refType.kind == tekFunc && expr.refType.funcRet != null as *TypeExpr { 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; f.retTypeName = expr.refType.funcRet.typeName;
if String_Eq(f.retTypeName, "") { f.retTypeName = "int"; }
f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet); f.retTypeKind = Lcx_ResolveTypeKind(expr.refType.funcRet);
} else { } else {
f.retTypeName = ""; f.retTypeName = "void";
f.retTypeKind = 0; f.retTypeKind = tyVoid;
}
} else {
f.retTypeName = "int";
f.retTypeKind = tyInt;
} }
// Copy capture metadata from AST // Copy capture metadata from AST
+1
View File
@@ -277,6 +277,7 @@ func lexKeywordKind(text: String) -> int {
if String_Eq(text, "switch") { return tkSwitch; } if String_Eq(text, "switch") { return tkSwitch; }
if String_Eq(text, "case") { return tkCase; } if String_Eq(text, "case") { return tkCase; }
if String_Eq(text, "default") { return tkDefault; } 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, "async") { return tkAsync; }
if String_Eq(text, "await") { return tkAwait; } if String_Eq(text, "await") { return tkAwait; }
if String_Eq(text, "spawn") { return tkSpawn; } if String_Eq(text, "spawn") { return tkSpawn; }
+9
View File
@@ -399,6 +399,15 @@ func parserParsePrimary(p: *Parser) -> *Expr {
return parserParseClosure(p); 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"); parserEmitDiag(p, line, col, "expected expression");
return parserMakeExpr(ekLiteral, line, col); return parserMakeExpr(ekLiteral, line, col);
} }
+9 -29
View File
@@ -136,7 +136,6 @@ func Sema_BuildFuncTypeExprFromDecl(decl: *Decl) -> *TypeExpr {
func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int { func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int {
if te == null as *TypeExpr { return tyUnknown; } if te == null as *TypeExpr { return tyUnknown; }
let name: String = te.typeName;
if te.kind == tekPointer { if te.kind == tekPointer {
return tyPointer; return tyPointer;
@@ -146,33 +145,7 @@ func Sema_ResolveType(sema: *Sema, te: *TypeExpr) -> int {
return tyFunc; return tyFunc;
} }
if String_Eq(name, "void") { return tyVoid; } return Type_FromName(te.typeName);
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;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -758,10 +731,17 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
return tyUnknown; return tyUnknown;
} }
// Block expression // Block expression (boolValue = true means unsafe block)
if kind == ekBlock { if kind == ekBlock {
if expr.refBlock != null as *Block { if expr.refBlock != null as *Block {
if expr.boolValue {
let prevChecked: bool = sema.checkedFunc;
sema.checkedFunc = false;
Sema_CheckBlock(sema, expr.refBlock); Sema_CheckBlock(sema, expr.refBlock);
sema.checkedFunc = prevChecked;
} else {
Sema_CheckBlock(sema, expr.refBlock);
}
} }
return tyVoid; return tyVoid;
} }
+20
View File
@@ -142,6 +142,7 @@ const tkDefer: int = 106;
const tkSwitch: int = 107; const tkSwitch: int = 107;
const tkCase: int = 108; const tkCase: int = 108;
const tkDefault: int = 109; const tkDefault: int = 109;
const tkUnsafe: int = 110;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Token struct // Token struct
@@ -165,6 +166,7 @@ func Token_IsKeyword(kind: int) -> bool {
if kind >= tkFunc && kind <= tkExtern { return true; } if kind >= tkFunc && kind <= tkExtern { return true; }
if kind >= tkAs && kind <= tkSuper { return true; } if kind >= tkAs && kind <= tkSuper { return true; }
if kind == tkSizeOf { return true; } if kind == tkSizeOf { return true; }
if kind >= tkDefer && kind <= tkUnsafe { return true; }
if kind >= tkAsync && kind <= tkSpawn { return true; } if kind >= tkAsync && kind <= tkSpawn { return true; }
return false; return false;
} }
@@ -215,6 +217,15 @@ func Token_KeywordKind(text: String) -> int {
if String_Eq(text, "self") { return tkSelf; } if String_Eq(text, "self") { return tkSelf; }
if String_Eq(text, "super") { return tkSuper; } if String_Eq(text, "super") { return tkSuper; }
if String_Eq(text, "sizeof") { return tkSizeOf; } 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, "true") { return tkBoolLiteral; }
if String_Eq(text, "false") { return tkBoolLiteral; } if String_Eq(text, "false") { return tkBoolLiteral; }
return tkIdent; return tkIdent;
@@ -259,6 +270,15 @@ func Token_KindName(kind: int) -> String {
if kind == tkNull { return "null"; } if kind == tkNull { return "null"; }
if kind == tkSelf { return "self"; } if kind == tkSelf { return "self"; }
if kind == tkSuper { return "super"; } 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 == tkLParen { return "("; }
if kind == tkRParen { return ")"; } if kind == tkRParen { return ")"; }
if kind == tkLBrace { return "{"; } if kind == tkLBrace { return "{"; }
+90
View File
@@ -186,4 +186,94 @@ func Type_ToString(t: Type) -> String {
if t.kind == tyFunc { return t.name; } if t.kind == tyFunc { return t.name; }
return "?"; 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;
}
} }
+54
View File
@@ -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
@@ -0,0 +1,7 @@
[Package]
Name = "type_mismatch"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -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
@@ -0,0 +1,6 @@
import Std::Io::PrintLine;
func Main() -> int {
let x: int = "hello";
return 0;
}
+7
View File
@@ -0,0 +1,7 @@
[Package]
Name = "undeclared"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -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
@@ -0,0 +1,6 @@
import Std::Io::PrintLine;
func Main() -> int {
PrintLine(noSuchVar);
return 0;
}
+137 -6
View File
@@ -4,7 +4,7 @@
# Usage: bux-lsp # Usage: bux-lsp
# The editor spawns this binary and communicates via stdin/stdout. # 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 # JSON-RPC Transport
@@ -175,19 +175,150 @@ proc analyzeFile(path: string, content: string): DocumentState =
i += 1 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", %*{ sendNotification(stream, "textDocument/publishDiagnostics", %*{
"uri": uri, "uri": uri,
"diagnostics": [] "diagnostics": arr
}) })
proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) = 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 doc.symbols = updated.symbols
publishDiagnostics(stream, doc.uri) let diags = runBuxcDiagnostics(path, doc.content)
publishDiagnostics(stream, doc.uri, diags)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Completion # Completion