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:
+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
|
||||
|
||||
+263
-99
@@ -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)
|
||||
var afterBinds: HirNode
|
||||
if guardHir != nil:
|
||||
afterBinds = HirNode(kind: hIf, ifCond: guardHir, ifThen: successBlock, ifElse: nil,
|
||||
typ: makeVoid(), loc: loc)
|
||||
else:
|
||||
if ifChain == nil:
|
||||
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
|
||||
typ: makeVoid(), loc: loc)
|
||||
else:
|
||||
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: ifChain,
|
||||
typ: makeVoid(), loc: loc)
|
||||
afterBinds = successBlock
|
||||
|
||||
if ifChain != nil:
|
||||
stmts.add(ifChain)
|
||||
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)
|
||||
|
||||
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):
|
||||
stmts.add(ctx.deferStmts[i])
|
||||
stmts.add(hirReturn(value, loc))
|
||||
if not dropTargetsVar(ctx.deferStmts[i], skipDrop):
|
||||
stmts.add(ctx.deferStmts[i])
|
||||
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:
|
||||
|
||||
+309
-50
@@ -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 @[]
|
||||
# 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
|
||||
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 not sema.unifyTypeParam(param.ptype, argTypes[i], tpNames, bindings, loc):
|
||||
return @[]
|
||||
var inferred: Type = nil
|
||||
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}:
|
||||
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}")
|
||||
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] = @[]
|
||||
|
||||
Reference in New Issue
Block a user