From 3eb1ad3a8279faae193e4a3b8e2d0ab6e78a0819 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 18 Jul 2026 21:52:14 +0300 Subject: [PATCH] feat: match guards, HOF inference, ownership C.2/C.3, LSP sema hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Makefile | 6 +- bootstrap/cli.nim | 6 +- bootstrap/hir.nim | 1 + bootstrap/hir_lower.nim | 362 ++++++++--- bootstrap/parser.nim | 36 +- bootstrap/sema.nim | 359 +++++++++-- docs/QUALITY_PLAN.md | 114 +++- examples/drop_early_return.bux | 62 ++ examples/generic_infer_hof.bux | 94 +++ examples/match_guards.bux | 83 +++ examples/ownership_checked.bux | 35 + examples/pattern_shadow.bux | 50 ++ src/ast.bux | 4 +- src/c_backend.bux | 121 +++- src/hir_lower.bux | 597 +++++++++++------- src/parser.bux | 36 +- src/sema.bux | 277 +++++++- tests/borrow_test.nim | 40 ++ tests/error_golden/exclusive_mut_let/bux.toml | 6 + .../exclusive_mut_let/expected.err | 7 + .../exclusive_mut_let/src/Main.bux | 7 + .../use_while_mut_borrow/bux.toml | 6 + .../use_while_mut_borrow/expected.err | 7 + .../use_while_mut_borrow/src/Main.bux | 7 + tools/lsp_server.nim | 380 ++++++++++- 25 files changed, 2246 insertions(+), 457 deletions(-) create mode 100644 examples/drop_early_return.bux create mode 100644 examples/generic_infer_hof.bux create mode 100644 examples/match_guards.bux create mode 100644 examples/ownership_checked.bux create mode 100644 examples/pattern_shadow.bux create mode 100644 tests/error_golden/exclusive_mut_let/bux.toml create mode 100644 tests/error_golden/exclusive_mut_let/expected.err create mode 100644 tests/error_golden/exclusive_mut_let/src/Main.bux create mode 100644 tests/error_golden/use_while_mut_borrow/bux.toml create mode 100644 tests/error_golden/use_while_mut_borrow/expected.err create mode 100644 tests/error_golden/use_while_mut_borrow/src/Main.bux diff --git a/Makefile b/Makefile index 659ae04..6270f1a 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SRC := bootstrap/main.nim OUT := buxc BUILD_DIR := build -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic struct_tuple_pat match_block nested_patterns +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ownership_checked drop_early_return ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp iter_generic generic_infer_hof struct_tuple_pat match_block nested_patterns match_guards pattern_shadow .PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp @@ -143,5 +143,5 @@ selfhost-loop: build lsp: tools/bux-lsp @echo "LSP server ready at tools/bux-lsp" -tools/bux-lsp: tools/lsp_server.nim - cd tools && $(NIM) c -o:bux-lsp lsp_server.nim +tools/bux-lsp: tools/lsp_server.nim bootstrap/*.nim + cd tools && $(NIM) c -d:release --opt:size --path:../bootstrap -o:bux-lsp lsp_server.nim diff --git a/bootstrap/cli.nim b/bootstrap/cli.nim index dbbd5f2..6120efd 100644 --- a/bootstrap/cli.nim +++ b/bootstrap/cli.nim @@ -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: diff --git a/bootstrap/hir.nim b/bootstrap/hir.nim index e3fe86d..d8d813b 100644 --- a/bootstrap/hir.nim +++ b/bootstrap/hir.nim @@ -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 diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 6efd1a4..e7dd5a4 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -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) diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 5767c14..54a84e1 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -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`). + ## 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: diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 576b068..78a76cc 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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, Array, Map + 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` + `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 → arg is *Map) - 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] = @[] diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 17ebc2f..d1ac313 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) > **Дата:** 2026-07-18 -> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **41+ examples**, match + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅ +> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **43+ examples**, match + guards + **generic HOF inference** + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅ > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -59,6 +59,7 @@ | B.2 | Function pointer types | `func(T)->U` fat ABI | ✅ bootstrap + selfhost | | B.3 | Match expression lowering (literals, ranges, enums) | Expression-context match → if-else | ✅ bootstrap + selfhost | | B.3b | Pattern bindings (`Some(value) => value`) | Payload idents bound in arm body | ✅ bootstrap + selfhost | +| B.3c | Match arm guards (`p if cond => …`) | Bindings visible in guard; sequential found-flag lower | ✅ bootstrap + selfhost | | B.4 | Closures multi-instance | Fat `BuxFn` + heap env | ✅ bootstrap + selfhost | | B.4b | Closures: `\|\|` empty params + loop/return body | Lexer `\|\|` vs empty closure; while/break/return | ✅ bootstrap + selfhost | | B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ | @@ -66,18 +67,18 @@ ### C — Gradual Ownership 2.0 (P1) -| # | Задача | Защо | -|---|--------|------| -| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | -| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | -| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | -| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | +| # | Задача | Защо | Статус | +|---|--------|------|--------| +| C.1 | Lifetime elision за common cases | Без `'a` в 90% от API-тата | ⏳ | +| C.2 | Exclusive `&mut` vs shared `&` data-flow | По-малко false negatives | ✅ let-bound + use-while + call conflict | +| C.3 | Auto-drop edge cases (early return, branches) | RAII да е надежден | ✅ bootstrap + selfhost | +| C.4 | `@[Release]` zero-cost path документация + golden tests | Killer story: safe default, free hot path | ✅ partial (unchecked path + goldens) | ### D — Tooling (P1) | # | Задача | Защо | Статус | |---|--------|------|--------| -| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ hover/def/outline + `buxc` diags (lightweight index; full sema later) | +| D.1 | LSP: hover, go-to-def, diagnostics | IDE = adoption | ✅ hover/def/outline + **sema types on hover** (v0.3.0) + `buxc` diags | | D.2 | `bux fmt` стабилен + CI check | Единен style | ⏳ | | D.3 | `bux test` с `--filter`, exit codes, summary table | CI-friendly | ⏳ partial (`bux test` exists) | | D.4 | `bux doc` от `///` comments | Самодокументиращ се stdlib | ⏳ | @@ -297,9 +298,98 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) --- +## Сесия 18 (match arm guards — B.3c) + +1. **Syntax:** `p if cond => body` (also after ranges: `1..10 if x % 2 == 0`) +2. **Parser fix (critical):** `isTypeArgListAhead` treated `x < 0` as generic when a later `x > 0` existed in another arm → infinite parse. Stop lookahead on `=>`, keywords, literals, arithmetic, comparisons. +3. **Sema:** bind inner pattern first; type-check guard as bool in arm scope +4. **HIR lower:** sequential `found` flag (no shared if-else DAG): + ``` + if (!found) { if (inner_cond) { binds; if (guard) { result = body; found = true; } } } + ``` + Bindings are in scope for the guard expression. +5. **Selfhost:** `pkGuarded` + `patGuardExpr`; same lower strategy; fix `return match {…}` to expand yield block before return +6. Example: `examples/match_guards.bux` (ident/literal/range/enum payload guards) +7. Verified: bootstrap + **buxc2** + all examples + error goldens + **selfhost-loop IDENTICAL ✓** + +--- + +## Сесия 19 (generic HOF type inference) + +1. **Bootstrap `inferTypeArgs`:** structural unify of param TypeExpr vs arg Type + - `*Iter` / `*Array` → extract T from pointee type args or mangled `Array_int` + - `func(T)->U` → bind T/U from function-value type (not whole func as T) + - bare `Acc` from init; multi-param `Iter_Fold` +2. **Selfhost:** improved `Sema_InferGenericArgs` + return-type subst after inference + - Fix: `ekCast` must type-check operand (was skipping → no inference under `as`) + - HIR fallback mono from first *Array/*Iter arg when count=0 +3. Works without explicit type args: + - `Array_Push(&nums, 1)`, `Array_Get`, `Array_Len`, `Array_Iter` + - `Iter_Map(&it, f)`, `Iter_Filter`, `Iter_Fold`, `Iter_Any` (int↔String) +4. Example: `examples/generic_infer_hof.bux` +5. Verified: bootstrap + **buxc2** + all examples + **selfhost-loop IDENTICAL ✓** + +--- + +## Сесия 20 (LSP hover from real sema — D.1) + +1. **`bux-lsp` 0.3.0** links bootstrap (`--path:../bootstrap`) and runs `analyzeFull` on open/save +2. **typeIndex:** global scope (stdlib + file) → hover signatures with real types +3. **File-local priority:** user decls override stdlib name collisions (`Max` vs `Math.Max`) +4. **Locals:** walk function bodies for `let`/`var` with explicit type annotations +5. **didChange:** fast lightweight rescan; keeps previous typeIndex until save/hover refresh +6. Hover shows ```bux signature``` + `_kind_ · sema` +7. Smoke: `Main() -> int`, `PrintLine(String) -> void`, `Max(a: T, b: T) -> T` + +--- + +## Сесия 21 (pattern binding shadowing) + +1. **Problem:** C/LIR function-scoped locals — nested `Some(n) => match … Some(n)` emitted store before `int n`, and `let v` + pattern `v` caused redeclaration. +2. **Bootstrap:** every pattern binding → unique C name `__pN_src` via `patternRenames` map; body/guard idents rewritten; **binds before body lower** inside `lowerMatch`. +3. **Selfhost:** same model — `Lcx_BindPatIdent` + `patMapFrom/To` rename table; arm-scoped push/pop of map. +4. Semantics: nested pattern name shadows correctly; outer `let v` survives after match that binds `v`. +5. Example: `examples/pattern_shadow.bux` +6. Verified: bootstrap + **buxc2** + all examples + **selfhost-loop IDENTICAL ✓** + +--- + +## Сесия 22 (Ownership 2.0 — C.2 + C.4 + *p= fix) + +1. **C.2 Exclusive &mut data-flow** (`@[Checked]`): + - Track long-lived let-bound borrows (`activeMutBorrows` / `activeSharedBorrows`) + - Reject: second `&mut x`, use/assign of `x` while mutably borrowed, `&x` while `&mut` live + - Call-site temps conflict with existing let-bound borrows +2. **C.4 Golden tests:** + - `tests/error_golden/exclusive_mut_let/` + - `tests/error_golden/use_while_mut_borrow/` +3. **Bugfix:** `*p = expr` now stores through the pointer (was assigning to a temp) — ownership examples finally mutate correctly +4. Example: `examples/ownership_checked.bux` (unchecked zero-cost + checked OK path) +5. Verified: 7 error goldens + borrow_test + all examples + selfhost-loop + +--- + +## Сесия 23 (Ownership 2.0 — C.3 auto-drop early return / branches) + +1. **Bootstrap auto-drop for `@[Drop]` + collections:** + - Parser: `@[Drop]` / `@[Release]` on structs and funcs (`declAttrs`) + - `autoDropFuncName` + monomorphize `Array_Drop`/`Free` (etc.) so stdlib links + - Inject `Type_Drop(&x)` on `let` via `deferStmts` +2. **Early return / multi-path:** + - Every `return` snapshots the full live defer stack (no clear-after-first-return) + - Materialize return value **before** Drop (`return a.id` is not use-after-drop) + - Move-on-return: skip Drop for a local returned by value (`return out`) +3. **Branch / loop scopes:** + - Bootstrap: `lowerBlock` scopes `deferStmts` — branch-local drops at block exit; siblings do not see each other + - Selfhost C backend: `CBE_EmitDefers` keeps stack for multi-return; `CBE_EmitAndPopDefersFrom` pops branch/loop locals after `if`/`while`/`loop` +4. **Selfhost fixes:** null-safe ret type; temp name counter; use function `retTypeName` for `__retdrop_N` +5. Example: `examples/drop_early_return.bux` (Early + Branched + Scoped → 5 drops) +6. Verified: bootstrap + **buxc2** drop tests, 7 error goldens, key examples, **selfhost-loop IDENTICAL ✓** + +--- + ## Следващи стъпки -1. LSP: wire hover types from real sema -2. Generic type inference for `Iter_Map` without explicit `` -3. Match arm guards (`p if cond => …`) -4. Pattern binding name shadowing (C locals are function-scoped) +1. C.1 Lifetime elision +2. Phase D tooling: `bux fmt` CI, `bux test --filter`, golden stdlib tests +3. LSP: position-sensitive locals; inferred `let` types diff --git a/examples/drop_early_return.bux b/examples/drop_early_return.bux new file mode 100644 index 0000000..1c1e38d --- /dev/null +++ b/examples/drop_early_return.bux @@ -0,0 +1,62 @@ +// C.3 — Auto-drop on early return and if branches +// @[Drop] types call Type_Drop at every exit (return + branch scope end). + +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +@[Drop] +struct Token { + id: int, + counter: *int +} + +func Token_Drop(self: *Token) { + if self.counter != null as *int { + *self.counter = *self.counter + 1; + } +} + +// Drop t on both early return and fallthrough return. +func Early(flag: int, counter: *int) -> int { + let t: Token = Token { id: 1, counter: counter }; + if flag == 0 { + return 0; + } + return 1; +} + +// Branch-local Tokens: only the taken branch's Drop runs. +func Branched(flag: int, counter: *int) -> int { + if flag == 1 { + let a: Token = Token { id: 10, counter: counter }; + return a.id; + } else { + let b: Token = Token { id: 20, counter: counter }; + return b.id; + } +} + +// Fallthrough: Drop at end of block without return. +func Scoped(counter: *int) { + let s: Token = Token { id: 99, counter: counter }; + discard s.id; +} + +func Main() -> int { + var drops: int = 0; + discard Early(0, &drops); + discard Early(1, &drops); + Test_AssertEqInt(drops, 2); + + discard Branched(1, &drops); + discard Branched(0, &drops); + Test_AssertEqInt(drops, 4); + + Scoped(&drops); + Test_AssertEqInt(drops, 5); + + PrintInt(drops); + PrintLine(""); + Test_Pass("drop_early_return"); + return 0; +} diff --git a/examples/generic_infer_hof.bux b/examples/generic_infer_hof.bux new file mode 100644 index 0000000..5e3e314 --- /dev/null +++ b/examples/generic_infer_hof.bux @@ -0,0 +1,94 @@ +// Generic HOF type inference — Iter_Map / Filter / Fold without explicit +// Also Array_Push / Array_Get / Array_Iter inferred from arguments. +import Std::Io::{PrintLine, PrintInt}; +import Std::Array::{ + Array, Array_New, Array_Push, Array_Get, Array_Len, Array_Free +}; +import Std::String::{String_FromInt, String_Len, String_Eq}; +import Std::Iter::{ + Array_Iter, Iter, + Iter_Map, Iter_Filter, Iter_Fold, Iter_Any, Iter_All +}; +import Std::Test::{ + Test_AssertEqInt, Test_AssertTrue, Test_AssertEqString, Test_Pass +}; + +func Double(x: int) -> int { + return x * 2; +} + +func IsEven(x: int) -> bool { + return (x % 2) == 0; +} + +func IntToString(x: int) -> String { + return String_FromInt(x); +} + +func StringLenAsInt(s: String) -> int { + return String_Len(s) as int; +} + +func AddLens(acc: int, s: String) -> int { + return acc + (String_Len(s) as int); +} + +func AddInt(acc: int, x: int) -> int { + return acc + x; +} + +func IsNonEmpty(s: String) -> bool { + return String_Len(s) > 0; +} + +func Main() -> int { + var nums: Array = Array_New(8); + // Type args inferred from &nums : *Array and value type + Array_Push(&nums, 1); + Array_Push(&nums, 2); + Array_Push(&nums, 3); + Array_Push(&nums, 4); + Array_Push(&nums, 5); + + // Iter_Map inferred from *Iter + func(int)->int + let itA: Iter = Array_Iter(&nums); + var doubled: Array = Iter_Map(&itA, Double); + Test_AssertEqInt(Array_Get(&doubled, 0), 2); + Test_AssertEqInt(Array_Get(&doubled, 4), 10); + + // Iter_Map + let itB: Iter = Array_Iter(&nums); + var asStr: Array = Iter_Map(&itB, IntToString); + Test_AssertEqInt(Array_Len(&asStr) as int, 5); + Test_AssertEqString(Array_Get(&asStr, 0), "1"); + Test_AssertEqString(Array_Get(&asStr, 4), "5"); + + // Iter_Map + let itC: Iter = Array_Iter(&asStr); + var lens: Array = Iter_Map(&itC, StringLenAsInt); + Test_AssertEqInt(Array_Get(&lens, 0), 1); + + // Iter_Filter + let itD: Iter = Array_Iter(&nums); + var evens: Array = Iter_Filter(&itD, IsEven); + Test_AssertEqInt(Array_Len(&evens) as int, 2); + Test_AssertEqInt(Array_Get(&evens, 0), 2); + Test_AssertEqInt(Array_Get(&evens, 1), 4); + + // Iter_Fold — Acc from init, T from *Iter + func + let itE: Iter = Array_Iter(&asStr); + let totalChars: int = Iter_Fold(&itE, 0, AddLens); + Test_AssertEqInt(totalChars, 5); + + let itF: Iter = Array_Iter(&nums); + let sum: int = Iter_Fold(&itF, 0, AddInt); + Test_AssertEqInt(sum, 15); + + let itG: Iter = Array_Iter(&asStr); + Test_AssertTrue(Iter_Any(&itG, IsNonEmpty)); + + PrintInt(sum); + PrintLine(""); + Test_Pass("generic_infer_hof"); + return 0; +} diff --git a/examples/match_guards.bux b/examples/match_guards.bux new file mode 100644 index 0000000..8306494 --- /dev/null +++ b/examples/match_guards.bux @@ -0,0 +1,83 @@ +// Match arm guards: `p if cond => body` +// Bindings from the pattern are in scope for the guard expression. +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +enum Option { + Some(int), + None +} + +func Classify(n: int) -> int { + return match n { + x if x < 0 => -1, + x if x == 0 => 0, + x if x > 0 => 1, + _ => 99 + }; +} + +func Main() -> int { + // Ident + guard + Test_AssertEqInt(Classify(-5), -1); + Test_AssertEqInt(Classify(0), 0); + Test_AssertEqInt(Classify(7), 1); + + // Literal + guard (second arm only when n is even) + let n: int = 4; + let a: int = match n { + 4 if n % 2 == 0 => 40, + 4 => 41, + _ => -1 + }; + Test_AssertEqInt(a, 40); + + let m: int = 4; + let b: int = match m { + 4 if m % 2 != 0 => 40, + 4 => 41, + _ => -1 + }; + Test_AssertEqInt(b, 41); + + // Range + guard + let k: int = 5; + let c: int = match k { + 1..10 if k % 2 == 0 => 100, + 1..10 => 101, + _ => -1 + }; + Test_AssertEqInt(c, 101); + + let j: int = 6; + let d: int = match j { + 1..10 if j % 2 == 0 => 100, + 1..10 => 101, + _ => -1 + }; + Test_AssertEqInt(d, 100); + + // Enum payload binding used in guard + let opt: Option = Option { tag: Option_Some }; + opt.data.Some_0 = 15; + let e: int = match opt { + Option::Some(v) if v > 10 => v * 2, + Option::Some(v) => v, + Option::None => 0 + }; + Test_AssertEqInt(e, 30); + + let small: Option = Option { tag: Option_Some }; + small.data.Some_0 = 3; + let f: int = match small { + Option::Some(v) if v > 10 => v * 2, + Option::Some(v) => v, + Option::None => 0 + }; + Test_AssertEqInt(f, 3); + + PrintInt(e); + PrintLine(""); + Test_Pass("match_guards"); + return 0; +} diff --git a/examples/ownership_checked.bux b/examples/ownership_checked.bux new file mode 100644 index 0000000..84d213f --- /dev/null +++ b/examples/ownership_checked.bux @@ -0,0 +1,35 @@ +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +func UncheckedInc(p: *int) { + *p = *p + 1; +} + +@[Checked] +func Inc(p: &mut int) { + *p = *p + 1; +} + +@[Checked] +func Get(p: &int) -> int { + return *p; +} + +@[Checked] +func OkBorrow() -> int { + var x: int = 10; + Inc(&x); + return Get(&x); +} + +func Main() -> int { + var n: int = 1; + UncheckedInc(&n); + Test_AssertEqInt(n, 2); + let r: int = OkBorrow(); + Test_AssertEqInt(r, 11); + PrintInt(r); + PrintLine(""); + Test_Pass("ownership_checked"); + return 0; +} diff --git a/examples/pattern_shadow.bux b/examples/pattern_shadow.bux new file mode 100644 index 0000000..a7e1eea --- /dev/null +++ b/examples/pattern_shadow.bux @@ -0,0 +1,50 @@ +import Std::Test::{Test_AssertEqInt, Test_Pass}; +import Std::Io::{PrintInt, PrintLine}; + +enum Option { + Some(int), + None +} + +func Main() -> int { + let a: Option = Option { tag: Option_Some }; + a.data.Some_0 = 10; + let b: Option = Option { tag: Option_Some }; + b.data.Some_0 = 20; + + // Same binding name `v` in two sequential matches + let x: int = match a { + Option::Some(v) => v + 1, + Option::None => 0 + }; + let y: int = match b { + Option::Some(v) => v + 2, + Option::None => 0 + }; + Test_AssertEqInt(x, 11); + Test_AssertEqInt(y, 22); + + // Nested: outer and inner both bind `n` + let nested: int = match a { + Option::Some(n) => match b { + Option::Some(n) => n, // should be b's payload (20), not a's + Option::None => -1 + }, + Option::None => -2 + }; + Test_AssertEqInt(nested, 20); + + // Shadow outer let with pattern binding + let v: int = 99; + let z: int = match a { + Option::Some(v) => v, // pattern v should be 10, not 99 + Option::None => 0 + }; + Test_AssertEqInt(z, 10); + Test_AssertEqInt(v, 99); // outer v unchanged + + PrintInt(nested); + PrintLine(""); + Test_Pass("pat_shadow"); + return 0; +} diff --git a/src/ast.bux b/src/ast.bux index 65f526f..96a6b12 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -66,6 +66,7 @@ const pkRange: int = 3; const pkEnum: int = 4; const pkStruct: int = 5; const pkTuple: int = 6; +const pkGuarded: int = 7; // `p if cond` — patChild1 = inner, patGuardExpr = condition struct Pattern { kind: int, @@ -78,10 +79,11 @@ struct Pattern { patEnumPath: String, // for pkEnum: "Enum::Variant" patStructName: String, // for pkStruct (type name) patFieldName: String, // for struct field entry: field name in Point { x: a } - patChild1: *Pattern, // range lo / nested + patChild1: *Pattern, // range lo / nested / guarded inner patChild2: *Pattern, // range hi / nested patArgs: *Pattern, // pkEnum/pkTuple/pkStruct field list (head) patNext: *Pattern, // next sibling in patArgs list + patGuardExpr: *Expr, // for pkGuarded: the `if` condition } // Match arm: pattern => body diff --git a/src/c_backend.bux b/src/c_backend.bux index 3557a03..51f7bc8 100644 --- a/src/c_backend.bux +++ b/src/c_backend.bux @@ -75,6 +75,8 @@ struct CEmitter { movedName5: String, movedName6: String, movedName7: String, + tmpCounter: int, + currentRetType: String, } func CBE_PushDefer(cbe: *CEmitter, node: *HirNode) { @@ -155,36 +157,56 @@ func CBE_GetAutoDropVarName(node: *HirNode) -> String { return varNode.strValue; } +// Emit one defer slot (shared by full-stack and scope-pop emitters). +func CBE_EmitOneDefer(cbe: *CEmitter, i: int) { + var dn: *HirNode = null as *HirNode; + if i == 0 { dn = cbe.defer0; } + if i == 1 { dn = cbe.defer1; } + if i == 2 { dn = cbe.defer2; } + if i == 3 { dn = cbe.defer3; } + if i == 4 { dn = cbe.defer4; } + if i == 5 { dn = cbe.defer5; } + if i == 6 { dn = cbe.defer6; } + if i == 7 { dn = cbe.defer7; } + // Skip auto-drop for moved variables + let deferVarName: String = CBE_GetAutoDropVarName(dn); + if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) { + return; + } + StringBuilder_Append(&cbe.sb, "\n"); + var sp: int = 0; + while sp < cbe.indent { + StringBuilder_Append(&cbe.sb, " "); + sp = sp + 1; + } + CBE_EmitExpr(cbe, dn); + StringBuilder_Append(&cbe.sb, ";"); +} + +// Emit all active defers (LIFO) without clearing the stack. +// Must NOT clear: multiple return paths each need the full defer list. +// (Clearing caused Early(flag) { if (0) return; return 1 } to drop only on first exit.) +// Stack is reset at the start of each function emission. func CBE_EmitDefers(cbe: *CEmitter) -> int { if cbe.deferCount == 0 { return 0; } var i: int = cbe.deferCount - 1; while i >= 0 { - var dn: *HirNode = null as *HirNode; - if i == 0 { dn = cbe.defer0; } - if i == 1 { dn = cbe.defer1; } - if i == 2 { dn = cbe.defer2; } - if i == 3 { dn = cbe.defer3; } - if i == 4 { dn = cbe.defer4; } - if i == 5 { dn = cbe.defer5; } - if i == 6 { dn = cbe.defer6; } - if i == 7 { dn = cbe.defer7; } - // Skip auto-drop for moved variables - let deferVarName: String = CBE_GetAutoDropVarName(dn); - if !String_Eq(deferVarName, "") && CBE_IsMoved(cbe, deferVarName) { - i = i - 1; - continue; - } - StringBuilder_Append(&cbe.sb, "\n"); - var sp: int = 0; - while sp < cbe.indent { - StringBuilder_Append(&cbe.sb, " "); - sp = sp + 1; - } - CBE_EmitExpr(cbe, dn); - StringBuilder_Append(&cbe.sb, ";"); + CBE_EmitOneDefer(cbe, i); i = i - 1; } - cbe.deferCount = 0; + return 1; +} + +// Emit branch/loop-local defers (indices fromIdx..count-1) then pop them. +// Outer defers stay live so sibling branches and later returns still drop correctly. +func CBE_EmitAndPopDefersFrom(cbe: *CEmitter, fromIdx: int) -> int { + if cbe.deferCount <= fromIdx { return 0; } + var i: int = cbe.deferCount - 1; + while i >= fromIdx { + CBE_EmitOneDefer(cbe, i); + i = i - 1; + } + cbe.deferCount = fromIdx; return 1; } @@ -381,20 +403,48 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { return; } - // Return + // Return — evaluate value first, then drop live locals, then return. + // (Emitting Drop before the value used to use-after-drop on `return a.id`.) if kind == hReturn { - // Track moved variables via return + // Track moved variables via return (skip auto-drop of moved-out locals) if node.child1 != null as *HirNode && node.child1.kind == hVar { CBE_AddMoved(cbe, node.child1.strValue); } - let hadDefers: int = CBE_EmitDefers(cbe); - if hadDefers != 0 { + if node.child1 != null as *HirNode && cbe.deferCount > 0 { + // Materialize into a temp so Drop cannot clobber the returned value. + // Prefer the enclosing function return type (field-access HIR often + // carries the base struct typeName, which is wrong for `return a.id`). + cbe.tmpCounter = cbe.tmpCounter + 1; + let tmpName: String = String_Concat("__retdrop_", String_FromInt(cbe.tmpCounter)); + var retCt: String = "int"; + if cbe.currentRetType != null as String && !String_Eq(cbe.currentRetType, "") && !String_Eq(cbe.currentRetType, "void") { + retCt = cbe.currentRetType; + } else if node.child1.typeKind != 0 { + retCt = CBackend_TypeToC(node.child1.typeKind); + } + StringBuilder_Append(&cbe.sb, CBE_CParamDecl(retCt, tmpName)); + StringBuilder_Append(&cbe.sb, " = "); + CBE_EmitExpr(cbe, node.child1); + StringBuilder_Append(&cbe.sb, ";"); + discard CBE_EmitDefers(cbe); StringBuilder_Append(&cbe.sb, "\n"); var sp: int = 0; while sp < cbe.indent { StringBuilder_Append(&cbe.sb, " "); sp = sp + 1; } + StringBuilder_Append(&cbe.sb, "return "); + StringBuilder_Append(&cbe.sb, tmpName); + return; + } + let hadDefers: int = CBE_EmitDefers(cbe); + if hadDefers != 0 { + StringBuilder_Append(&cbe.sb, "\n"); + var sp2: int = 0; + while sp2 < cbe.indent { + StringBuilder_Append(&cbe.sb, " "); + sp2 = sp2 + 1; + } } StringBuilder_Append(&cbe.sb, "return"); if node.child1 != null as *HirNode { @@ -444,15 +494,17 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { return; } - // If + // If — each branch has its own defer scope (locals do not leak) if kind == hIf { StringBuilder_Append(&cbe.sb, "if ("); CBE_EmitExpr(cbe, node.child1); StringBuilder_Append(&cbe.sb, ") {\n"); if node.child2 != null as *HirNode { + let savedThen: int = cbe.deferCount; cbe.indent = cbe.indent + 1; CBE_EmitExpr(cbe, node.child2); + discard CBE_EmitAndPopDefersFrom(cbe, savedThen); cbe.indent = cbe.indent - 1; } var sp: int = 0; @@ -464,8 +516,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { let elseBlock: *HirNode = node.extraData as *HirNode; if elseBlock != null as *HirNode { StringBuilder_Append(&cbe.sb, " else {\n"); + let savedElse: int = cbe.deferCount; cbe.indent = cbe.indent + 1; CBE_EmitExpr(cbe, elseBlock); + discard CBE_EmitAndPopDefersFrom(cbe, savedElse); cbe.indent = cbe.indent - 1; sp = 0; while sp < cbe.indent { @@ -479,14 +533,16 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { return; } - // While + // While — loop-body locals dropped each iteration if kind == hWhile { StringBuilder_Append(&cbe.sb, "while ("); CBE_EmitExpr(cbe, node.child1); StringBuilder_Append(&cbe.sb, ") {\n"); if node.child2 != null as *HirNode { + let savedW: int = cbe.deferCount; cbe.indent = cbe.indent + 1; CBE_EmitExpr(cbe, node.child2); + discard CBE_EmitAndPopDefersFrom(cbe, savedW); cbe.indent = cbe.indent - 1; } var sp: int = 0; @@ -502,8 +558,10 @@ func CBE_EmitExpr(cbe: *CEmitter, node: *HirNode) { if kind == hLoop { StringBuilder_Append(&cbe.sb, "while (1) {\n"); if node.child1 != null as *HirNode { + let savedL: int = cbe.deferCount; cbe.indent = cbe.indent + 1; CBE_EmitExpr(cbe, node.child1); + discard CBE_EmitAndPopDefersFrom(cbe, savedL); cbe.indent = cbe.indent - 1; } var sp: int = 0; @@ -1266,6 +1324,7 @@ func CBackend_Generate(mod: *HirModule) -> String { cbe.mod = mod; cbe.deferCount = 0; cbe.movedCount = 0; + cbe.tmpCounter = 0; // Header StringBuilder_Append(&cbe.sb, "// Generated by Bux C Backend v2\n"); @@ -1566,6 +1625,8 @@ func CBackend_Generate(mod: *HirModule) -> String { cbe.checkedFunc = mod.funcs[i].checkedFunc; cbe.deferCount = 0; cbe.movedCount = 0; + cbe.tmpCounter = 0; + cbe.currentRetType = mod.funcs[i].retTypeName; var hasReturn: bool = false; cbe.indent = 1; CBE_EmitExpr(cbe, body); diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 89c1a8d..7b57008 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -34,6 +34,101 @@ struct LowerCtx { // Borrow checker state checkedFunc: bool, releaseFunc: bool, + // Pattern binding renames: source name → unique C local (shadowing-safe) + patMapCount: int, + patMapFrom0: String, + patMapTo0: String, + patMapFrom1: String, + patMapTo1: String, + patMapFrom2: String, + patMapTo2: String, + patMapFrom3: String, + patMapTo3: String, + patMapFrom4: String, + patMapTo4: String, + patMapFrom5: String, + patMapTo5: String, + patMapFrom6: String, + patMapTo6: String, + patMapFrom7: String, + patMapTo7: String, +} + +func Lcx_PatLookup(ctx: *LowerCtx, src: String) -> String { + // Most recent rename wins (scan from end) + var i: int = ctx.patMapCount - 1; + while i >= 0 { + var from: String = ""; + var to: String = ""; + if i == 0 { from = ctx.patMapFrom0; to = ctx.patMapTo0; } + else if i == 1 { from = ctx.patMapFrom1; to = ctx.patMapTo1; } + else if i == 2 { from = ctx.patMapFrom2; to = ctx.patMapTo2; } + else if i == 3 { from = ctx.patMapFrom3; to = ctx.patMapTo3; } + else if i == 4 { from = ctx.patMapFrom4; to = ctx.patMapTo4; } + else if i == 5 { from = ctx.patMapFrom5; to = ctx.patMapTo5; } + else if i == 6 { from = ctx.patMapFrom6; to = ctx.patMapTo6; } + else if i == 7 { from = ctx.patMapFrom7; to = ctx.patMapTo7; } + if String_Eq(from, src) { return to; } + i = i - 1; + } + return ""; +} + +func Lcx_PatPush(ctx: *LowerCtx, src: String, dst: String) { + if ctx.patMapCount >= 8 { return; } + let i: int = ctx.patMapCount; + if i == 0 { ctx.patMapFrom0 = src; ctx.patMapTo0 = dst; } + else if i == 1 { ctx.patMapFrom1 = src; ctx.patMapTo1 = dst; } + else if i == 2 { ctx.patMapFrom2 = src; ctx.patMapTo2 = dst; } + else if i == 3 { ctx.patMapFrom3 = src; ctx.patMapTo3 = dst; } + else if i == 4 { ctx.patMapFrom4 = src; ctx.patMapTo4 = dst; } + else if i == 5 { ctx.patMapFrom5 = src; ctx.patMapTo5 = dst; } + else if i == 6 { ctx.patMapFrom6 = src; ctx.patMapTo6 = dst; } + else if i == 7 { ctx.patMapFrom7 = src; ctx.patMapTo7 = dst; } + ctx.patMapCount = ctx.patMapCount + 1; +} + +func Lcx_FreshPatName(ctx: *LowerCtx, src: String) -> String { + ctx.varCounter = ctx.varCounter + 1; + var safe: String = src; + if String_Eq(src, "") || String_Eq(src, "_") { safe = "x"; } + return String_Concat(String_Concat("__p", String_FromInt(ctx.varCounter as int64)), + String_Concat("_", safe)); +} + +// Alloca unique C local + store + scope define + rename map for pattern binding. +func Lcx_BindPatIdent(ctx: *LowerCtx, src: String, ty: String, value: *HirNode, + line: uint32, col: uint32) -> *HirNode { + if String_Eq(src, "") || String_Eq(src, "_") { return null as *HirNode; } + let cName: String = Lcx_FreshPatName(ctx, src); + Lcx_PatPush(ctx, src, cName); + let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + alloca.kind = hAlloca; + alloca.line = line; + alloca.column = col; + alloca.strValue = cName; + alloca.typeName = ty; + let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + store.kind = hStore; + store.line = line; + store.column = col; + let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + v.kind = hVar; + v.strValue = cName; + store.child1 = v; + store.child2 = value; + alloca.child3 = store; + var bsym: Symbol; + bsym.kind = skVar; + bsym.name = src; + bsym.typeKind = tyInt; + bsym.typeName = ty; + bsym.refType = null as *TypeExpr; + bsym.isMutable = false; + bsym.isPublic = false; + bsym.decl = null as *Decl; + discard Scope_Define(ctx.scope, bsym); + return alloca; } // --------------------------------------------------------------------------- @@ -265,6 +360,27 @@ func Lcx_FindGenericStruct(ctx: *LowerCtx, name: String) -> *Decl { return null as *Decl; } +// Extract element type from mangled collection name: Array_int → int, Iter_String → String +func Lcx_ExtractElemFromName(typeName: String) -> String { + if String_Eq(typeName, "") { return ""; } + let len: uint = bux_strlen(typeName); + // "Array_" prefix (6 chars) + if len > 6 { + let p: String = bux_str_slice(typeName, 0, 6); + if String_Eq(p, "Array_") { + return bux_str_slice(typeName, 6, len - 6); + } + } + // "Iter_" prefix (5 chars) + if len > 5 { + let p: String = bux_str_slice(typeName, 0, 5); + if String_Eq(p, "Iter_") { + return bux_str_slice(typeName, 5, len - 5); + } + } + return ""; +} + func Lcx_MangleName(base: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String { let r: String = String_Concat(base, "_"); r = String_Concat(r, typeArg0); @@ -487,6 +603,11 @@ func Lcx_PatternCond(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, if pat == null as *Pattern { return null as *HirNode; } let kind: int = pat.kind; + // Guarded: condition is only the inner pattern; guard applied after binds + if kind == pkGuarded { + return Lcx_PatternCond(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col); + } + if kind == pkWildcard || kind == pkIdent { return null as *HirNode; } @@ -568,40 +689,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, subjectEnumName: String, subjectHasData: bool, line: uint32, col: uint32) -> *HirNode { if pat == null as *Pattern { return null as *HirNode; } + // Guarded: bind from inner pattern + if pat.kind == pkGuarded { + return Lcx_PatternBindings(ctx, subject, pat.patChild1, subjectEnumName, subjectHasData, line, col); + } if pat.kind == pkIdent { - // `_` is wildcard, not a binding - if String_Eq(pat.patIdent, "_") { return null as *HirNode; } let ty: String = "int"; if subject != null as *HirNode && !String_Eq(subject.typeName, "") { ty = subject.typeName; } - let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - alloca.kind = hAlloca; - alloca.line = line; - alloca.column = col; - alloca.strValue = pat.patIdent; - alloca.typeName = ty; - let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - store.kind = hStore; - store.line = line; - store.column = col; - let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - v.kind = hVar; - v.strValue = pat.patIdent; - store.child1 = v; - store.child2 = subject; - alloca.child3 = store; - var bsym: Symbol; - bsym.kind = skVar; - bsym.name = pat.patIdent; - bsym.typeKind = tyInt; - bsym.typeName = ty; - bsym.refType = null as *TypeExpr; - bsym.isMutable = false; - bsym.isPublic = false; - bsym.decl = null as *Decl; - discard Scope_Define(ctx.scope, bsym); - return alloca; + return Lcx_BindPatIdent(ctx, pat.patIdent, ty, subject, line, col); } // Tuple: (a, b) → a = subject._0; b = subject._1 @@ -629,38 +726,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, fLoad.column = col; fLoad.child1 = fPtr; fLoad.typeName = "int"; - let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - alloca.kind = hAlloca; - alloca.line = line; - alloca.column = col; - alloca.strValue = elem.patIdent; - alloca.typeName = "int"; - let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - store.kind = hStore; - store.line = line; - store.column = col; - let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - v.kind = hVar; - v.strValue = elem.patIdent; - store.child1 = v; - store.child2 = fLoad; - alloca.child3 = store; - var bsym: Symbol; - bsym.kind = skVar; - bsym.name = elem.patIdent; - bsym.typeKind = tyInt; - bsym.typeName = "int"; - bsym.refType = null as *TypeExpr; - bsym.isMutable = false; - bsym.isPublic = false; - bsym.decl = null as *Decl; - discard Scope_Define(ctx.scope, bsym); - if head == null as *HirNode { - head = alloca; - tail = store; - } else { - tail.child3 = alloca; - tail = store; + let bound: *HirNode = Lcx_BindPatIdent(ctx, elem.patIdent, "int", fLoad, line, col); + if bound != null as *HirNode { + if head == null as *HirNode { + head = bound; + tail = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } else { + tail.child3 = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } } } elem = elem.patNext; @@ -690,38 +765,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, fLoad.column = col; fLoad.child1 = fPtr; fLoad.typeName = "int"; - let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - alloca.kind = hAlloca; - alloca.line = line; - alloca.column = col; - alloca.strValue = field.patIdent; - alloca.typeName = "int"; - let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - store.kind = hStore; - store.line = line; - store.column = col; - let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - v.kind = hVar; - v.strValue = field.patIdent; - store.child1 = v; - store.child2 = fLoad; - alloca.child3 = store; - var bsym: Symbol; - bsym.kind = skVar; - bsym.name = field.patIdent; - bsym.typeKind = tyInt; - bsym.typeName = "int"; - bsym.refType = null as *TypeExpr; - bsym.isMutable = false; - bsym.isPublic = false; - bsym.decl = null as *Decl; - discard Scope_Define(ctx.scope, bsym); - if head == null as *HirNode { - head = alloca; - tail = store; - } else { - tail.child3 = alloca; - tail = store; + let bound: *HirNode = Lcx_BindPatIdent(ctx, field.patIdent, "int", fLoad, line, col); + if bound != null as *HirNode { + if head == null as *HirNode { + head = bound; + tail = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } else { + tail.child3 = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } } } field = field.patNext; @@ -827,41 +880,16 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, fLoad.typeName = ftype; if arg.kind == pkIdent && !String_Eq(arg.patIdent, "_") { - let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - alloca.kind = hAlloca; - alloca.line = line; - alloca.column = col; - alloca.strValue = arg.patIdent; - alloca.typeName = ftype; - - let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - store.kind = hStore; - store.line = line; - store.column = col; - let bv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - bv.kind = hVar; - bv.strValue = arg.patIdent; - store.child1 = bv; - store.child2 = fLoad; - alloca.child3 = store; - - var bsym: Symbol; - bsym.kind = skVar; - bsym.name = arg.patIdent; - bsym.typeKind = tyInt; - bsym.typeName = ftype; - bsym.refType = null as *TypeExpr; - bsym.isMutable = false; - bsym.isPublic = false; - bsym.decl = null as *Decl; - discard Scope_Define(ctx.scope, bsym); - - if head == null as *HirNode { - head = alloca; - tail = store; - } else { - tail.child3 = alloca; - tail = store; + let bound: *HirNode = Lcx_BindPatIdent(ctx, arg.patIdent, ftype, fLoad, line, col); + if bound != null as *HirNode { + if head == null as *HirNode { + head = bound; + tail = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } else { + tail.child3 = bound; + while tail.child3 != null as *HirNode { tail = tail.child3; } + } } } else if arg.kind == pkTuple || arg.kind == pkStruct || arg.kind == pkEnum { // Nested pattern on payload field @@ -913,7 +941,9 @@ func Lcx_AppendToChain(head: *HirNode, node: *HirNode) { cur.child3 = node; } -// Lower match expr → hBlock: alloca result; if-else stores; strValue = result name +// Lower match expr → sequential ifs with a found flag (no shared HIR DAG). +// Each arm: if (!found) { if (cond) { binds; if (guard) { result=body; found=true; } } } +// Guards see pattern bindings. Supports pkGuarded (`p if cond`). func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { let line: uint32 = expr.line; let col: uint32 = expr.column; @@ -921,6 +951,8 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { ctx.varCounter = ctx.varCounter + 1; let resultName: String = String_Concat("__match_", String_FromInt(ctx.varCounter as int64)); + ctx.varCounter = ctx.varCounter + 1; + let foundName: String = String_Concat("__found_", String_FromInt(ctx.varCounter as int64)); // Result type from sema refType, default int var typeName: String = "int"; @@ -938,7 +970,7 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { } } - // Alloca result + // Alloca result + found flag let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; allocaNode.kind = hAlloca; allocaNode.line = line; @@ -946,35 +978,48 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { allocaNode.strValue = resultName; allocaNode.typeName = typeName; - // Collect arms into a temporary array via reverse build of if-chain - // First count arms and build from last to first - var armCount: int = 0; - var arm: *MatchArm = expr.matchArms; - while arm != null as *MatchArm { - armCount = armCount + 1; - arm = arm.next; - } + let foundAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundAlloca.kind = hAlloca; + foundAlloca.line = line; + foundAlloca.column = col; + foundAlloca.strValue = foundName; + foundAlloca.typeName = "bool"; + allocaNode.child3 = foundAlloca; - // Build if-else from last arm to first - var ifChain: *HirNode = null as *HirNode; - var ai: int = armCount - 1; - while ai >= 0 { - // Find arm at index ai - var cur: *MatchArm = expr.matchArms; - var j: int = 0; - while j < ai && cur != null as *MatchArm { - cur = cur.next; - j = j + 1; - } - if cur == null as *MatchArm { - ai = ai - 1; - continue; - } + let foundInit: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundInit.kind = hStore; + foundInit.line = line; + foundInit.column = col; + let foundVar0: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundVar0.kind = hVar; + foundVar0.strValue = foundName; + foundInit.child1 = foundVar0; + foundInit.child2 = Lcx_MakeLitHir(tkBoolLiteral, "false", line, col); + foundAlloca.child3 = foundInit; - // Pattern bindings before body (so body idents resolve) - let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col); + // Chain of arm ifs (forward order), linked via child3 + var tail: *HirNode = foundInit; + var cur: *MatchArm = expr.matchArms; + while cur != null as *MatchArm { + // Snapshot rename map so this arm's bindings don't leak to later arms + let savedMapCount: int = ctx.patMapCount; + var bindPat: *Pattern = cur.pattern; + var guardExpr: *Expr = null as *Expr; + if cur.pattern != null as *Pattern && cur.pattern.kind == pkGuarded { + bindPat = cur.pattern.patChild1; + guardExpr = cur.pattern.patGuardExpr; + } + // Bindings before guard/body so renames are active and allocas precede uses + let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col); + var guardHirEarly: *HirNode = null as *HirNode; + if guardExpr != null as *Expr { + guardHirEarly = Lcx_LowerExpr(ctx, guardExpr); + } let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body); - // result = body (expand block/match yield: run stmts then store result var) + // Pop this arm's renames (nested matches already restored themselves) + ctx.patMapCount = savedMapCount; + + // store result = body let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; storeNode.kind = hStore; storeNode.line = line; @@ -992,59 +1037,95 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode { storeNode.child2 = bodyHir; } - // armBlock = bindings → body stmts → store - let armBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - armBlock.kind = hBlock; - armBlock.line = line; - armBlock.column = col; - var chainHead: *HirNode = bindHead; - if chainHead == null as *HirNode { - chainHead = bodyPrefix; - } else if bodyPrefix != null as *HirNode { - var bt0: *HirNode = chainHead; - while bt0.child3 != null as *HirNode { bt0 = bt0.child3; } - bt0.child3 = bodyPrefix; - } - if chainHead != null as *HirNode { - armBlock.child1 = chainHead; - var bt: *HirNode = chainHead; - while bt.child3 != null as *HirNode { bt = bt.child3; } - bt.child3 = storeNode; + // found = true + let foundSet: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundSet.kind = hStore; + foundSet.line = line; + foundSet.column = col; + let foundVar1: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundVar1.kind = hVar; + foundVar1.strValue = foundName; + foundSet.child1 = foundVar1; + foundSet.child2 = Lcx_MakeLitHir(tkBoolLiteral, "true", line, col); + storeNode.child3 = foundSet; + + // success block: bodyPrefix → store → found=true + let successBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + successBlock.kind = hBlock; + successBlock.line = line; + successBlock.column = col; + if bodyPrefix != null as *HirNode { + successBlock.child1 = bodyPrefix; + var sbt: *HirNode = bodyPrefix; + while sbt.child3 != null as *HirNode { sbt = sbt.child3; } + sbt.child3 = storeNode; } else { - armBlock.child1 = storeNode; + successBlock.child1 = storeNode; } - let cond: *HirNode = Lcx_PatternCond(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col); - if cond == null as *HirNode { - // Always-true arm - if ifChain == null as *HirNode { - ifChain = armBlock; - } else { - let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - ifNode.kind = hIf; - ifNode.line = line; - ifNode.column = col; - ifNode.child1 = Lcx_MakeTrueHir(line, col); - ifNode.child2 = armBlock; - ifNode.extraData = ifChain as *void; - ifChain = ifNode; - } - } else { - let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; - ifNode.kind = hIf; - ifNode.line = line; - ifNode.column = col; - ifNode.child1 = cond; - ifNode.child2 = armBlock; - ifNode.extraData = ifChain as *void; - ifChain = ifNode; + // Optional guard wraps success (already lowered with renames active) + var afterBinds: *HirNode = successBlock; + if guardHirEarly != null as *HirNode { + let guardIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + guardIf.kind = hIf; + guardIf.line = line; + guardIf.column = col; + guardIf.child1 = guardHirEarly; + guardIf.child2 = successBlock; + afterBinds = guardIf; } - ai = ai - 1; + + // armInner: binds → afterBinds + let armInner: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + armInner.kind = hBlock; + armInner.line = line; + armInner.column = col; + if bindHead != null as *HirNode { + armInner.child1 = bindHead; + var ibt: *HirNode = bindHead; + while ibt.child3 != null as *HirNode { ibt = ibt.child3; } + ibt.child3 = afterBinds; + } else { + armInner.child1 = afterBinds; + } + + // Optional pattern condition + let cond: *HirNode = Lcx_PatternCond(ctx, subject, bindPat, subjectEnumName, subjectHasData, line, col); + var armBody: *HirNode = armInner; + if cond != null as *HirNode { + let condIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + condIf.kind = hIf; + condIf.line = line; + condIf.column = col; + condIf.child1 = cond; + condIf.child2 = armInner; + armBody = condIf; + } + + // if (!found) armBody + let foundLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + foundLoad.kind = hVar; + foundLoad.strValue = foundName; + foundLoad.typeName = "bool"; + let notFound: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + notFound.kind = hUnary; + notFound.line = line; + notFound.column = col; + notFound.intValue = tkBang; + notFound.child1 = foundLoad; + + let tryIf: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + tryIf.kind = hIf; + tryIf.line = line; + tryIf.column = col; + tryIf.child1 = notFound; + tryIf.child2 = armBody; + + tail.child3 = tryIf; + tail = tryIf; + cur = cur.next; } - // Chain: alloca → ifChain - allocaNode.child3 = ifChain; - let block: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; block.kind = hBlock; block.line = line; @@ -1153,6 +1234,18 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { // Identifier → variable reference if kind == ekIdent { + // Pattern binding rename: source name → unique C local + let ren: String = Lcx_PatLookup(ctx, expr.strValue); + if !String_Eq(ren, "") { + n.kind = hVar; + n.strValue = ren; + let rsym: Symbol = Scope_Lookup(ctx.scope, expr.strValue); + n.typeKind = rsym.typeKind; + if rsym.typeName != null as String && !String_Eq(rsym.typeName, "") { + n.typeName = rsym.typeName; + } + return n; + } // Capture rewriting: if inside closure body and this ident is captured, // emit field access on env instance instead of bare variable if ctx.closureDepth > 0 && ctx.currentClosureExpr != null as *Expr && !String_Eq(ctx.envInstanceName, "") { @@ -1564,18 +1657,65 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { n.kind = hCall; n.strValue = expr.child1.strValue; - // Generic call monomorphization - if expr.child1 != null as *Expr && expr.child1.genericTypeArgCount > 0 { - let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue); - if genDecl != null as *Decl { - var typeArg0: String = expr.child1.genericTypeArg0; - var typeArg1: String = expr.child1.genericTypeArg1; - if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; } - if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; } - if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; } - if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; } - let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, expr.child1.genericTypeArgCount); - n.strValue = mangled; + // Generic call monomorphization (explicit / inferred type args) + if expr.child1 != null as *Expr { + var argc: int = expr.child1.genericTypeArgCount; + var typeArg0: String = expr.child1.genericTypeArg0; + var typeArg1: String = expr.child1.genericTypeArg1; + // Fallback: infer T from first *Array/*Iter arg when sema left count=0 + if argc == 0 { + let genTry: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue); + if genTry != null as *Decl && genTry.typeParamCount > 0 { + if expr.callArgs != null as *ExprList && expr.callArgs.expr != null as *Expr { + var a0: *Expr = expr.callArgs.expr; + var te: *TypeExpr = a0.refType; + if te == null as *TypeExpr && a0.kind == ekUnary && a0.intValue == tkAmp { + if a0.child1 != null as *Expr { te = a0.child1.refType; } + } + if te != null as *TypeExpr { + // Unwrap pointer + if (te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef) + && te.pointerPointee != null as *TypeExpr { + te = te.pointerPointee; + } + var elem: String = ""; + if te.typeArgCount > 0 { + elem = te.typeArgName0; + } else { + // Mangled Array_int / Iter_String + elem = Lcx_ExtractElemFromName(te.typeName); + } + if !String_Eq(elem, "") { + typeArg0 = elem; + argc = 1; + // Second type arg from func-typed second argument if needed + if genTry.typeParamCount >= 2 && expr.callArgs.next != null as *ExprList { + let a1e: *Expr = expr.callArgs.next.expr; + if a1e != null as *Expr && a1e.kind == ekIdent { + let fsym: Symbol = Scope_Lookup(ctx.scope, a1e.strValue); + if fsym.kind == skFunc && fsym.decl != null as *Decl + && fsym.decl.retType != null as *TypeExpr + && fsym.decl.retType.kind == tekNamed { + typeArg1 = fsym.decl.retType.typeName; + argc = 2; + } + } + } + } + } + } + } + } + if argc > 0 { + let genDecl: *Decl = Lcx_FindGenericFunc(ctx, expr.child1.strValue); + if genDecl != null as *Decl { + if String_Eq(typeArg0, ctx.substParam0) { typeArg0 = ctx.substArg0; } + if String_Eq(typeArg0, ctx.substParam1) { typeArg0 = ctx.substArg1; } + if String_Eq(typeArg1, ctx.substParam0) { typeArg1 = ctx.substArg0; } + if String_Eq(typeArg1, ctx.substParam1) { typeArg1 = ctx.substArg1; } + let mangled: String = Lcx_GenerateFuncInstance(ctx, genDecl, typeArg0, typeArg1, argc); + n.strValue = mangled; + } } } @@ -2476,10 +2616,33 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode { // Return if kind == skReturn { - n.kind = hReturn; if stmt.child1 != null as *Expr { - n.child1 = Lcx_LowerExpr(ctx, stmt.child1); + let retVal: *HirNode = Lcx_LowerExpr(ctx, stmt.child1); + // `return match { ... }` / other multi-stmt yields: expand stmts then return result var + if retVal != null as *HirNode && Lcx_IsMatchYield(retVal) { + let retVar: *HirNode = Lcx_YieldVarOf(retVal); + let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + retNode.kind = hReturn; + retNode.line = line; + retNode.column = col; + retNode.child1 = retVar; + retVal.strValue = ""; + var lastIn: *HirNode = retVal.child1; + if lastIn == null as *HirNode { + retVal.child1 = retNode; + } else { + while lastIn.child3 != null as *HirNode { + lastIn = lastIn.child3; + } + lastIn.child3 = retNode; + } + return retVal; + } + n.kind = hReturn; + n.child1 = retVal; + return n; } + n.kind = hReturn; return n; } diff --git a/src/parser.bux b/src/parser.bux index 5f0d2c8..56617ff 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -58,14 +58,27 @@ func parserPeek(p: *Parser, ahead: int) -> int { return tkEndOfFile; } -// Lookahead to determine if '<' starts a type argument list. +// Lookahead to determine if '<' starts a type argument list (`Foo`). +// Must not treat value comparisons `x < 0` as generics when a later `x > 0` +// exists (e.g. multiple match arm guards). func parserIsTypeArgListAhead(p: *Parser) -> bool { if !parserCheck(p, tkLt) { return false; } var depth: int = 0; var ahead: int = 0; while true { let kind: int = parserPeek(p, ahead); - if kind == tkEndOfFile || kind == tkLBrace || kind == tkSemicolon { + // Hard stops: cannot appear inside <...> type args + if kind == tkEndOfFile || kind == tkLBrace || kind == tkRBrace || kind == tkSemicolon + || kind == tkFatArrow || kind == tkIf || kind == tkElse || kind == tkWhile + || kind == tkFor || kind == tkMatch || kind == tkReturn || kind == tkLet || kind == tkVar + || kind == tkEq || kind == tkNe || kind == tkLe || kind == tkGe + || kind == tkAmpAmp || kind == tkPipePipe || kind == tkAssign { + return false; + } + // Literals / arithmetic ⇒ value expression, not type args + if kind == tkIntLiteral || kind == tkFloatLiteral || kind == tkStringLiteral + || kind == tkCharLiteral || kind == tkBoolLiteral + || kind == tkPlus || kind == tkMinus || kind == tkSlash || kind == tkPercent { return false; } if kind == tkLt { @@ -729,6 +742,7 @@ func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern { pat.patChild2 = null as *Pattern; pat.patArgs = null as *Pattern; pat.patNext = null as *Pattern; + pat.patGuardExpr = null as *Expr; return pat; } @@ -904,6 +918,24 @@ func parserParsePattern(p: *Parser) -> *Pattern { pat.patRangeInclusive = inclusive; pat.patChild1 = left; pat.patChild2 = right; + // Range can still take a guard: `1..10 if x % 2 == 0` + if parserCheck(p, tkIf) { + discard parserAdvance(p); + let guard: *Expr = parserParseExpr(p); + let gpat: *Pattern = parserMakePattern(pkGuarded, line, col); + gpat.patChild1 = pat; + gpat.patGuardExpr = guard; + return gpat; + } + return pat; + } + // Guarded pattern: `p if cond` (bindings from p visible in cond) + if parserCheck(p, tkIf) { + discard parserAdvance(p); + let guard: *Expr = parserParseExpr(p); + let pat: *Pattern = parserMakePattern(pkGuarded, line, col); + pat.patChild1 = left; + pat.patGuardExpr = guard; return pat; } return left; diff --git a/src/sema.bux b/src/sema.bux index 84489df..6a1460f 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -388,6 +388,11 @@ func Sema_AddCapture(closureExpr: *Expr, name: String, typeKind: int) { // Enum payloads: Option::Some(value) → value:int (from variant field type). func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) { if pat == null as *Pattern { return; } + // Guarded: bind from inner pattern only (`p if cond`) + if pat.kind == pkGuarded { + Sema_BindPattern(sema, pat.patChild1, subject); + return; + } if pat.kind == pkIdent { var sym: Symbol; Sema_ZeroInitSymbol(&sym); @@ -807,8 +812,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { ai = ai + 1; } } - // Trait bounds checking for explicit generic calls: Max(...) - // Must happen before indirect/direct call returns + // Trait bounds + inference for generic calls: Max / Iter_Map / Array_Push if expr.child1.kind == ekIdent { let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue); if sym.kind == skFunc && sym.decl != null as *Decl { @@ -831,13 +835,21 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { return tyVoid; } } - // Direct call to named function + // Direct call to named function — substitute return type with inferred args if expr.child1.kind == ekIdent { let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue); if sym.kind == skFunc && sym.decl != null as *Decl { if sym.decl.retType != null as *TypeExpr { - expr.refType = sym.decl.retType; - return Sema_ResolveType(sema, sym.decl.retType); + var retTe: *TypeExpr = sym.decl.retType; + // Substitute type params in return type when we inferred args + if expr.child1.genericTypeArgCount > 0 { + retTe = Sema_SubstTypeExpr(sym.decl.retType, + sym.decl.typeParam0, expr.child1.genericTypeArg0, + sym.decl.typeParam1, expr.child1.genericTypeArg1, + expr.child1.genericTypeArgCount); + } + expr.refType = retTe; + return Sema_ResolveType(sema, retTe); } } } @@ -851,6 +863,10 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { // Cast — return target type if kind == ekCast { + // Must type-check the operand (enables generic inference on nested calls) + if expr.child1 != null as *Expr { + discard Sema_CheckExpr(sema, expr.child1); + } if expr.refType != null as *TypeExpr { return Sema_ResolveType(sema, expr.refType); } @@ -990,6 +1006,16 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { let savedScope: *Scope = sema.scope; sema.scope = &armScope; Sema_BindPattern(sema, arm.pattern, expr.child1); + // Type-check `p if guard` (must be bool; sees pattern bindings) + if arm.pattern != null as *Pattern && arm.pattern.kind == pkGuarded { + if arm.pattern.patGuardExpr != null as *Expr { + let gt: int = Sema_CheckExpr(sema, arm.pattern.patGuardExpr); + if gt != tyBool && gt != tyUnknown { + Sema_EmitError(sema, arm.pattern.line, arm.pattern.column, + "match guard condition must be bool"); + } + } + } let bt: int = Sema_CheckExpr(sema, arm.body); sema.scope = savedScope; if first { @@ -1677,19 +1703,213 @@ func Sema_ExtractElemType(te: *TypeExpr) -> String { return ""; } -// Infer generic type argument from call arguments. -// Supports Array_* and Iter_* stdlib functions. +// Substitute type params in a TypeExpr (shallow clone). Used for call return types +// after inference: Array + U=String → Array with typeArgName0=String / Array_String. +func Sema_SubstTypeExpr(te: *TypeExpr, p0: String, a0: String, p1: String, a1: String, argc: int) -> *TypeExpr { + if te == null as *TypeExpr { return null as *TypeExpr; } + let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + r.kind = te.kind; + r.line = te.line; + r.column = te.column; + r.typeName = te.typeName; + r.pathStr = te.pathStr; + r.pathCount = te.pathCount; + r.typeArgName0 = te.typeArgName0; + r.typeArgName1 = te.typeArgName1; + r.typeArgCount = te.typeArgCount; + r.sliceElement = te.sliceElement; + r.pointerPointee = te.pointerPointee; + r.funcParams = te.funcParams; + r.funcRet = te.funcRet; + r.funcParamCount = te.funcParamCount; + r.tupleElems = te.tupleElems; + r.tupleCount = te.tupleCount; + + if te.kind == tekNamed { + // Bare type param → concrete named type + if te.typeArgCount == 0 { + if argc >= 1 && String_Eq(te.typeName, p0) { + r.typeName = a0; + return r; + } + if argc >= 2 && String_Eq(te.typeName, p1) { + r.typeName = a1; + return r; + } + } + // Named with type args: Array → Array_String (mangled) for downstream mono + if te.typeArgCount > 0 { + var na0: String = te.typeArgName0; + var na1: String = te.typeArgName1; + if argc >= 1 && String_Eq(na0, p0) { na0 = a0; } + if argc >= 2 && String_Eq(na0, p1) { na0 = a1; } + if argc >= 1 && String_Eq(na1, p0) { na1 = a0; } + if argc >= 2 && String_Eq(na1, p1) { na1 = a1; } + r.typeArgName0 = na0; + r.typeArgName1 = na1; + // Mangle for monomorphized struct name (Array_int, Iter_String) + if te.typeArgCount == 1 && !String_Eq(na0, "") { + r.typeName = String_Concat(String_Concat(te.typeName, "_"), na0); + r.typeArgCount = 0; + r.typeArgName0 = ""; + } else if te.typeArgCount >= 2 { + r.typeName = String_Concat(String_Concat(te.typeName, "_"), + String_Concat(na0, String_Concat("_", na1))); + r.typeArgCount = 0; + r.typeArgName0 = ""; + r.typeArgName1 = ""; + } + return r; + } + } + if te.kind == tekPointer || te.kind == tekRef || te.kind == tekMutRef { + r.pointerPointee = Sema_SubstTypeExpr(te.pointerPointee, p0, a0, p1, a1, argc); + return r; + } + if te.kind == tekFunc { + r.funcRet = Sema_SubstTypeExpr(te.funcRet, p0, a0, p1, a1, argc); + return r; + } + return r; +} + +// Bind a type param name on the call callee if not already set. +func Sema_BindInferredArg(expr: *Expr, funcDecl: *Decl, tpName: String, typeName: String) { + if String_Eq(tpName, "") || String_Eq(typeName, "") { return; } + if expr.child1 == null as *Expr { return; } + if funcDecl.typeParamCount >= 1 && String_Eq(tpName, funcDecl.typeParam0) { + if String_Eq(expr.child1.genericTypeArg0, "") { + expr.child1.genericTypeArg0 = typeName; + } + if expr.child1.genericTypeArgCount < 1 { expr.child1.genericTypeArgCount = 1; } + } + if funcDecl.typeParamCount >= 2 && String_Eq(tpName, funcDecl.typeParam1) { + if String_Eq(expr.child1.genericTypeArg1, "") { + expr.child1.genericTypeArg1 = typeName; + } + if expr.child1.genericTypeArgCount < 2 { expr.child1.genericTypeArgCount = 2; } + } +} + +// Resolve concrete type name for a value expression (for bare type-param params). +func Sema_ArgTypeName(argExpr: *Expr) -> String { + if argExpr == null as *Expr { return ""; } + var argType: *TypeExpr = argExpr.refType; + if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp { + if argExpr.child1 != null as *Expr { argType = argExpr.child1.refType; } + } + if argType == null as *TypeExpr { return ""; } + if argType.kind == tekNamed { return argType.typeName; } + if argType.kind == tekPointer && argType.pointerPointee != null as *TypeExpr { + if argType.pointerPointee.kind == tekNamed { + return argType.pointerPointee.typeName; + } + } + return ""; +} + +// Infer type args from a param TypeExpr pattern against a concrete arg TypeExpr. +func Sema_UnifyInfer(expr: *Expr, funcDecl: *Decl, pattern: *TypeExpr, concrete: *TypeExpr) { + if pattern == null as *TypeExpr || concrete == null as *TypeExpr { return; } + + // Bare type param: T / Acc / U + if pattern.kind == tekNamed && pattern.typeArgCount == 0 { + if String_Eq(pattern.typeName, funcDecl.typeParam0) || String_Eq(pattern.typeName, funcDecl.typeParam1) { + var cn: String = ""; + if concrete.kind == tekNamed { cn = concrete.typeName; } + Sema_BindInferredArg(expr, funcDecl, pattern.typeName, cn); + return; + } + } + + // *T / &T — unwrap pointer/ref on both sides + if pattern.kind == tekPointer || pattern.kind == tekRef || pattern.kind == tekMutRef { + var conc: *TypeExpr = concrete; + if concrete.kind == tekPointer || concrete.kind == tekRef || concrete.kind == tekMutRef { + conc = concrete.pointerPointee; + } + Sema_UnifyInfer(expr, funcDecl, pattern.pointerPointee, conc); + return; + } + + // Named with type args: Iter, Array, Map + if pattern.kind == tekNamed && pattern.typeArgCount > 0 { + // concrete may be Iter with typeArgName0, or mangled Iter_int + if concrete.kind == tekNamed { + if concrete.typeArgCount > 0 { + if pattern.typeArgCount >= 1 && !String_Eq(pattern.typeArgName0, "") { + let te0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te0.kind = tekNamed; + te0.typeName = pattern.typeArgName0; + te0.typeArgCount = 0; + let ce0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + ce0.kind = tekNamed; + ce0.typeName = concrete.typeArgName0; + ce0.typeArgCount = 0; + Sema_UnifyInfer(expr, funcDecl, te0, ce0); + } + if pattern.typeArgCount >= 2 && !String_Eq(pattern.typeArgName1, "") { + let te1: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te1.kind = tekNamed; + te1.typeName = pattern.typeArgName1; + te1.typeArgCount = 0; + let ce1: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + ce1.kind = tekNamed; + ce1.typeName = concrete.typeArgName1; + ce1.typeArgCount = 0; + Sema_UnifyInfer(expr, funcDecl, te1, ce1); + } + } else { + // Mangled Array_int / Iter_String + let elem: String = Sema_ExtractElemType(concrete); + if !String_Eq(elem, "") && pattern.typeArgCount >= 1 { + let te0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te0.kind = tekNamed; + te0.typeName = pattern.typeArgName0; + te0.typeArgCount = 0; + let ce0: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + ce0.kind = tekNamed; + ce0.typeName = elem; + ce0.typeArgCount = 0; + Sema_UnifyInfer(expr, funcDecl, te0, ce0); + } + } + } + return; + } + + // func(T)->U vs concrete function type + if pattern.kind == tekFunc { + var conc: *TypeExpr = concrete; + // Named function used as value: build tekFunc from its decl if needed — refType may already be tekFunc + if conc.kind == tekFunc { + // Params + var pp: *TypeExprList = pattern.funcParams; + var cp: *TypeExprList = conc.funcParams; + while pp != null as *TypeExprList && cp != null as *TypeExprList { + Sema_UnifyInfer(expr, funcDecl, pp.te, cp.te); + pp = pp.next; + cp = cp.next; + } + if pattern.funcRet != null as *TypeExpr && conc.funcRet != null as *TypeExpr { + Sema_UnifyInfer(expr, funcDecl, pattern.funcRet, conc.funcRet); + } + } + return; + } +} + +// Infer generic type arguments from call arguments (structural). +// Handles *Array, *Iter, func(T)->U, bare Acc, etc. func Sema_InferGenericArgs(sema: *Sema, funcDecl: *Decl, expr: *Expr) { if expr.callArgs == null as *ExprList { return; } + if expr.child1 == null as *Expr { return; } var argList: *ExprList = expr.callArgs; var pi: int = 0; while argList != null as *ExprList && pi < funcDecl.paramCount { let argExpr: *Expr = argList.expr; if argExpr == null as *Expr { argList = argList.next; pi = pi + 1; continue; } - var argType: *TypeExpr = argExpr.refType; - if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp { - argType = argExpr.child1.refType; - } + var paramType: *TypeExpr = null as *TypeExpr; if pi == 0 { paramType = funcDecl.param0.refParamType; } else if pi == 1 { paramType = funcDecl.param1.refParamType; } @@ -1701,20 +1921,29 @@ func Sema_InferGenericArgs(sema: *Sema, funcDecl: *Decl, expr: *Expr) { else if pi == 7 { paramType = funcDecl.param7.refParamType; } else if pi == 8 { paramType = funcDecl.param8.refParamType; } - if paramType != null as *TypeExpr && paramType.kind == tekNamed && argType != null as *TypeExpr { - let inferred: String = Sema_ExtractElemType(argType); - var typeName: String = inferred; - if String_Eq(typeName, "") && argType.kind == tekNamed { - typeName = argType.typeName; + var argType: *TypeExpr = argExpr.refType; + // &x → use type of x, wrap as pointer if pattern expects pointer + if argType == null as *TypeExpr && argExpr.kind == ekUnary && argExpr.intValue == tkAmp { + if argExpr.child1 != null as *Expr { + argType = argExpr.child1.refType; } - if !String_Eq(typeName, "") { - if funcDecl.typeParamCount >= 1 && String_Eq(paramType.typeName, funcDecl.typeParam0) && String_Eq(expr.child1.genericTypeArg0, "") { - expr.child1.genericTypeArg0 = typeName; - if expr.child1.genericTypeArgCount < 1 { expr.child1.genericTypeArgCount = 1; } - } - if funcDecl.typeParamCount >= 2 && String_Eq(paramType.typeName, funcDecl.typeParam1) && String_Eq(expr.child1.genericTypeArg1, "") { - expr.child1.genericTypeArg1 = typeName; - if expr.child1.genericTypeArgCount < 2 { expr.child1.genericTypeArgCount = 2; } + } + // Named function as value: synthesize tekFunc from its declaration + if (argType == null as *TypeExpr || argType.kind != tekFunc) && argExpr.kind == ekIdent { + let fsym: Symbol = Scope_Lookup(sema.scope, argExpr.strValue); + if fsym.kind == skFunc && fsym.decl != null as *Decl { + argType = Sema_BuildFuncTypeExprFromDecl(fsym.decl); + } + } + + if paramType != null as *TypeExpr && argType != null as *TypeExpr { + Sema_UnifyInfer(expr, funcDecl, paramType, argType); + // If pattern is *T and arg is bare T (from &x we unwrapped), re-wrap + if (paramType.kind == tekPointer || paramType.kind == tekRef || paramType.kind == tekMutRef) + && argExpr.kind == ekUnary && argExpr.intValue == tkAmp { + // already handled via unwrap of pattern against pointee type of variable + if argExpr.child1 != null as *Expr && argExpr.child1.refType != null as *TypeExpr { + Sema_UnifyInfer(expr, funcDecl, paramType.pointerPointee, argExpr.child1.refType); } } } diff --git a/tests/borrow_test.nim b/tests/borrow_test.nim index d1c1786..49cd120 100644 --- a/tests/borrow_test.nim +++ b/tests/borrow_test.nim @@ -195,6 +195,46 @@ func Main() -> int { """) check(not res.hasErrors) + test "@[Checked] rejects two let-bound &mut of same var": + let res = checkSource(""" +@[Checked] +func Main() -> int { + var x: int = 1; + let a: &mut int = &x; + let b: &mut int = &x; + return 0; +} +""") + check(res.hasErrors) + check(res.diagnostics[0].message.contains("already mutably borrowed")) + + test "@[Checked] rejects assign while mutably borrowed": + let res = checkSource(""" +@[Checked] +func Main() -> int { + var x: int = 1; + let a: &mut int = &x; + x = 2; + return *a; +} +""") + check(res.hasErrors) + check(res.diagnostics[0].message.contains("mutably borrowed")) + + test "@[Checked] rejects shared borrow while mutably borrowed": + let res = checkSource(""" +@[Checked] +func Main() -> int { + var x: int = 1; + let a: &mut int = &x; + let b: &int = &x; + return 0; +} +""") + check(res.hasErrors) + check(res.diagnostics[0].message.contains("shared-borrow") or + res.diagnostics[0].message.contains("mutably borrowed")) + test "borrow & expr with shared ref": let res = checkSource(""" struct Point { diff --git a/tests/error_golden/exclusive_mut_let/bux.toml b/tests/error_golden/exclusive_mut_let/bux.toml new file mode 100644 index 0000000..ee47475 --- /dev/null +++ b/tests/error_golden/exclusive_mut_let/bux.toml @@ -0,0 +1,6 @@ +[Package] +Name = "exclusive_mut_let" +Version = "0.1.0" +Type = "bin" +[Build] +Output = "Bin" diff --git a/tests/error_golden/exclusive_mut_let/expected.err b/tests/error_golden/exclusive_mut_let/expected.err new file mode 100644 index 0000000..64d866c --- /dev/null +++ b/tests/error_golden/exclusive_mut_let/expected.err @@ -0,0 +1,7 @@ +error: type errors in project +error: cannot mutably borrow 'x': already mutably borrowed + --> FILE:5:23 + | + 5 | let b: &mut int = &x; + | ^ + = help: only one active '&mut' borrow is allowed at a time; end the borrow before reuse diff --git a/tests/error_golden/exclusive_mut_let/src/Main.bux b/tests/error_golden/exclusive_mut_let/src/Main.bux new file mode 100644 index 0000000..12b224b --- /dev/null +++ b/tests/error_golden/exclusive_mut_let/src/Main.bux @@ -0,0 +1,7 @@ +@[Checked] +func Main() -> int { + var x: int = 1; + let a: &mut int = &x; + let b: &mut int = &x; + return 0; +} diff --git a/tests/error_golden/use_while_mut_borrow/bux.toml b/tests/error_golden/use_while_mut_borrow/bux.toml new file mode 100644 index 0000000..5c6f02a --- /dev/null +++ b/tests/error_golden/use_while_mut_borrow/bux.toml @@ -0,0 +1,6 @@ +[Package] +Name = "use_while_mut_borrow" +Version = "0.1.0" +Type = "bin" +[Build] +Output = "Bin" diff --git a/tests/error_golden/use_while_mut_borrow/expected.err b/tests/error_golden/use_while_mut_borrow/expected.err new file mode 100644 index 0000000..5fcccde --- /dev/null +++ b/tests/error_golden/use_while_mut_borrow/expected.err @@ -0,0 +1,7 @@ +error: type errors in project +error: cannot assign to 'x' while it is mutably borrowed + --> FILE:5:5 + | + 5 | x = 2; + | ^ + = help: only one active '&mut' borrow is allowed at a time; end the borrow before reuse diff --git a/tests/error_golden/use_while_mut_borrow/src/Main.bux b/tests/error_golden/use_while_mut_borrow/src/Main.bux new file mode 100644 index 0000000..d236552 --- /dev/null +++ b/tests/error_golden/use_while_mut_borrow/src/Main.bux @@ -0,0 +1,7 @@ +@[Checked] +func Main() -> int { + var x: int = 1; + let a: &mut int = &x; + x = 2; + return *a; +} diff --git a/tools/lsp_server.nim b/tools/lsp_server.nim index 508014f..74abf20 100644 --- a/tools/lsp_server.nim +++ b/tools/lsp_server.nim @@ -3,8 +3,12 @@ # # Usage: bux-lsp # The editor spawns this binary and communicates via stdin/stdout. +# +# Hover uses real bootstrap sema types when possible (globals + stdlib); +# completion/outline still use a fast lightweight scan. -import std/[json, os, strutils, streams, tables, osproc] +import std/[json, os, strutils, streams, tables, osproc, sequtils] +import lexer, parser, ast, sema, types, scope, source_location # --------------------------------------------------------------------------- # JSON-RPC Transport @@ -83,18 +87,25 @@ type kind: string ## function | variable | struct | enum | … detail: string ## signature / type annotation container: string ## optional parent (module / type) + fromSema: bool ## detail came from real type checker DocumentState = ref object uri: string content: string version: int symbols: Table[string, SymbolInfo] ordered: seq[string] ## declaration order for outline + ## Full-project type index for hover (includes stdlib after sema enrich) + typeIndex: Table[string, string] ## name → type / signature string + kindIndex: Table[string, string] ## name → kind label var documents = initTable[string, DocumentState]() rootPath = "" rootUri = "" workspaceSymbols = initTable[string, tuple[uri: string, info: SymbolInfo]]() + cachedStdlibDir = "" + cachedStdlibDecls: seq[Decl] = @[] + stdlibLoaded = false proc getDoc(uri: string): DocumentState = if not documents.hasKey(uri): @@ -326,6 +337,313 @@ proc analyzeFile(path: string, content: string): DocumentState = if not matchedTypeKw: inc i +# --------------------------------------------------------------------------- +# Real sema types for hover +# --------------------------------------------------------------------------- + +proc typeExprToStr(te: TypeExpr): string = + if te == nil: return "?" + case te.kind + of tekNamed: + result = te.typeName + if te.typeArgs.len > 0: + result &= "<" & te.typeArgs.mapIt(typeExprToStr(it)).join(", ") & ">" + of tekPath: + result = te.pathSegments.join("::") + of tekPointer: + result = "*" & typeExprToStr(te.pointerPointee) + of tekOwn: + result = "own " & typeExprToStr(te.pointerPointee) + of tekRef: + result = "&" & typeExprToStr(te.pointerPointee) + of tekMutRef: + result = "&mut " & typeExprToStr(te.pointerPointee) + of tekSlice: + result = typeExprToStr(te.sliceElement) & "[]" + of tekTuple: + result = "(" & te.tupleElements.mapIt(typeExprToStr(it)).join(", ") & ")" + of tekFunc: + let ps = te.funcParams.mapIt(typeExprToStr(it)).join(", ") + let ret = if te.funcRet != nil: typeExprToStr(te.funcRet) else: "void" + result = "func(" & ps & ") -> " & ret + of tekSelf: + result = "self" + of tekDynRef: + result = "&dyn " & te.dynInterface + +proc formatFuncDetail(name: string, decl: Decl): string = + if decl == nil or decl.kind != dkFunc: + return "func " & name + var parts: seq[string] = @[] + for p in decl.declFuncParams: + var s = p.name + if p.ptype != nil: + s &= ": " & typeExprToStr(p.ptype) + parts.add(s) + result = "func " & name & "(" & parts.join(", ") & ")" + if decl.declFuncTypeParams.len > 0: + let tps = decl.declFuncTypeParams.mapIt(it.name).join(", ") + result = "func " & name & "<" & tps & ">(" & parts.join(", ") & ")" + if decl.declFuncReturnType != nil: + result &= " -> " & typeExprToStr(decl.declFuncReturnType) + +proc symbolKindFromSema(sk: SymbolKind): string = + case sk + of skFunc: "function" + of skVar: "variable" + of skConst: "constant" + of skType: "type" + of skModule: "module" + +proc findStdlibDirLocal(root: string): string = + if root.len == 0: return "" + let candidates = @[ + root / "lib", + root / ".." / "lib", + getAppDir() / "lib", + getAppDir() / ".." / "lib", + getCurrentDir() / "lib" + ] + for c in candidates: + if dirExists(c): + return c.absolutePath + # Walk up from root looking for lib/ + var cur = root.absolutePath + for _ in 0 .. 6: + let lib = cur / "lib" + if dirExists(lib): return lib + let parent = cur.parentDir + if parent == cur: break + cur = parent + return "" + +proc loadStdlibDecls(stdlibDir: string): seq[Decl] = + result = @[] + if stdlibDir.len == 0 or not dirExists(stdlibDir): + return + for path in walkDirRec(stdlibDir): + if not path.endsWith(".bux"): continue + try: + let source = readFile(path) + let lexRes = tokenize(source, path) + if lexRes.hasErrors: continue + let parseRes = parse(lexRes.tokens, path) + if parseRes.diagnostics.len > 0: continue + for item in parseRes.module.items: + if item.kind == dkModule: + for sub in item.declModuleItems: + result.add(sub) + else: + result.add(item) + except: + discard + +proc ensureStdlibCached() = + if stdlibLoaded: return + stdlibLoaded = true + cachedStdlibDir = findStdlibDirLocal(rootPath) + if cachedStdlibDir.len == 0: + # try from open document path later + return + cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir) + +proc enrichWithSema(doc: DocumentState) = + ## Run real bootstrap sema (file + stdlib) and fill typeIndex + upgrade symbols. + if doc.content.len == 0: return + let path = uriToPath(doc.uri) + ensureStdlibCached() + if cachedStdlibDecls.len == 0 and rootPath.len == 0: + # Try stdlib relative to the file + let tryRoot = path.parentDir.parentDir # …/src/Main.bux → package + cachedStdlibDir = findStdlibDirLocal(tryRoot) + if cachedStdlibDir.len > 0: + cachedStdlibDecls = loadStdlibDecls(cachedStdlibDir) + + try: + let lexRes = tokenize(doc.content, path) + if lexRes.hasErrors: + return + let parseRes = parse(lexRes.tokens, path) + # Build unified module: stdlib first, then this file + var unified = newModule("lsp") + for d in cachedStdlibDecls: + unified.items.add(d) + for d in parseRes.module.items: + if d.kind == dkModule: + for sub in d.declModuleItems: + unified.items.add(sub) + else: + unified.items.add(d) + + let (semaRes, semaCtx) = analyzeFull(unified) + discard semaRes # diagnostics already published via buxc + + doc.typeIndex = initTable[string, string]() + doc.kindIndex = initTable[string, string]() + + # Index entire global scope for hover (includes stdlib) + if semaCtx.globalScope != nil: + for name, sym in semaCtx.globalScope.table.pairs: + if name.len == 0: continue + var detail = "" + var kind = symbolKindFromSema(sym.kind) + if sym.kind == skFunc and sym.decl != nil and sym.decl.kind == dkFunc: + detail = formatFuncDetail(name, sym.decl) + elif sym.typ != nil and not sym.typ.isUnknown: + detail = name & ": " & sym.typ.toString + if sym.kind == skFunc: + detail = sym.typ.toString # already "func(...) -> T" + if not detail.startsWith("func"): + detail = "func " & name & " — " & detail + else: + # inject name: func name(...) + detail = detail.replace("func(", "func " & name & "(") + elif sym.kind == skConst: + detail = "const " & name & ": " & sym.typ.toString + elif sym.kind == skType: + detail = "type " & name + kind = "type" + else: + detail = (if sym.isMutable: "var " else: "let ") & name & ": " & sym.typ.toString + elif sym.decl != nil: + case sym.decl.kind + of dkStruct: + detail = "struct " & name + kind = "struct" + of dkEnum: + detail = "enum " & name + kind = "enum" + of dkUnion: + detail = "union " & name + kind = "struct" + of dkInterface: + detail = "interface " & name + kind = "interface" + of dkTypeAlias: + detail = "type " & name + if sym.decl.declAliasType != nil: + detail &= " = " & typeExprToStr(sym.decl.declAliasType) + kind = "type" + of dkFunc: + detail = formatFuncDetail(name, sym.decl) + kind = "function" + else: + detail = name + else: + detail = name + + doc.typeIndex[name] = detail + doc.kindIndex[name] = kind + + # Upgrade file-local symbols (already found by lightweight scan) + if doc.symbols.hasKey(name): + var info = doc.symbols[name] + info.detail = detail + info.kind = kind + info.fromSema = true + doc.symbols[name] = info + + # Prefer THIS file's declarations over stdlib when names collide + # (e.g. user `Max` vs lib/Math `Max(int64,int64)`). + for d in parseRes.module.items: + proc indexDecl(dd: Decl) = + case dd.kind + of dkFunc: + let n = dd.declFuncName + if n.len == 0: return + let detail = formatFuncDetail(n, dd) + doc.typeIndex[n] = detail + doc.kindIndex[n] = "function" + if doc.symbols.hasKey(n): + var info = doc.symbols[n] + info.detail = detail + info.kind = "function" + info.fromSema = true + doc.symbols[n] = info + else: + let line = max(0, int(dd.loc.line) - 1) + let col = max(0, int(dd.loc.column) - 1) + doc.symbols[n] = SymbolInfo( + line: line, col: col, kind: "function", detail: detail, + container: "", fromSema: true) + if n notin doc.ordered: doc.ordered.add(n) + of dkStruct: + let n = dd.declStructName + doc.typeIndex[n] = "struct " & n + doc.kindIndex[n] = "struct" + of dkEnum: + let n = dd.declEnumName + doc.typeIndex[n] = "enum " & n + doc.kindIndex[n] = "enum" + of dkTypeAlias: + let n = dd.declAliasName + var detail = "type " & n + if dd.declAliasType != nil: + detail &= " = " & typeExprToStr(dd.declAliasType) + doc.typeIndex[n] = detail + doc.kindIndex[n] = "type" + else: + discard + if d.kind == dkModule: + for sub in d.declModuleItems: + indexDecl(sub) + else: + indexDecl(d) + + # Walk this file's AST for local lets with explicit types (function bodies) + proc walkBlock(blk: Block, container: string) = + if blk == nil: return + for stmt in blk.stmts: + case stmt.kind + of skLet: + let n = stmt.stmtLetName + if n.len == 0: continue + var typStr = "" + if stmt.stmtLetType != nil: + typStr = typeExprToStr(stmt.stmtLetType) + let kw = if stmt.stmtLetMut: "var" else: "let" + let detail = if typStr.len > 0: kw & " " & n & ": " & typStr else: kw & " " & n + let loc = stmt.loc + let line = max(0, int(loc.line) - 1) + let col = max(0, int(loc.column) - 1) + # Prefer sema-enriched detail if name already global; else add local + if not doc.symbols.hasKey(n) or not doc.symbols[n].fromSema: + doc.symbols[n] = SymbolInfo( + line: line, col: col, kind: "variable", detail: detail, + container: container, fromSema: typStr.len > 0) + if n notin doc.ordered: + doc.ordered.add(n) + if typStr.len > 0: + doc.typeIndex[n] = detail + doc.kindIndex[n] = "variable" + of skExpr: + if stmt.stmtExpr != nil and stmt.stmtExpr.kind == ekBlock: + walkBlock(stmt.stmtExpr.exprBlock, container) + of skIf: + walkBlock(stmt.stmtIfThen, container) + walkBlock(stmt.stmtIfElse, container) + for br in stmt.stmtIfElseIfs: + walkBlock(br.blk, container) + of skWhile: + walkBlock(stmt.stmtWhileBody, container) + of skFor: + walkBlock(stmt.stmtForBody, container) + of skLoop: + walkBlock(stmt.stmtLoopBody, container) + else: + discard + + for d in parseRes.module.items: + if d.kind == dkFunc and d.declFuncBody != nil: + walkBlock(d.declFuncBody, d.declFuncName) + elif d.kind == dkModule: + for sub in d.declModuleItems: + if sub.kind == dkFunc and sub.declFuncBody != nil: + walkBlock(sub.declFuncBody, sub.declFuncName) + + except: + discard # sema failures must not crash the LSP + # --------------------------------------------------------------------------- # Diagnostics — run `buxc check` when available and parse Rust-style errors # --------------------------------------------------------------------------- @@ -470,6 +788,8 @@ proc analyzeAndPublishDiagnostics(stream: FileStream, doc: DocumentState) = let updated = analyzeFile(path, doc.content) doc.symbols = updated.symbols doc.ordered = updated.ordered + # Keep / refresh real types for hover (does not replace lightweight outline) + enrichWithSema(doc) let diags = runBuxcDiagnostics(path, doc.content) publishDiagnostics(stream, doc.uri, diags) @@ -628,7 +948,7 @@ proc handleDefinition(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = # --------------------------------------------------------------------------- proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = - ## Hover with accurate range for the word under the cursor. + ## Hover with accurate range; prefer real sema types when available. let uri = paramsNode["textDocument"]["uri"].getStr() let position = paramsNode["position"] let lineNum = position["line"].getInt() @@ -640,6 +960,10 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = return ensureAnalyzed(doc) + # Lazy sema enrich on first hover if not yet run (e.g. only didChange so far) + if doc.typeIndex.len == 0 and doc.content.len > 0: + enrichWithSema(doc) + let lines = doc.content.split("\n") if lineNum >= lines.len: sendResponse(stream, id, newJNull()) @@ -656,19 +980,41 @@ proc handleHover(stream: FileStream, id: JsonNode, paramsNode: JsonNode) = return let word = l[start ..< endC] - var info: SymbolInfo + var detail = "" + var kind = "" var found = false + + # Prefer file-local symbol (may be sema-upgraded) if doc.symbols.hasKey(word): - info = doc.symbols[word] + let info = doc.symbols[word] + detail = info.detail + kind = info.kind + found = true + # Prefer pure sema typeIndex when richer + if doc.typeIndex.hasKey(word) and doc.typeIndex[word].len >= detail.len: + detail = doc.typeIndex[word] + if doc.kindIndex.hasKey(word): + kind = doc.kindIndex[word] + elif doc.typeIndex.hasKey(word): + detail = doc.typeIndex[word] + kind = if doc.kindIndex.hasKey(word): doc.kindIndex[word] else: "symbol" found = true elif workspaceSymbols.hasKey(word): - info = workspaceSymbols[word].info + let info = workspaceSymbols[word].info + detail = info.detail + kind = info.kind found = true + if not found: sendResponse(stream, id, newJNull()) return - let md = "```bux\n" & info.detail & "\n```\n\n_" & info.kind & "_" + var md = "```bux\n" & detail & "\n```\n\n_" & kind & "_" + if doc.symbols.hasKey(word) and doc.symbols[word].fromSema: + md &= " · sema" + elif doc.typeIndex.hasKey(word): + md &= " · sema" + sendResponse(stream, id, %*{ "contents": {"kind": "markdown", "value": md}, "range": { @@ -742,7 +1088,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = "hoverProvider": true, "documentSymbolProvider": true }, - "serverInfo": {"name": "bux-lsp", "version": "0.2.0"} + "serverInfo": {"name": "bux-lsp", "version": "0.3.0"} }) if paramsNode.hasKey("rootPath") and paramsNode["rootPath"].kind != JNull: rootPath = paramsNode["rootPath"].getStr() @@ -750,10 +1096,14 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = rootUri = paramsNode["rootUri"].getStr() if rootPath.len == 0: rootPath = uriToPath(rootUri) + # Preload stdlib for hover types + if rootPath.len > 0: + ensureStdlibCached() of "initialized": if rootPath.len > 0: scanWorkspace(rootPath) + ensureStdlibCached() of "shutdown": sendResponse(stream, id, %*{}) @@ -769,6 +1119,7 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = doc.content = content if td.hasKey("version"): doc.version = td["version"].getInt() + # Lightweight scan + sema enrich + buxc diagnostics analyzeAndPublishDiagnostics(stream, doc) of "textDocument/didChange": @@ -780,16 +1131,25 @@ proc handleMessage(stream: FileStream, msg: JsonNode) = doc.content = changes[changes.len - 1]["text"].getStr() if td.hasKey("version"): doc.version = td["version"].getInt() - # Refresh symbols immediately (no buxc — diagnostics on save) + # Fast path: lightweight symbols only; keep previous typeIndex until save/hover refresh let updated = analyzeFile(uriToPath(uri), doc.content) doc.symbols = updated.symbols doc.ordered = updated.ordered + # Re-apply typeIndex details onto matching names (don't drop sema types mid-edit) + for name, detail in doc.typeIndex.pairs: + if doc.symbols.hasKey(name): + var info = doc.symbols[name] + info.detail = detail + info.fromSema = true + if doc.kindIndex.hasKey(name): + info.kind = doc.kindIndex[name] + doc.symbols[name] = info of "textDocument/didSave": let td = paramsNode["textDocument"] let uri = td["uri"].getStr() - let doc = getDoc(uri) - analyzeAndPublishDiagnostics(stream, doc) + discard getDoc(uri) + analyzeAndPublishDiagnostics(stream, getDoc(uri)) of "textDocument/completion": handleCompletion(stream, id, paramsNode)