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:
@@ -3,7 +3,7 @@ SRC := bootstrap/main.nim
|
||||
OUT := buxc
|
||||
BUILD_DIR := build
|
||||
|
||||
EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt 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
|
||||
|
||||
@@ -143,5 +143,5 @@ selfhost-loop: build
|
||||
lsp: tools/bux-lsp
|
||||
@echo "LSP server ready at tools/bux-lsp"
|
||||
|
||||
tools/bux-lsp: tools/lsp_server.nim
|
||||
cd tools && $(NIM) c -o:bux-lsp lsp_server.nim
|
||||
tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim
|
||||
cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim
|
||||
|
||||
+5
-1
@@ -180,6 +180,8 @@ proc underlineLength(lineText: string, col: uint32, message: string): int =
|
||||
proc hintForMessage(msg: string): string =
|
||||
## Actionable help text for common compiler errors.
|
||||
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:
|
||||
return "ensure the right-hand side type matches the left-hand side"
|
||||
if "undeclared identifier" in m:
|
||||
@@ -193,7 +195,9 @@ proc hintForMessage(msg: string): string =
|
||||
if "shared reference" in m or "checked function" in m:
|
||||
return "use '&mut T' for mutation, or drop @[Checked] for unchecked code"
|
||||
if "double mutable borrow" in m or "already mutably borrowed" in m:
|
||||
return "only one active '&mut' borrow is allowed at a time"
|
||||
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:
|
||||
return "the previous statement may be incomplete (missing value or ';')"
|
||||
if "expected type" in m:
|
||||
|
||||
@@ -163,6 +163,7 @@ type
|
||||
HirMatchArm* = object
|
||||
pattern*: Pattern
|
||||
body*: HirNode
|
||||
guard*: HirNode ## optional: lowered `if cond` from `p if cond => body`
|
||||
|
||||
HirFunc* = object
|
||||
name*: string
|
||||
|
||||
+260
-96
@@ -31,13 +31,83 @@ type
|
||||
## All func types that need BuxFn_* typedefs (including locals)
|
||||
seenFatTypes*: seq[Type]
|
||||
## 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]
|
||||
## Active renames: source pattern name → unique C local (for shadowing)
|
||||
patternRenames*: Table[string, string]
|
||||
|
||||
proc freshName(ctx: var LowerCtx): string =
|
||||
inc 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 =
|
||||
inc ctx.tryCounter
|
||||
result = "__try_" & $ctx.tryCounter
|
||||
@@ -73,9 +143,6 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode =
|
||||
return nil
|
||||
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 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
|
||||
return nil
|
||||
of pkGuarded:
|
||||
# Guard: inner pattern AND guard expression (lowered later if needed)
|
||||
# For now only support always-true inner + guard as bool expr via lowerExpr path.
|
||||
# Guards are not yet fully lowered here (require expr lowering of guard).
|
||||
return nil
|
||||
# Condition is only the inner pattern; guard is applied after bindings in lowerMatch.
|
||||
return matchPatternCond(ctx, subject, pattern.patGuardedInner, subjectEnumName, subjectHasData, loc)
|
||||
else:
|
||||
# Struct/tuple patterns: not yet fully lowered — always-true
|
||||
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,
|
||||
subjectEnumName: string, subjectHasData: bool,
|
||||
loc: SourceLocation): seq[HirNode] =
|
||||
## 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`
|
||||
## Ident catch-all: `x` → `x = subject`
|
||||
result = @[]
|
||||
@@ -142,10 +224,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
|
||||
case pattern.kind
|
||||
of pkIdent:
|
||||
let ty = if subject.typ != nil: subject.typ else: makeUnknown()
|
||||
if pattern.patIdent notin ctx.patternBoundNames:
|
||||
result.add(hirAlloca(pattern.patIdent, ty, loc))
|
||||
ctx.patternBoundNames.incl(pattern.patIdent)
|
||||
result.add(hirStore(hirVar(pattern.patIdent, ty, loc), subject, loc))
|
||||
result.add(ctx.bindPatLocal(pattern.patIdent, ty, subject, loc))
|
||||
of pkEnum:
|
||||
if not subjectHasData:
|
||||
return
|
||||
@@ -195,10 +274,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
|
||||
typ: makePointer(fieldTy), loc: loc)
|
||||
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
|
||||
if arg.kind == pkIdent:
|
||||
if arg.patIdent notin ctx.patternBoundNames:
|
||||
result.add(hirAlloca(arg.patIdent, fieldTy, loc))
|
||||
ctx.patternBoundNames.incl(arg.patIdent)
|
||||
result.add(hirStore(hirVar(arg.patIdent, fieldTy, loc), fieldLoad, loc))
|
||||
result.add(ctx.bindPatLocal(arg.patIdent, fieldTy, fieldLoad, loc))
|
||||
else:
|
||||
# Nested: Option::Some((a, b)), Pair::Two(Point { x, y })
|
||||
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)
|
||||
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
|
||||
if nf.pattern.kind == pkIdent:
|
||||
if nf.pattern.patIdent notin ctx.patternBoundNames:
|
||||
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))
|
||||
result.add(ctx.bindPatLocal(nf.pattern.patIdent, fieldTy, fieldLoad, loc))
|
||||
else:
|
||||
result.add(ctx.matchPatternBindings(fieldLoad, nf.pattern, subjectEnumName, subjectHasData, loc))
|
||||
of pkGuarded:
|
||||
@@ -241,10 +314,7 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
|
||||
typ: makePointer(fieldTy), loc: loc)
|
||||
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
|
||||
if elem.kind == pkIdent:
|
||||
if elem.patIdent notin ctx.patternBoundNames:
|
||||
result.add(hirAlloca(elem.patIdent, fieldTy, loc))
|
||||
ctx.patternBoundNames.incl(elem.patIdent)
|
||||
result.add(hirStore(hirVar(elem.patIdent, fieldTy, loc), fieldLoad, loc))
|
||||
result.add(ctx.bindPatLocal(elem.patIdent, fieldTy, fieldLoad, loc))
|
||||
else:
|
||||
# Nested patterns: recurse with field as subject
|
||||
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)
|
||||
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
|
||||
if fpat.kind == pkIdent:
|
||||
if fpat.patIdent notin ctx.patternBoundNames:
|
||||
result.add(hirAlloca(fpat.patIdent, fieldTy, loc))
|
||||
ctx.patternBoundNames.incl(fpat.patIdent)
|
||||
result.add(hirStore(hirVar(fpat.patIdent, fieldTy, loc), fieldLoad, loc))
|
||||
result.add(ctx.bindPatLocal(fpat.patIdent, fieldTy, fieldLoad, loc))
|
||||
else:
|
||||
result.add(ctx.matchPatternBindings(fieldLoad, fpat, subjectEnumName, subjectHasData, loc))
|
||||
else:
|
||||
discard
|
||||
|
||||
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode =
|
||||
## Lower match expression to a block with if-else chain.
|
||||
proc lowerMatch(ctx: var LowerCtx, subject: HirNode, astArms: seq[MatchArm], typ: Type, loc: SourceLocation): HirNode =
|
||||
## Lower match expression to sequential ifs with a `found` flag.
|
||||
## 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 resultName = ctx.freshName()
|
||||
let foundName = ctx.freshName()
|
||||
var stmts: seq[HirNode] = @[]
|
||||
|
||||
if hasResult:
|
||||
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 subjectHasData = false
|
||||
if subject.typ != nil and subject.typ.kind == tkNamed:
|
||||
subjectEnumName = subject.typ.name
|
||||
subjectHasData = ctx.enumHasDataVariants(subjectEnumName)
|
||||
|
||||
proc makeArmBlock(body: HirNode, bindStmts: seq[HirNode]): HirNode =
|
||||
var armStmts: seq[HirNode] = bindStmts
|
||||
for arm in astArms:
|
||||
# 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:
|
||||
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc))
|
||||
elif body != nil:
|
||||
# Void match: evaluate body for side effects
|
||||
armStmts.add(body)
|
||||
return hirBlock(armStmts, nil, makeVoid(), loc)
|
||||
successStmts.add(hirStore(hirVar(resultName, typ, loc), bodyHir, loc))
|
||||
elif bodyHir != nil:
|
||||
successStmts.add(bodyHir)
|
||||
successStmts.add(hirStore(hirVar(foundName, makeBool(), 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 ifChain: HirNode = 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,
|
||||
var afterBinds: HirNode
|
||||
if guardHir != nil:
|
||||
afterBinds = HirNode(kind: hIf, ifCond: guardHir, ifThen: successBlock, ifElse: nil,
|
||||
typ: makeVoid(), loc: loc)
|
||||
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)
|
||||
|
||||
if ifChain != nil:
|
||||
stmts.add(ifChain)
|
||||
let notFound = HirNode(kind: hUnary, unaryOp: tkBang,
|
||||
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:
|
||||
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.seenFatTypes = @[]
|
||||
result.patternBoundNames = initHashSet[string]()
|
||||
result.patternRenames = initTable[string, string]()
|
||||
|
||||
proc sanitizeFatPart(s: string): string =
|
||||
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)
|
||||
else: return makeUnknown()
|
||||
|
||||
# Forward declarations
|
||||
proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode
|
||||
# Forward declarations (lowerExpr already declared above for lowerMatch)
|
||||
proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode
|
||||
proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode
|
||||
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 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 =
|
||||
## Lower an expression, coercing &Concrete to &dyn Trait if needed.
|
||||
let lowered = ctx.lowerExpr(arg)
|
||||
@@ -997,6 +1111,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
|
||||
of ekIdent:
|
||||
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
|
||||
if ctx.closureDepth > 0 and ctx.currentClosureExpr != nil and ctx.envInstanceName != "":
|
||||
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(expr.exprAssignValue))
|
||||
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 value = ctx.lowerExpr(expr.exprAssignValue)
|
||||
return HirNode(kind: hAssign, assignOp: expr.exprAssignOp,
|
||||
@@ -1473,29 +1600,30 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
|
||||
of ekMatch:
|
||||
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
|
||||
# only reference pattern bindings (not yet in varTypeExprs during resolve).
|
||||
var matchTyp = typ
|
||||
if matchTyp == nil or matchTyp.kind == tkUnknown:
|
||||
if ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind notin {tkVoid, tkUnknown}:
|
||||
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:
|
||||
# Register bind types only (do NOT call matchPatternBindings here — that
|
||||
# would mark names as already alloca'd and lowerMatch would skip them).
|
||||
if arm.pattern != nil and arm.pattern.kind == pkEnum and subjectHasData:
|
||||
var bindPat = arm.pattern
|
||||
if bindPat != nil and bindPat.kind == pkGuarded:
|
||||
bindPat = bindPat.patGuardedInner
|
||||
if bindPat != nil and bindPat.kind == pkEnum and subjectHasData:
|
||||
var enumName = ""
|
||||
var variantName = ""
|
||||
if arm.pattern.patEnumPath.len >= 2:
|
||||
enumName = arm.pattern.patEnumPath[0]
|
||||
variantName = arm.pattern.patEnumPath[^1]
|
||||
elif arm.pattern.patEnumPath.len == 1:
|
||||
variantName = arm.pattern.patEnumPath[0]
|
||||
if bindPat.patEnumPath.len >= 2:
|
||||
enumName = bindPat.patEnumPath[0]
|
||||
variantName = bindPat.patEnumPath[^1]
|
||||
elif bindPat.patEnumPath.len == 1:
|
||||
variantName = bindPat.patEnumPath[0]
|
||||
enumName = subjectEnumName
|
||||
var fieldTypes: seq[Type] = @[]
|
||||
let enumSym = ctx.globalScope.lookup(enumName)
|
||||
@@ -1505,19 +1633,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
|
||||
for f in v.fields:
|
||||
fieldTypes.add(ctx.resolveTypeExpr(f))
|
||||
break
|
||||
for i, arg in arm.pattern.patEnumArgs:
|
||||
for i, arg in bindPat.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 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()
|
||||
ctx.varTypeExprs[arm.pattern.patIdent] = typeToTypeExpr(ty)
|
||||
arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body)))
|
||||
# Re-resolve match type now that bindings are registered
|
||||
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)
|
||||
ctx.varTypeExprs[bindPat.patIdent] = typeToTypeExpr(ty)
|
||||
# Binds + body lower happen inside lowerMatch (unique C names + renames)
|
||||
return lowerMatch(ctx, subject, expr.exprMatchArms, matchTyp, loc)
|
||||
|
||||
of ekSizeOf:
|
||||
let ty = ctx.resolveTypeExpr(expr.exprSizeOfType)
|
||||
@@ -1663,6 +1787,13 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
if initHir != nil:
|
||||
let store = hirStore(varNode, initHir, loc)
|
||||
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).
|
||||
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
|
||||
var stmts = 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):
|
||||
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
|
||||
stmts.add(ctx.deferStmts[i])
|
||||
stmts.add(hirReturn(value, loc))
|
||||
stmts.add(hirReturn(retVal, loc))
|
||||
return hirBlock(stmts, nil, makeVoid(), loc)
|
||||
|
||||
of skIf:
|
||||
@@ -1904,11 +2050,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode =
|
||||
|
||||
of skMatch:
|
||||
let subject = ctx.lowerExpr(stmt.stmtMatchSubject)
|
||||
var arms: seq[HirMatchArm] = @[]
|
||||
for arm in stmt.stmtMatchArms:
|
||||
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))
|
||||
# Statement match: binds + body lower inside lowerMatch (unique C names)
|
||||
return ctx.flushPending(lowerMatch(ctx, subject, stmt.stmtMatchArms, makeVoid(), loc))
|
||||
|
||||
of skSwitch:
|
||||
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).
|
||||
## Last skExpr becomes the block result. Statement blocks (func body, if/while)
|
||||
## 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
|
||||
let deferBase = ctx.deferStmts.len
|
||||
var stmts: seq[HirNode] = @[]
|
||||
for s in blk.stmts:
|
||||
let hir = ctx.lowerStmt(s)
|
||||
@@ -1966,6 +2115,16 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block, asExpr = false): HirNode =
|
||||
let last = stmts[^1]
|
||||
stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc)
|
||||
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()
|
||||
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 oldVarTypeExprs = ctx.varTypeExprs
|
||||
let oldPatternBound = ctx.patternBoundNames
|
||||
let oldPatternRenames = ctx.patternRenames
|
||||
ctx.currentFuncRetType = retType
|
||||
ctx.currentFuncDecl = decl
|
||||
ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function
|
||||
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.
|
||||
for p in funcParams:
|
||||
if p.ptype != nil:
|
||||
@@ -2032,12 +2195,13 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc =
|
||||
if not hasReturn:
|
||||
for i in countdown(ctx.deferStmts.len - 1, 0):
|
||||
body.blockStmts.add(ctx.deferStmts[i])
|
||||
ctx.deferStmts = @[]
|
||||
ctx.deferStmts = oldDefers
|
||||
|
||||
ctx.currentFuncDecl = oldFuncDecl
|
||||
ctx.currentFuncRetType = oldFuncRetType
|
||||
ctx.varTypeExprs = oldVarTypeExprs
|
||||
ctx.patternBoundNames = oldPatternBound
|
||||
ctx.patternRenames = oldPatternRenames
|
||||
|
||||
result = HirFunc(name: funcName, params: params, retType: retType,
|
||||
body: body, isPublic: decl.isPublic)
|
||||
|
||||
+28
-8
@@ -63,14 +63,23 @@ proc checkAny(p: Parser, kinds: openArray[TokenKind]): bool =
|
||||
return false
|
||||
|
||||
proc isTypeArgListAhead(p: Parser): bool =
|
||||
## Lookahead to determine if '<' starts a type argument list.
|
||||
## Returns true if we can find a matching '>' before EOF, '{', or ';'.
|
||||
## Lookahead to determine if '<' starts a type argument list (`Foo<int>`).
|
||||
## 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
|
||||
var depth = 0
|
||||
var ahead = 0
|
||||
while true:
|
||||
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
|
||||
if kind == tkLt:
|
||||
inc depth
|
||||
@@ -170,6 +179,8 @@ type
|
||||
targetOs*: string
|
||||
checked*: bool ## @[Checked] — enable borrow checking
|
||||
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 =
|
||||
while p.check(tkAt):
|
||||
@@ -180,6 +191,10 @@ proc parseAttrs(p: var Parser): ParsedAttrs =
|
||||
result.checked = true
|
||||
elif name == "Shared":
|
||||
result.shared = true
|
||||
elif name == "Drop":
|
||||
result.drop = true
|
||||
elif name == "Release":
|
||||
result.release = true
|
||||
elif name == "Import":
|
||||
discard p.expect(tkLParen, "expected '('")
|
||||
let key = p.expect(tkIdent, "expected attribute key").text
|
||||
@@ -375,8 +390,8 @@ proc parsePattern(p: var Parser): Pattern =
|
||||
let inclusive = p.check(tkDotDotEqual)
|
||||
discard p.advance()
|
||||
let right = p.parsePrimaryPattern()
|
||||
return Pattern(kind: pkRange, loc: loc, patRangeLo: left, patRangeHi: right, patRangeInclusive: inclusive)
|
||||
# Guarded pattern
|
||||
left = Pattern(kind: pkRange, loc: loc, patRangeLo: left, patRangeHi: right, patRangeInclusive: inclusive)
|
||||
# Guarded pattern: `p if cond` (also after range: `1..10 if x % 2 == 0`)
|
||||
if p.check(tkIf):
|
||||
discard p.advance()
|
||||
let guard = p.parseExpr()
|
||||
@@ -1242,6 +1257,8 @@ proc parseFuncDecl(p: var Parser, isPublic: bool, isAsm: bool, attrs: ParsedAttr
|
||||
discard p.advance()
|
||||
var declAttrs: seq[string] = @[]
|
||||
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,
|
||||
declAttrs: declAttrs,
|
||||
declFuncAsm: isAsm, declFuncCallConv: attrs.callConv,
|
||||
@@ -1250,7 +1267,7 @@ proc parseFuncDecl(p: var Parser, isPublic: bool, isAsm: bool, attrs: ParsedAttr
|
||||
declFuncParams: params, declFuncReturnType: retType,
|
||||
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
|
||||
discard p.expect(tkStruct, "expected 'struct'")
|
||||
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:
|
||||
discard p.advance()
|
||||
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,
|
||||
declStructFields: fields)
|
||||
|
||||
@@ -1567,7 +1587,7 @@ proc parseDecl(p: var Parser): Decl =
|
||||
of tkFunc:
|
||||
return p.parseFuncDecl(isPublic, false, attrs, isConst, isAsync)
|
||||
of tkStruct:
|
||||
return p.parseStructDecl(isPublic)
|
||||
return p.parseStructDecl(isPublic, attrs)
|
||||
of tkEnum:
|
||||
return p.parseEnumDecl(isPublic)
|
||||
of tkUnion:
|
||||
|
||||
+305
-46
@@ -1,4 +1,4 @@
|
||||
import std/[strformat, tables, strutils]
|
||||
import std/[strformat, tables, strutils, sets]
|
||||
import ast, types, scope, source_location, token
|
||||
|
||||
type
|
||||
@@ -44,6 +44,12 @@ type
|
||||
checkedFunc*: bool ## true inside @[Checked] function
|
||||
currentFuncIsAsync*: bool ## true inside async func
|
||||
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
|
||||
closureDepth*: int ## nesting depth inside closures
|
||||
currentClosureExpr*: Expr ## current closure being analyzed
|
||||
@@ -101,6 +107,63 @@ proc hasErrors*(res: SemaResult): bool =
|
||||
return true
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -172,6 +235,14 @@ proc typeToTypeExpr*(t: Type): TypeExpr =
|
||||
else:
|
||||
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)
|
||||
|
||||
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:
|
||||
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],
|
||||
loc: SourceLocation): seq[TypeExpr] =
|
||||
## 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.
|
||||
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:
|
||||
let tpName = tp.name
|
||||
# Lifetime params are inferred from ref lifetime positions
|
||||
if tp.isLifetime:
|
||||
var found = false
|
||||
for i, param in funcDecl.declFuncParams:
|
||||
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
|
||||
break
|
||||
if found:
|
||||
result.add(TypeExpr(kind: tekNamed, typeName: "lifetime"))
|
||||
continue
|
||||
# If not found in refs, treat as uninferrable
|
||||
if not found:
|
||||
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:
|
||||
if i >= argTypes.len: break
|
||||
# Skip pointer params — type param is inside the pointee and we cannot
|
||||
# structurally extract it (e.g., *Map<K,V> → arg is *Map<int,String>)
|
||||
if param.ptype.kind in {tekOwn, tekPointer}:
|
||||
if param.ptype == nil: continue
|
||||
var refsTp = false
|
||||
for n in tpNames:
|
||||
if typeExprReferencesTypeParam(param.ptype, n):
|
||||
refsTp = true
|
||||
break
|
||||
if not refsTp:
|
||||
continue
|
||||
if typeExprReferencesTypeParam(param.ptype, tpName):
|
||||
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}")
|
||||
if not sema.unifyTypeParam(param.ptype, argTypes[i], tpNames, bindings, loc):
|
||||
return @[]
|
||||
if inferred != nil and not inferred.isUnknown:
|
||||
result.add(typeToTypeExpr(inferred))
|
||||
else:
|
||||
# Cannot infer this type parameter from arguments
|
||||
|
||||
# Emit results in decl order
|
||||
for tp in funcDecl.declFuncTypeParams:
|
||||
if tp.isLifetime:
|
||||
result.add(TypeExpr(kind: tekNamed, typeName: "lifetime"))
|
||||
continue
|
||||
if not bindings.hasKey(tp.name):
|
||||
return @[]
|
||||
let t = bindings[tp.name]
|
||||
if t == nil or t.isUnknown:
|
||||
return @[]
|
||||
result.add(typeToTypeExpr(t))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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:
|
||||
sema.emitError(expr.loc, &"use of moved value '{expr.exprIdent}'")
|
||||
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)
|
||||
if sym == nil:
|
||||
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 first.typ
|
||||
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
|
||||
of tkBang:
|
||||
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)
|
||||
if movedIdx >= 0:
|
||||
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)
|
||||
if not value.isAssignableTo(target):
|
||||
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:
|
||||
if expectedParams[i].isMutRef and i < expr.exprCallArgs.len:
|
||||
let arg = expr.exprCallArgs[i]
|
||||
if arg.kind == ekUnary and arg.exprUnaryOp == tkAmp and arg.exprUnaryOperand.kind == ekIdent:
|
||||
mutRefArgs.add((idx: i, name: arg.exprUnaryOperand.exprIdent))
|
||||
let bname = extractBorrowedIdent(arg)
|
||||
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 j in i+1 ..< mutRefArgs.len:
|
||||
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:
|
||||
var armScope = newScope(scope)
|
||||
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)
|
||||
if resultType.isUnknown:
|
||||
resultType = armType
|
||||
@@ -1507,13 +1741,19 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
return makePointer(makeVoid())
|
||||
of ekBorrow:
|
||||
let operand = sema.checkExpr(expr.exprBorrowOperand, scope)
|
||||
# borrow &mut expr returns the same type as the original (reference)
|
||||
# The borrow is tracked in the borrow checker
|
||||
if sema.checkedFunc and expr.exprBorrowMutable:
|
||||
# Track: variable "operand" is mutably borrowed here
|
||||
# For now, just validate the type
|
||||
discard
|
||||
return operand
|
||||
# Explicit `borrow` — track only when bound via let (checkCreateBorrow on skLet).
|
||||
# Here we validate conflicts for free-standing borrow expressions used as temps.
|
||||
if sema.checkedFunc:
|
||||
let bname = extractBorrowedIdent(expr)
|
||||
if bname.len > 0:
|
||||
if expr.exprBorrowMutable:
|
||||
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:
|
||||
return sema.checkExpr(expr.exprSpreadOperand, scope)
|
||||
of ekStringInterp:
|
||||
@@ -1582,6 +1822,19 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type =
|
||||
let initSym = scope.lookup(stmt.stmtLetInit.exprIdent)
|
||||
if initSym != nil and initSym.isOwn:
|
||||
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()
|
||||
of skIf:
|
||||
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:
|
||||
var armScope = newScope(scope)
|
||||
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)
|
||||
return makeVoid()
|
||||
of skReturn:
|
||||
@@ -1704,6 +1961,8 @@ proc checkFunc(sema: var Sema, decl: Decl) =
|
||||
sema.currentFuncIsAsync = decl.declFuncIsAsync
|
||||
if sema.checkedFunc:
|
||||
sema.movedVars = @[]
|
||||
sema.activeMutBorrows = initTable[string, SourceLocation]()
|
||||
sema.activeSharedBorrows = initTable[string, int]()
|
||||
var funcScope = newScope(sema.globalScope)
|
||||
# Add type parameters to type table for resolution
|
||||
var addedTypeParams: seq[string] = @[]
|
||||
|
||||
+102
-12
@@ -1,7 +1,7 @@
|
||||
# Bux — План към „добър“ език (v0.5 → v1.0)
|
||||
|
||||
> **Дата:** 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.
|
||||
|
||||
---
|
||||
@@ -59,6 +59,7 @@
|
||||
| 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.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.4b | Closures: `\|\|` empty params + loop/return body | Lexer `\|\|` vs empty closure; while/break/return | ✅ bootstrap + selfhost |
|
||||
| B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ |
|
||||
@@ -66,18 +67,18 @@
|
||||
|
||||
### C — Gradual Ownership 2.0 (P1)
|
||||
|
||||
| # | Задача | Защо |
|
||||
|---|--------|------|
|
||||
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата |
|
||||
| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives |
|
||||
| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден |
|
||||
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path |
|
||||
| # | Задача | Защо | Статус |
|
||||
|---|--------|------|--------|
|
||||
| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ⏳ |
|
||||
| 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 да е надежден | ✅ bootstrap + selfhost |
|
||||
| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) |
|
||||
|
||||
### 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.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) |
|
||||
| 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
|
||||
2. Generic type inference for `Iter_Map` without explicit `<T,U>`
|
||||
3. Match arm guards (`p if cond => …`)
|
||||
4. Pattern binding name shadowing (C locals are function-scoped)
|
||||
1. C.1 Lifetime elision
|
||||
2. Phase D tooling: `bux fmt` CI, `bux test --filter`, golden stdlib tests
|
||||
3. LSP: position-sensitive locals; inferred `let` types
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -66,6 +66,7 @@ const pkRange: int = 3;
|
||||
const pkEnum: int = 4;
|
||||
const pkStruct: int = 5;
|
||||
const pkTuple: int = 6;
|
||||
const pkGuarded: int = 7; // `p if cond` — patChild1 = inner, patGuardExpr = condition
|
||||
|
||||
struct Pattern {
|
||||
kind: int,
|
||||
@@ -78,10 +79,11 @@ struct Pattern {
|
||||
patEnumPath: String, // for pkEnum: "Enum::Variant"
|
||||
patStructName: String, // for pkStruct (type name)
|
||||
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
|
||||
patArgs: *Pattern, // pkEnum/pkTuple/pkStruct field list (head)
|
||||
patNext: *Pattern, // next sibling in patArgs list
|
||||
patGuardExpr: *Expr, // for pkGuarded: the `if` condition
|
||||
}
|
||||
|
||||
// Match arm: pattern => body
|
||||
|
||||
+74
-13
@@ -75,6 +75,8 @@ struct CEmitter {
|
||||
movedName5: String,
|
||||
movedName6: String,
|
||||
movedName7: String,
|
||||
tmpCounter: int,
|
||||
currentRetType: String,
|
||||
}
|
||||
|
||||
func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) {
|
||||
@@ -155,10 +157,8 @@ func CBE_GetAutoDropVarName(node: *HirNode) -> String {
|
||||
return varNode.strValue;
|
||||
}
|
||||
|
||||
func CBE_EmitDefers(cbe: *CEmitter) -> int {
|
||||
if cbe.deferCount == 0 { return 0; }
|
||||
var i: int = cbe.deferCount - 1;
|
||||
while i >= 0 {
|
||||
// Emit one defer slot (shared by full-stack and scope-pop emitters).
|
||||
func CBE_EmitOneDefer(cbe: *CEmitter, i: int) {
|
||||
var dn: *HirNode = null as *HirNode;
|
||||
if i == 0 { dn = cbe.defer0; }
|
||||
if i == 1 { dn = cbe.defer1; }
|
||||
@@ -171,8 +171,7 @@ func CBE_EmitDefers(cbe: *CEmitter) -> int {
|
||||
// Skip auto-drop for moved variables
|
||||
let deferVarName: String = CBE_GetAutoDropVarName(dn);
|
||||
if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) {
|
||||
i = i - 1;
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
StringBuilder_Append(&cbe.sb, "\n");
|
||||
var sp: int = 0;
|
||||
@@ -182,9 +181,32 @@ func CBE_EmitDefers(cbe: *CEmitter) -> int {
|
||||
}
|
||||
CBE_EmitExpr(cbe, dn);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -381,20 +403,48 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
||||
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 {
|
||||
// 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 {
|
||||
CBE_AddMoved(cbe, node.child1.strValue);
|
||||
}
|
||||
let hadDefers: int = CBE_EmitDefers(cbe);
|
||||
if hadDefers != 0 {
|
||||
if node.child1 != null as *HirNode && cbe.deferCount > 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");
|
||||
var sp: int = 0;
|
||||
while sp < cbe.indent {
|
||||
StringBuilder_Append(&cbe.sb, " ");
|
||||
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");
|
||||
if node.child1 != null as *HirNode {
|
||||
@@ -444,15 +494,17 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If
|
||||
// If — each branch has its own defer scope (locals do not leak)
|
||||
if kind == hIf {
|
||||
|
||||
StringBuilder_Append(&cbe.sb, "if (");
|
||||
CBE_EmitExpr(cbe, node.child1);
|
||||
StringBuilder_Append(&cbe.sb, ") {\n");
|
||||
if node.child2 != null as *HirNode {
|
||||
let savedThen: int = cbe.deferCount;
|
||||
cbe.indent = cbe.indent + 1;
|
||||
CBE_EmitExpr(cbe, node.child2);
|
||||
discard CBE_EmitAndPopDefersFrom(cbe, savedThen);
|
||||
cbe.indent = cbe.indent - 1;
|
||||
}
|
||||
var sp: int = 0;
|
||||
@@ -464,8 +516,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
||||
let elseBlock: *HirNode = node.extraData as *HirNode;
|
||||
if elseBlock != null as *HirNode {
|
||||
StringBuilder_Append(&cbe.sb, " else {\n");
|
||||
let savedElse: int = cbe.deferCount;
|
||||
cbe.indent = cbe.indent + 1;
|
||||
CBE_EmitExpr(cbe, elseBlock);
|
||||
discard CBE_EmitAndPopDefersFrom(cbe, savedElse);
|
||||
cbe.indent = cbe.indent - 1;
|
||||
sp = 0;
|
||||
while sp < cbe.indent {
|
||||
@@ -479,14 +533,16 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
// While
|
||||
// While — loop-body locals dropped each iteration
|
||||
if kind == hWhile {
|
||||
StringBuilder_Append(&cbe.sb, "while (");
|
||||
CBE_EmitExpr(cbe, node.child1);
|
||||
StringBuilder_Append(&cbe.sb, ") {\n");
|
||||
if node.child2 != null as *HirNode {
|
||||
let savedW: int = cbe.deferCount;
|
||||
cbe.indent = cbe.indent + 1;
|
||||
CBE_EmitExpr(cbe, node.child2);
|
||||
discard CBE_EmitAndPopDefersFrom(cbe, savedW);
|
||||
cbe.indent = cbe.indent - 1;
|
||||
}
|
||||
var sp: int = 0;
|
||||
@@ -502,8 +558,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) {
|
||||
if kind == hLoop {
|
||||
StringBuilder_Append(&cbe.sb, "while (1) {\n");
|
||||
if node.child1 != null as *HirNode {
|
||||
let savedL: int = cbe.deferCount;
|
||||
cbe.indent = cbe.indent + 1;
|
||||
CBE_EmitExpr(cbe, node.child1);
|
||||
discard CBE_EmitAndPopDefersFrom(cbe, savedL);
|
||||
cbe.indent = cbe.indent - 1;
|
||||
}
|
||||
var sp: int = 0;
|
||||
@@ -1266,6 +1324,7 @@ func CBackend_Generate(mod: *HirModule) -> String {
|
||||
cbe.mod = mod;
|
||||
cbe.deferCount = 0;
|
||||
cbe.movedCount = 0;
|
||||
cbe.tmpCounter = 0;
|
||||
|
||||
// Header
|
||||
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.deferCount = 0;
|
||||
cbe.movedCount = 0;
|
||||
cbe.tmpCounter = 0;
|
||||
cbe.currentRetType = mod.funcs[i].retTypeName;
|
||||
var hasReturn: bool = false;
|
||||
cbe.indent = 1;
|
||||
CBE_EmitExpr(cbe, body);
|
||||
|
||||
+365
-202
@@ -34,6 +34,101 @@ struct LowerCtx {
|
||||
// Borrow checker state
|
||||
checkedFunc: 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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
let r: String = String_Concat(base, "_");
|
||||
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; }
|
||||
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 {
|
||||
return null as *HirNode;
|
||||
}
|
||||
@@ -568,40 +689,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
|
||||
subjectEnumName: String, subjectHasData: bool,
|
||||
line: uint32, col: uint32) -> *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 {
|
||||
// `_` is wildcard, not a binding
|
||||
if String_Eq(pat.patIdent, "_") { return null as *HirNode; }
|
||||
let ty: String = "int";
|
||||
if subject != null as *HirNode && !String_Eq(subject.typeName, "") {
|
||||
ty = subject.typeName;
|
||||
}
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
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;
|
||||
return Lcx_BindPatIdent(ctx, pat.patIdent, ty, subject, line, col);
|
||||
}
|
||||
|
||||
// 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.child1 = fPtr;
|
||||
fLoad.typeName = "int";
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
alloca.kind = hAlloca;
|
||||
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);
|
||||
let bound: *HirNode = Lcx_BindPatIdent(ctx, elem.patIdent, "int", fLoad, line, col);
|
||||
if bound != null as *HirNode {
|
||||
if head == null as *HirNode {
|
||||
head = alloca;
|
||||
tail = store;
|
||||
head = bound;
|
||||
tail = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
} else {
|
||||
tail.child3 = alloca;
|
||||
tail = store;
|
||||
tail.child3 = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
}
|
||||
}
|
||||
}
|
||||
elem = elem.patNext;
|
||||
@@ -690,38 +765,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
|
||||
fLoad.column = col;
|
||||
fLoad.child1 = fPtr;
|
||||
fLoad.typeName = "int";
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
alloca.kind = hAlloca;
|
||||
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);
|
||||
let bound: *HirNode = Lcx_BindPatIdent(ctx, field.patIdent, "int", fLoad, line, col);
|
||||
if bound != null as *HirNode {
|
||||
if head == null as *HirNode {
|
||||
head = alloca;
|
||||
tail = store;
|
||||
head = bound;
|
||||
tail = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
} else {
|
||||
tail.child3 = alloca;
|
||||
tail = store;
|
||||
tail.child3 = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
}
|
||||
}
|
||||
}
|
||||
field = field.patNext;
|
||||
@@ -827,41 +880,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
|
||||
fLoad.typeName = ftype;
|
||||
|
||||
if arg.kind == pkIdent && !String_Eq(arg.patIdent, "_") {
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
alloca.kind = hAlloca;
|
||||
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);
|
||||
|
||||
let bound: *HirNode = Lcx_BindPatIdent(ctx, arg.patIdent, ftype, fLoad, line, col);
|
||||
if bound != null as *HirNode {
|
||||
if head == null as *HirNode {
|
||||
head = alloca;
|
||||
tail = store;
|
||||
head = bound;
|
||||
tail = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
} else {
|
||||
tail.child3 = alloca;
|
||||
tail = store;
|
||||
tail.child3 = bound;
|
||||
while tail.child3 != null as *HirNode { tail = tail.child3; }
|
||||
}
|
||||
}
|
||||
} else if arg.kind == pkTuple || arg.kind == pkStruct || arg.kind == pkEnum {
|
||||
// Nested pattern on payload field
|
||||
@@ -913,7 +941,9 @@ func Lcx_AppendToChain(head: *HirNode, node: *HirNode) {
|
||||
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 {
|
||||
let line: uint32 = expr.line;
|
||||
let col: uint32 = expr.column;
|
||||
@@ -921,6 +951,8 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
|
||||
ctx.varCounter = ctx.varCounter + 1;
|
||||
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
|
||||
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;
|
||||
allocaNode.kind = hAlloca;
|
||||
allocaNode.line = line;
|
||||
@@ -946,35 +978,48 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
allocaNode.strValue = resultName;
|
||||
allocaNode.typeName = typeName;
|
||||
|
||||
// Collect arms into a temporary array via reverse build of if-chain
|
||||
// First count arms and build from last to first
|
||||
var armCount: int = 0;
|
||||
var arm: *MatchArm = expr.matchArms;
|
||||
while arm != null as *MatchArm {
|
||||
armCount = armCount + 1;
|
||||
arm = arm.next;
|
||||
}
|
||||
let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
foundAlloca.kind = hAlloca;
|
||||
foundAlloca.line = line;
|
||||
foundAlloca.column = col;
|
||||
foundAlloca.strValue = foundName;
|
||||
foundAlloca.typeName = "bool";
|
||||
allocaNode.child3 = foundAlloca;
|
||||
|
||||
// Build if-else from last arm to first
|
||||
var ifChain: *HirNode = null as *HirNode;
|
||||
var ai: int = armCount - 1;
|
||||
while ai >= 0 {
|
||||
// Find arm at index ai
|
||||
let foundInit: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
foundInit.kind = hStore;
|
||||
foundInit.line = line;
|
||||
foundInit.column = col;
|
||||
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 j: int = 0;
|
||||
while j < ai && cur != null as *MatchArm {
|
||||
cur = cur.next;
|
||||
j = j + 1;
|
||||
while cur != null as *MatchArm {
|
||||
// Snapshot rename map so this arm's bindings don't leak to later arms
|
||||
let savedMapCount: int = ctx.patMapCount;
|
||||
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 {
|
||||
ai = ai - 1;
|
||||
continue;
|
||||
// Bindings before guard/body so renames are active and allocas precede uses
|
||||
let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col);
|
||||
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);
|
||||
// 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;
|
||||
storeNode.kind = hStore;
|
||||
storeNode.line = line;
|
||||
@@ -992,58 +1037,94 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
storeNode.child2 = bodyHir;
|
||||
}
|
||||
|
||||
// armBlock = bindings → body stmts → store
|
||||
let armBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
armBlock.kind = hBlock;
|
||||
armBlock.line = line;
|
||||
armBlock.column = col;
|
||||
var chainHead: *HirNode = bindHead;
|
||||
if chainHead == null as *HirNode {
|
||||
chainHead = bodyPrefix;
|
||||
} else if bodyPrefix != null as *HirNode {
|
||||
var bt0: *HirNode = chainHead;
|
||||
while bt0.child3 != null as *HirNode { bt0 = bt0.child3; }
|
||||
bt0.child3 = bodyPrefix;
|
||||
}
|
||||
if chainHead != null as *HirNode {
|
||||
armBlock.child1 = chainHead;
|
||||
var bt: *HirNode = chainHead;
|
||||
while bt.child3 != null as *HirNode { bt = bt.child3; }
|
||||
bt.child3 = storeNode;
|
||||
// found = true
|
||||
let foundSet: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
foundSet.kind = hStore;
|
||||
foundSet.line = line;
|
||||
foundSet.column = col;
|
||||
let foundVar1: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
foundVar1.kind = hVar;
|
||||
foundVar1.strValue = foundName;
|
||||
foundSet.child1 = foundVar1;
|
||||
foundSet.child2 = Lcx_MakeLitHir(tkBoolLiteral, "true", line, col);
|
||||
storeNode.child3 = foundSet;
|
||||
|
||||
// success block: bodyPrefix → store → found=true
|
||||
let successBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
successBlock.kind = hBlock;
|
||||
successBlock.line = line;
|
||||
successBlock.column = col;
|
||||
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 {
|
||||
armBlock.child1 = storeNode;
|
||||
successBlock.child1 = storeNode;
|
||||
}
|
||||
|
||||
let cond: *HirNode = Lcx_PatternCond(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col);
|
||||
if cond == null as *HirNode {
|
||||
// Always-true arm
|
||||
if ifChain == null as *HirNode {
|
||||
ifChain = armBlock;
|
||||
} else {
|
||||
let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
ifNode.kind = hIf;
|
||||
ifNode.line = line;
|
||||
ifNode.column = col;
|
||||
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;
|
||||
// Optional guard wraps success (already lowered with renames active)
|
||||
var afterBinds: *HirNode = successBlock;
|
||||
if guardHirEarly != null as *HirNode {
|
||||
let guardIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
guardIf.kind = hIf;
|
||||
guardIf.line = line;
|
||||
guardIf.column = col;
|
||||
guardIf.child1 = guardHirEarly;
|
||||
guardIf.child2 = successBlock;
|
||||
afterBinds = guardIf;
|
||||
}
|
||||
|
||||
// Chain: alloca → ifChain
|
||||
allocaNode.child3 = ifChain;
|
||||
// armInner: binds → afterBinds
|
||||
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;
|
||||
block.kind = hBlock;
|
||||
@@ -1153,6 +1234,18 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
|
||||
// Identifier → variable reference
|
||||
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,
|
||||
// emit field access on env instance instead of bare variable
|
||||
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.strValue = expr.child1.strValue;
|
||||
|
||||
// Generic call monomorphization
|
||||
if expr.child1 != null as *Expr && expr.child1.genericTypeArgCount > 0 {
|
||||
let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue);
|
||||
if genDecl != null as *Decl {
|
||||
// Generic call monomorphization (explicit / inferred type args)
|
||||
if expr.child1 != null as *Expr {
|
||||
var argc: int = expr.child1.genericTypeArgCount;
|
||||
var typeArg0: String = expr.child1.genericTypeArg0;
|
||||
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.substParam1) { typeArg0 = ctx.substArg1; }
|
||||
if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; }
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lower arguments into child1/child2/extraData
|
||||
var arg: *ExprList = expr.callArgs;
|
||||
@@ -2476,10 +2616,33 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
|
||||
|
||||
// Return
|
||||
if kind == skReturn {
|
||||
n.kind = hReturn;
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+34
-2
@@ -58,14 +58,27 @@ func parserPeek(p: *Parser, ahead: int) -> int {
|
||||
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 {
|
||||
if !parserCheck(p, tkLt) { return false; }
|
||||
var depth: int = 0;
|
||||
var ahead: int = 0;
|
||||
while true {
|
||||
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;
|
||||
}
|
||||
if kind == tkLt {
|
||||
@@ -729,6 +742,7 @@ func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern {
|
||||
pat.patChild2 = null as *Pattern;
|
||||
pat.patArgs = null as *Pattern;
|
||||
pat.patNext = null as *Pattern;
|
||||
pat.patGuardExpr = null as *Expr;
|
||||
return pat;
|
||||
}
|
||||
|
||||
@@ -904,6 +918,24 @@ func parserParsePattern(p: *Parser) -> *Pattern {
|
||||
pat.patRangeInclusive = inclusive;
|
||||
pat.patChild1 = left;
|
||||
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 left;
|
||||
|
||||
+252
-23
@@ -388,6 +388,11 @@ func Sema_AddCapture(closureExpr: *Expr, name: String, typeKind: int) {
|
||||
// Enum payloads: Option::Some(value) → value:int (from variant field type).
|
||||
func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) {
|
||||
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 {
|
||||
var sym: Symbol;
|
||||
Sema_ZeroInitSymbol(&sym);
|
||||
@@ -807,8 +812,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
|
||||
ai = ai + 1;
|
||||
}
|
||||
}
|
||||
// Trait bounds checking for explicit generic calls: Max<Circle>(...)
|
||||
// Must happen before indirect/direct call returns
|
||||
// Trait bounds + inference for generic calls: Max / Iter_Map / Array_Push
|
||||
if expr.child1.kind == ekIdent {
|
||||
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
|
||||
if sym.kind == skFunc && sym.decl != null as *Decl {
|
||||
@@ -831,13 +835,21 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
|
||||
return tyVoid;
|
||||
}
|
||||
}
|
||||
// Direct call to named function
|
||||
// Direct call to named function — substitute return type with inferred args
|
||||
if expr.child1.kind == ekIdent {
|
||||
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
|
||||
if sym.kind == skFunc && sym.decl != null as *Decl {
|
||||
if sym.decl.retType != null as *TypeExpr {
|
||||
expr.refType = sym.decl.retType;
|
||||
return Sema_ResolveType(sema, sym.decl.retType);
|
||||
var retTe: *TypeExpr = 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
|
||||
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 {
|
||||
return Sema_ResolveType(sema, expr.refType);
|
||||
}
|
||||
@@ -990,6 +1006,16 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
|
||||
let savedScope: *Scope = sema.scope;
|
||||
sema.scope = &armScope;
|
||||
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);
|
||||
sema.scope = savedScope;
|
||||
if first {
|
||||
@@ -1677,19 +1703,213 @@ func Sema_ExtractElemType(te: *TypeExpr) -> String {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Infer generic type argument from call arguments.
|
||||
// Supports Array_* and Iter_* stdlib functions.
|
||||
// Substitute type params in a TypeExpr (shallow clone). Used for call return types
|
||||
// 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) {
|
||||
if expr.callArgs == null as *ExprList { return; }
|
||||
if expr.child1 == null as *Expr { return; }
|
||||
var argList: *ExprList = expr.callArgs;
|
||||
var pi: int = 0;
|
||||
while argList != null as *ExprList && pi < funcDecl.paramCount {
|
||||
let argExpr: *Expr = argList.expr;
|
||||
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;
|
||||
if pi == 0 { paramType = funcDecl.param0.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 == 8 { paramType = funcDecl.param8.refParamType; }
|
||||
|
||||
if paramType != null as *TypeExpr && paramType.kind == tekNamed && argType != null as *TypeExpr {
|
||||
let inferred: String = Sema_ExtractElemType(argType);
|
||||
var typeName: String = inferred;
|
||||
if String_Eq(typeName, "") && argType.kind == tekNamed {
|
||||
typeName = argType.typeName;
|
||||
var argType: *TypeExpr = argExpr.refType;
|
||||
// &x → use type of x, wrap as pointer if pattern expects pointer
|
||||
if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp {
|
||||
if argExpr.child1 != null as *Expr {
|
||||
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, "") {
|
||||
expr.child1.genericTypeArg1 = typeName;
|
||||
if expr.child1.genericTypeArgCount < 2 { expr.child1.genericTypeArgCount = 2; }
|
||||
// Named function as value: synthesize tekFunc from its declaration
|
||||
if (argType == null as *TypeExpr || argType.kind != tekFunc) && argExpr.kind == ekIdent {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,46 @@ func Main() -> int {
|
||||
""")
|
||||
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":
|
||||
let res = checkSource("""
|
||||
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
@@ -3,8 +3,12 @@
|
||||
#
|
||||
# Usage: bux-lsp
|
||||
# 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
|
||||
@@ -83,18 +87,25 @@ type
|
||||
kind: string ## function | variable | struct | enum | …
|
||||
detail: string ## signature / type annotation
|
||||
container: string ## optional parent (module / type)
|
||||
fromSema: bool ## detail came from real type checker
|
||||
DocumentState = ref object
|
||||
uri: string
|
||||
content: string
|
||||
version: int
|
||||
symbols: Table[string, SymbolInfo]
|
||||
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
|
||||
documents = initTable[string, DocumentState]()
|
||||
rootPath = ""
|
||||
rootUri = ""
|
||||
workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]()
|
||||
cachedStdlibDir = ""
|
||||
cachedStdlibDecls: seq[Decl] = @[]
|
||||
stdlibLoaded = false
|
||||
|
||||
proc getDoc(uri: string): DocumentState =
|
||||
if not documents.hasKey(uri):
|
||||
@@ -326,6 +337,313 @@ proc analyzeFile(path: string, content: string): DocumentState =
|
||||
if not matchedTypeKw:
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -470,6 +788,8 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) =
|
||||
let updated = analyzeFile(path, doc.content)
|
||||
doc.symbols = updated.symbols
|
||||
doc.ordered = updated.ordered
|
||||
# Keep / refresh real types for hover (does not replace lightweight outline)
|
||||
enrichWithSema(doc)
|
||||
let diags = runBuxcDiagnostics(path, doc.content)
|
||||
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) =
|
||||
## 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 position = paramsNode["position"]
|
||||
let lineNum = position["line"].getInt()
|
||||
@@ -640,6 +960,10 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
return
|
||||
|
||||
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")
|
||||
if lineNum >= lines.len:
|
||||
sendResponse(stream, id, newJNull())
|
||||
@@ -656,19 +980,41 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) =
|
||||
return
|
||||
let word = l[start ..< endC]
|
||||
|
||||
var info: SymbolInfo
|
||||
var detail = ""
|
||||
var kind = ""
|
||||
var found = false
|
||||
|
||||
# Prefer file-local symbol (may be sema-upgraded)
|
||||
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
|
||||
elif workspaceSymbols.hasKey(word):
|
||||
info = workspaceSymbols[word].info
|
||||
let info = workspaceSymbols[word].info
|
||||
detail = info.detail
|
||||
kind = info.kind
|
||||
found = true
|
||||
|
||||
if not found:
|
||||
sendResponse(stream, id, newJNull())
|
||||
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, %*{
|
||||
"contents": {"kind": "markdown", "value": md},
|
||||
"range": {
|
||||
@@ -742,7 +1088,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
||||
"hoverProvider": 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:
|
||||
rootPath = paramsNode["rootPath"].getStr()
|
||||
@@ -750,10 +1096,14 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
||||
rootUri = paramsNode["rootUri"].getStr()
|
||||
if rootPath.len == 0:
|
||||
rootPath = uriToPath(rootUri)
|
||||
# Preload stdlib for hover types
|
||||
if rootPath.len > 0:
|
||||
ensureStdlibCached()
|
||||
|
||||
of "initialized":
|
||||
if rootPath.len > 0:
|
||||
scanWorkspace(rootPath)
|
||||
ensureStdlibCached()
|
||||
|
||||
of "shutdown":
|
||||
sendResponse(stream, id, %*{})
|
||||
@@ -769,6 +1119,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
||||
doc.content = content
|
||||
if td.hasKey("version"):
|
||||
doc.version = td["version"].getInt()
|
||||
# Lightweight scan + sema enrich + buxc diagnostics
|
||||
analyzeAndPublishDiagnostics(stream, doc)
|
||||
|
||||
of "textDocument/didChange":
|
||||
@@ -780,16 +1131,25 @@ proc handleMessage(stream: FileStream, msg: JsonNode) =
|
||||
doc.content = changes[changes.len - 1]["text"].getStr()
|
||||
if td.hasKey("version"):
|
||||
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)
|
||||
doc.symbols = updated.symbols
|
||||
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":
|
||||
let td = paramsNode["textDocument"]
|
||||
let uri = td["uri"].getStr()
|
||||
let doc = getDoc(uri)
|
||||
analyzeAndPublishDiagnostics(stream, doc)
|
||||
discard getDoc(uri)
|
||||
analyzeAndPublishDiagnostics(stream, getDoc(uri))
|
||||
|
||||
of "textDocument/completion":
|
||||
handleCompletion(stream, id, paramsNode)
|
||||
|
||||
Reference in New Issue
Block a user