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

Sessions 18–23 quality work:

- B.3c match arm guards + sequential found-flag lower (bootstrap + selfhost)
- Generic HOF type inference (Array/Iter map/filter/fold without type args)
- Pattern binding shadowing via unique C locals (__pN_src)
- Ownership C.2 exclusive &mut data-flow + C.4 goldens; *p= store-through fix
- Ownership C.3 auto-drop on early return/branches: scoped defers, move-on-return,
  Drop monomorphization, materialize return before Drop
- LSP 0.3.0: hover from real sema types
- Examples and QUALITY_PLAN session log; selfhost-loop identical
This commit is contained in:
2026-07-18 21:52:14 +03:00
parent 66f11d1869
commit 3eb1ad3a82
25 changed files with 2246 additions and 457 deletions
+5 -1
View File
@@ -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:
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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] = @[]