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
+64 -5
View File
@@ -57,6 +57,22 @@ proc cEscape(s: string): string =
of '\0': result.add("\\0")
else: result.add(c)
proc sanitizeCTypeNamePart(s: string): string =
## Make a C type string safe for use inside a typedef name.
result = s
result = result.replace("const char*", "cstr")
result = result.replace("unsigned int", "uint")
result = result.replace(" ", "_")
result = result.replace("*", "Ptr")
result = result.replace("(", "")
result = result.replace(")", "")
result = result.replace(",", "_")
result = result.replace(".", "_")
proc typeToCStr(typ: Type): string
proc funcFatTypeName*(typ: Type): string
proc funcCodePtrType*(typ: Type): string
proc typeToCStr(typ: Type): string =
## Convert a Bux Type to a C type string.
if typ == nil: return "int"
@@ -105,13 +121,44 @@ proc typeToCStr(typ: Type): string =
of "float64": return "double"
of "bool": return "bool"
else: return typ.name
of tkTuple:
## (T, U) → typedef struct { T _0; U _1; } Tuple_T_U;
if typ.inner.len == 0:
return "Tuple_Empty"
var parts: seq[string] = @[]
for e in typ.inner:
parts.add(sanitizeCTypeNamePart(typeToCStr(e)))
return "Tuple_" & parts.join("_")
of tkFunc:
if typ.inner.len == 0: return "void (*)(void)"
let params = typ.inner[0..^2].mapIt(typeToCStr(it)).join(", ")
let ret = typeToCStr(typ.inner[^1])
return ret & " (*)(" & params & ")"
## Fat function pointer: { code(env, args...), env }
## Enables multi-instance closures with captures.
return funcFatTypeName(typ)
else: return "int"
proc funcFatTypeName*(typ: Type): string =
## BuxFn_<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 =
if node == nil: return "int"
result = typeToCStr(node.typ)
@@ -498,7 +545,12 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
for e in node.tupleInitElements:
elems.add(lowerExpr(ctx, e))
let t = b.freshTemp()
b.emitRawC(&"/* tuple */ {t.strVal} = {{{elems.mapIt($it).join(\", \")}}};")
let typeName = typeToCStr(node.typ)
b.emitAlloca(t.strVal, typeName)
var fields: seq[string] = @[]
for i, e in elems:
fields.add(&"._{i} = {lirValToC(e)}")
b.emitRawC(&"{t.strVal} = ({typeName}){{{fields.join(\", \")}}};")
return t
# ── If expression (ternary) ──
@@ -780,6 +832,13 @@ proc lowerModuleToLir*(hirMod: HirModule): LirBuilder =
ctx.funcRetType = retCT
ctx.builder.beginFunc(f.name, params, retCT, f.isPublic)
# Closure thunks: materialize env from fat-func env pointer (by-value copy)
if f.captureNames.len > 0 and f.envStructName.len > 0 and f.envInstanceName.len > 0:
ctx.builder.emitRawC(&"struct {f.envStructName} {f.envInstanceName} = *((struct {f.envStructName}*)__env);")
elif f.params.len > 0 and f.params[0].name == "__env":
# Capture-less closure thunk still receives env
ctx.builder.emitRawC("(void)__env;")
if f.body != nil:
if f.body.kind == hBlock:
for stmt in f.body.blockStmts: