feat: match guards, HOF inference, ownership C.2/C.3, LSP sema hover

Sessions 18–23 quality work:

- B.3c match arm guards + sequential found-flag lower (bootstrap + selfhost)
- Generic HOF type inference (Array/Iter map/filter/fold without type args)
- Pattern binding shadowing via unique C locals (__pN_src)
- Ownership C.2 exclusive &mut data-flow + C.4 goldens; *p= store-through fix
- Ownership C.3 auto-drop on early return/branches: scoped defers, move-on-return,
  Drop monomorphization, materialize return before Drop
- LSP 0.3.0: hover from real sema types
- Examples and QUALITY_PLAN session log; selfhost-loop identical
This commit is contained in:
2026-07-18 21:52:14 +03:00
parent 66f11d1869
commit 3eb1ad3a82
25 changed files with 2246 additions and 457 deletions
+3 -3
View File
@@ -3,7 +3,7 @@ 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 stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic struct_tuple_pat match_block nested_patterns 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 ownership_checked drop_early_return 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 iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow
.PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp .PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp
@@ -143,5 +143,5 @@ selfhost-loop: build
lsp: tools/bux-lsp lsp: tools/bux-lsp
@echo "LSP server ready at tools/bux-lsp" @echo "LSP server ready at tools/bux-lsp"
tools/bux-lsp: tools/lsp_server.nim tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
cd tools && $(NIM) c -o:bux-lsp lsp_server.nim cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
+5 -1
View File
@@ -180,6 +180,8 @@ proc underlineLength(lineText: string, col: uint32, message: string): int =
proc hintForMessage(msg: string): string = proc hintForMessage(msg: string): string =
## Actionable help text for common compiler errors. ## Actionable help text for common compiler errors.
let m = msg.toLowerAscii() let m = msg.toLowerAscii()
if "while it is mutably borrowed" in m:
return "only one active '&mut' borrow is allowed at a time; end the borrow before reuse"
if "cannot assign" in m: if "cannot assign" in m:
return "ensure the right-hand side type matches the left-hand side" return "ensure the right-hand side type matches the left-hand side"
if "undeclared identifier" in m: if "undeclared identifier" in m:
@@ -193,7 +195,9 @@ proc hintForMessage(msg: string): string =
if "shared reference" in m or "checked function" in m: if "shared reference" in m or "checked function" in m:
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code" return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
if "double mutable borrow" in m or "already mutably borrowed" in m: if "double mutable borrow" in m or "already mutably borrowed" in m:
return "only one active '&mut' borrow is allowed at a time" return "only one active '&mut' borrow is allowed at a time; end the borrow before reuse"
if "shared-borrow" in m or "shared-borrowed" in m:
return "exclusive '&mut' and shared '&' cannot overlap on the same variable"
if "expected expression" in m: if "expected expression" in m:
return "the previous statement may be incomplete (missing value or ';')" return "the previous statement may be incomplete (missing value or ';')"
if "expected type" in m: if "expected type" in m:
+1
View File
@@ -163,6 +163,7 @@ type
HirMatchArm* = object HirMatchArm* = object
pattern*: Pattern pattern*: Pattern
body*: HirNode body*: HirNode
guard*: HirNode ## optional: lowered `if cond` from `p if cond => body`
HirFunc* = object HirFunc* = object
name*: string name*: string
+260 -96
View File
@@ -31,13 +31,83 @@ type
## All func types that need BuxFn_* typedefs (including locals) ## All func types that need BuxFn_* typedefs (including locals)
seenFatTypes*: seq[Type] seenFatTypes*: seq[Type]
## Pattern-binding names already alloca'd in the current function ## Pattern-binding names already alloca'd in the current function
## (avoids `int v;` twice when two matches bind the same name) ## (legacy; unique mangled names are preferred for shadowing safety)
patternBoundNames*: HashSet[string] patternBoundNames*: HashSet[string]
## Active renames: source pattern name → unique C local (for shadowing)
patternRenames*: Table[string, string]
proc freshName(ctx: var LowerCtx): string = proc freshName(ctx: var LowerCtx): string =
inc ctx.varCounter inc ctx.varCounter
result = "__tmp_" & $ctx.varCounter result = "__tmp_" & $ctx.varCounter
proc freshPatName(ctx: var LowerCtx, src: string): string =
## Unique C name for a pattern binding (allows shadowing outer lets / nested matches).
inc ctx.varCounter
let safe = if src.len > 0 and src != "_": src else: "x"
result = "__p" & $ctx.varCounter & "_" & safe
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
proc namedTypeArg(name: string): TypeExpr =
TypeExpr(kind: tekNamed, typeName: name)
proc ensureDropMono(ctx: var LowerCtx, dropBase: string, freeBase: string, typeArgs: seq[TypeExpr]) =
## Monomorphize Free (if any) then Drop so the C linker finds them.
if freeBase.len > 0:
discard ctx.generateMethodInstance(freeBase, typeArgs)
discard ctx.generateMethodInstance(dropBase, typeArgs)
proc dropTargetsVar(n: HirNode, name: string): bool =
## True if n is Type_Drop(&name) / collection Drop of that local.
if n == nil or name.len == 0: return false
if n.kind == hCall and n.callArgs.len >= 1:
let a = n.callArgs[0]
if a != nil and a.kind == hUnary and a.unaryOp == tkAmp and
a.unaryOperand != nil and a.unaryOperand.kind == hVar:
return a.unaryOperand.varName == name
return false
proc autoDropFuncName(ctx: var LowerCtx, ty: Type): string =
## Return `Type_Drop` if this type should be auto-dropped, else "".
## Also monomorphizes generic Drop/Free helpers for stdlib collections.
if ty == nil: return ""
var typeName = ""
if ty.kind == tkNamed:
typeName = ty.name
else:
return ""
# User type with @[Drop]
let sym = ctx.globalScope.lookup(typeName)
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct:
if "Drop" in sym.decl.declAttrs:
return typeName & "_Drop"
# Explicit Type_Drop function exists (extend … for Drop)
let dropSym = ctx.globalScope.lookup(typeName & "_Drop")
if dropSym != nil and dropSym.kind == skFunc:
return typeName & "_Drop"
# Stdlib mangled collections: Array_int → Array_Drop_int
if typeName.startsWith("Array_"):
let elem = typeName[6 .. ^1]
ctx.ensureDropMono("Array_Drop", "Array_Free", @[namedTypeArg(elem)])
return "Array_Drop_" & elem
if typeName.startsWith("Map_"):
let rest = typeName[4 .. ^1]
let us = rest.find('_')
if us > 0:
let k = rest[0 ..< us]
let v = rest[us+1 .. ^1]
ctx.ensureDropMono("Map_Drop", "Map_Free", @[namedTypeArg(k), namedTypeArg(v)])
return "Map_Drop_" & rest
if typeName.startsWith("Set_"):
let elem = typeName[4 .. ^1]
ctx.ensureDropMono("Set_Drop", "Set_Free", @[namedTypeArg(elem)])
return "Set_Drop_" & elem
if typeName.startsWith("Channel_"):
let elem = typeName[8 .. ^1]
ctx.ensureDropMono("Channel_Drop", "Channel_Free", @[namedTypeArg(elem)])
return "Channel_Drop_" & elem
return ""
proc freshTryVar(ctx: var LowerCtx): string = proc freshTryVar(ctx: var LowerCtx): string =
inc ctx.tryCounter inc ctx.tryCounter
result = "__try_" & $ctx.tryCounter result = "__try_" & $ctx.tryCounter
@@ -73,9 +143,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode =
return nil return nil
return hirLit(pat.patLit, litTokenType(pat.patLit), loc) return hirLit(pat.patLit, litTokenType(pat.patLit), loc)
proc matchAlwaysTrue(loc: SourceLocation): HirNode =
hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc)
proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type
proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
@@ -123,18 +190,33 @@ proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
# Single-segment enum path — always-true fallback # Single-segment enum path — always-true fallback
return nil return nil
of pkGuarded: of pkGuarded:
# Guard: inner pattern AND guard expression (lowered later if needed) # Condition is only the inner pattern; guard is applied after bindings in lowerMatch.
# For now only support always-true inner + guard as bool expr via lowerExpr path. return matchPatternCond(ctx, subject, pattern.patGuardedInner, subjectEnumName, subjectHasData, loc)
# Guards are not yet fully lowered here (require expr lowering of guard).
return nil
else: else:
# Struct/tuple patterns: not yet fully lowered — always-true # Struct/tuple patterns: not yet fully lowered — always-true
return nil return nil
# lowerMatch calls lowerExpr for arm bodies after emitting bindings
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
proc bindPatLocal(ctx: var LowerCtx, srcName: string, ty: Type, subject: HirNode,
loc: SourceLocation): seq[HirNode] =
## Allocate a unique C local for a pattern binding and map source name → C name.
result = @[]
if srcName.len == 0 or srcName == "_":
return
let cName = ctx.freshPatName(srcName)
ctx.patternRenames[srcName] = cName
ctx.patternBoundNames.incl(srcName)
result.add(hirAlloca(cName, ty, loc))
result.add(hirStore(hirVar(cName, ty, loc), subject, loc))
proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
subjectEnumName: string, subjectHasData: bool, subjectEnumName: string, subjectHasData: bool,
loc: SourceLocation): seq[HirNode] = loc: SourceLocation): seq[HirNode] =
## Emit alloca+store for identifiers bound by a match pattern. ## Emit alloca+store for identifiers bound by a match pattern.
## Each binding gets a unique C name (`__pN_src`) so nested matches and
## outer `let` can share source names without C redeclaration / use-before-decl.
## Enum payload: `Option::Some(value)` → `value = subject.data.Some_0` ## Enum payload: `Option::Some(value)` → `value = subject.data.Some_0`
## Ident catch-all: `x` → `x = subject` ## Ident catch-all: `x` → `x = subject`
result = @[] result = @[]
@@ -142,10 +224,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
case pattern.kind case pattern.kind
of pkIdent: of pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown() let ty = if subject.typ != nil: subject.typ else: makeUnknown()
if pattern.patIdent notin ctx.patternBoundNames: result.add(ctx.bindPatLocal(pattern.patIdent, ty, subject, loc))
result.add(hirAlloca(pattern.patIdent, ty, loc))
ctx.patternBoundNames.incl(pattern.patIdent)
result.add(hirStore(hirVar(pattern.patIdent, ty, loc), subject, loc))
of pkEnum: of pkEnum:
if not subjectHasData: if not subjectHasData:
return return
@@ -195,10 +274,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc) typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc) let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if arg.kind == pkIdent: if arg.kind == pkIdent:
if arg.patIdent notin ctx.patternBoundNames: result.add(ctx.bindPatLocal(arg.patIdent, fieldTy, fieldLoad, loc))
result.add(hirAlloca(arg.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(arg.patIdent)
result.add(hirStore(hirVar(arg.patIdent, fieldTy, loc), fieldLoad, loc))
else: else:
# Nested: Option::Some((a, b)), Pair::Two(Point { x, y }) # Nested: Option::Some((a, b)), Pair::Two(Point { x, y })
result.add(ctx.matchPatternBindings(fieldLoad, arg, subjectEnumName, subjectHasData, loc)) result.add(ctx.matchPatternBindings(fieldLoad, arg, subjectEnumName, subjectHasData, loc))
@@ -220,10 +296,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc) typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc) let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if nf.pattern.kind == pkIdent: if nf.pattern.kind == pkIdent:
if nf.pattern.patIdent notin ctx.patternBoundNames: result.add(ctx.bindPatLocal(nf.pattern.patIdent, fieldTy, fieldLoad, loc))
result.add(hirAlloca(nf.pattern.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(nf.pattern.patIdent)
result.add(hirStore(hirVar(nf.pattern.patIdent, fieldTy, loc), fieldLoad, loc))
else: else:
result.add(ctx.matchPatternBindings(fieldLoad, nf.pattern, subjectEnumName, subjectHasData, loc)) result.add(ctx.matchPatternBindings(fieldLoad, nf.pattern, subjectEnumName, subjectHasData, loc))
of pkGuarded: of pkGuarded:
@@ -241,10 +314,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc) typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc) let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if elem.kind == pkIdent: if elem.kind == pkIdent:
if elem.patIdent notin ctx.patternBoundNames: result.add(ctx.bindPatLocal(elem.patIdent, fieldTy, fieldLoad, loc))
result.add(hirAlloca(elem.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(elem.patIdent)
result.add(hirStore(hirVar(elem.patIdent, fieldTy, loc), fieldLoad, loc))
else: else:
# Nested patterns: recurse with field as subject # Nested patterns: recurse with field as subject
result.add(ctx.matchPatternBindings(fieldLoad, elem, subjectEnumName, subjectHasData, loc)) result.add(ctx.matchPatternBindings(fieldLoad, elem, subjectEnumName, subjectHasData, loc))
@@ -269,68 +339,114 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
typ: makePointer(fieldTy), loc: loc) typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc) let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
if fpat.kind == pkIdent: if fpat.kind == pkIdent:
if fpat.patIdent notin ctx.patternBoundNames: result.add(ctx.bindPatLocal(fpat.patIdent, fieldTy, fieldLoad, loc))
result.add(hirAlloca(fpat.patIdent, fieldTy, loc))
ctx.patternBoundNames.incl(fpat.patIdent)
result.add(hirStore(hirVar(fpat.patIdent, fieldTy, loc), fieldLoad, loc))
else: else:
result.add(ctx.matchPatternBindings(fieldLoad, fpat, subjectEnumName, subjectHasData, loc)) result.add(ctx.matchPatternBindings(fieldLoad, fpat, subjectEnumName, subjectHasData, loc))
else: else:
discard discard
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode = proc lowerMatch(ctx: var LowerCtx, subject: HirNode, astArms: seq[MatchArm], typ: Type, loc: SourceLocation): HirNode =
## Lower match expression to a block with if-else chain. ## Lower match expression to sequential ifs with a `found` flag.
## Supports: enum tags + payload bindings, integer/bool/char/string literals, ## Supports: enum tags + payload bindings, integer/bool/char/string literals,
## ranges, wildcard/ident catch-all. ## ranges, wildcard/ident catch-all, and `p if guard` arms.
##
## Each arm:
## 1. emit unique pattern bindings (sets patternRenames)
## 2. lower guard + body (idents use renames)
## 3. restore renames
## if (!found) { if (cond) { binds; if (guard) { result=body; found=true } } }
let hasResult = typ != nil and typ.kind != tkVoid and typ.kind != tkUnknown let hasResult = typ != nil and typ.kind != tkVoid and typ.kind != tkUnknown
let resultName = ctx.freshName() let resultName = ctx.freshName()
let foundName = ctx.freshName()
var stmts: seq[HirNode] = @[] var stmts: seq[HirNode] = @[]
if hasResult: if hasResult:
stmts.add(hirAlloca(resultName, typ, loc)) stmts.add(hirAlloca(resultName, typ, loc))
stmts.add(hirAlloca(foundName, makeBool(), loc))
stmts.add(hirStore(hirVar(foundName, makeBool(), loc),
hirLit(Token(kind: tkBoolLiteral, text: "false", loc: loc), makeBool(), loc), loc))
# Determine whether the matched enum has data variants (needs .tag access).
var subjectEnumName = "" var subjectEnumName = ""
var subjectHasData = false var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed: if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName) subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
proc makeArmBlock(body: HirNode, bindStmts: seq[HirNode]): HirNode = for arm in astArms:
var armStmts: seq[HirNode] = bindStmts # Snapshot renames so this arm's bindings don't leak to later arms
let savedRenames = ctx.patternRenames
var innerPat = arm.pattern
if arm.pattern != nil and arm.pattern.kind == pkGuarded:
innerPat = arm.pattern.patGuardedInner
# Register bind types for resolveExprType during body lower
if innerPat != nil and innerPat.kind == pkEnum and subjectHasData:
var enumName = ""
var variantName = ""
if innerPat.patEnumPath.len >= 2:
enumName = innerPat.patEnumPath[0]
variantName = innerPat.patEnumPath[^1]
elif innerPat.patEnumPath.len == 1:
variantName = innerPat.patEnumPath[0]
enumName = subjectEnumName
var fieldTypes: seq[Type] = @[]
let enumSym = ctx.globalScope.lookup(enumName)
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
for v in enumSym.decl.declEnumVariants:
if v.name == variantName:
for f in v.fields:
fieldTypes.add(ctx.resolveTypeExpr(f))
break
for i, arg in innerPat.patEnumArgs:
if arg != nil and arg.kind == pkIdent:
let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt()
ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft)
elif innerPat != nil and innerPat.kind == pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown()
ctx.varTypeExprs[innerPat.patIdent] = typeToTypeExpr(ty)
# Bindings BEFORE body so (1) renames active (2) alloca precedes use in C
let binds = matchPatternBindings(ctx, subject, innerPat, subjectEnumName, subjectHasData, loc)
var guardHir: HirNode = nil
if arm.pattern != nil and arm.pattern.kind == pkGuarded and arm.pattern.patGuardedExpr != nil:
guardHir = ctx.lowerExpr(arm.pattern.patGuardedExpr)
let bodyHir = ctx.lowerExpr(arm.body)
# Pop this arm's renames (nested matches already restored themselves)
ctx.patternRenames = savedRenames
var successStmts: seq[HirNode] = @[]
if hasResult: if hasResult:
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) successStmts.add(hirStore(hirVar(resultName, typ, loc), bodyHir, loc))
elif body != nil: elif bodyHir != nil:
# Void match: evaluate body for side effects successStmts.add(bodyHir)
armStmts.add(body) successStmts.add(hirStore(hirVar(foundName, makeBool(), loc),
return hirBlock(armStmts, nil, makeVoid(), loc) hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc), loc))
let successBlock = hirBlock(successStmts, nil, makeVoid(), loc)
# Build if-else chain from arms (last arm is the outermost else) var afterBinds: HirNode
var ifChain: HirNode = nil if guardHir != nil:
afterBinds = HirNode(kind: hIf, ifCond: guardHir, ifThen: successBlock, ifElse: nil,
for i in countdown(arms.len - 1, 0):
let arm = arms[i]
let binds = matchPatternBindings(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc)
let armBlock = makeArmBlock(arm.body, binds)
let cond = matchPatternCond(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc)
if cond == nil:
# Always-true arm (wildcard / incomplete pattern)
if ifChain == nil:
ifChain = armBlock
else:
ifChain = HirNode(kind: hIf, ifCond: matchAlwaysTrue(loc), ifThen: armBlock,
ifElse: ifChain, typ: makeVoid(), loc: loc)
else:
if ifChain == nil:
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
typ: makeVoid(), loc: loc) typ: makeVoid(), loc: loc)
else: else:
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: ifChain, afterBinds = successBlock
var armInnerStmts = binds
armInnerStmts.add(afterBinds)
let armInner = hirBlock(armInnerStmts, nil, makeVoid(), loc)
let cond = matchPatternCond(ctx, subject, innerPat, subjectEnumName, subjectHasData, loc)
let armBody = if cond == nil: armInner
else: HirNode(kind: hIf, ifCond: cond, ifThen: armInner, ifElse: nil,
typ: makeVoid(), loc: loc) typ: makeVoid(), loc: loc)
if ifChain != nil: let notFound = HirNode(kind: hUnary, unaryOp: tkBang,
stmts.add(ifChain) unaryOperand: hirVar(foundName, makeBool(), loc),
typ: makeBool(), loc: loc)
stmts.add(HirNode(kind: hIf, ifCond: notFound, ifThen: armBody, ifElse: nil,
typ: makeVoid(), loc: loc))
if hasResult: if hasResult:
return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc) return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc)
@@ -357,6 +473,7 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx =
result.funcAdapterSigs = initTable[string, Type]() result.funcAdapterSigs = initTable[string, Type]()
result.seenFatTypes = @[] result.seenFatTypes = @[]
result.patternBoundNames = initHashSet[string]() result.patternBoundNames = initHashSet[string]()
result.patternRenames = initTable[string, string]()
proc sanitizeFatPart(s: string): string = proc sanitizeFatPart(s: string): string =
result = s.replace("const char*", "cstr").replace("unsigned int", "uint") result = s.replace("const char*", "cstr").replace("unsigned int", "uint")
@@ -544,8 +661,7 @@ proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type =
return makeFunc(params, ret) return makeFunc(params, ret)
else: return makeUnknown() else: return makeUnknown()
# Forward declarations # Forward declarations (lowerExpr already declared above for lowerMatch)
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc
@@ -854,8 +970,6 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
return typeToTypeExpr(concreteArgs[0]) return typeToTypeExpr(concreteArgs[0])
return TypeExpr(kind: tekNamed, typeName: "unknown") return TypeExpr(kind: tekNamed, typeName: "unknown")
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
proc lowerExprWithDynRefCoerce(ctx: var LowerCtx, arg: Expr, expectedType: Type): HirNode = proc lowerExprWithDynRefCoerce(ctx: var LowerCtx, arg: Expr, expectedType: Type): HirNode =
## Lower an expression, coercing &Concrete to &dyn Trait if needed. ## Lower an expression, coercing &Concrete to &dyn Trait if needed.
let lowered = ctx.lowerExpr(arg) let lowered = ctx.lowerExpr(arg)
@@ -997,6 +1111,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekIdent: of ekIdent:
let name = expr.exprIdent let name = expr.exprIdent
# Pattern binding rename: source name → unique C local (`__pN_v`)
if ctx.patternRenames.hasKey(name):
let cName = ctx.patternRenames[name]
return hirVar(cName, typ, loc)
# Capture rewriting: if inside closure and ident is captured # Capture rewriting: if inside closure and ident is captured
if ctx.closureDepth > 0 and ctx.currentClosureExpr != nil and ctx.envInstanceName != "": if ctx.closureDepth > 0 and ctx.currentClosureExpr != nil and ctx.envInstanceName != "":
let idx = ctx.currentClosureExpr.captureNames.find(name) let idx = ctx.currentClosureExpr.captureNames.find(name)
@@ -1286,6 +1404,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
args.add(ctx.lowerExpr(idxExpr)) args.add(ctx.lowerExpr(idxExpr))
args.add(ctx.lowerExpr(expr.exprAssignValue)) args.add(ctx.lowerExpr(expr.exprAssignValue))
return hirCall(calleeName, args, makeVoid(), loc) return hirCall(calleeName, args, makeVoid(), loc)
# `*p = value` must store through the pointer, not assign to a loaded temp.
# Represent as hAssign to hLoad(loadPtr=p) so LIR emits `*p = value`.
if expr.exprAssignTarget.kind == ekUnary and expr.exprAssignTarget.exprUnaryOp == tkStar:
let destPtr = ctx.lowerExpr(expr.exprAssignTarget.exprUnaryOperand)
let value = ctx.lowerExpr(expr.exprAssignValue)
let loadTarget = HirNode(kind: hLoad, loadPtr: destPtr, typ: typ, loc: loc)
return HirNode(kind: hAssign, assignOp: tkAssign,
assignTarget: loadTarget, assignValue: value,
typ: makeVoid(), loc: loc)
let target = ctx.lowerExpr(expr.exprAssignTarget) let target = ctx.lowerExpr(expr.exprAssignTarget)
let value = ctx.lowerExpr(expr.exprAssignValue) let value = ctx.lowerExpr(expr.exprAssignValue)
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp, return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
@@ -1473,29 +1600,30 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekMatch: of ekMatch:
let subject = ctx.lowerExpr(expr.exprMatchSubject) let subject = ctx.lowerExpr(expr.exprMatchSubject)
var subjectEnumName = ""
var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
# Prefer resolved match type; fall back to function return type when arms # Prefer resolved match type; fall back to function return type when arms
# only reference pattern bindings (not yet in varTypeExprs during resolve). # only reference pattern bindings (not yet in varTypeExprs during resolve).
var matchTyp = typ var matchTyp = typ
if matchTyp == nil or matchTyp.kind == tkUnknown: if matchTyp == nil or matchTyp.kind == tkUnknown:
if ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind notin {tkVoid, tkUnknown}: if ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind notin {tkVoid, tkUnknown}:
matchTyp = ctx.currentFuncRetType matchTyp = ctx.currentFuncRetType
var arms: seq[HirMatchArm] = @[] # Register bind types early so matchTyp fallback can resolve arm bodies
var subjectEnumName = ""
var subjectHasData = false
if subject.typ != nil and subject.typ.kind == tkNamed:
subjectEnumName = subject.typ.name
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
for arm in expr.exprMatchArms: for arm in expr.exprMatchArms:
# Register bind types only (do NOT call matchPatternBindings here — that var bindPat = arm.pattern
# would mark names as already alloca'd and lowerMatch would skip them). if bindPat != nil and bindPat.kind == pkGuarded:
if arm.pattern != nil and arm.pattern.kind == pkEnum and subjectHasData: bindPat = bindPat.patGuardedInner
if bindPat != nil and bindPat.kind == pkEnum and subjectHasData:
var enumName = "" var enumName = ""
var variantName = "" var variantName = ""
if arm.pattern.patEnumPath.len >= 2: if bindPat.patEnumPath.len >= 2:
enumName = arm.pattern.patEnumPath[0] enumName = bindPat.patEnumPath[0]
variantName = arm.pattern.patEnumPath[^1] variantName = bindPat.patEnumPath[^1]
elif arm.pattern.patEnumPath.len == 1: elif bindPat.patEnumPath.len == 1:
variantName = arm.pattern.patEnumPath[0] variantName = bindPat.patEnumPath[0]
enumName = subjectEnumName enumName = subjectEnumName
var fieldTypes: seq[Type] = @[] var fieldTypes: seq[Type] = @[]
let enumSym = ctx.globalScope.lookup(enumName) let enumSym = ctx.globalScope.lookup(enumName)
@@ -1505,19 +1633,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
for f in v.fields: for f in v.fields:
fieldTypes.add(ctx.resolveTypeExpr(f)) fieldTypes.add(ctx.resolveTypeExpr(f))
break break
for i, arg in arm.pattern.patEnumArgs: for i, arg in bindPat.patEnumArgs:
if arg != nil and arg.kind == pkIdent: if arg != nil and arg.kind == pkIdent:
let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt() let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt()
ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft) ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft)
elif arm.pattern != nil and arm.pattern.kind == pkIdent: elif bindPat != nil and bindPat.kind == pkIdent:
let ty = if subject.typ != nil: subject.typ else: makeUnknown() let ty = if subject.typ != nil: subject.typ else: makeUnknown()
ctx.varTypeExprs[arm.pattern.patIdent] = typeToTypeExpr(ty) ctx.varTypeExprs[bindPat.patIdent] = typeToTypeExpr(ty)
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body))) # Binds + body lower happen inside lowerMatch (unique C names + renames)
# Re-resolve match type now that bindings are registered return lowerMatch(ctx, subject, expr.exprMatchArms, matchTyp, loc)
if matchTyp == nil or matchTyp.kind == tkUnknown:
if arms.len > 0 and arms[0].body != nil and arms[0].body.typ != nil:
matchTyp = arms[0].body.typ
return lowerMatch(ctx, subject, arms, matchTyp, loc)
of ekSizeOf: of ekSizeOf:
let ty = ctx.resolveTypeExpr(expr.exprSizeOfType) let ty = ctx.resolveTypeExpr(expr.exprSizeOfType)
@@ -1663,6 +1787,13 @@ 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)
# Auto-Drop: @[Drop] types and Array/Map/etc. with TypeName_Drop
let dropName = ctx.autoDropFuncName(allocaType)
if dropName.len > 0:
let addrOf = hirUnary(tkAmp, hirVar(stmt.stmtLetName, allocaType, loc),
makePointer(allocaType), loc)
let dropCall = hirCall(dropName, @[addrOf], makeVoid(), loc)
ctx.deferStmts.add(dropCall)
# Capture filling for closures is done at the ekClosure site (heap env). # Capture filling for closures is done at the ekClosure site (heap env).
return hirBlock(stmts, nil, makeVoid(), loc) return hirBlock(stmts, nil, makeVoid(), loc)
@@ -1670,10 +1801,25 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil let value = if stmt.stmtReturnValue != nil: ctx.lowerExpr(stmt.stmtReturnValue) else: nil
var stmts = ctx.pendingStmts var stmts = ctx.pendingStmts
ctx.pendingStmts = @[] ctx.pendingStmts = @[]
# Add defers in reverse order (LIFO) # Move-on-return: do not Drop a local that is returned by value.
var skipDrop = ""
if value != nil and value.kind == hVar:
skipDrop = value.varName
# Materialize the return value BEFORE drops so `return a.id` is not
# use-after-drop (drops are separate stmts; LIR evaluates return expr last).
var retVal = value
if value != nil and ctx.deferStmts.len > 0:
let retTy = if value.typ != nil: value.typ else: makeUnknown()
if retTy.kind != tkVoid:
let tmp = ctx.freshName()
stmts.add(hirAlloca(tmp, retTy, loc))
stmts.add(hirStore(hirVar(tmp, retTy, loc), value, loc))
retVal = hirVar(tmp, retTy, loc)
# Add defers in reverse order (LIFO); snapshot full stack for every return path
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i]) stmts.add(ctx.deferStmts[i])
stmts.add(hirReturn(value, loc)) stmts.add(hirReturn(retVal, loc))
return hirBlock(stmts, nil, makeVoid(), loc) return hirBlock(stmts, nil, makeVoid(), loc)
of skIf: of skIf:
@@ -1904,11 +2050,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
of skMatch: of skMatch:
let subject = ctx.lowerExpr(stmt.stmtMatchSubject) let subject = ctx.lowerExpr(stmt.stmtMatchSubject)
var arms: seq[HirMatchArm] = @[] # Statement match: binds + body lower inside lowerMatch (unique C names)
for arm in stmt.stmtMatchArms: return ctx.flushPending(lowerMatch(ctx, subject, stmt.stmtMatchArms, makeVoid(), loc))
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
# Statement match: lower to if-else chain (void result)
return ctx.flushPending(lowerMatch(ctx, subject, arms, makeVoid(), loc))
of skSwitch: of skSwitch:
let subject = ctx.lowerExpr(stmt.stmtSwitchExpr) let subject = ctx.lowerExpr(stmt.stmtSwitchExpr)
@@ -1941,7 +2084,13 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
## asExpr=true: block is used as a value (`let x = { ... }`, match arm body). ## asExpr=true: block is used as a value (`let x = { ... }`, match arm body).
## Last skExpr becomes the block result. Statement blocks (func body, if/while) ## Last skExpr becomes the block result. Statement blocks (func body, if/while)
## keep asExpr=false so trailing void calls stay as statements. ## keep asExpr=false so trailing void calls stay as statements.
##
## Auto-drop / defer scope: locals introduced in this block are dropped at
## block exit (LIFO). Nested if/while bodies get their own scope so branch-
## local drops do not leak into sibling branches. Early return still injects
## the full live stack (see skReturn).
if blk == nil: return nil if blk == nil: return nil
let deferBase = ctx.deferStmts.len
var stmts: seq[HirNode] = @[] var stmts: seq[HirNode] = @[]
for s in blk.stmts: for s in blk.stmts:
let hir = ctx.lowerStmt(s) let hir = ctx.lowerStmt(s)
@@ -1966,6 +2115,16 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
let last = stmts[^1] let last = stmts[^1]
stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc) stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc)
expr = last.blockExpr expr = last.blockExpr
# Scope exit: Drop locals introduced in this block (not outer ones).
# Skip Drop for a local that is the block result (move into expr / caller).
var skipDrop = ""
if expr != nil and expr.kind == hVar:
skipDrop = expr.varName
if ctx.deferStmts.len > deferBase:
for i in countdown(ctx.deferStmts.len - 1, deferBase):
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
stmts.add(ctx.deferStmts[i])
ctx.deferStmts.setLen(deferBase)
let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid() let typ = if expr != nil and expr.typ != nil: expr.typ else: makeVoid()
return hirBlock(stmts, expr, typ, blk.loc, isScope = true) return hirBlock(stmts, expr, typ, blk.loc, isScope = true)
@@ -2008,10 +2167,14 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
let oldFuncRetType = ctx.currentFuncRetType let oldFuncRetType = ctx.currentFuncRetType
let oldVarTypeExprs = ctx.varTypeExprs let oldVarTypeExprs = ctx.varTypeExprs
let oldPatternBound = ctx.patternBoundNames let oldPatternBound = ctx.patternBoundNames
let oldPatternRenames = ctx.patternRenames
ctx.currentFuncRetType = retType ctx.currentFuncRetType = retType
ctx.currentFuncDecl = decl ctx.currentFuncDecl = decl
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
ctx.patternBoundNames = initHashSet[string]() ctx.patternBoundNames = initHashSet[string]()
ctx.patternRenames = initTable[string, string]()
let oldDefers = ctx.deferStmts
ctx.deferStmts = @[]
# Add parameters to varTypeExprs after clearing so they are visible in the body. # Add parameters to varTypeExprs after clearing so they are visible in the body.
for p in funcParams: for p in funcParams:
if p.ptype != nil: if p.ptype != nil:
@@ -2032,12 +2195,13 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
if not hasReturn: if not hasReturn:
for i in countdown(ctx.deferStmts.len - 1, 0): for i in countdown(ctx.deferStmts.len - 1, 0):
body.blockStmts.add(ctx.deferStmts[i]) body.blockStmts.add(ctx.deferStmts[i])
ctx.deferStmts = @[] ctx.deferStmts = oldDefers
ctx.currentFuncDecl = oldFuncDecl ctx.currentFuncDecl = oldFuncDecl
ctx.currentFuncRetType = oldFuncRetType ctx.currentFuncRetType = oldFuncRetType
ctx.varTypeExprs = oldVarTypeExprs ctx.varTypeExprs = oldVarTypeExprs
ctx.patternBoundNames = oldPatternBound ctx.patternBoundNames = oldPatternBound
ctx.patternRenames = oldPatternRenames
result = HirFunc(name: funcName, params: params, retType: retType, result = HirFunc(name: funcName, params: params, retType: retType,
body: body, isPublic: decl.isPublic) body: body, isPublic: decl.isPublic)
+28 -8
View File
@@ -63,14 +63,23 @@ proc checkAny(p: Parser, kinds: openArray[TokenKind]): bool =
return false return false
proc isTypeArgListAhead(p: Parser): bool = proc isTypeArgListAhead(p: Parser): bool =
## Lookahead to determine if '<' starts a type argument list. ## Lookahead to determine if '<' starts a type argument list (`Foo<int>`).
## Returns true if we can find a matching '>' before EOF, '{', or ';'. ## Returns true if we find a matching '>' before tokens that cannot appear
## inside type arguments. Must NOT match comparison `x < 0` when a later
## `x > 0` exists in the same expression/region (e.g. match arm guards).
if not p.check(tkLt): return false if not p.check(tkLt): return false
var depth = 0 var depth = 0
var ahead = 0 var ahead = 0
while true: while true:
let kind = p.peek(ahead) let kind = p.peek(ahead)
if kind == tkEndOfFile or kind == tkLBrace or kind == tkSemicolon: # Hard stops: cannot appear inside `<...>` type args
if kind in {tkEndOfFile, tkLBrace, tkRBrace, tkSemicolon, tkFatArrow,
tkIf, tkElse, tkWhile, tkFor, tkMatch, tkReturn, tkLet, tkVar,
tkEq, tkNe, tkLe, tkGe, tkAmpAmp, tkPipePipe, tkAssign}:
return false
# Literals and comparison/arithmetic mean this is a value expression, not types
if kind in {tkIntLiteral, tkFloatLiteral, tkStringLiteral, tkCharLiteral,
tkBoolLiteral, tkPlus, tkMinus, tkSlash, tkPercent}:
return false return false
if kind == tkLt: if kind == tkLt:
inc depth inc depth
@@ -170,6 +179,8 @@ type
targetOs*: string targetOs*: string
checked*: bool ## @[Checked] — enable borrow checking checked*: bool ## @[Checked] — enable borrow checking
shared*: bool ## @[Shared] — mark function as thread-safe shared*: bool ## @[Shared] — mark function as thread-safe
drop*: bool ## @[Drop] — auto-call Type_Drop at scope exit
release*: bool ## @[Release] — explicit zero-cost (no borrow checks)
proc parseAttrs(p: var Parser): ParsedAttrs = proc parseAttrs(p: var Parser): ParsedAttrs =
while p.check(tkAt): while p.check(tkAt):
@@ -180,6 +191,10 @@ proc parseAttrs(p: var Parser): ParsedAttrs =
result.checked = true result.checked = true
elif name == "Shared": elif name == "Shared":
result.shared = true result.shared = true
elif name == "Drop":
result.drop = true
elif name == "Release":
result.release = true
elif name == "Import": elif name == "Import":
discard p.expect(tkLParen, "expected '('") discard p.expect(tkLParen, "expected '('")
let key = p.expect(tkIdent, "expected attribute key").text let key = p.expect(tkIdent, "expected attribute key").text
@@ -375,8 +390,8 @@ proc parsePattern(p: var Parser): Pattern =
let inclusive = p.check(tkDotDotEqual) let inclusive = p.check(tkDotDotEqual)
discard p.advance() discard p.advance()
let right = p.parsePrimaryPattern() let right = p.parsePrimaryPattern()
return Pattern(kind: pkRange, loc: loc, patRangeLo: left, patRangeHi: right, patRangeInclusive: inclusive) left = Pattern(kind: pkRange, loc: loc, patRangeLo: left, patRangeHi: right, patRangeInclusive: inclusive)
# Guarded pattern # Guarded pattern: `p if cond` (also after range: `1..10 if x % 2 == 0`)
if p.check(tkIf): if p.check(tkIf):
discard p.advance() discard p.advance()
let guard = p.parseExpr() let guard = p.parseExpr()
@@ -1242,6 +1257,8 @@ proc parseFuncDecl(p: var Parser, isPublic: bool, isAsm: bool, attrs: ParsedAttr
discard p.advance() discard p.advance()
var declAttrs: seq[string] = @[] var declAttrs: seq[string] = @[]
if attrs.checked: declAttrs.add("Checked") if attrs.checked: declAttrs.add("Checked")
if attrs.release: declAttrs.add("Release")
if attrs.drop: declAttrs.add("Drop")
return Decl(kind: dkFunc, loc: loc, isPublic: isPublic, return Decl(kind: dkFunc, loc: loc, isPublic: isPublic,
declAttrs: declAttrs, declAttrs: declAttrs,
declFuncAsm: isAsm, declFuncCallConv: attrs.callConv, declFuncAsm: isAsm, declFuncCallConv: attrs.callConv,
@@ -1250,7 +1267,7 @@ proc parseFuncDecl(p: var Parser, isPublic: bool, isAsm: bool, attrs: ParsedAttr
declFuncParams: params, declFuncReturnType: retType, declFuncParams: params, declFuncReturnType: retType,
declFuncBody: body) declFuncBody: body)
proc parseStructDecl(p: var Parser, isPublic: bool): Decl = proc parseStructDecl(p: var Parser, isPublic: bool, attrs: ParsedAttrs = ParsedAttrs()): Decl =
let loc = p.currentLoc let loc = p.currentLoc
discard p.expect(tkStruct, "expected 'struct'") discard p.expect(tkStruct, "expected 'struct'")
let name = p.expect(tkIdent, "expected struct name").text let name = p.expect(tkIdent, "expected struct name").text
@@ -1279,7 +1296,10 @@ proc parseStructDecl(p: var Parser, isPublic: bool): Decl =
if p.pos == startPos: if p.pos == startPos:
discard p.advance() discard p.advance()
discard p.expect(tkRBrace, "expected '}' to close struct") discard p.expect(tkRBrace, "expected '}' to close struct")
return Decl(kind: dkStruct, loc: loc, isPublic: isPublic, var declAttrs: seq[string] = @[]
if attrs.drop: declAttrs.add("Drop")
if attrs.checked: declAttrs.add("Checked")
return Decl(kind: dkStruct, loc: loc, isPublic: isPublic, declAttrs: declAttrs,
declStructName: name, declStructTypeParams: typeParams, declStructName: name, declStructTypeParams: typeParams,
declStructFields: fields) declStructFields: fields)
@@ -1567,7 +1587,7 @@ proc parseDecl(p: var Parser): Decl =
of tkFunc: of tkFunc:
return p.parseFuncDecl(isPublic, false, attrs, isConst, isAsync) return p.parseFuncDecl(isPublic, false, attrs, isConst, isAsync)
of tkStruct: of tkStruct:
return p.parseStructDecl(isPublic) return p.parseStructDecl(isPublic, attrs)
of tkEnum: of tkEnum:
return p.parseEnumDecl(isPublic) return p.parseEnumDecl(isPublic)
of tkUnion: of tkUnion:
+305 -46
View File
@@ -1,4 +1,4 @@
import std/[strformat, tables, strutils] import std/[strformat, tables, strutils, sets]
import ast, types, scope, source_location, token import ast, types, scope, source_location, token
type type
@@ -44,6 +44,12 @@ type
checkedFunc*: bool ## true inside @[Checked] function checkedFunc*: bool ## true inside @[Checked] function
currentFuncIsAsync*: bool ## true inside async func currentFuncIsAsync*: bool ## true inside async func
movedVars*: seq[string] ## variables moved in current checked function movedVars*: seq[string] ## variables moved in current checked function
## Active exclusive borrows: source var → borrow site (let-bound &mut lasts for rest of fn)
activeMutBorrows*: Table[string, SourceLocation]
## Active shared borrows of a source var (count of live &T lets)
activeSharedBorrows*: Table[string, int]
## When true, ekIdent skips use-while-borrowed (we're forming `&x` itself)
suppressUseWhileBorrow*: bool
currentRetType*: Type ## return type of the function being checked currentRetType*: Type ## return type of the function being checked
closureDepth*: int ## nesting depth inside closures closureDepth*: int ## nesting depth inside closures
currentClosureExpr*: Expr ## current closure being analyzed currentClosureExpr*: Expr ## current closure being analyzed
@@ -101,6 +107,63 @@ proc hasErrors*(res: SemaResult): bool =
return true return true
return false return false
# ---------------------------------------------------------------------------
# Borrow checker helpers (@[Checked] exclusive &mut / shared & data-flow)
# ---------------------------------------------------------------------------
proc extractBorrowedIdent*(e: Expr): string =
## Identify the source variable of `&x`, `borrow x`, `borrow &mut x`, etc.
if e == nil: return ""
case e.kind
of ekUnary:
if e.exprUnaryOp == tkAmp and e.exprUnaryOperand != nil:
if e.exprUnaryOperand.kind == ekIdent:
return e.exprUnaryOperand.exprIdent
if e.exprUnaryOperand.kind == ekUnary and e.exprUnaryOperand.exprUnaryOp == tkAmp and
e.exprUnaryOperand.exprUnaryOperand != nil and
e.exprUnaryOperand.exprUnaryOperand.kind == ekIdent:
return e.exprUnaryOperand.exprUnaryOperand.exprIdent
of ekBorrow:
return extractBorrowedIdent(e.exprBorrowOperand)
else:
discard
return ""
proc checkCreateBorrow(sema: var Sema, varName: string, isMut: bool, loc: SourceLocation) =
## Register a long-lived (let-bound) borrow of `varName` in a @[Checked] function.
if not sema.checkedFunc or varName.len == 0:
return
if isMut:
if sema.activeMutBorrows.hasKey(varName):
sema.emitError(loc, &"cannot mutably borrow '{varName}': already mutably borrowed")
return
if sema.activeSharedBorrows.getOrDefault(varName, 0) > 0:
sema.emitError(loc, &"cannot mutably borrow '{varName}' while it is shared-borrowed")
return
sema.activeMutBorrows[varName] = loc
else:
if sema.activeMutBorrows.hasKey(varName):
sema.emitError(loc, &"cannot shared-borrow '{varName}' while it is mutably borrowed")
return
sema.activeSharedBorrows[varName] = sema.activeSharedBorrows.getOrDefault(varName, 0) + 1
proc checkUseWhileBorrowed(sema: var Sema, varName: string, loc: SourceLocation, isWrite: bool) =
## Reject uses of a variable that has an active exclusive (&mut) borrow.
if not sema.checkedFunc or varName.len == 0 or sema.suppressUseWhileBorrow:
return
if sema.activeMutBorrows.hasKey(varName):
let kind = if isWrite: "assign to" else: "use"
sema.emitError(loc, &"cannot {kind} '{varName}' while it is mutably borrowed")
proc checkTempMutBorrow(sema: var Sema, varName: string, loc: SourceLocation) =
## Temporary &mut in a call argument — conflict with existing long-lived borrows.
if not sema.checkedFunc or varName.len == 0:
return
if sema.activeMutBorrows.hasKey(varName):
sema.emitError(loc, &"cannot mutably borrow '{varName}': already mutably borrowed")
elif sema.activeSharedBorrows.getOrDefault(varName, 0) > 0:
sema.emitError(loc, &"cannot mutably borrow '{varName}' while it is shared-borrowed")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Generic type inference helpers # Generic type inference helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -172,6 +235,14 @@ proc typeToTypeExpr*(t: Type): TypeExpr =
else: else:
TypeExpr(kind: tekNamed, typeName: "void") TypeExpr(kind: tekNamed, typeName: "void")
of tkVoid: TypeExpr(kind: tekNamed, typeName: "void") of tkVoid: TypeExpr(kind: tekNamed, typeName: "void")
of tkFunc:
var params: seq[TypeExpr] = @[]
if t.inner.len > 0:
for i in 0 ..< t.inner.len - 1:
params.add(typeToTypeExpr(t.inner[i]))
let ret = typeToTypeExpr(t.inner[^1])
return TypeExpr(kind: tekFunc, funcParams: params, funcRet: ret)
TypeExpr(kind: tekNamed, typeName: "void")
else: TypeExpr(kind: tekNamed, typeName: t.toString) else: TypeExpr(kind: tekNamed, typeName: t.toString)
proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]): Type = proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]): Type =
@@ -213,58 +284,189 @@ proc substituteTypeInType(sema: var Sema, t: Type, subst: Table[string, Type]):
else: else:
return t return t
proc unifyTypeParam(sema: var Sema, pattern: TypeExpr, concrete: Type,
tpNames: HashSet[string],
bindings: var Table[string, Type],
loc: SourceLocation): bool =
## Structural match of a parameter TypeExpr against a concrete argument Type.
## Binds type parameters (names in `tpNames`) into `bindings`.
## Returns false on hard mismatch; true when matching succeeds (unknowns ok).
if pattern == nil:
return true
if concrete == nil or concrete.isUnknown:
return true
case pattern.kind
of tekNamed:
let name = pattern.typeName
# Bare type parameter: T
if name in tpNames and pattern.typeArgs.len == 0:
if bindings.hasKey(name):
let prev = bindings[name]
if prev == concrete:
return true
if concrete.isAssignableTo(prev):
return true
if prev.isAssignableTo(concrete):
bindings[name] = concrete
return true
sema.emitError(loc,
&"conflicting types for type parameter '{name}': " &
&"{prev.toString} vs {concrete.toString}")
return false
bindings[name] = concrete
return true
# Named type with type args: Iter<T>, Array<U>, Map<K,V>
if pattern.typeArgs.len > 0:
if concrete.kind != tkNamed:
return false
# Allow monomorphized names like Iter_int? Prefer structural Iter + inner.
if concrete.name != name and not concrete.name.startsWith(name & "_"):
return false
if concrete.name == name:
if concrete.inner.len < pattern.typeArgs.len:
return false
for i, ta in pattern.typeArgs:
if i >= concrete.inner.len: break
if not sema.unifyTypeParam(ta, concrete.inner[i], tpNames, bindings, loc):
return false
return true
# Monomorphized form Iter_int — best-effort: only if single type arg
if pattern.typeArgs.len == 1 and concrete.name.startsWith(name & "_"):
let suffix = concrete.name[name.len + 1 .. ^1]
# Only bind if pattern arg is a type param
let ta = pattern.typeArgs[0]
if ta != nil and ta.kind == tekNamed and ta.typeName in tpNames and ta.typeArgs.len == 0:
let mono = makeNamed(suffix)
# Prefer known primitives
let prim =
case suffix
of "int": makeInt()
of "bool": makeBool()
of "String", "str": makeStr()
of "float", "float64": makeFloat64()
else: mono
return sema.unifyTypeParam(ta, prim, tpNames, bindings, loc)
return false
# Concrete named type (not a param): int, String, Foo / primitives
let expected =
case name
of "int": makeInt()
of "int8": makeInt8()
of "int16": makeInt16()
of "int32": makeInt32()
of "int64": makeInt64()
of "uint": makeUInt()
of "uint8": makeUInt8()
of "uint16": makeUInt16()
of "uint32": makeUInt32()
of "uint64": makeUInt64()
of "bool": makeBool()
of "float", "float64": makeFloat64()
of "float32": makeFloat32()
of "String", "str": makeStr()
of "void": makeVoid()
else: makeNamed(name)
if concrete == expected:
return true
if concrete.kind == tkNamed and expected.kind == tkNamed:
return concrete.name == expected.name
return concrete.isAssignableTo(expected) or expected.isAssignableTo(concrete)
of tekPointer, tekOwn:
if not concrete.isPointer or concrete.inner.len == 0:
return false
return sema.unifyTypeParam(pattern.pointerPointee, concrete.inner[0], tpNames, bindings, loc)
of tekRef, tekMutRef:
if not concrete.isPointer or concrete.inner.len == 0:
return false
return sema.unifyTypeParam(pattern.pointerPointee, concrete.inner[0], tpNames, bindings, loc)
of tekFunc:
# concrete: tkFunc with inner = params ++ [ret]
if concrete.kind != tkFunc:
return false
let nParams = pattern.funcParams.len
if concrete.inner.len != nParams + 1:
# Allow fat-func mismatch length if unknown
return false
for i, p in pattern.funcParams:
if not sema.unifyTypeParam(p, concrete.inner[i], tpNames, bindings, loc):
return false
if pattern.funcRet != nil:
if not sema.unifyTypeParam(pattern.funcRet, concrete.inner[^1], tpNames, bindings, loc):
return false
return true
of tekSlice:
if not concrete.isSlice or concrete.inner.len == 0:
return false
return sema.unifyTypeParam(pattern.sliceElement, concrete.inner[0], tpNames, bindings, loc)
of tekTuple:
if concrete.kind != tkTuple or concrete.inner.len != pattern.tupleElements.len:
return false
for i, elem in pattern.tupleElements:
if not sema.unifyTypeParam(elem, concrete.inner[i], tpNames, bindings, loc):
return false
return true
of tekPath, tekDynRef, tekSelf:
return true # best-effort skip
proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type], proc inferTypeArgs(sema: var Sema, funcDecl: Decl, argTypes: seq[Type],
loc: SourceLocation): seq[TypeExpr] = loc: SourceLocation): seq[TypeExpr] =
## Infer type arguments from argument types for a generic function call. ## Infer type arguments from argument types for a generic function call.
## Uses structural matching so `*Iter<T>` + `func(T)->U` yield T and U.
## Returns empty seq if inference fails for any type parameter. ## Returns empty seq if inference fails for any type parameter.
result = @[] result = @[]
var tpNames = initHashSet[string]()
for tp in funcDecl.declFuncTypeParams:
if not tp.isLifetime:
tpNames.incl(tp.name)
var bindings = initTable[string, Type]()
# Lifetime params: mark as found if any ref param uses them
for tp in funcDecl.declFuncTypeParams: for tp in funcDecl.declFuncTypeParams:
let tpName = tp.name
# Lifetime params are inferred from ref lifetime positions
if tp.isLifetime: if tp.isLifetime:
var found = false var found = false
for i, param in funcDecl.declFuncParams: for i, param in funcDecl.declFuncParams:
if i >= argTypes.len: break if i >= argTypes.len: break
if param.ptype.kind in {tekRef, tekMutRef} and param.ptype.refLifetime == tpName: if param.ptype.kind in {tekRef, tekMutRef} and param.ptype.refLifetime == tp.name:
found = true found = true
break break
if found: if not found:
result.add(TypeExpr(kind: tekNamed, typeName: "lifetime"))
continue
# If not found in refs, treat as uninferrable
return @[] return @[]
var inferred: Type = nil # lifetimes don't go into monomorph name; placeholder
bindings[tp.name] = makeNamed("lifetime")
# Unify each param pattern with the corresponding argument type
for i, param in funcDecl.declFuncParams: for i, param in funcDecl.declFuncParams:
if i >= argTypes.len: break if i >= argTypes.len: break
# Skip pointer params — type param is inside the pointee and we cannot if param.ptype == nil: continue
# structurally extract it (e.g., *Map<K,V> → arg is *Map<int,String>) var refsTp = false
if param.ptype.kind in {tekOwn, tekPointer}: for n in tpNames:
if typeExprReferencesTypeParam(param.ptype, n):
refsTp = true
break
if not refsTp:
continue continue
if typeExprReferencesTypeParam(param.ptype, tpName): if not sema.unifyTypeParam(param.ptype, argTypes[i], tpNames, bindings, loc):
var argType = argTypes[i]
# If type param is inside a ref/pointer pointee, unwrap the arg type
if param.ptype.kind in {tekRef, tekMutRef, tekPointer} and
typeExprReferencesTypeParam(param.ptype.pointerPointee, tpName) and
argType.isPointer and argType.inner.len > 0:
argType = argType.inner[0]
if inferred == nil:
inferred = argType
elif inferred != argType:
# Check if one is assignable to the other (wider type wins)
if argTypes[i].isAssignableTo(inferred):
discard # inferred stays the same
elif inferred.isAssignableTo(argTypes[i]):
inferred = argTypes[i]
else:
sema.emitError(loc,
&"conflicting types for type parameter '{tpName}': " &
&"{inferred.toString} vs {argType.toString}")
return @[] return @[]
if inferred != nil and not inferred.isUnknown:
result.add(typeToTypeExpr(inferred)) # Emit results in decl order
else: for tp in funcDecl.declFuncTypeParams:
# Cannot infer this type parameter from arguments if tp.isLifetime:
result.add(TypeExpr(kind: tekNamed, typeName: "lifetime"))
continue
if not bindings.hasKey(tp.name):
return @[] return @[]
let t = bindings[tp.name]
if t == nil or t.isUnknown:
return @[]
result.add(typeToTypeExpr(t))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Type resolution from AST TypeExpr # Type resolution from AST TypeExpr
@@ -914,6 +1116,8 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
if sema.checkedFunc and expr.exprIdent in sema.movedVars: if sema.checkedFunc and expr.exprIdent in sema.movedVars:
sema.emitError(expr.loc, &"use of moved value '{expr.exprIdent}'") sema.emitError(expr.loc, &"use of moved value '{expr.exprIdent}'")
return makeUnknown() return makeUnknown()
# Exclusive borrow: cannot read original while &mut is live
sema.checkUseWhileBorrowed(expr.exprIdent, expr.loc, isWrite = false)
let sym = scope.lookup(expr.exprIdent) let sym = scope.lookup(expr.exprIdent)
if sym == nil: if sym == nil:
sema.emitError(expr.loc, &"undeclared identifier '{expr.exprIdent}'") sema.emitError(expr.loc, &"undeclared identifier '{expr.exprIdent}'")
@@ -946,7 +1150,15 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
return makeUnknown() return makeUnknown()
return first.typ return first.typ
of ekUnary: of ekUnary:
let operandType = sema.checkExpr(expr.exprUnaryOperand, scope) # Forming `&x` must not count as a "use" of x (borrow creation is checked separately)
var operandType: Type
if expr.exprUnaryOp == tkAmp:
let savedSup = sema.suppressUseWhileBorrow
sema.suppressUseWhileBorrow = true
operandType = sema.checkExpr(expr.exprUnaryOperand, scope)
sema.suppressUseWhileBorrow = savedSup
else:
operandType = sema.checkExpr(expr.exprUnaryOperand, scope)
case expr.exprUnaryOp case expr.exprUnaryOp
of tkBang: of tkBang:
if not operandType.isBool: if not operandType.isBool:
@@ -1035,7 +1247,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
let movedIdx = sema.movedVars.find(expr.exprAssignTarget.exprIdent) let movedIdx = sema.movedVars.find(expr.exprAssignTarget.exprIdent)
if movedIdx >= 0: if movedIdx >= 0:
sema.movedVars.delete(movedIdx) sema.movedVars.delete(movedIdx)
let target = sema.checkExpr(expr.exprAssignTarget, scope) # Cannot assign to var while it is mutably borrowed (single message; suppress rvalue use-check)
sema.checkUseWhileBorrowed(expr.exprAssignTarget.exprIdent, expr.loc, isWrite = true)
var target: Type
if expr.exprAssignTarget != nil and expr.exprAssignTarget.kind == ekIdent:
let savedSup = sema.suppressUseWhileBorrow
sema.suppressUseWhileBorrow = true
target = sema.checkExpr(expr.exprAssignTarget, scope)
sema.suppressUseWhileBorrow = savedSup
else:
target = sema.checkExpr(expr.exprAssignTarget, scope)
let value = sema.checkExpr(expr.exprAssignValue, scope) let value = sema.checkExpr(expr.exprAssignValue, scope)
if not value.isAssignableTo(target): if not value.isAssignableTo(target):
sema.emitError(expr.loc, &"cannot assign {value.toString} to {target.toString}") sema.emitError(expr.loc, &"cannot assign {value.toString} to {target.toString}")
@@ -1207,8 +1428,16 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
for i in 0 ..< argTypes.len: for i in 0 ..< argTypes.len:
if expectedParams[i].isMutRef and i < expr.exprCallArgs.len: if expectedParams[i].isMutRef and i < expr.exprCallArgs.len:
let arg = expr.exprCallArgs[i] let arg = expr.exprCallArgs[i]
if arg.kind == ekUnary and arg.exprUnaryOp == tkAmp and arg.exprUnaryOperand.kind == ekIdent: let bname = extractBorrowedIdent(arg)
mutRefArgs.add((idx: i, name: arg.exprUnaryOperand.exprIdent)) if bname.len > 0:
mutRefArgs.add((idx: i, name: bname))
# Conflict with long-lived let-bound &mut
sema.checkTempMutBorrow(bname, arg.loc)
elif expectedParams[i].isRef and i < expr.exprCallArgs.len:
let bname = extractBorrowedIdent(expr.exprCallArgs[i])
if bname.len > 0 and sema.activeMutBorrows.hasKey(bname):
sema.emitError(expr.exprCallArgs[i].loc,
&"cannot shared-borrow '{bname}' while it is mutably borrowed")
for i in 0 ..< mutRefArgs.len: for i in 0 ..< mutRefArgs.len:
for j in i+1 ..< mutRefArgs.len: for j in i+1 ..< mutRefArgs.len:
if mutRefArgs[i].name == mutRefArgs[j].name: if mutRefArgs[i].name == mutRefArgs[j].name:
@@ -1472,6 +1701,11 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
for arm in expr.exprMatchArms: for arm in expr.exprMatchArms:
var armScope = newScope(scope) var armScope = newScope(scope)
sema.extractPatternBindings(arm.pattern, armScope, subjectType) sema.extractPatternBindings(arm.pattern, armScope, subjectType)
# Type-check `p if guard` condition (must be bool; sees pattern bindings)
if arm.pattern != nil and arm.pattern.kind == pkGuarded and arm.pattern.patGuardedExpr != nil:
let guardTy = sema.checkExpr(arm.pattern.patGuardedExpr, armScope)
if not guardTy.isBool and not guardTy.isUnknown:
sema.emitError(arm.pattern.patGuardedExpr.loc, "match guard condition must be bool")
let armType = sema.checkExpr(arm.body, armScope) let armType = sema.checkExpr(arm.body, armScope)
if resultType.isUnknown: if resultType.isUnknown:
resultType = armType resultType = armType
@@ -1507,13 +1741,19 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
return makePointer(makeVoid()) return makePointer(makeVoid())
of ekBorrow: of ekBorrow:
let operand = sema.checkExpr(expr.exprBorrowOperand, scope) let operand = sema.checkExpr(expr.exprBorrowOperand, scope)
# borrow &mut expr returns the same type as the original (reference) # Explicit `borrow` — track only when bound via let (checkCreateBorrow on skLet).
# The borrow is tracked in the borrow checker # Here we validate conflicts for free-standing borrow expressions used as temps.
if sema.checkedFunc and expr.exprBorrowMutable: if sema.checkedFunc:
# Track: variable "operand" is mutably borrowed here let bname = extractBorrowedIdent(expr)
# For now, just validate the type if bname.len > 0:
discard if expr.exprBorrowMutable:
return operand sema.checkTempMutBorrow(bname, expr.loc)
elif sema.activeMutBorrows.hasKey(bname):
sema.emitError(expr.loc,
&"cannot shared-borrow '{bname}' while it is mutably borrowed")
if expr.exprBorrowMutable:
return makeMutRef(operand)
return makeRef(operand)
of ekSpread: of ekSpread:
return sema.checkExpr(expr.exprSpreadOperand, scope) return sema.checkExpr(expr.exprSpreadOperand, scope)
of ekStringInterp: of ekStringInterp:
@@ -1582,6 +1822,19 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
let initSym = scope.lookup(stmt.stmtLetInit.exprIdent) let initSym = scope.lookup(stmt.stmtLetInit.exprIdent)
if initSym != nil and initSym.isOwn: if initSym != nil and initSym.isOwn:
sema.movedVars.add(stmt.stmtLetInit.exprIdent) sema.movedVars.add(stmt.stmtLetInit.exprIdent)
# Long-lived borrow: `let r: &mut T = &x` / `let r: &T = &x`
if sema.checkedFunc and stmt.stmtLetInit != nil:
let bname = extractBorrowedIdent(stmt.stmtLetInit)
if bname.len > 0:
var isMut = false
if declaredType.isMutRef:
isMut = true
elif declaredType.isRef:
isMut = false
else:
# Untyped let + `&x` is typed as &mut by unary lowering
isMut = initType.isMutRef
sema.checkCreateBorrow(bname, isMut, stmt.stmtLetInit.loc)
return makeVoid() return makeVoid()
of skIf: of skIf:
let condType = sema.checkExpr(stmt.stmtIfCond, scope) let condType = sema.checkExpr(stmt.stmtIfCond, scope)
@@ -1631,6 +1884,10 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
for arm in stmt.stmtMatchArms: for arm in stmt.stmtMatchArms:
var armScope = newScope(scope) var armScope = newScope(scope)
sema.extractPatternBindings(arm.pattern, armScope, subjectType) sema.extractPatternBindings(arm.pattern, armScope, subjectType)
if arm.pattern != nil and arm.pattern.kind == pkGuarded and arm.pattern.patGuardedExpr != nil:
let guardTy = sema.checkExpr(arm.pattern.patGuardedExpr, armScope)
if not guardTy.isBool and not guardTy.isUnknown:
sema.emitError(arm.pattern.patGuardedExpr.loc, "match guard condition must be bool")
discard sema.checkExpr(arm.body, armScope) discard sema.checkExpr(arm.body, armScope)
return makeVoid() return makeVoid()
of skReturn: of skReturn:
@@ -1704,6 +1961,8 @@ proc checkFunc(sema: var Sema, decl: Decl) =
sema.currentFuncIsAsync = decl.declFuncIsAsync sema.currentFuncIsAsync = decl.declFuncIsAsync
if sema.checkedFunc: if sema.checkedFunc:
sema.movedVars = @[] sema.movedVars = @[]
sema.activeMutBorrows = initTable[string, SourceLocation]()
sema.activeSharedBorrows = initTable[string, int]()
var funcScope = newScope(sema.globalScope) var funcScope = newScope(sema.globalScope)
# Add type parameters to type table for resolution # Add type parameters to type table for resolution
var addedTypeParams: seq[string] = @[] var addedTypeParams: seq[string] = @[]
+102 -12
View File
@@ -1,7 +1,7 @@
# Bux — План към „добър“ език (v0.5 → v1.0) # Bux — План към „добър“ език (v0.5 → v1.0)
> **Дата:** 2026-07-18 > **Дата:** 2026-07-18
> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **41+ examples**, match + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅ > **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **43+ examples**, match + guards + **generic HOF inference** + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅
> **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain.
--- ---
@@ -59,6 +59,7 @@
| B.2 | Function pointer types | `func(T)->U` fat ABI | ✅ bootstrap + selfhost | | B.2 | Function pointer types | `func(T)->U` fat ABI | ✅ bootstrap + selfhost |
| B.3 | Match expression lowering (literals, ranges, enums) | Expression-context match → if-else | ✅ bootstrap + selfhost | | B.3 | Match expression lowering (literals, ranges, enums) | Expression-context match → if-else | ✅ bootstrap + selfhost |
| B.3b | Pattern bindings (`Some(value) => value`) | Payload idents bound in arm body | ✅ bootstrap + selfhost | | B.3b | Pattern bindings (`Some(value) => value`) | Payload idents bound in arm body | ✅ bootstrap + selfhost |
| B.3c | Match arm guards (`p if cond => …`) | Bindings visible in guard; sequential found-flag lower | ✅ bootstrap + selfhost |
| B.4 | Closures multi-instance | Fat `BuxFn` + heap env | ✅ bootstrap + selfhost | | B.4 | Closures multi-instance | Fat `BuxFn` + heap env | ✅ bootstrap + selfhost |
| B.4b | Closures: `\|\|` empty params + loop/return body | Lexer `\|\|` vs empty closure; while/break/return | ✅ bootstrap + selfhost | | B.4b | Closures: `\|\|` empty params + loop/return body | Lexer `\|\|` vs empty closure; while/break/return | ✅ bootstrap + selfhost |
| B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ | | B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ |
@@ -66,18 +67,18 @@
### C — Gradual Ownership 2.0 (P1) ### C — Gradual Ownership 2.0 (P1)
| # | Задача | Защо | | # | Задача | Защо | Статус |
|---|--------|------| |---|--------|------|--------|
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | | C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ⏳ |
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | | C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict |
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | | C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost |
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | | C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) |
### D — Tooling (P1) ### D — Tooling (P1)
| # | Задача | Защо | Статус | | # | Задача | Защо | Статус |
|---|--------|------|--------| |---|--------|------|--------|
| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ hover/def/outline + `buxc` diags (lightweight index; full sema later) | | D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ hover/def/outline + **sema types on hover** (v0.3.0) + `buxc` diags |
| D.2 | `bux fmt` стабилен + CI check | Единен style | ⏳ | | D.2 | `bux fmt` стабилен + CI check | Единен style | ⏳ |
| D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) | | D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) |
| D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ⏳ | | D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ⏳ |
@@ -297,9 +298,98 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth)
--- ---
## Сесия 18 (match arm guards — B.3c)
1. **Syntax:** `p if cond => body` (also after ranges: `1..10 if x % 2 == 0`)
2. **Parser fix (critical):** `isTypeArgListAhead` treated `x < 0` as generic when a later `x > 0` existed in another arm → infinite parse. Stop lookahead on `=>`, keywords, literals, arithmetic, comparisons.
3. **Sema:** bind inner pattern first; type-check guard as bool in arm scope
4. **HIR lower:** sequential `found` flag (no shared if-else DAG):
```
if (!found) { if (inner_cond) { binds; if (guard) { result = body; found = true; } } }
```
Bindings are in scope for the guard expression.
5. **Selfhost:** `pkGuarded` + `patGuardExpr`; same lower strategy; fix `return match {…}` to expand yield block before return
6. Example: `examples/match_guards.bux` (ident/literal/range/enum payload guards)
7. Verified: bootstrap + **buxc2** + all examples + error goldens + **selfhost-loop IDENTICAL ✓**
---
## Сесия 19 (generic HOF type inference)
1. **Bootstrap `inferTypeArgs`:** structural unify of param TypeExpr vs arg Type
- `*Iter<T>` / `*Array<T>` → extract T from pointee type args or mangled `Array_int`
- `func(T)->U` → bind T/U from function-value type (not whole func as T)
- bare `Acc` from init; multi-param `Iter_Fold<T,Acc>`
2. **Selfhost:** improved `Sema_InferGenericArgs` + return-type subst after inference
- Fix: `ekCast` must type-check operand (was skipping → no inference under `as`)
- HIR fallback mono from first *Array/*Iter arg when count=0
3. Works without explicit type args:
- `Array_Push(&nums, 1)`, `Array_Get`, `Array_Len`, `Array_Iter`
- `Iter_Map(&it, f)`, `Iter_Filter`, `Iter_Fold`, `Iter_Any` (int↔String)
4. Example: `examples/generic_infer_hof.bux`
5. Verified: bootstrap + **buxc2** + all examples + **selfhost-loop IDENTICAL ✓**
---
## Сесия 20 (LSP hover from real sema — D.1)
1. **`bux-lsp` 0.3.0** links bootstrap (`--path:../bootstrap`) and runs `analyzeFull` on open/save
2. **typeIndex:** global scope (stdlib + file) → hover signatures with real types
3. **File-local priority:** user decls override stdlib name collisions (`Max<T>` vs `Math.Max`)
4. **Locals:** walk function bodies for `let`/`var` with explicit type annotations
5. **didChange:** fast lightweight rescan; keeps previous typeIndex until save/hover refresh
6. Hover shows ```bux signature``` + `_kind_ · sema`
7. Smoke: `Main() -> int`, `PrintLine(String) -> void`, `Max<T>(a: T, b: T) -> T`
---
## Сесия 21 (pattern binding shadowing)
1. **Problem:** C/LIR function-scoped locals — nested `Some(n) => match … Some(n)` emitted store before `int n`, and `let v` + pattern `v` caused redeclaration.
2. **Bootstrap:** every pattern binding → unique C name `__pN_src` via `patternRenames` map; body/guard idents rewritten; **binds before body lower** inside `lowerMatch`.
3. **Selfhost:** same model — `Lcx_BindPatIdent` + `patMapFrom/To` rename table; arm-scoped push/pop of map.
4. Semantics: nested pattern name shadows correctly; outer `let v` survives after match that binds `v`.
5. Example: `examples/pattern_shadow.bux`
6. Verified: bootstrap + **buxc2** + all examples + **selfhost-loop IDENTICAL ✓**
---
## Сесия 22 (Ownership 2.0 — C.2 + C.4 + *p= fix)
1. **C.2 Exclusive &mut data-flow** (`@[Checked]`):
- Track long-lived let-bound borrows (`activeMutBorrows` / `activeSharedBorrows`)
- Reject: second `&mut x`, use/assign of `x` while mutably borrowed, `&x` while `&mut` live
- Call-site temps conflict with existing let-bound borrows
2. **C.4 Golden tests:**
- `tests/error_golden/exclusive_mut_let/`
- `tests/error_golden/use_while_mut_borrow/`
3. **Bugfix:** `*p = expr` now stores through the pointer (was assigning to a temp) — ownership examples finally mutate correctly
4. Example: `examples/ownership_checked.bux` (unchecked zero-cost + checked OK path)
5. Verified: 7 error goldens + borrow_test + all examples + selfhost-loop
---
## Сесия 23 (Ownership 2.0 — C.3 auto-drop early return / branches)
1. **Bootstrap auto-drop for `@[Drop]` + collections:**
- Parser: `@[Drop]` / `@[Release]` on structs and funcs (`declAttrs`)
- `autoDropFuncName` + monomorphize `Array_Drop`/`Free` (etc.) so stdlib links
- Inject `Type_Drop(&x)` on `let` via `deferStmts`
2. **Early return / multi-path:**
- Every `return` snapshots the full live defer stack (no clear-after-first-return)
- Materialize return value **before** Drop (`return a.id` is not use-after-drop)
- Move-on-return: skip Drop for a local returned by value (`return out`)
3. **Branch / loop scopes:**
- Bootstrap: `lowerBlock` scopes `deferStmts` — branch-local drops at block exit; siblings do not see each other
- Selfhost C backend: `CBE_EmitDefers` keeps stack for multi-return; `CBE_EmitAndPopDefersFrom` pops branch/loop locals after `if`/`while`/`loop`
4. **Selfhost fixes:** null-safe ret type; temp name counter; use function `retTypeName` for `__retdrop_N`
5. Example: `examples/drop_early_return.bux` (Early + Branched + Scoped → 5 drops)
6. Verified: bootstrap + **buxc2** drop tests, 7 error goldens, key examples, **selfhost-loop IDENTICAL ✓**
---
## Следващи стъпки ## Следващи стъпки
1. LSP: wire hover types from real sema 1. C.1 Lifetime elision
2. Generic type inference for `Iter_Map` without explicit `<T,U>` 2. Phase D tooling: `bux fmt` CI, `bux test --filter`, golden stdlib tests
3. Match arm guards (`p if cond => …`) 3. LSP: position-sensitive locals; inferred `let` types
4. Pattern binding name shadowing (C locals are function-scoped)
+62
View File
@@ -0,0 +1,62 @@
// C.3 — Auto-drop on early return and if branches
// @[Drop] types call Type_Drop at every exit (return + branch scope end).
import Std::Io::{PrintLine, PrintInt};
import Std::Test::{Test_AssertEqInt, Test_Pass};
@[Drop]
struct Token {
id: int,
counter: *int
}
func Token_Drop(self: *Token) {
if self.counter != null as *int {
*self.counter = *self.counter + 1;
}
}
// Drop t on both early return and fallthrough return.
func Early(flag: int, counter: *int) -> int {
let t: Token = Token { id: 1, counter: counter };
if flag == 0 {
return 0;
}
return 1;
}
// Branch-local Tokens: only the taken branch's Drop runs.
func Branched(flag: int, counter: *int) -> int {
if flag == 1 {
let a: Token = Token { id: 10, counter: counter };
return a.id;
} else {
let b: Token = Token { id: 20, counter: counter };
return b.id;
}
}
// Fallthrough: Drop at end of block without return.
func Scoped(counter: *int) {
let s: Token = Token { id: 99, counter: counter };
discard s.id;
}
func Main() -> int {
var drops: int = 0;
discard Early(0, &drops);
discard Early(1, &drops);
Test_AssertEqInt(drops, 2);
discard Branched(1, &drops);
discard Branched(0, &drops);
Test_AssertEqInt(drops, 4);
Scoped(&drops);
Test_AssertEqInt(drops, 5);
PrintInt(drops);
PrintLine("");
Test_Pass("drop_early_return");
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
// Generic HOF type inference — Iter_Map / Filter / Fold without explicit <T,U>
// Also Array_Push / Array_Get / Array_Iter inferred from arguments.
import Std::Io::{PrintLine, PrintInt};
import Std::Array::{
Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Free
};
import Std::String::{String_FromInt, String_Len, String_Eq};
import Std::Iter::{
Array_Iter, Iter,
Iter_Map, Iter_Filter, Iter_Fold, Iter_Any, Iter_All
};
import Std::Test::{
Test_AssertEqInt, Test_AssertTrue, Test_AssertEqString, Test_Pass
};
func Double(x: int) -> int {
return x * 2;
}
func IsEven(x: int) -> bool {
return (x % 2) == 0;
}
func IntToString(x: int) -> String {
return String_FromInt(x);
}
func StringLenAsInt(s: String) -> int {
return String_Len(s) as int;
}
func AddLens(acc: int, s: String) -> int {
return acc + (String_Len(s) as int);
}
func AddInt(acc: int, x: int) -> int {
return acc + x;
}
func IsNonEmpty(s: String) -> bool {
return String_Len(s) > 0;
}
func Main() -> int {
var nums: Array<int> = Array_New<int>(8);
// Type args inferred from &nums : *Array<int> and value type
Array_Push(&nums, 1);
Array_Push(&nums, 2);
Array_Push(&nums, 3);
Array_Push(&nums, 4);
Array_Push(&nums, 5);
// Iter_Map<int,int> inferred from *Iter<int> + func(int)->int
let itA: Iter<int> = Array_Iter(&nums);
var doubled: Array<int> = Iter_Map(&itA, Double);
Test_AssertEqInt(Array_Get(&doubled, 0), 2);
Test_AssertEqInt(Array_Get(&doubled, 4), 10);
// Iter_Map<int,String>
let itB: Iter<int> = Array_Iter(&nums);
var asStr: Array<String> = Iter_Map(&itB, IntToString);
Test_AssertEqInt(Array_Len(&asStr) as int, 5);
Test_AssertEqString(Array_Get(&asStr, 0), "1");
Test_AssertEqString(Array_Get(&asStr, 4), "5");
// Iter_Map<String,int>
let itC: Iter<String> = Array_Iter(&asStr);
var lens: Array<int> = Iter_Map(&itC, StringLenAsInt);
Test_AssertEqInt(Array_Get(&lens, 0), 1);
// Iter_Filter<int>
let itD: Iter<int> = Array_Iter(&nums);
var evens: Array<int> = Iter_Filter(&itD, IsEven);
Test_AssertEqInt(Array_Len(&evens) as int, 2);
Test_AssertEqInt(Array_Get(&evens, 0), 2);
Test_AssertEqInt(Array_Get(&evens, 1), 4);
// Iter_Fold<String,int> — Acc from init, T from *Iter + func
let itE: Iter<String> = Array_Iter(&asStr);
let totalChars: int = Iter_Fold(&itE, 0, AddLens);
Test_AssertEqInt(totalChars, 5);
let itF: Iter<int> = Array_Iter(&nums);
let sum: int = Iter_Fold(&itF, 0, AddInt);
Test_AssertEqInt(sum, 15);
let itG: Iter<String> = Array_Iter(&asStr);
Test_AssertTrue(Iter_Any(&itG, IsNonEmpty));
PrintInt(sum);
PrintLine("");
Test_Pass("generic_infer_hof");
return 0;
}
+83
View File
@@ -0,0 +1,83 @@
// Match arm guards: `p if cond => body`
// Bindings from the pattern are in scope for the guard expression.
import Std::Io::{PrintLine, PrintInt};
import Std::Test::{Test_AssertEqInt, Test_Pass};
enum Option {
Some(int),
None
}
func Classify(n: int) -> int {
return match n {
x if x < 0 => -1,
x if x == 0 => 0,
x if x > 0 => 1,
_ => 99
};
}
func Main() -> int {
// Ident + guard
Test_AssertEqInt(Classify(-5), -1);
Test_AssertEqInt(Classify(0), 0);
Test_AssertEqInt(Classify(7), 1);
// Literal + guard (second arm only when n is even)
let n: int = 4;
let a: int = match n {
4 if n % 2 == 0 => 40,
4 => 41,
_ => -1
};
Test_AssertEqInt(a, 40);
let m: int = 4;
let b: int = match m {
4 if m % 2 != 0 => 40,
4 => 41,
_ => -1
};
Test_AssertEqInt(b, 41);
// Range + guard
let k: int = 5;
let c: int = match k {
1..10 if k % 2 == 0 => 100,
1..10 => 101,
_ => -1
};
Test_AssertEqInt(c, 101);
let j: int = 6;
let d: int = match j {
1..10 if j % 2 == 0 => 100,
1..10 => 101,
_ => -1
};
Test_AssertEqInt(d, 100);
// Enum payload binding used in guard
let opt: Option = Option { tag: Option_Some };
opt.data.Some_0 = 15;
let e: int = match opt {
Option::Some(v) if v > 10 => v * 2,
Option::Some(v) => v,
Option::None => 0
};
Test_AssertEqInt(e, 30);
let small: Option = Option { tag: Option_Some };
small.data.Some_0 = 3;
let f: int = match small {
Option::Some(v) if v > 10 => v * 2,
Option::Some(v) => v,
Option::None => 0
};
Test_AssertEqInt(f, 3);
PrintInt(e);
PrintLine("");
Test_Pass("match_guards");
return 0;
}
+35
View File
@@ -0,0 +1,35 @@
import Std::Io::{PrintLine, PrintInt};
import Std::Test::{Test_AssertEqInt, Test_Pass};
func UncheckedInc(p: *int) {
*p = *p + 1;
}
@[Checked]
func Inc(p: &mut int) {
*p = *p + 1;
}
@[Checked]
func Get(p: &int) -> int {
return *p;
}
@[Checked]
func OkBorrow() -> int {
var x: int = 10;
Inc(&x);
return Get(&x);
}
func Main() -> int {
var n: int = 1;
UncheckedInc(&n);
Test_AssertEqInt(n, 2);
let r: int = OkBorrow();
Test_AssertEqInt(r, 11);
PrintInt(r);
PrintLine("");
Test_Pass("ownership_checked");
return 0;
}
+50
View File
@@ -0,0 +1,50 @@
import Std::Test::{Test_AssertEqInt, Test_Pass};
import Std::Io::{PrintInt, PrintLine};
enum Option {
Some(int),
None
}
func Main() -> int {
let a: Option = Option { tag: Option_Some };
a.data.Some_0 = 10;
let b: Option = Option { tag: Option_Some };
b.data.Some_0 = 20;
// Same binding name `v` in two sequential matches
let x: int = match a {
Option::Some(v) => v + 1,
Option::None => 0
};
let y: int = match b {
Option::Some(v) => v + 2,
Option::None => 0
};
Test_AssertEqInt(x, 11);
Test_AssertEqInt(y, 22);
// Nested: outer and inner both bind `n`
let nested: int = match a {
Option::Some(n) => match b {
Option::Some(n) => n, // should be b's payload (20), not a's
Option::None => -1
},
Option::None => -2
};
Test_AssertEqInt(nested, 20);
// Shadow outer let with pattern binding
let v: int = 99;
let z: int = match a {
Option::Some(v) => v, // pattern v should be 10, not 99
Option::None => 0
};
Test_AssertEqInt(z, 10);
Test_AssertEqInt(v, 99); // outer v unchanged
PrintInt(nested);
PrintLine("");
Test_Pass("pat_shadow");
return 0;
}
+3 -1
View File
@@ -66,6 +66,7 @@ const pkRange: int = 3;
const pkEnum: int = 4; const pkEnum: int = 4;
const pkStruct: int = 5; const pkStruct: int = 5;
const pkTuple: int = 6; const pkTuple: int = 6;
const pkGuarded: int = 7; // `p if cond` — patChild1 = inner, patGuardExpr = condition
struct Pattern { struct Pattern {
kind: int, kind: int,
@@ -78,10 +79,11 @@ struct Pattern {
patEnumPath: String, // for pkEnum: "Enum::Variant" patEnumPath: String, // for pkEnum: "Enum::Variant"
patStructName: String, // for pkStruct (type name) patStructName: String, // for pkStruct (type name)
patFieldName: String, // for struct field entry: field name in Point { x: a } patFieldName: String, // for struct field entry: field name in Point { x: a }
patChild1: *Pattern, // range lo / nested patChild1: *Pattern, // range lo / nested / guarded inner
patChild2: *Pattern, // range hi / nested patChild2: *Pattern, // range hi / nested
patArgs: *Pattern, // pkEnum/pkTuple/pkStruct field list (head) patArgs: *Pattern, // pkEnum/pkTuple/pkStruct field list (head)
patNext: *Pattern, // next sibling in patArgs list patNext: *Pattern, // next sibling in patArgs list
patGuardExpr: *Expr, // for pkGuarded: the `if` condition
} }
// Match arm: pattern => body // Match arm: pattern => body
+74 -13
View File
@@ -75,6 +75,8 @@ struct CEmitter {
movedName5: String, movedName5: String,
movedName6: String, movedName6: String,
movedName7: String, movedName7: String,
tmpCounter: int,
currentRetType: String,
} }
func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) { func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
@@ -155,10 +157,8 @@ func CBE_GetAutoDropVarName(node: *HirNode) -> String {
return varNode.strValue; return varNode.strValue;
} }
func CBE_EmitDefers(cbe: *CEmitter) -> int { // Emit one defer slot (shared by full-stack and scope-pop emitters).
if cbe.deferCount == 0 { return 0; } func CBE_EmitOneDefer(cbe: *CEmitter, i: int) {
var i: int = cbe.deferCount - 1;
while i >= 0 {
var dn: *HirNode = null as *HirNode; var dn: *HirNode = null as *HirNode;
if i == 0 { dn = cbe.defer0; } if i == 0 { dn = cbe.defer0; }
if i == 1 { dn = cbe.defer1; } if i == 1 { dn = cbe.defer1; }
@@ -171,8 +171,7 @@ func CBE_EmitDefers(cbe: *CEmitter) -> int {
// Skip auto-drop for moved variables // Skip auto-drop for moved variables
let deferVarName: String = CBE_GetAutoDropVarName(dn); let deferVarName: String = CBE_GetAutoDropVarName(dn);
if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) { if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) {
i = i - 1; return;
continue;
} }
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0; var sp: int = 0;
@@ -182,9 +181,32 @@ func CBE_EmitDefers(cbe: *CEmitter) -> int {
} }
CBE_EmitExpr(cbe, dn); CBE_EmitExpr(cbe, dn);
StringBuilder_Append(&cbe.sb, ";"); StringBuilder_Append(&cbe.sb, ";");
}
// Emit all active defers (LIFO) without clearing the stack.
// Must NOT clear: multiple return paths each need the full defer list.
// (Clearing caused Early(flag) { if (0) return; return 1 } to drop only on first exit.)
// Stack is reset at the start of each function emission.
func CBE_EmitDefers(cbe: *CEmitter) -> int {
if cbe.deferCount == 0 { return 0; }
var i: int = cbe.deferCount - 1;
while i >= 0 {
CBE_EmitOneDefer(cbe, i);
i = i - 1; i = i - 1;
} }
cbe.deferCount = 0; return 1;
}
// Emit branch/loop-local defers (indices fromIdx..count-1) then pop them.
// Outer defers stay live so sibling branches and later returns still drop correctly.
func CBE_EmitAndPopDefersFrom(cbe: *CEmitter, fromIdx: int) -> int {
if cbe.deferCount <= fromIdx { return 0; }
var i: int = cbe.deferCount - 1;
while i >= fromIdx {
CBE_EmitOneDefer(cbe, i);
i = i - 1;
}
cbe.deferCount = fromIdx;
return 1; return 1;
} }
@@ -381,20 +403,48 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return; return;
} }
// Return // Return — evaluate value first, then drop live locals, then return.
// (Emitting Drop before the value used to use-after-drop on `return a.id`.)
if kind == hReturn { if kind == hReturn {
// Track moved variables via return // Track moved variables via return (skip auto-drop of moved-out locals)
if node.child1 != null as *HirNode && node.child1.kind == hVar { if node.child1 != null as *HirNode && node.child1.kind == hVar {
CBE_AddMoved(cbe, node.child1.strValue); CBE_AddMoved(cbe, node.child1.strValue);
} }
let hadDefers: int = CBE_EmitDefers(cbe); if node.child1 != null as *HirNode && cbe.deferCount > 0 {
if hadDefers != 0 { // Materialize into a temp so Drop cannot clobber the returned value.
// Prefer the enclosing function return type (field-access HIR often
// carries the base struct typeName, which is wrong for `return a.id`).
cbe.tmpCounter = cbe.tmpCounter + 1;
let tmpName: String = String_Concat("__retdrop_", String_FromInt(cbe.tmpCounter));
var retCt: String = "int";
if cbe.currentRetType != null as String && !String_Eq(cbe.currentRetType, "") && !String_Eq(cbe.currentRetType, "void") {
retCt = cbe.currentRetType;
} else if node.child1.typeKind != 0 {
retCt = CBackend_TypeToC(node.child1.typeKind);
}
StringBuilder_Append(&cbe.sb, CBE_CParamDecl(retCt, tmpName));
StringBuilder_Append(&cbe.sb, " = ");
CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, ";");
discard CBE_EmitDefers(cbe);
StringBuilder_Append(&cbe.sb, "\n"); StringBuilder_Append(&cbe.sb, "\n");
var sp: int = 0; var sp: int = 0;
while sp < cbe.indent { while sp < cbe.indent {
StringBuilder_Append(&cbe.sb, " "); StringBuilder_Append(&cbe.sb, " ");
sp = sp + 1; sp = sp + 1;
} }
StringBuilder_Append(&cbe.sb, "return ");
StringBuilder_Append(&cbe.sb, tmpName);
return;
}
let hadDefers: int = CBE_EmitDefers(cbe);
if hadDefers != 0 {
StringBuilder_Append(&cbe.sb, "\n");
var sp2: int = 0;
while sp2 < cbe.indent {
StringBuilder_Append(&cbe.sb, " ");
sp2 = sp2 + 1;
}
} }
StringBuilder_Append(&cbe.sb, "return"); StringBuilder_Append(&cbe.sb, "return");
if node.child1 != null as *HirNode { if node.child1 != null as *HirNode {
@@ -444,15 +494,17 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return; return;
} }
// If // If — each branch has its own defer scope (locals do not leak)
if kind == hIf { if kind == hIf {
StringBuilder_Append(&cbe.sb, "if ("); StringBuilder_Append(&cbe.sb, "if (");
CBE_EmitExpr(cbe, node.child1); CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, ") {\n"); StringBuilder_Append(&cbe.sb, ") {\n");
if node.child2 != null as *HirNode { if node.child2 != null as *HirNode {
let savedThen: int = cbe.deferCount;
cbe.indent = cbe.indent + 1; cbe.indent = cbe.indent + 1;
CBE_EmitExpr(cbe, node.child2); CBE_EmitExpr(cbe, node.child2);
discard CBE_EmitAndPopDefersFrom(cbe, savedThen);
cbe.indent = cbe.indent - 1; cbe.indent = cbe.indent - 1;
} }
var sp: int = 0; var sp: int = 0;
@@ -464,8 +516,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
let elseBlock: *HirNode = node.extraData as *HirNode; let elseBlock: *HirNode = node.extraData as *HirNode;
if elseBlock != null as *HirNode { if elseBlock != null as *HirNode {
StringBuilder_Append(&cbe.sb, " else {\n"); StringBuilder_Append(&cbe.sb, " else {\n");
let savedElse: int = cbe.deferCount;
cbe.indent = cbe.indent + 1; cbe.indent = cbe.indent + 1;
CBE_EmitExpr(cbe, elseBlock); CBE_EmitExpr(cbe, elseBlock);
discard CBE_EmitAndPopDefersFrom(cbe, savedElse);
cbe.indent = cbe.indent - 1; cbe.indent = cbe.indent - 1;
sp = 0; sp = 0;
while sp < cbe.indent { while sp < cbe.indent {
@@ -479,14 +533,16 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
return; return;
} }
// While // While — loop-body locals dropped each iteration
if kind == hWhile { if kind == hWhile {
StringBuilder_Append(&cbe.sb, "while ("); StringBuilder_Append(&cbe.sb, "while (");
CBE_EmitExpr(cbe, node.child1); CBE_EmitExpr(cbe, node.child1);
StringBuilder_Append(&cbe.sb, ") {\n"); StringBuilder_Append(&cbe.sb, ") {\n");
if node.child2 != null as *HirNode { if node.child2 != null as *HirNode {
let savedW: int = cbe.deferCount;
cbe.indent = cbe.indent + 1; cbe.indent = cbe.indent + 1;
CBE_EmitExpr(cbe, node.child2); CBE_EmitExpr(cbe, node.child2);
discard CBE_EmitAndPopDefersFrom(cbe, savedW);
cbe.indent = cbe.indent - 1; cbe.indent = cbe.indent - 1;
} }
var sp: int = 0; var sp: int = 0;
@@ -502,8 +558,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
if kind == hLoop { if kind == hLoop {
StringBuilder_Append(&cbe.sb, "while (1) {\n"); StringBuilder_Append(&cbe.sb, "while (1) {\n");
if node.child1 != null as *HirNode { if node.child1 != null as *HirNode {
let savedL: int = cbe.deferCount;
cbe.indent = cbe.indent + 1; cbe.indent = cbe.indent + 1;
CBE_EmitExpr(cbe, node.child1); CBE_EmitExpr(cbe, node.child1);
discard CBE_EmitAndPopDefersFrom(cbe, savedL);
cbe.indent = cbe.indent - 1; cbe.indent = cbe.indent - 1;
} }
var sp: int = 0; var sp: int = 0;
@@ -1266,6 +1324,7 @@ func CBackend_Generate(mod: *HirModule) -> String {
cbe.mod = mod; cbe.mod = mod;
cbe.deferCount = 0; cbe.deferCount = 0;
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.tmpCounter = 0;
// Header // Header
StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n"); StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n");
@@ -1566,6 +1625,8 @@ func CBackend_Generate(mod: *HirModule) -> String {
cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.checkedFunc = mod.funcs[i].checkedFunc;
cbe.deferCount = 0; cbe.deferCount = 0;
cbe.movedCount = 0; cbe.movedCount = 0;
cbe.tmpCounter = 0;
cbe.currentRetType = mod.funcs[i].retTypeName;
var hasReturn: bool = false; var hasReturn: bool = false;
cbe.indent = 1; cbe.indent = 1;
CBE_EmitExpr(cbe, body); CBE_EmitExpr(cbe, body);
+365 -202
View File
@@ -34,6 +34,101 @@ struct LowerCtx {
// Borrow checker state // Borrow checker state
checkedFunc: bool, checkedFunc: bool,
releaseFunc: bool, releaseFunc: bool,
// Pattern binding renames: source name → unique C local (shadowing-safe)
patMapCount: int,
patMapFrom0: String,
patMapTo0: String,
patMapFrom1: String,
patMapTo1: String,
patMapFrom2: String,
patMapTo2: String,
patMapFrom3: String,
patMapTo3: String,
patMapFrom4: String,
patMapTo4: String,
patMapFrom5: String,
patMapTo5: String,
patMapFrom6: String,
patMapTo6: String,
patMapFrom7: String,
patMapTo7: String,
}
func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String {
// Most recent rename wins (scan from end)
var i: int = ctx.patMapCount - 1;
while i >= 0 {
var from: String = "";
var to: String = "";
if i == 0 { from = ctx.patMapFrom0; to = ctx.patMapTo0; }
else if i == 1 { from = ctx.patMapFrom1; to = ctx.patMapTo1; }
else if i == 2 { from = ctx.patMapFrom2; to = ctx.patMapTo2; }
else if i == 3 { from = ctx.patMapFrom3; to = ctx.patMapTo3; }
else if i == 4 { from = ctx.patMapFrom4; to = ctx.patMapTo4; }
else if i == 5 { from = ctx.patMapFrom5; to = ctx.patMapTo5; }
else if i == 6 { from = ctx.patMapFrom6; to = ctx.patMapTo6; }
else if i == 7 { from = ctx.patMapFrom7; to = ctx.patMapTo7; }
if String_Eq(from, src) { return to; }
i = i - 1;
}
return "";
}
func Lcx_PatPush(ctx: *LowerCtx, src: String, dst: String) {
if ctx.patMapCount >= 8 { return; }
let i: int = ctx.patMapCount;
if i == 0 { ctx.patMapFrom0 = src; ctx.patMapTo0 = dst; }
else if i == 1 { ctx.patMapFrom1 = src; ctx.patMapTo1 = dst; }
else if i == 2 { ctx.patMapFrom2 = src; ctx.patMapTo2 = dst; }
else if i == 3 { ctx.patMapFrom3 = src; ctx.patMapTo3 = dst; }
else if i == 4 { ctx.patMapFrom4 = src; ctx.patMapTo4 = dst; }
else if i == 5 { ctx.patMapFrom5 = src; ctx.patMapTo5 = dst; }
else if i == 6 { ctx.patMapFrom6 = src; ctx.patMapTo6 = dst; }
else if i == 7 { ctx.patMapFrom7 = src; ctx.patMapTo7 = dst; }
ctx.patMapCount = ctx.patMapCount + 1;
}
func Lcx_FreshPatName(ctx: *LowerCtx, src: String) -> String {
ctx.varCounter = ctx.varCounter + 1;
var safe: String = src;
if String_Eq(src, "") || String_Eq(src, "_") { safe = "x"; }
return String_Concat(String_Concat("__p", String_FromInt(ctx.varCounter as int64)),
String_Concat("_", safe));
}
// Alloca unique C local + store + scope define + rename map for pattern binding.
func Lcx_BindPatIdent(ctx: *LowerCtx, src: String, ty: String, value: *HirNode,
line: uint32, col: uint32) -> *HirNode {
if String_Eq(src, "") || String_Eq(src, "_") { return null as *HirNode; }
let cName: String = Lcx_FreshPatName(ctx, src);
Lcx_PatPush(ctx, src, cName);
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
alloca.kind = hAlloca;
alloca.line = line;
alloca.column = col;
alloca.strValue = cName;
alloca.typeName = ty;
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = cName;
store.child1 = v;
store.child2 = value;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = src;
bsym.typeKind = tyInt;
bsym.typeName = ty;
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
return alloca;
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -265,6 +360,27 @@ func Lcx_FindGenericStruct(ctx: *LowerCtx, name: String) -> *Decl {
return null as *Decl; return null as *Decl;
} }
// Extract element type from mangled collection name: Array_int → int, Iter_String → String
func Lcx_ExtractElemFromName(typeName: String) -> String {
if String_Eq(typeName, "") { return ""; }
let len: uint = bux_strlen(typeName);
// "Array_" prefix (6 chars)
if len > 6 {
let p: String = bux_str_slice(typeName, 0, 6);
if String_Eq(p, "Array_") {
return bux_str_slice(typeName, 6, len - 6);
}
}
// "Iter_" prefix (5 chars)
if len > 5 {
let p: String = bux_str_slice(typeName, 0, 5);
if String_Eq(p, "Iter_") {
return bux_str_slice(typeName, 5, len - 5);
}
}
return "";
}
func Lcx_MangleName(base: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String { func Lcx_MangleName(base: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
let r: String = String_Concat(base, "_"); let r: String = String_Concat(base, "_");
r = String_Concat(r, typeArg0); r = String_Concat(r, typeArg0);
@@ -487,6 +603,11 @@ func Lcx_PatternCond(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
if pat == null as *Pattern { return null as *HirNode; } if pat == null as *Pattern { return null as *HirNode; }
let kind: int = pat.kind; let kind: int = pat.kind;
// Guarded: condition is only the inner pattern; guard applied after binds
if kind == pkGuarded {
return Lcx_PatternCond(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col);
}
if kind == pkWildcard || kind == pkIdent { if kind == pkWildcard || kind == pkIdent {
return null as *HirNode; return null as *HirNode;
} }
@@ -568,40 +689,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
subjectEnumName: String, subjectHasData: bool, subjectEnumName: String, subjectHasData: bool,
line: uint32, col: uint32) -> *HirNode { line: uint32, col: uint32) -> *HirNode {
if pat == null as *Pattern { return null as *HirNode; } if pat == null as *Pattern { return null as *HirNode; }
// Guarded: bind from inner pattern
if pat.kind == pkGuarded {
return Lcx_PatternBindings(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col);
}
if pat.kind == pkIdent { if pat.kind == pkIdent {
// `_` is wildcard, not a binding
if String_Eq(pat.patIdent, "_") { return null as *HirNode; }
let ty: String = "int"; let ty: String = "int";
if subject != null as *HirNode && !String_Eq(subject.typeName, "") { if subject != null as *HirNode && !String_Eq(subject.typeName, "") {
ty = subject.typeName; ty = subject.typeName;
} }
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; return Lcx_BindPatIdent(ctx, pat.patIdent, ty, subject, line, col);
alloca.kind = hAlloca;
alloca.line = line;
alloca.column = col;
alloca.strValue = pat.patIdent;
alloca.typeName = ty;
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = pat.patIdent;
store.child1 = v;
store.child2 = subject;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = pat.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = ty;
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
return alloca;
} }
// Tuple: (a, b) → a = subject._0; b = subject._1 // Tuple: (a, b) → a = subject._0; b = subject._1
@@ -629,38 +726,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
fLoad.column = col; fLoad.column = col;
fLoad.child1 = fPtr; fLoad.child1 = fPtr;
fLoad.typeName = "int"; fLoad.typeName = "int";
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let bound: *HirNode = Lcx_BindPatIdent(ctx, elem.patIdent, "int", fLoad, line, col);
alloca.kind = hAlloca; if bound != null as *HirNode {
alloca.line = line;
alloca.column = col;
alloca.strValue = elem.patIdent;
alloca.typeName = "int";
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = elem.patIdent;
store.child1 = v;
store.child2 = fLoad;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = elem.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = "int";
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
if head == null as *HirNode { if head == null as *HirNode {
head = alloca; head = bound;
tail = store; tail = bound;
while tail.child3 != null as *HirNode { tail = tail.child3; }
} else { } else {
tail.child3 = alloca; tail.child3 = bound;
tail = store; while tail.child3 != null as *HirNode { tail = tail.child3; }
}
} }
} }
elem = elem.patNext; elem = elem.patNext;
@@ -690,38 +765,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
fLoad.column = col; fLoad.column = col;
fLoad.child1 = fPtr; fLoad.child1 = fPtr;
fLoad.typeName = "int"; fLoad.typeName = "int";
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let bound: *HirNode = Lcx_BindPatIdent(ctx, field.patIdent, "int", fLoad, line, col);
alloca.kind = hAlloca; if bound != null as *HirNode {
alloca.line = line;
alloca.column = col;
alloca.strValue = field.patIdent;
alloca.typeName = "int";
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = field.patIdent;
store.child1 = v;
store.child2 = fLoad;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = field.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = "int";
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
if head == null as *HirNode { if head == null as *HirNode {
head = alloca; head = bound;
tail = store; tail = bound;
while tail.child3 != null as *HirNode { tail = tail.child3; }
} else { } else {
tail.child3 = alloca; tail.child3 = bound;
tail = store; while tail.child3 != null as *HirNode { tail = tail.child3; }
}
} }
} }
field = field.patNext; field = field.patNext;
@@ -827,41 +880,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
fLoad.typeName = ftype; fLoad.typeName = ftype;
if arg.kind == pkIdent && !String_Eq(arg.patIdent, "_") { if arg.kind == pkIdent && !String_Eq(arg.patIdent, "_") {
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let bound: *HirNode = Lcx_BindPatIdent(ctx, arg.patIdent, ftype, fLoad, line, col);
alloca.kind = hAlloca; if bound != null as *HirNode {
alloca.line = line;
alloca.column = col;
alloca.strValue = arg.patIdent;
alloca.typeName = ftype;
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let bv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
bv.kind = hVar;
bv.strValue = arg.patIdent;
store.child1 = bv;
store.child2 = fLoad;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = arg.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = ftype;
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
if head == null as *HirNode { if head == null as *HirNode {
head = alloca; head = bound;
tail = store; tail = bound;
while tail.child3 != null as *HirNode { tail = tail.child3; }
} else { } else {
tail.child3 = alloca; tail.child3 = bound;
tail = store; while tail.child3 != null as *HirNode { tail = tail.child3; }
}
} }
} else if arg.kind == pkTuple || arg.kind == pkStruct || arg.kind == pkEnum { } else if arg.kind == pkTuple || arg.kind == pkStruct || arg.kind == pkEnum {
// Nested pattern on payload field // Nested pattern on payload field
@@ -913,7 +941,9 @@ func Lcx_AppendToChain(head: *HirNode, node: *HirNode) {
cur.child3 = node; cur.child3 = node;
} }
// Lower match expr → hBlock: alloca result; if-else stores; strValue = result name // Lower match expr → sequential ifs with a found flag (no shared HIR DAG).
// Each arm: if (!found) { if (cond) { binds; if (guard) { result=body; found=true; } } }
// Guards see pattern bindings. Supports pkGuarded (`p if cond`).
func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
let line: uint32 = expr.line; let line: uint32 = expr.line;
let col: uint32 = expr.column; let col: uint32 = expr.column;
@@ -921,6 +951,8 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
ctx.varCounter = ctx.varCounter + 1; ctx.varCounter = ctx.varCounter + 1;
let resultName: String = String_Concat("__match_", String_FromInt(ctx.varCounter as int64)); let resultName: String = String_Concat("__match_", String_FromInt(ctx.varCounter as int64));
ctx.varCounter = ctx.varCounter + 1;
let foundName: String = String_Concat("__found_", String_FromInt(ctx.varCounter as int64));
// Result type from sema refType, default int // Result type from sema refType, default int
var typeName: String = "int"; var typeName: String = "int";
@@ -938,7 +970,7 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
} }
} }
// Alloca result // Alloca result + found flag
let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
allocaNode.kind = hAlloca; allocaNode.kind = hAlloca;
allocaNode.line = line; allocaNode.line = line;
@@ -946,35 +978,48 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
allocaNode.strValue = resultName; allocaNode.strValue = resultName;
allocaNode.typeName = typeName; allocaNode.typeName = typeName;
// Collect arms into a temporary array via reverse build of if-chain let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
// First count arms and build from last to first foundAlloca.kind = hAlloca;
var armCount: int = 0; foundAlloca.line = line;
var arm: *MatchArm = expr.matchArms; foundAlloca.column = col;
while arm != null as *MatchArm { foundAlloca.strValue = foundName;
armCount = armCount + 1; foundAlloca.typeName = "bool";
arm = arm.next; allocaNode.child3 = foundAlloca;
}
// Build if-else from last arm to first let foundInit: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
var ifChain: *HirNode = null as *HirNode; foundInit.kind = hStore;
var ai: int = armCount - 1; foundInit.line = line;
while ai >= 0 { foundInit.column = col;
// Find arm at index ai let foundVar0: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
foundVar0.kind = hVar;
foundVar0.strValue = foundName;
foundInit.child1 = foundVar0;
foundInit.child2 = Lcx_MakeLitHir(tkBoolLiteral, "false", line, col);
foundAlloca.child3 = foundInit;
// Chain of arm ifs (forward order), linked via child3
var tail: *HirNode = foundInit;
var cur: *MatchArm = expr.matchArms; var cur: *MatchArm = expr.matchArms;
var j: int = 0; while cur != null as *MatchArm {
while j < ai && cur != null as *MatchArm { // Snapshot rename map so this arm's bindings don't leak to later arms
cur = cur.next; let savedMapCount: int = ctx.patMapCount;
j = j + 1; var bindPat: *Pattern = cur.pattern;
var guardExpr: *Expr = null as *Expr;
if cur.pattern != null as *Pattern && cur.pattern.kind == pkGuarded {
bindPat = cur.pattern.patChild1;
guardExpr = cur.pattern.patGuardExpr;
} }
if cur == null as *MatchArm { // Bindings before guard/body so renames are active and allocas precede uses
ai = ai - 1; let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col);
continue; var guardHirEarly: *HirNode = null as *HirNode;
if guardExpr != null as *Expr {
guardHirEarly = Lcx_LowerExpr(ctx, guardExpr);
} }
// Pattern bindings before body (so body idents resolve)
let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col);
let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body); let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body);
// result = body (expand block/match yield: run stmts then store result var) // Pop this arm's renames (nested matches already restored themselves)
ctx.patMapCount = savedMapCount;
// store result = body
let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
storeNode.kind = hStore; storeNode.kind = hStore;
storeNode.line = line; storeNode.line = line;
@@ -992,58 +1037,94 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
storeNode.child2 = bodyHir; storeNode.child2 = bodyHir;
} }
// armBlock = bindings → body stmts → store // found = true
let armBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let foundSet: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
armBlock.kind = hBlock; foundSet.kind = hStore;
armBlock.line = line; foundSet.line = line;
armBlock.column = col; foundSet.column = col;
var chainHead: *HirNode = bindHead; let foundVar1: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
if chainHead == null as *HirNode { foundVar1.kind = hVar;
chainHead = bodyPrefix; foundVar1.strValue = foundName;
} else if bodyPrefix != null as *HirNode { foundSet.child1 = foundVar1;
var bt0: *HirNode = chainHead; foundSet.child2 = Lcx_MakeLitHir(tkBoolLiteral, "true", line, col);
while bt0.child3 != null as *HirNode { bt0 = bt0.child3; } storeNode.child3 = foundSet;
bt0.child3 = bodyPrefix;
} // success block: bodyPrefix → store → found=true
if chainHead != null as *HirNode { let successBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
armBlock.child1 = chainHead; successBlock.kind = hBlock;
var bt: *HirNode = chainHead; successBlock.line = line;
while bt.child3 != null as *HirNode { bt = bt.child3; } successBlock.column = col;
bt.child3 = storeNode; if bodyPrefix != null as *HirNode {
successBlock.child1 = bodyPrefix;
var sbt: *HirNode = bodyPrefix;
while sbt.child3 != null as *HirNode { sbt = sbt.child3; }
sbt.child3 = storeNode;
} else { } else {
armBlock.child1 = storeNode; successBlock.child1 = storeNode;
} }
let cond: *HirNode = Lcx_PatternCond(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col); // Optional guard wraps success (already lowered with renames active)
if cond == null as *HirNode { var afterBinds: *HirNode = successBlock;
// Always-true arm if guardHirEarly != null as *HirNode {
if ifChain == null as *HirNode { let guardIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
ifChain = armBlock; guardIf.kind = hIf;
} else { guardIf.line = line;
let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; guardIf.column = col;
ifNode.kind = hIf; guardIf.child1 = guardHirEarly;
ifNode.line = line; guardIf.child2 = successBlock;
ifNode.column = col; afterBinds = guardIf;
ifNode.child1 = Lcx_MakeTrueHir(line, col);
ifNode.child2 = armBlock;
ifNode.extraData = ifChain as *void;
ifChain = ifNode;
}
} else {
let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
ifNode.kind = hIf;
ifNode.line = line;
ifNode.column = col;
ifNode.child1 = cond;
ifNode.child2 = armBlock;
ifNode.extraData = ifChain as *void;
ifChain = ifNode;
}
ai = ai - 1;
} }
// Chain: alloca → ifChain // armInner: binds → afterBinds
allocaNode.child3 = ifChain; let armInner: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
armInner.kind = hBlock;
armInner.line = line;
armInner.column = col;
if bindHead != null as *HirNode {
armInner.child1 = bindHead;
var ibt: *HirNode = bindHead;
while ibt.child3 != null as *HirNode { ibt = ibt.child3; }
ibt.child3 = afterBinds;
} else {
armInner.child1 = afterBinds;
}
// Optional pattern condition
let cond: *HirNode = Lcx_PatternCond(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col);
var armBody: *HirNode = armInner;
if cond != null as *HirNode {
let condIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
condIf.kind = hIf;
condIf.line = line;
condIf.column = col;
condIf.child1 = cond;
condIf.child2 = armInner;
armBody = condIf;
}
// if (!found) armBody
let foundLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
foundLoad.kind = hVar;
foundLoad.strValue = foundName;
foundLoad.typeName = "bool";
let notFound: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
notFound.kind = hUnary;
notFound.line = line;
notFound.column = col;
notFound.intValue = tkBang;
notFound.child1 = foundLoad;
let tryIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
tryIf.kind = hIf;
tryIf.line = line;
tryIf.column = col;
tryIf.child1 = notFound;
tryIf.child2 = armBody;
tail.child3 = tryIf;
tail = tryIf;
cur = cur.next;
}
let block: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; let block: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
block.kind = hBlock; block.kind = hBlock;
@@ -1153,6 +1234,18 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
// Identifier → variable reference // Identifier → variable reference
if kind == ekIdent { if kind == ekIdent {
// Pattern binding rename: source name → unique C local
let ren: String = Lcx_PatLookup(ctx, expr.strValue);
if !String_Eq(ren, "") {
n.kind = hVar;
n.strValue = ren;
let rsym: Symbol = Scope_Lookup(ctx.scope, expr.strValue);
n.typeKind = rsym.typeKind;
if rsym.typeName != null as String && !String_Eq(rsym.typeName, "") {
n.typeName = rsym.typeName;
}
return n;
}
// Capture rewriting: if inside closure body and this ident is captured, // Capture rewriting: if inside closure body and this ident is captured,
// emit field access on env instance instead of bare variable // emit field access on env instance instead of bare variable
if ctx.closureDepth > 0 && ctx.currentClosureExpr != null as *Expr && !String_Eq(ctx.envInstanceName, "") { if ctx.closureDepth > 0 && ctx.currentClosureExpr != null as *Expr && !String_Eq(ctx.envInstanceName, "") {
@@ -1564,20 +1657,67 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
n.kind = hCall; n.kind = hCall;
n.strValue = expr.child1.strValue; n.strValue = expr.child1.strValue;
// Generic call monomorphization // Generic call monomorphization (explicit / inferred type args)
if expr.child1 != null as *Expr && expr.child1.genericTypeArgCount > 0 { if expr.child1 != null as *Expr {
let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue); var argc: int = expr.child1.genericTypeArgCount;
if genDecl != null as *Decl {
var typeArg0: String = expr.child1.genericTypeArg0; var typeArg0: String = expr.child1.genericTypeArg0;
var typeArg1: String = expr.child1.genericTypeArg1; var typeArg1: String = expr.child1.genericTypeArg1;
// Fallback: infer T from first *Array/*Iter arg when sema left count=0
if argc == 0 {
let genTry: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
if genTry != null as *Decl && genTry.typeParamCount > 0 {
if expr.callArgs != null as *ExprList && expr.callArgs.expr != null as *Expr {
var a0: *Expr = expr.callArgs.expr;
var te: *TypeExpr = a0.refType;
if te == null as *TypeExpr && a0.kind == ekUnary && a0.intValue == tkAmp {
if a0.child1 != null as *Expr { te = a0.child1.refType; }
}
if te != null as *TypeExpr {
// Unwrap pointer
if (te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef)
&& te.pointerPointee != null as *TypeExpr {
te = te.pointerPointee;
}
var elem: String = "";
if te.typeArgCount > 0 {
elem = te.typeArgName0;
} else {
// Mangled Array_int / Iter_String
elem = Lcx_ExtractElemFromName(te.typeName);
}
if !String_Eq(elem, "") {
typeArg0 = elem;
argc = 1;
// Second type arg from func-typed second argument if needed
if genTry.typeParamCount >= 2 && expr.callArgs.next != null as *ExprList {
let a1e: *Expr = expr.callArgs.next.expr;
if a1e != null as *Expr && a1e.kind == ekIdent {
let fsym: Symbol = Scope_Lookup(ctx.scope, a1e.strValue);
if fsym.kind == skFunc && fsym.decl != null as *Decl
&& fsym.decl.retType != null as *TypeExpr
&& fsym.decl.retType.kind == tekNamed {
typeArg1 = fsym.decl.retType.typeName;
argc = 2;
}
}
}
}
}
}
}
}
if argc > 0 {
let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
if genDecl != null as *Decl {
if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; } if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; }
if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; } if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; }
if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; } if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; }
if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; } if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; }
let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, expr.child1.genericTypeArgCount); let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, argc);
n.strValue = mangled; n.strValue = mangled;
} }
} }
}
// Lower arguments into child1/child2/extraData // Lower arguments into child1/child2/extraData
var arg: *ExprList = expr.callArgs; var arg: *ExprList = expr.callArgs;
@@ -2476,10 +2616,33 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
// Return // Return
if kind == skReturn { if kind == skReturn {
n.kind = hReturn;
if stmt.child1 != null as *Expr { if stmt.child1 != null as *Expr {
n.child1 = Lcx_LowerExpr(ctx, stmt.child1); let retVal: *HirNode = Lcx_LowerExpr(ctx, stmt.child1);
// `return match { ... }` / other multi-stmt yields: expand stmts then return result var
if retVal != null as *HirNode && Lcx_IsMatchYield(retVal) {
let retVar: *HirNode = Lcx_YieldVarOf(retVal);
let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
retNode.kind = hReturn;
retNode.line = line;
retNode.column = col;
retNode.child1 = retVar;
retVal.strValue = "";
var lastIn: *HirNode = retVal.child1;
if lastIn == null as *HirNode {
retVal.child1 = retNode;
} else {
while lastIn.child3 != null as *HirNode {
lastIn = lastIn.child3;
} }
lastIn.child3 = retNode;
}
return retVal;
}
n.kind = hReturn;
n.child1 = retVal;
return n;
}
n.kind = hReturn;
return n; return n;
} }
+34 -2
View File
@@ -58,14 +58,27 @@ func parserPeek(p: *Parser, ahead: int) -> int {
return tkEndOfFile; return tkEndOfFile;
} }
// Lookahead to determine if '<' starts a type argument list. // Lookahead to determine if '<' starts a type argument list (`Foo<int>`).
// Must not treat value comparisons `x < 0` as generics when a later `x > 0`
// exists (e.g. multiple match arm guards).
func parserIsTypeArgListAhead(p: *Parser) -> bool { func parserIsTypeArgListAhead(p: *Parser) -> bool {
if !parserCheck(p, tkLt) { return false; } if !parserCheck(p, tkLt) { return false; }
var depth: int = 0; var depth: int = 0;
var ahead: int = 0; var ahead: int = 0;
while true { while true {
let kind: int = parserPeek(p, ahead); let kind: int = parserPeek(p, ahead);
if kind == tkEndOfFile || kind == tkLBrace || kind == tkSemicolon { // Hard stops: cannot appear inside <...> type args
if kind == tkEndOfFile || kind == tkLBrace || kind == tkRBrace || kind == tkSemicolon
|| kind == tkFatArrow || kind == tkIf || kind == tkElse || kind == tkWhile
|| kind == tkFor || kind == tkMatch || kind == tkReturn || kind == tkLet || kind == tkVar
|| kind == tkEq || kind == tkNe || kind == tkLe || kind == tkGe
|| kind == tkAmpAmp || kind == tkPipePipe || kind == tkAssign {
return false;
}
// Literals / arithmetic ⇒ value expression, not type args
if kind == tkIntLiteral || kind == tkFloatLiteral || kind == tkStringLiteral
|| kind == tkCharLiteral || kind == tkBoolLiteral
|| kind == tkPlus || kind == tkMinus || kind == tkSlash || kind == tkPercent {
return false; return false;
} }
if kind == tkLt { if kind == tkLt {
@@ -729,6 +742,7 @@ func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern {
pat.patChild2 = null as *Pattern; pat.patChild2 = null as *Pattern;
pat.patArgs = null as *Pattern; pat.patArgs = null as *Pattern;
pat.patNext = null as *Pattern; pat.patNext = null as *Pattern;
pat.patGuardExpr = null as *Expr;
return pat; return pat;
} }
@@ -904,6 +918,24 @@ func parserParsePattern(p: *Parser) -> *Pattern {
pat.patRangeInclusive = inclusive; pat.patRangeInclusive = inclusive;
pat.patChild1 = left; pat.patChild1 = left;
pat.patChild2 = right; pat.patChild2 = right;
// Range can still take a guard: `1..10 if x % 2 == 0`
if parserCheck(p, tkIf) {
discard parserAdvance(p);
let guard: *Expr = parserParseExpr(p);
let gpat: *Pattern = parserMakePattern(pkGuarded, line, col);
gpat.patChild1 = pat;
gpat.patGuardExpr = guard;
return gpat;
}
return pat;
}
// Guarded pattern: `p if cond` (bindings from p visible in cond)
if parserCheck(p, tkIf) {
discard parserAdvance(p);
let guard: *Expr = parserParseExpr(p);
let pat: *Pattern = parserMakePattern(pkGuarded, line, col);
pat.patChild1 = left;
pat.patGuardExpr = guard;
return pat; return pat;
} }
return left; return left;
+252 -23
View File
@@ -388,6 +388,11 @@ func Sema_AddCapture(closureExpr: *Expr, name: String, typeKind: int) {
// Enum payloads: Option::Some(value) → value:int (from variant field type). // Enum payloads: Option::Some(value) → value:int (from variant field type).
func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) { func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) {
if pat == null as *Pattern { return; } if pat == null as *Pattern { return; }
// Guarded: bind from inner pattern only (`p if cond`)
if pat.kind == pkGuarded {
Sema_BindPattern(sema, pat.patChild1, subject);
return;
}
if pat.kind == pkIdent { if pat.kind == pkIdent {
var sym: Symbol; var sym: Symbol;
Sema_ZeroInitSymbol(&sym); Sema_ZeroInitSymbol(&sym);
@@ -807,8 +812,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
ai = ai + 1; ai = ai + 1;
} }
} }
// Trait bounds checking for explicit generic calls: Max<Circle>(...) // Trait bounds + inference for generic calls: Max / Iter_Map / Array_Push
// Must happen before indirect/direct call returns
if expr.child1.kind == ekIdent { if expr.child1.kind == ekIdent {
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue); let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
if sym.kind == skFunc && sym.decl != null as *Decl { if sym.kind == skFunc && sym.decl != null as *Decl {
@@ -831,13 +835,21 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
return tyVoid; return tyVoid;
} }
} }
// Direct call to named function // Direct call to named function — substitute return type with inferred args
if expr.child1.kind == ekIdent { if expr.child1.kind == ekIdent {
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue); let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
if sym.kind == skFunc && sym.decl != null as *Decl { if sym.kind == skFunc && sym.decl != null as *Decl {
if sym.decl.retType != null as *TypeExpr { if sym.decl.retType != null as *TypeExpr {
expr.refType = sym.decl.retType; var retTe: *TypeExpr = sym.decl.retType;
return Sema_ResolveType(sema, sym.decl.retType); // Substitute type params in return type when we inferred args
if expr.child1.genericTypeArgCount > 0 {
retTe = Sema_SubstTypeExpr(sym.decl.retType,
sym.decl.typeParam0, expr.child1.genericTypeArg0,
sym.decl.typeParam1, expr.child1.genericTypeArg1,
expr.child1.genericTypeArgCount);
}
expr.refType = retTe;
return Sema_ResolveType(sema, retTe);
} }
} }
} }
@@ -851,6 +863,10 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
// Cast — return target type // Cast — return target type
if kind == ekCast { if kind == ekCast {
// Must type-check the operand (enables generic inference on nested calls)
if expr.child1 != null as *Expr {
discard Sema_CheckExpr(sema, expr.child1);
}
if expr.refType != null as *TypeExpr { if expr.refType != null as *TypeExpr {
return Sema_ResolveType(sema, expr.refType); return Sema_ResolveType(sema, expr.refType);
} }
@@ -990,6 +1006,16 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
let savedScope: *Scope = sema.scope; let savedScope: *Scope = sema.scope;
sema.scope = &armScope; sema.scope = &armScope;
Sema_BindPattern(sema, arm.pattern, expr.child1); Sema_BindPattern(sema, arm.pattern, expr.child1);
// Type-check `p if guard` (must be bool; sees pattern bindings)
if arm.pattern != null as *Pattern && arm.pattern.kind == pkGuarded {
if arm.pattern.patGuardExpr != null as *Expr {
let gt: int = Sema_CheckExpr(sema, arm.pattern.patGuardExpr);
if gt != tyBool && gt != tyUnknown {
Sema_EmitError(sema, arm.pattern.line, arm.pattern.column,
"match guard condition must be bool");
}
}
}
let bt: int = Sema_CheckExpr(sema, arm.body); let bt: int = Sema_CheckExpr(sema, arm.body);
sema.scope = savedScope; sema.scope = savedScope;
if first { if first {
@@ -1677,19 +1703,213 @@ func Sema_ExtractElemType(te: *TypeExpr) -> String {
return ""; return "";
} }
// Infer generic type argument from call arguments. // Substitute type params in a TypeExpr (shallow clone). Used for call return types
// Supports Array_* and Iter_* stdlib functions. // after inference: Array<U> + U=String → Array with typeArgName0=String / Array_String.
func Sema_SubstTypeExpr(te: *TypeExpr, p0: String, a0: String, p1: String, a1: String, argc: int) -> *TypeExpr {
if te == null as *TypeExpr { return null as *TypeExpr; }
let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
r.kind = te.kind;
r.line = te.line;
r.column = te.column;
r.typeName = te.typeName;
r.pathStr = te.pathStr;
r.pathCount = te.pathCount;
r.typeArgName0 = te.typeArgName0;
r.typeArgName1 = te.typeArgName1;
r.typeArgCount = te.typeArgCount;
r.sliceElement = te.sliceElement;
r.pointerPointee = te.pointerPointee;
r.funcParams = te.funcParams;
r.funcRet = te.funcRet;
r.funcParamCount = te.funcParamCount;
r.tupleElems = te.tupleElems;
r.tupleCount = te.tupleCount;
if te.kind == tekNamed {
// Bare type param → concrete named type
if te.typeArgCount == 0 {
if argc >= 1 && String_Eq(te.typeName, p0) {
r.typeName = a0;
return r;
}
if argc >= 2 && String_Eq(te.typeName, p1) {
r.typeName = a1;
return r;
}
}
// Named with type args: Array<U> → Array_String (mangled) for downstream mono
if te.typeArgCount > 0 {
var na0: String = te.typeArgName0;
var na1: String = te.typeArgName1;
if argc >= 1 && String_Eq(na0, p0) { na0 = a0; }
if argc >= 2 && String_Eq(na0, p1) { na0 = a1; }
if argc >= 1 && String_Eq(na1, p0) { na1 = a0; }
if argc >= 2 && String_Eq(na1, p1) { na1 = a1; }
r.typeArgName0 = na0;
r.typeArgName1 = na1;
// Mangle for monomorphized struct name (Array_int, Iter_String)
if te.typeArgCount == 1 && !String_Eq(na0, "") {
r.typeName = String_Concat(String_Concat(te.typeName, "_"), na0);
r.typeArgCount = 0;
r.typeArgName0 = "";
} else if te.typeArgCount >= 2 {
r.typeName = String_Concat(String_Concat(te.typeName, "_"),
String_Concat(na0, String_Concat("_", na1)));
r.typeArgCount = 0;
r.typeArgName0 = "";
r.typeArgName1 = "";
}
return r;
}
}
if te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef {
r.pointerPointee = Sema_SubstTypeExpr(te.pointerPointee, p0, a0, p1, a1, argc);
return r;
}
if te.kind == tekFunc {
r.funcRet = Sema_SubstTypeExpr(te.funcRet, p0, a0, p1, a1, argc);
return r;
}
return r;
}
// Bind a type param name on the call callee if not already set.
func Sema_BindInferredArg(expr: *Expr, funcDecl: *Decl, tpName: String, typeName: String) {
if String_Eq(tpName, "") || String_Eq(typeName, "") { return; }
if expr.child1 == null as *Expr { return; }
if funcDecl.typeParamCount >= 1 && String_Eq(tpName, funcDecl.typeParam0) {
if String_Eq(expr.child1.genericTypeArg0, "") {
expr.child1.genericTypeArg0 = typeName;
}
if expr.child1.genericTypeArgCount < 1 { expr.child1.genericTypeArgCount = 1; }
}
if funcDecl.typeParamCount >= 2 && String_Eq(tpName, funcDecl.typeParam1) {
if String_Eq(expr.child1.genericTypeArg1, "") {
expr.child1.genericTypeArg1 = typeName;
}
if expr.child1.genericTypeArgCount < 2 { expr.child1.genericTypeArgCount = 2; }
}
}
// Resolve concrete type name for a value expression (for bare type-param params).
func Sema_ArgTypeName(argExpr: *Expr) -> String {
if argExpr == null as *Expr { return ""; }
var argType: *TypeExpr = argExpr.refType;
if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp {
if argExpr.child1 != null as *Expr { argType = argExpr.child1.refType; }
}
if argType == null as *TypeExpr { return ""; }
if argType.kind == tekNamed { return argType.typeName; }
if argType.kind == tekPointer && argType.pointerPointee != null as *TypeExpr {
if argType.pointerPointee.kind == tekNamed {
return argType.pointerPointee.typeName;
}
}
return "";
}
// Infer type args from a param TypeExpr pattern against a concrete arg TypeExpr.
func Sema_UnifyInfer(expr: *Expr, funcDecl: *Decl, pattern: *TypeExpr, concrete: *TypeExpr) {
if pattern == null as *TypeExpr || concrete == null as *TypeExpr { return; }
// Bare type param: T / Acc / U
if pattern.kind == tekNamed && pattern.typeArgCount == 0 {
if String_Eq(pattern.typeName, funcDecl.typeParam0) || String_Eq(pattern.typeName, funcDecl.typeParam1) {
var cn: String = "";
if concrete.kind == tekNamed { cn = concrete.typeName; }
Sema_BindInferredArg(expr, funcDecl, pattern.typeName, cn);
return;
}
}
// *T / &T — unwrap pointer/ref on both sides
if pattern.kind == tekPointer || pattern.kind == tekRef || pattern.kind == tekMutRef {
var conc: *TypeExpr = concrete;
if concrete.kind == tekPointer || concrete.kind == tekRef || concrete.kind == tekMutRef {
conc = concrete.pointerPointee;
}
Sema_UnifyInfer(expr, funcDecl, pattern.pointerPointee, conc);
return;
}
// Named with type args: Iter<T>, Array<U>, Map<K,V>
if pattern.kind == tekNamed && pattern.typeArgCount > 0 {
// concrete may be Iter with typeArgName0, or mangled Iter_int
if concrete.kind == tekNamed {
if concrete.typeArgCount > 0 {
if pattern.typeArgCount >= 1 && !String_Eq(pattern.typeArgName0, "") {
let te0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te0.kind = tekNamed;
te0.typeName = pattern.typeArgName0;
te0.typeArgCount = 0;
let ce0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
ce0.kind = tekNamed;
ce0.typeName = concrete.typeArgName0;
ce0.typeArgCount = 0;
Sema_UnifyInfer(expr, funcDecl, te0, ce0);
}
if pattern.typeArgCount >= 2 && !String_Eq(pattern.typeArgName1, "") {
let te1: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te1.kind = tekNamed;
te1.typeName = pattern.typeArgName1;
te1.typeArgCount = 0;
let ce1: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
ce1.kind = tekNamed;
ce1.typeName = concrete.typeArgName1;
ce1.typeArgCount = 0;
Sema_UnifyInfer(expr, funcDecl, te1, ce1);
}
} else {
// Mangled Array_int / Iter_String
let elem: String = Sema_ExtractElemType(concrete);
if !String_Eq(elem, "") && pattern.typeArgCount >= 1 {
let te0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te0.kind = tekNamed;
te0.typeName = pattern.typeArgName0;
te0.typeArgCount = 0;
let ce0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
ce0.kind = tekNamed;
ce0.typeName = elem;
ce0.typeArgCount = 0;
Sema_UnifyInfer(expr, funcDecl, te0, ce0);
}
}
}
return;
}
// func(T)->U vs concrete function type
if pattern.kind == tekFunc {
var conc: *TypeExpr = concrete;
// Named function used as value: build tekFunc from its decl if needed — refType may already be tekFunc
if conc.kind == tekFunc {
// Params
var pp: *TypeExprList = pattern.funcParams;
var cp: *TypeExprList = conc.funcParams;
while pp != null as *TypeExprList && cp != null as *TypeExprList {
Sema_UnifyInfer(expr, funcDecl, pp.te, cp.te);
pp = pp.next;
cp = cp.next;
}
if pattern.funcRet != null as *TypeExpr && conc.funcRet != null as *TypeExpr {
Sema_UnifyInfer(expr, funcDecl, pattern.funcRet, conc.funcRet);
}
}
return;
}
}
// Infer generic type arguments from call arguments (structural).
// Handles *Array<T>, *Iter<T>, func(T)->U, bare Acc, etc.
func Sema_InferGenericArgs(sema: *Sema, funcDecl: *Decl, expr: *Expr) { func Sema_InferGenericArgs(sema: *Sema, funcDecl: *Decl, expr: *Expr) {
if expr.callArgs == null as *ExprList { return; } if expr.callArgs == null as *ExprList { return; }
if expr.child1 == null as *Expr { return; }
var argList: *ExprList = expr.callArgs; var argList: *ExprList = expr.callArgs;
var pi: int = 0; var pi: int = 0;
while argList != null as *ExprList && pi < funcDecl.paramCount { while argList != null as *ExprList && pi < funcDecl.paramCount {
let argExpr: *Expr = argList.expr; let argExpr: *Expr = argList.expr;
if argExpr == null as *Expr { argList = argList.next; pi = pi + 1; continue; } if argExpr == null as *Expr { argList = argList.next; pi = pi + 1; continue; }
var argType: *TypeExpr = argExpr.refType;
if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp {
argType = argExpr.child1.refType;
}
var paramType: *TypeExpr = null as *TypeExpr; var paramType: *TypeExpr = null as *TypeExpr;
if pi == 0 { paramType = funcDecl.param0.refParamType; } if pi == 0 { paramType = funcDecl.param0.refParamType; }
else if pi == 1 { paramType = funcDecl.param1.refParamType; } else if pi == 1 { paramType = funcDecl.param1.refParamType; }
@@ -1701,20 +1921,29 @@ func Sema_InferGenericArgs(sema: *Sema, funcDecl: *Decl, expr: *Expr) {
else if pi == 7 { paramType = funcDecl.param7.refParamType; } else if pi == 7 { paramType = funcDecl.param7.refParamType; }
else if pi == 8 { paramType = funcDecl.param8.refParamType; } else if pi == 8 { paramType = funcDecl.param8.refParamType; }
if paramType != null as *TypeExpr && paramType.kind == tekNamed && argType != null as *TypeExpr { var argType: *TypeExpr = argExpr.refType;
let inferred: String = Sema_ExtractElemType(argType); // &x → use type of x, wrap as pointer if pattern expects pointer
var typeName: String = inferred; if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp {
if String_Eq(typeName, "") && argType.kind == tekNamed { if argExpr.child1 != null as *Expr {
typeName = argType.typeName; argType = argExpr.child1.refType;
} }
if !String_Eq(typeName, "") {
if funcDecl.typeParamCount >= 1 && String_Eq(paramType.typeName, funcDecl.typeParam0) && String_Eq(expr.child1.genericTypeArg0, "") {
expr.child1.genericTypeArg0 = typeName;
if expr.child1.genericTypeArgCount < 1 { expr.child1.genericTypeArgCount = 1; }
} }
if funcDecl.typeParamCount >= 2 && String_Eq(paramType.typeName, funcDecl.typeParam1) && String_Eq(expr.child1.genericTypeArg1, "") { // Named function as value: synthesize tekFunc from its declaration
expr.child1.genericTypeArg1 = typeName; if (argType == null as *TypeExpr || argType.kind != tekFunc) && argExpr.kind == ekIdent {
if expr.child1.genericTypeArgCount < 2 { expr.child1.genericTypeArgCount = 2; } let fsym: Symbol = Scope_Lookup(sema.scope, argExpr.strValue);
if fsym.kind == skFunc && fsym.decl != null as *Decl {
argType = Sema_BuildFuncTypeExprFromDecl(fsym.decl);
}
}
if paramType != null as *TypeExpr && argType != null as *TypeExpr {
Sema_UnifyInfer(expr, funcDecl, paramType, argType);
// If pattern is *T and arg is bare T (from &x we unwrapped), re-wrap
if (paramType.kind == tekPointer || paramType.kind == tekRef || paramType.kind == tekMutRef)
&& argExpr.kind == ekUnary && argExpr.intValue == tkAmp {
// already handled via unwrap of pattern against pointee type of variable
if argExpr.child1 != null as *Expr && argExpr.child1.refType != null as *TypeExpr {
Sema_UnifyInfer(expr, funcDecl, paramType.pointerPointee, argExpr.child1.refType);
} }
} }
} }
+40
View File
@@ -195,6 +195,46 @@ func Main() -> int {
""") """)
check(not res.hasErrors) check(not res.hasErrors)
test "@[Checked] rejects two let-bound &mut of same var":
let res = checkSource("""
@[Checked]
func Main() -> int {
var x: int = 1;
let a: &mut int = &x;
let b: &mut int = &x;
return 0;
}
""")
check(res.hasErrors)
check(res.diagnostics[0].message.contains("already mutably borrowed"))
test "@[Checked] rejects assign while mutably borrowed":
let res = checkSource("""
@[Checked]
func Main() -> int {
var x: int = 1;
let a: &mut int = &x;
x = 2;
return *a;
}
""")
check(res.hasErrors)
check(res.diagnostics[0].message.contains("mutably borrowed"))
test "@[Checked] rejects shared borrow while mutably borrowed":
let res = checkSource("""
@[Checked]
func Main() -> int {
var x: int = 1;
let a: &mut int = &x;
let b: &int = &x;
return 0;
}
""")
check(res.hasErrors)
check(res.diagnostics[0].message.contains("shared-borrow") or
res.diagnostics[0].message.contains("mutably borrowed"))
test "borrow & expr with shared ref": test "borrow & expr with shared ref":
let res = checkSource(""" let res = checkSource("""
struct Point { struct Point {
@@ -0,0 +1,6 @@
[Package]
Name = "exclusive_mut_let"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -0,0 +1,7 @@
error: type errors in project
error: cannot mutably borrow 'x': already mutably borrowed
--> FILE:5:23
|
5 | let b: &mut int = &x;
| ^
= help: only one active '&mut' borrow is allowed at a time; end the borrow before reuse
@@ -0,0 +1,7 @@
@[Checked]
func Main() -> int {
var x: int = 1;
let a: &mut int = &x;
let b: &mut int = &x;
return 0;
}
@@ -0,0 +1,6 @@
[Package]
Name = "use_while_mut_borrow"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -0,0 +1,7 @@
error: type errors in project
error: cannot assign to 'x' while it is mutably borrowed
--> FILE:5:5
|
5 | x = 2;
| ^
= help: only one active '&mut' borrow is allowed at a time; end the borrow before reuse
@@ -0,0 +1,7 @@
@[Checked]
func Main() -> int {
var x: int = 1;
let a: &mut int = &x;
x = 2;
return *a;
}
+370 -10
View File
@@ -3,8 +3,12 @@
# #
# 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.
#
# Hover uses real bootstrap sema types when possible (globals + stdlib);
# completion/outline still use a fast lightweight scan.
import std/[json, os, strutils, streams, tables, osproc] import std/[json, os, strutils, streams, tables, osproc, sequtils]
import lexer, parser, ast, sema, types, scope, source_location
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# JSON-RPC Transport # JSON-RPC Transport
@@ -83,18 +87,25 @@ type
kind: string ## function | variable | struct | enum | … kind: string ## function | variable | struct | enum | …
detail: string ## signature / type annotation detail: string ## signature / type annotation
container: string ## optional parent (module / type) container: string ## optional parent (module / type)
fromSema: bool ## detail came from real type checker
DocumentState = ref object DocumentState = ref object
uri: string uri: string
content: string content: string
version: int version: int
symbols: Table[string, SymbolInfo] symbols: Table[string, SymbolInfo]
ordered: seq[string] ## declaration order for outline ordered: seq[string] ## declaration order for outline
## Full-project type index for hover (includes stdlib after sema enrich)
typeIndex: Table[string, string] ## name → type / signature string
kindIndex: Table[string, string] ## name → kind label
var var
documents = initTable[string, DocumentState]() documents = initTable[string, DocumentState]()
rootPath = "" rootPath = ""
rootUri = "" rootUri = ""
workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]() workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]()
cachedStdlibDir = ""
cachedStdlibDecls: seq[Decl] = @[]
stdlibLoaded = false
proc getDoc(uri: string): DocumentState = proc getDoc(uri: string): DocumentState =
if not documents.hasKey(uri): if not documents.hasKey(uri):
@@ -326,6 +337,313 @@ proc analyzeFile(path: string, content: string): DocumentState =
if not matchedTypeKw: if not matchedTypeKw:
inc i inc i
# ---------------------------------------------------------------------------
# Real sema types for hover
# ---------------------------------------------------------------------------
proc typeExprToStr(te: TypeExpr): string =
if te == nil: return "?"
case te.kind
of tekNamed:
result = te.typeName
if te.typeArgs.len > 0:
result &= "<" & te.typeArgs.mapIt(typeExprToStr(it)).join(", ") & ">"
of tekPath:
result = te.pathSegments.join("::")
of tekPointer:
result = "*" & typeExprToStr(te.pointerPointee)
of tekOwn:
result = "own " & typeExprToStr(te.pointerPointee)
of tekRef:
result = "&" & typeExprToStr(te.pointerPointee)
of tekMutRef:
result = "&mut " & typeExprToStr(te.pointerPointee)
of tekSlice:
result = typeExprToStr(te.sliceElement) & "[]"
of tekTuple:
result = "(" & te.tupleElements.mapIt(typeExprToStr(it)).join(", ") & ")"
of tekFunc:
let ps = te.funcParams.mapIt(typeExprToStr(it)).join(", ")
let ret = if te.funcRet != nil: typeExprToStr(te.funcRet) else: "void"
result = "func(" & ps & ") -> " & ret
of tekSelf:
result = "self"
of tekDynRef:
result = "&dyn " & te.dynInterface
proc formatFuncDetail(name: string, decl: Decl): string =
if decl == nil or decl.kind != dkFunc:
return "func " & name
var parts: seq[string] = @[]
for p in decl.declFuncParams:
var s = p.name
if p.ptype != nil:
s &= ": " & typeExprToStr(p.ptype)
parts.add(s)
result = "func " & name & "(" & parts.join(", ") & ")"
if decl.declFuncTypeParams.len > 0:
let tps = decl.declFuncTypeParams.mapIt(it.name).join(", ")
result = "func " & name & "<" & tps & ">(" & parts.join(", ") & ")"
if decl.declFuncReturnType != nil:
result &= " -> " & typeExprToStr(decl.declFuncReturnType)
proc symbolKindFromSema(sk: SymbolKind): string =
case sk
of skFunc: "function"
of skVar: "variable"
of skConst: "constant"
of skType: "type"
of skModule: "module"
proc findStdlibDirLocal(root: string): string =
if root.len == 0: return ""
let candidates = @[
root / "lib",
root / ".." / "lib",
getAppDir() / "lib",
getAppDir() / ".." / "lib",
getCurrentDir() / "lib"
]
for c in candidates:
if dirExists(c):
return c.absolutePath
# Walk up from root looking for lib/
var cur = root.absolutePath
for _ in 0 .. 6:
let lib = cur / "lib"
if dirExists(lib): return lib
let parent = cur.parentDir
if parent == cur: break
cur = parent
return ""
proc loadStdlibDecls(stdlibDir: string): seq[Decl] =
result = @[]
if stdlibDir.len == 0 or not dirExists(stdlibDir):
return
for path in walkDirRec(stdlibDir):
if not path.endsWith(".bux"): continue
try:
let source = readFile(path)
let lexRes = tokenize(source, path)
if lexRes.hasErrors: continue
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0: continue
for item in parseRes.module.items:
if item.kind == dkModule:
for sub in item.declModuleItems:
result.add(sub)
else:
result.add(item)
except:
discard
proc ensureStdlibCached() =
if stdlibLoaded: return
stdlibLoaded = true
cachedStdlibDir = findStdlibDirLocal(rootPath)
if cachedStdlibDir.len == 0:
# try from open document path later
return
cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir)
proc enrichWithSema(doc: DocumentState) =
## Run real bootstrap sema (file + stdlib) and fill typeIndex + upgrade symbols.
if doc.content.len == 0: return
let path = uriToPath(doc.uri)
ensureStdlibCached()
if cachedStdlibDecls.len == 0 and rootPath.len == 0:
# Try stdlib relative to the file
let tryRoot = path.parentDir.parentDir # …/src/Main.bux → package
cachedStdlibDir = findStdlibDirLocal(tryRoot)
if cachedStdlibDir.len > 0:
cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir)
try:
let lexRes = tokenize(doc.content, path)
if lexRes.hasErrors:
return
let parseRes = parse(lexRes.tokens, path)
# Build unified module: stdlib first, then this file
var unified = newModule("lsp")
for d in cachedStdlibDecls:
unified.items.add(d)
for d in parseRes.module.items:
if d.kind == dkModule:
for sub in d.declModuleItems:
unified.items.add(sub)
else:
unified.items.add(d)
let (semaRes, semaCtx) = analyzeFull(unified)
discard semaRes # diagnostics already published via buxc
doc.typeIndex = initTable[string, string]()
doc.kindIndex = initTable[string, string]()
# Index entire global scope for hover (includes stdlib)
if semaCtx.globalScope != nil:
for name, sym in semaCtx.globalScope.table.pairs:
if name.len == 0: continue
var detail = ""
var kind = symbolKindFromSema(sym.kind)
if sym.kind == skFunc and sym.decl != nil and sym.decl.kind == dkFunc:
detail = formatFuncDetail(name, sym.decl)
elif sym.typ != nil and not sym.typ.isUnknown:
detail = name & ": " & sym.typ.toString
if sym.kind == skFunc:
detail = sym.typ.toString # already "func(...) -> T"
if not detail.startsWith("func"):
detail = "func " & name & "" & detail
else:
# inject name: func name(...)
detail = detail.replace("func(", "func " & name & "(")
elif sym.kind == skConst:
detail = "const " & name & ": " & sym.typ.toString
elif sym.kind == skType:
detail = "type " & name
kind = "type"
else:
detail = (if sym.isMutable: "var " else: "let ") & name & ": " & sym.typ.toString
elif sym.decl != nil:
case sym.decl.kind
of dkStruct:
detail = "struct " & name
kind = "struct"
of dkEnum:
detail = "enum " & name
kind = "enum"
of dkUnion:
detail = "union " & name
kind = "struct"
of dkInterface:
detail = "interface " & name
kind = "interface"
of dkTypeAlias:
detail = "type " & name
if sym.decl.declAliasType != nil:
detail &= " = " & typeExprToStr(sym.decl.declAliasType)
kind = "type"
of dkFunc:
detail = formatFuncDetail(name, sym.decl)
kind = "function"
else:
detail = name
else:
detail = name
doc.typeIndex[name] = detail
doc.kindIndex[name] = kind
# Upgrade file-local symbols (already found by lightweight scan)
if doc.symbols.hasKey(name):
var info = doc.symbols[name]
info.detail = detail
info.kind = kind
info.fromSema = true
doc.symbols[name] = info
# Prefer THIS file's declarations over stdlib when names collide
# (e.g. user `Max<T>` vs lib/Math `Max(int64,int64)`).
for d in parseRes.module.items:
proc indexDecl(dd: Decl) =
case dd.kind
of dkFunc:
let n = dd.declFuncName
if n.len == 0: return
let detail = formatFuncDetail(n, dd)
doc.typeIndex[n] = detail
doc.kindIndex[n] = "function"
if doc.symbols.hasKey(n):
var info = doc.symbols[n]
info.detail = detail
info.kind = "function"
info.fromSema = true
doc.symbols[n] = info
else:
let line = max(0, int(dd.loc.line) - 1)
let col = max(0, int(dd.loc.column) - 1)
doc.symbols[n] = SymbolInfo(
line: line, col: col, kind: "function", detail: detail,
container: "", fromSema: true)
if n notin doc.ordered: doc.ordered.add(n)
of dkStruct:
let n = dd.declStructName
doc.typeIndex[n] = "struct " & n
doc.kindIndex[n] = "struct"
of dkEnum:
let n = dd.declEnumName
doc.typeIndex[n] = "enum " & n
doc.kindIndex[n] = "enum"
of dkTypeAlias:
let n = dd.declAliasName
var detail = "type " & n
if dd.declAliasType != nil:
detail &= " = " & typeExprToStr(dd.declAliasType)
doc.typeIndex[n] = detail
doc.kindIndex[n] = "type"
else:
discard
if d.kind == dkModule:
for sub in d.declModuleItems:
indexDecl(sub)
else:
indexDecl(d)
# Walk this file's AST for local lets with explicit types (function bodies)
proc walkBlock(blk: Block, container: string) =
if blk == nil: return
for stmt in blk.stmts:
case stmt.kind
of skLet:
let n = stmt.stmtLetName
if n.len == 0: continue
var typStr = ""
if stmt.stmtLetType != nil:
typStr = typeExprToStr(stmt.stmtLetType)
let kw = if stmt.stmtLetMut: "var" else: "let"
let detail = if typStr.len > 0: kw & " " & n & ": " & typStr else: kw & " " & n
let loc = stmt.loc
let line = max(0, int(loc.line) - 1)
let col = max(0, int(loc.column) - 1)
# Prefer sema-enriched detail if name already global; else add local
if not doc.symbols.hasKey(n) or not doc.symbols[n].fromSema:
doc.symbols[n] = SymbolInfo(
line: line, col: col, kind: "variable", detail: detail,
container: container, fromSema: typStr.len > 0)
if n notin doc.ordered:
doc.ordered.add(n)
if typStr.len > 0:
doc.typeIndex[n] = detail
doc.kindIndex[n] = "variable"
of skExpr:
if stmt.stmtExpr != nil and stmt.stmtExpr.kind == ekBlock:
walkBlock(stmt.stmtExpr.exprBlock, container)
of skIf:
walkBlock(stmt.stmtIfThen, container)
walkBlock(stmt.stmtIfElse, container)
for br in stmt.stmtIfElseIfs:
walkBlock(br.blk, container)
of skWhile:
walkBlock(stmt.stmtWhileBody, container)
of skFor:
walkBlock(stmt.stmtForBody, container)
of skLoop:
walkBlock(stmt.stmtLoopBody, container)
else:
discard
for d in parseRes.module.items:
if d.kind == dkFunc and d.declFuncBody != nil:
walkBlock(d.declFuncBody, d.declFuncName)
elif d.kind == dkModule:
for sub in d.declModuleItems:
if sub.kind == dkFunc and sub.declFuncBody != nil:
walkBlock(sub.declFuncBody, sub.declFuncName)
except:
discard # sema failures must not crash the LSP
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Diagnostics — run `buxc check` when available and parse Rust-style errors # Diagnostics — run `buxc check` when available and parse Rust-style errors
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -470,6 +788,8 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
let updated = analyzeFile(path, doc.content) let updated = analyzeFile(path, doc.content)
doc.symbols = updated.symbols doc.symbols = updated.symbols
doc.ordered = updated.ordered doc.ordered = updated.ordered
# Keep / refresh real types for hover (does not replace lightweight outline)
enrichWithSema(doc)
let diags = runBuxcDiagnostics(path, doc.content) let diags = runBuxcDiagnostics(path, doc.content)
publishDiagnostics(stream, doc.uri, diags) publishDiagnostics(stream, doc.uri, diags)
@@ -628,7 +948,7 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
## Hover with accurate range for the word under the cursor. ## Hover with accurate range; prefer real sema types when available.
let uri = paramsNode["textDocument"]["uri"].getStr() let uri = paramsNode["textDocument"]["uri"].getStr()
let position = paramsNode["position"] let position = paramsNode["position"]
let lineNum = position["line"].getInt() let lineNum = position["line"].getInt()
@@ -640,6 +960,10 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
return return
ensureAnalyzed(doc) ensureAnalyzed(doc)
# Lazy sema enrich on first hover if not yet run (e.g. only didChange so far)
if doc.typeIndex.len == 0 and doc.content.len > 0:
enrichWithSema(doc)
let lines = doc.content.split("\n") let lines = doc.content.split("\n")
if lineNum >= lines.len: if lineNum >= lines.len:
sendResponse(stream, id, newJNull()) sendResponse(stream, id, newJNull())
@@ -656,19 +980,41 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
return return
let word = l[start ..< endC] let word = l[start ..< endC]
var info: SymbolInfo var detail = ""
var kind = ""
var found = false var found = false
# Prefer file-local symbol (may be sema-upgraded)
if doc.symbols.hasKey(word): if doc.symbols.hasKey(word):
info = doc.symbols[word] let info = doc.symbols[word]
detail = info.detail
kind = info.kind
found = true
# Prefer pure sema typeIndex when richer
if doc.typeIndex.hasKey(word) and doc.typeIndex[word].len >= detail.len:
detail = doc.typeIndex[word]
if doc.kindIndex.hasKey(word):
kind = doc.kindIndex[word]
elif doc.typeIndex.hasKey(word):
detail = doc.typeIndex[word]
kind = if doc.kindIndex.hasKey(word): doc.kindIndex[word] else: "symbol"
found = true found = true
elif workspaceSymbols.hasKey(word): elif workspaceSymbols.hasKey(word):
info = workspaceSymbols[word].info let info = workspaceSymbols[word].info
detail = info.detail
kind = info.kind
found = true found = true
if not found: if not found:
sendResponse(stream, id, newJNull()) sendResponse(stream, id, newJNull())
return return
let md = "```bux\n" & info.detail & "\n```\n\n_" & info.kind & "_" var md = "```bux\n" & detail & "\n```\n\n_" & kind & "_"
if doc.symbols.hasKey(word) and doc.symbols[word].fromSema:
md &= " · sema"
elif doc.typeIndex.hasKey(word):
md &= " · sema"
sendResponse(stream, id, %*{ sendResponse(stream, id, %*{
"contents": {"kind": "markdown", "value": md}, "contents": {"kind": "markdown", "value": md},
"range": { "range": {
@@ -742,7 +1088,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
"hoverProvider": true, "hoverProvider": true,
"documentSymbolProvider": true "documentSymbolProvider": true
}, },
"serverInfo": {"name": "bux-lsp", "version": "0.2.0"} "serverInfo": {"name": "bux-lsp", "version": "0.3.0"}
}) })
if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull: if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull:
rootPath = paramsNode["rootPath"].getStr() rootPath = paramsNode["rootPath"].getStr()
@@ -750,10 +1096,14 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
rootUri = paramsNode["rootUri"].getStr() rootUri = paramsNode["rootUri"].getStr()
if rootPath.len == 0: if rootPath.len == 0:
rootPath = uriToPath(rootUri) rootPath = uriToPath(rootUri)
# Preload stdlib for hover types
if rootPath.len > 0:
ensureStdlibCached()
of "initialized": of "initialized":
if rootPath.len > 0: if rootPath.len > 0:
scanWorkspace(rootPath) scanWorkspace(rootPath)
ensureStdlibCached()
of "shutdown": of "shutdown":
sendResponse(stream, id, %*{}) sendResponse(stream, id, %*{})
@@ -769,6 +1119,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
doc.content = content doc.content = content
if td.hasKey("version"): if td.hasKey("version"):
doc.version = td["version"].getInt() doc.version = td["version"].getInt()
# Lightweight scan + sema enrich + buxc diagnostics
analyzeAndPublishDiagnostics(stream, doc) analyzeAndPublishDiagnostics(stream, doc)
of "textDocument/didChange": of "textDocument/didChange":
@@ -780,16 +1131,25 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
doc.content = changes[changes.len - 1]["text"].getStr() doc.content = changes[changes.len - 1]["text"].getStr()
if td.hasKey("version"): if td.hasKey("version"):
doc.version = td["version"].getInt() doc.version = td["version"].getInt()
# Refresh symbols immediately (no buxc — diagnostics on save) # Fast path: lightweight symbols only; keep previous typeIndex until save/hover refresh
let updated = analyzeFile(uriToPath(uri), doc.content) let updated = analyzeFile(uriToPath(uri), doc.content)
doc.symbols = updated.symbols doc.symbols = updated.symbols
doc.ordered = updated.ordered doc.ordered = updated.ordered
# Re-apply typeIndex details onto matching names (don't drop sema types mid-edit)
for name, detail in doc.typeIndex.pairs:
if doc.symbols.hasKey(name):
var info = doc.symbols[name]
info.detail = detail
info.fromSema = true
if doc.kindIndex.hasKey(name):
info.kind = doc.kindIndex[name]
doc.symbols[name] = info
of "textDocument/didSave": of "textDocument/didSave":
let td = paramsNode["textDocument"] let td = paramsNode["textDocument"]
let uri = td["uri"].getStr() let uri = td["uri"].getStr()
let doc = getDoc(uri) discard getDoc(uri)
analyzeAndPublishDiagnostics(stream, doc) analyzeAndPublishDiagnostics(stream, getDoc(uri))
of "textDocument/completion": of "textDocument/completion":
handleCompletion(stream, id, paramsNode) handleCompletion(stream, id, paramsNode)