feat: try/unwrap payload types, LSP format, macro paste, freestanding runtime
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

- Type `?`/`!` as Result/Option Ok payload (not always int); fix unwrap C types
- LSP 0.18 document formatting (bux fmt) + VS Code format-on-save
- Macro `:type` generics (Array_New<$t>) and operators-only tt paste
- Ship runtime_freestanding.c + BUX_RUNTIME=freestanding + smokes/examples
This commit is contained in:
2026-07-28 16:56:35 +03:00
parent db7ba1dff2
commit ec5984762b
22 changed files with 1332 additions and 68 deletions
+11 -6
View File
@@ -14,9 +14,10 @@ type
## Which C runtime shim to link (session 75 — Linux / cloud / embedded).
RuntimeFlavor* = enum
rfFull ## rt/runtime.c — POSIX + OpenSSL
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly
rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
rfFull ## rt/runtime.c — POSIX + OpenSSL
rfMinimal ## rt/runtime_minimal.c — thin, static/container/embed friendly
rfFreestanding ## rt/runtime_freestanding.c — no-libc research spike
rfWin ## rt/runtime_win.c — Windows/MinGW (historical)
GlobalOptions* = object
color*: ColorMode
@@ -63,7 +64,7 @@ Registry / toolchain env:
BUX_REGISTRY_INSECURE=1 Allow self-signed HTTPS registry (dev/smoke)
BUX_CFLAGS Extra flags appended to the C compiler line
BUX_CC C compiler binary (overrides --target pick)
BUX_RUNTIME full|minimal|thin|embed|win (default: full on Unix)
BUX_RUNTIME full|minimal|thin|embed|freestanding|win (default: full on Unix)
BUX_STATIC=1 Same as --static
Global options:
@@ -136,8 +137,10 @@ proc resolveRuntimeFlavor(opts: GlobalOptions): RuntimeFlavor =
case e
of "full", "posix":
return rfFull
of "minimal", "thin", "embed", "embedded", "freestanding":
of "minimal", "thin", "embed", "embedded":
return rfMinimal
of "freestanding", "bare", "nolibc":
return rfFreestanding
of "win", "windows":
return rfWin
of "":
@@ -159,10 +162,12 @@ proc runtimeFileName(flavor: RuntimeFlavor): string =
case flavor
of rfFull: "runtime.c"
of rfMinimal: "runtime_minimal.c"
of rfFreestanding: "runtime_freestanding.c"
of rfWin: "runtime_win.c"
proc isThinRuntime(flavor: RuntimeFlavor): bool =
flavor in {rfMinimal, rfWin}
## Thin = no OpenSSL / pthread link needs
flavor in {rfMinimal, rfFreestanding, rfWin}
proc findOnPath(bin: string): bool =
## True if `bin` resolves as an executable on PATH (or is an absolute path).
+117 -15
View File
@@ -2101,6 +2101,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
of ekTry:
let operand = ctx.lowerExpr(expr.exprTryOperand)
var operandType = ctx.resolveExprType(expr.exprTryOperand)
# Keep original type args for Ok payload (before mangling to Result_T_E)
let payloadArgs: seq[Type] =
if operandType != nil and operandType.kind == tkNamed: operandType.inner
else: @[]
var typeName = ""
var errTag = ""
@@ -2112,7 +2116,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
# Upgrade bare generic enum name to concrete monomorphization.
# Sema stores Result/Option without mangled type-args; try needs Result_int_String_Tag.
if ctx.genericEnums.hasKey(typeName):
if ctx.genericEnums.hasKey(typeName) or typeName == "Result" or typeName == "Option":
# Prefer resolving call/ident TypeExpr with type args
if expr.exprTryOperand != nil:
if expr.exprTryOperand.kind == ekIdent and ctx.varTypeExprs.hasKey(expr.exprTryOperand.exprIdent):
@@ -2131,14 +2135,21 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
(resolved.name == typeName or resolved.name.startsWith(typeName & "_")):
typeName = resolved.name
# Enclosing function return type (must match for `?` propagation)
let stillBare = operandType == nil or operandType.kind != tkNamed or
typeName == operandType.name
var stillBare = typeName == "Result" or typeName == "Option"
if stillBare and ctx.currentFuncRetType != nil and
ctx.currentFuncRetType.kind == tkNamed:
let rn = ctx.currentFuncRetType.name
if rn.startsWith(typeName & "_"):
typeName = rn
operandType = makeNamed(typeName)
stillBare = false
# Prefer mangled name from type args: Result + [String,String] → Result_String_String
if stillBare and payloadArgs.len > 0 and
(typeName == "Result" or typeName == "Option"):
var mangled = typeName
for a in payloadArgs:
mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p")
typeName = mangled
operandType = Type(kind: tkNamed, name: typeName, inner: payloadArgs)
# Err tag / Ok field from base or concrete name
let baseForTags =
@@ -2157,6 +2168,41 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
errTag = typeName & "_Err"
okField = "Ok_0"
# Payload type (Ok_0 / Some_0) — must match T of Result<T,E>, not always int
var okType = makeInt()
if payloadArgs.len >= 1:
okType = payloadArgs[0]
else:
var enumSym = ctx.globalScope.lookup(typeName)
var enumDecl: Decl = nil
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
enumDecl = enumSym.decl
elif ctx.structInstMap.hasKey(typeName):
let (baseName, _) = ctx.structInstMap[typeName]
let baseSym = ctx.globalScope.lookup(baseName)
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkEnum:
enumDecl = baseSym.decl
elif typeName.startsWith("Result_"):
let baseSym = ctx.globalScope.lookup("Result")
if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl
elif typeName.startsWith("Option_"):
let baseSym = ctx.globalScope.lookup("Option")
if baseSym != nil and baseSym.decl != nil: enumDecl = baseSym.decl
if enumDecl != nil:
var subst = initTable[string, Type]()
if ctx.structInstMap.hasKey(typeName):
var ti = 0
for tp in enumDecl.declEnumTypeParams:
if ti < ctx.structInstMap[typeName][1].len:
subst[tp.name] = ctx.structInstMap[typeName][1][ti]
inc ti
for variant in enumDecl.declEnumVariants:
for i, f in variant.fields:
let fieldName = variant.name & "_" & $i
if fieldName == okField:
okType = substituteType(ctx, f, subst)
break
let tmpName = ctx.freshTryVar()
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
let tmpVar = hirVar(tmpName, operandType, loc)
@@ -2179,8 +2225,8 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
typ: makeNamed(typeName & "_Data"), loc: loc)
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
typ: makePointer(makeInt()), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc)
typ: makePointer(okType), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
ctx.pendingStmts.add(tmpAlloca)
ctx.pendingStmts.add(tmpStore)
@@ -2193,14 +2239,66 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
var errTag = "Result_Err"
var typeName = "Result"
if operandType.kind == tkNamed:
var okField = "Ok_0"
if operandType != nil and operandType.kind == tkNamed:
typeName = operandType.name
if typeName == "Option":
errTag = "Option_None"
if typeName == "Option" or typeName.startsWith("Option_"):
errTag = if typeName == "Option": "Option_None" else: typeName & "_None"
okField = "Some_0"
elif typeName == "Result":
errTag = "Result_Err"
else:
errTag = typeName & "_Err"
# Mangle Result + type args when still bare
if operandType != nil and operandType.kind == tkNamed and operandType.inner.len > 0 and
(typeName == "Result" or typeName == "Option"):
var mangled = typeName
for a in operandType.inner:
mangled = mangled & "_" & a.toString.replace(" ", "").replace("*", "p")
typeName = mangled
if typeName.startsWith("Option_"):
errTag = typeName & "_None"
okField = "Some_0"
else:
errTag = typeName & "_Err"
okField = "Ok_0"
var okType = makeInt()
if operandType != nil and operandType.kind == tkNamed and operandType.inner.len >= 1:
okType = operandType.inner[0]
else:
var enumDecl: Decl = nil
let enumSym = ctx.globalScope.lookup(
if typeName.startsWith("Result_"): "Result"
elif typeName.startsWith("Option_"): "Option"
else: typeName)
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
enumDecl = enumSym.decl
if enumDecl != nil:
var subst = initTable[string, Type]()
if ctx.structInstMap.hasKey(typeName):
var ti = 0
for tp in enumDecl.declEnumTypeParams:
if ti < ctx.structInstMap[typeName][1].len:
subst[tp.name] = ctx.structInstMap[typeName][1][ti]
inc ti
elif operandType != nil and operandType.kind == tkNamed:
var ti = 0
for tp in enumDecl.declEnumTypeParams:
if ti < operandType.inner.len:
subst[tp.name] = operandType.inner[ti]
inc ti
for variant in enumDecl.declEnumVariants:
for i, f in variant.fields:
if variant.name & "_" & $i == okField:
okType = substituteType(ctx, f, subst)
# Same shape as `?`: stack temporary of the Result/Option value (not a pointer).
# Typing the var as *Result made C emit `tmp->tag` on a value (invalid).
let tmpName = ctx.freshTryVar()
let tmpAlloca = hirAlloca(tmpName, operandType, loc)
let tmpVar = hirVar(tmpName, makePointer(operandType), loc)
let tmpVar = hirVar(tmpName, operandType, loc)
let tmpStore = hirStore(tmpVar, operand, loc)
let tagPtr = HirNode(kind: hFieldPtr, fieldPtrBase: tmpVar, fieldName: "tag",
@@ -2210,11 +2308,15 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
let errConst = hirVar(errTag, makeNamed(typeName & "_Tag"), loc)
let cond = hirBinary(tkEq, tagLoad, errConst, makeBool(), loc)
# On error: call bux_panic("unwrap failed")
# On error: call bux_panic("unwrap failed") then exit (do not continue with garbage)
let panicTok = Token(kind: tkStringLiteral, text: "\"unwrap failed\"", loc: loc)
let panicMsg = HirNode(kind: hLit, litToken: panicTok, typ: makeStr(), loc: loc)
let panicCall = hirCall("bux_panic", @[panicMsg], makeVoid(), loc)
let thenBlock = hirBlock(@[panicCall], nil, makeVoid(), loc)
let exitLit = HirNode(kind: hLit,
litToken: Token(kind: tkIntLiteral, text: "1", loc: loc),
typ: makeInt(), loc: loc)
let exitCall = hirCall("bux_exit", @[exitLit], makeVoid(), loc)
let thenBlock = hirBlock(@[panicCall, exitCall], nil, makeVoid(), loc)
let ifNode = HirNode(kind: hIf, ifCond: cond, ifThen: thenBlock,
ifElse: nil, typ: makeVoid(), loc: loc)
@@ -2223,9 +2325,9 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode =
typ: makePointer(makeNamed(typeName & "_Data")), loc: loc)
let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr,
typ: makeNamed(typeName & "_Data"), loc: loc)
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: "Ok_0",
typ: makePointer(makeInt()), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: makeInt(), loc: loc)
let okPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: okField,
typ: makePointer(okType), loc: loc)
let okLoad = HirNode(kind: hLoad, loadPtr: okPtr, typ: okType, loc: loc)
ctx.pendingStmts.add(tmpAlloca)
ctx.pendingStmts.add(tmpStore)
+71 -3
View File
@@ -17,6 +17,17 @@ type
proc emitErr(res: var MacroExpandResult, loc: SourceLocation, msg: string) =
res.diagnostics.add(MacroDiagnostic(loc: loc, message: msg))
proc isBinaryPasteOp*(k: TokenKind): bool =
## Operators allowed as `$op:tt` paste into `$op($a, $b)` → `a OP b`.
case k
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar,
tkAmp, tkPipe, tkCaret, tkShl, tkShr,
tkAmpAmp, tkPipePipe,
tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
true
else:
false
# ---------------------------------------------------------------------------
# Deep clone (bootstrap has no Ast_Clone*)
# ---------------------------------------------------------------------------
@@ -152,9 +163,12 @@ proc cloneExpr*(e: Expr): Expr =
for a in e.exprCallArgs:
result.exprCallArgs.add(cloneExpr(a))
of ekGenericCall:
var gtas: seq[TypeExpr] = @[]
for ta in e.exprGenericTypeArgs:
gtas.add(cloneTypeExpr(ta))
result = Expr(kind: ekGenericCall, loc: e.loc,
exprGenericCallee: e.exprGenericCallee,
exprGenericTypeArgs: e.exprGenericTypeArgs)
exprGenericTypeArgs: gtas)
of ekIndex:
result = Expr(kind: ekIndex, loc: e.loc,
exprIndexObj: cloneExpr(e.exprIndexObj),
@@ -165,9 +179,12 @@ proc cloneExpr*(e: Expr): Expr =
exprFieldObj: cloneExpr(e.exprFieldObj),
exprFieldName: e.exprFieldName)
of ekStructInit:
var stas: seq[TypeExpr] = @[]
for ta in e.exprStructInitTypeArgs:
stas.add(cloneTypeExpr(ta))
result = Expr(kind: ekStructInit, loc: e.loc,
exprStructInitName: e.exprStructInitName,
exprStructInitTypeArgs: e.exprStructInitTypeArgs,
exprStructInitTypeArgs: stas,
exprStructInitFields: @[])
for f in e.exprStructInitFields:
result.exprStructInitFields.add((f.name, cloneExpr(f.value)))
@@ -895,6 +912,12 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
of ekRange:
c.exprRangeLo = substExpr(c.exprRangeLo, env, callLoc)
c.exprRangeHi = substExpr(c.exprRangeHi, env, callLoc)
of ekGenericCall:
# Array_New<$t>(…) / Foo<$t, $u> — substitute type fragments in type args
var gtas: seq[TypeExpr] = @[]
for ta in c.exprGenericTypeArgs:
gtas.add(substType(ta, env, callLoc))
c.exprGenericTypeArgs = gtas
of ekCall:
c.exprCallCallee = substExpr(c.exprCallCallee, env, callLoc)
var args: seq[Expr] = @[]
@@ -950,12 +973,37 @@ proc substExpr(e: Expr, env: MacroEnv, callLoc: SourceLocation): Expr =
argNames.add("")
c.exprCallArgs = args
c.exprCallArgNames = argNames
# Operators-only paste: `$op($a, $b)` where `$op:tt` is a bound binary operator
# → rebuild as `$a OP $b` (post-1.0).
if c.exprCallCallee != nil and c.exprCallCallee.kind == ekMacroTt and
c.exprCallCallee.exprMacroTtInner != nil and
c.exprCallCallee.exprMacroTtInner.kind == ekLiteral and
c.exprCallArgs.len == 2:
let opTok = c.exprCallCallee.exprMacroTtInner.exprLit
if opTok.kind.isBinaryPasteOp:
result = Expr(kind: ekBinary, loc: callLoc,
exprBinaryOp: opTok.kind,
exprBinaryLeft: c.exprCallArgs[0],
exprBinaryRight: c.exprCallArgs[1])
return
# Callee already unwrapped to op literal by value-position MacroTt splice
if c.exprCallCallee != nil and c.exprCallCallee.kind == ekLiteral and
c.exprCallArgs.len == 2 and c.exprCallCallee.exprLit.kind.isBinaryPasteOp:
result = Expr(kind: ekBinary, loc: callLoc,
exprBinaryOp: c.exprCallCallee.exprLit.kind,
exprBinaryLeft: c.exprCallArgs[0],
exprBinaryRight: c.exprCallArgs[1])
return
of ekIndex:
c.exprIndexObj = substExpr(c.exprIndexObj, env, callLoc)
c.exprIndexIdx = substExpr(c.exprIndexIdx, env, callLoc)
of ekField:
c.exprFieldObj = substExpr(c.exprFieldObj, env, callLoc)
of ekStructInit:
var stas: seq[TypeExpr] = @[]
for ta in c.exprStructInitTypeArgs:
stas.add(substType(ta, env, callLoc))
c.exprStructInitTypeArgs = stas
var fields: seq[tuple[name: string, value: Expr]] = @[]
for f in c.exprStructInitFields:
fields.add((f.name, substExpr(f.value, env, callLoc)))
@@ -1247,6 +1295,25 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
exprMacroTtInner: inner, exprMacroTtGroup: true)
result = @[callee, group]
## Operators-only juxta: single binary arg `1 + 2` matches
## `$a:expr, $op:tt, $b:expr` (or juxta without commas).
proc juxtaBinarySplit(rule: MacroRule, inArgs: seq[Expr]): seq[Expr] =
result = inArgs
if inArgs.len != 1 or inArgs[0] == nil: return
if inArgs[0].kind != ekBinary: return
if rule.frags.len != 3: return
if rule.frags[0].isRep or rule.frags[1].isRep or rule.frags[2].isRep: return
let k0 = if rule.frags[0].kinds.len > 0: rule.frags[0].kinds[0] else: rule.frags[0].kind
let k1 = if rule.frags[1].kinds.len > 0: rule.frags[1].kinds[0] else: rule.frags[1].kind
let k2 = if rule.frags[2].kinds.len > 0: rule.frags[2].kinds[0] else: rule.frags[2].kind
if k0 != mfkExpr or k1 != mfkTt or k2 != mfkExpr: return
if not inArgs[0].exprBinaryOp.isBinaryPasteOp: return
let opTok = Token(kind: inArgs[0].exprBinaryOp, text: "", loc: inArgs[0].loc)
let lit = Expr(kind: ekLiteral, loc: inArgs[0].loc, exprLit: opTok)
let opTt = Expr(kind: ekMacroTt, loc: inArgs[0].loc,
exprMacroTtInner: lit, exprMacroTtGroup: false)
result = @[inArgs[0].exprBinaryLeft, opTt, inArgs[0].exprBinaryRight]
var matched: MacroRule
var env: MacroEnv
var found = false
@@ -1257,7 +1324,8 @@ proc expandOneCall(call: Expr, macros: Table[string, Decl],
var gi = 0
var ai = 0
let useGroups = nReps > 1 and groups.len > 1
let flat = juxtaCallSplit(rule, args)
var flat = juxtaCallSplit(rule, args)
flat = juxtaBinarySplit(rule, flat)
for frag in rule.frags:
if failed: break
+26
View File
@@ -434,10 +434,22 @@ proc isMacroStmtStart(p: Parser): bool =
p.peek() in {tkLet, tkVar, tkIf, tkWhile, tkFor, tkLoop, tkMatch, tkReturn,
tkBreak, tkContinue, tkDefer, tkSwitch, tkDo}
proc isBinaryPasteOpToken(k: TokenKind): bool =
## Same set as macroexpand.isBinaryPasteOp — kept local to avoid cycles.
case k
of tkPlus, tkMinus, tkStar, tkSlash, tkPercent, tkStarStar,
tkAmp, tkPipe, tkCaret, tkShl, tkShr,
tkAmpAmp, tkPipePipe,
tkEq, tkNe, tkLt, tkLe, tkGt, tkGe:
true
else:
false
proc parseMacroArg(p: var Parser): Expr =
## Macro call argument:
## - statement keywords → ekMacroStmt
## - `_` / pattern-only starts → ekMacroPat (also `$p:pat` from expr via coerce)
## - bare binary operators (`+`, `*`, `==`, …) → ekMacroTt (operators-only paste)
## - else expression
let loc = p.currentLoc
if p.isMacroStmtStart():
@@ -447,6 +459,20 @@ proc parseMacroArg(p: var Parser): Expr =
if p.check(tkUnderscore):
let pat = p.parsePattern()
return Expr(kind: ekMacroPat, loc: loc, exprMacroPat: pat)
# Operators-only tt: `apply_op!(+, 1, 2)` / `apply_op!(*, 2, 3)`.
# Bare ops are not primary exprs. Unary-capable tokens (`*`, `-`, …) are only
# claimed when the next token ends the arg (`,`, `)`, `;`) so `*int` still
# parses as a type/expr and juxta `a * b` still works as ekBinary.
let pk = p.peek()
if isBinaryPasteOpToken(pk):
let nxt = p.peek(1)
let endsArg = nxt in {tkComma, tkRParen, tkSemicolon, tkEndOfFile, tkNewLine}
let unaryCapable = pk in {tkStar, tkMinus, tkBang, tkAmp, tkTilde,
tkPlusPlus, tkMinusMinus}
if endsArg or not unaryCapable:
let tok = p.advance()
let lit = Expr(kind: ekLiteral, loc: loc, exprLit: tok)
return Expr(kind: ekMacroTt, loc: loc, exprMacroTtInner: lit, exprMacroTtGroup: false)
p.parseExpr()
proc parseMacroRepExpr(p: var Parser): Expr =
+57 -7
View File
@@ -1078,6 +1078,58 @@ proc collectGlobals*(sema: var Sema) =
proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type
proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type
proc isResultOrOptionName(name: string): bool =
name == "Result" or name.startsWith("Result_") or
name == "Option" or name.startsWith("Option_")
proc extractResultOptionPayload*(sema: var Sema, opTy: Type, loc: SourceLocation, opKind: string): Type =
## Payload type of `Result`/`Option` for `?` (try) and `!` (unwrap).
## Prefer type-args (`Result<T,E>` → T); fall back to Ok/Some field on the enum decl.
if opTy == nil or opTy.isUnknown:
return makeUnknown()
if opTy.kind != tkNamed:
sema.emitError(loc, opKind & " requires Result or Option operand")
return makeUnknown()
let name = opTy.name
if not isResultOrOptionName(name):
sema.emitError(loc, opKind & " requires Result or Option, got " & opTy.toString)
return makeUnknown()
# Result<T,E> / Option<T> store payload in .inner
if opTy.inner.len >= 1:
return opTy.inner[0]
# Bare / monomorphized name: look up Ok(T) / Some(T) on the enum decl
var enumSym = sema.globalScope.lookup(name)
if (enumSym == nil or enumSym.decl == nil or enumSym.decl.kind != dkEnum):
if name.startsWith("Result_"):
enumSym = sema.globalScope.lookup("Result")
elif name.startsWith("Option_"):
enumSym = sema.globalScope.lookup("Option")
let wantVariant =
if name == "Option" or name.startsWith("Option_"): "Some"
else: "Ok"
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
var subst = initTable[string, Type]()
# Result_String_String → try to bind type params from mangled suffix when possible
if opTy.inner.len == 0 and enumSym.decl.declEnumTypeParams.len > 0 and
(name.startsWith("Result_") or name.startsWith("Option_")):
let prefix = if name.startsWith("Result_"): "Result_" else: "Option_"
let rest = name[prefix.len .. ^1]
# Split on '_' is imperfect for nested types; for simple T_E it works
let parts = rest.split('_')
var pi = 0
for tp in enumSym.decl.declEnumTypeParams:
if pi < parts.len and parts[pi].len > 0:
# Re-resolve simple type names (int, String, …)
let te = TypeExpr(kind: tekNamed, typeName: parts[pi])
subst[tp.name] = sema.resolveType(te)
inc pi
for variant in enumSym.decl.declEnumVariants:
if variant.name == wantVariant and variant.fields.len > 0:
let raw = sema.resolveType(variant.fields[0])
return sema.substituteTypeInType(raw, subst)
# Unknown payload — don't invent int (breaks String Results)
return makeUnknown()
proc typeImplements(sema: Sema, t: Type, interfaceName: string): bool =
## Check if a type implements an interface by verifying all required methods exist.
if t.isUnknown: return true
@@ -1838,14 +1890,12 @@ proc checkExpr*(sema: var Sema, expr: Expr, scope: Scope): Type =
discard sema.checkExpr(expr.exprIsOperand, scope)
return makeBool()
of ekTry:
discard sema.checkExpr(expr.exprTryOperand, scope)
# For now, assume Result<int, String> -> int
# TODO: check operand is Result/Option and current function returns same type
return makeInt()
let opTy = sema.checkExpr(expr.exprTryOperand, scope)
# Payload of Result/Option; validates operand is Result or Option
return sema.extractResultOptionPayload(opTy, expr.loc, "try operator (`?`)")
of ekUnwrap:
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
# Unwrap: extract Ok value or panic on Err
return makeInt()
let opTy = sema.checkExpr(expr.exprUnwrapOperand, scope)
return sema.extractResultOptionPayload(opTy, expr.loc, "unwrap operator (`!`)")
of ekBlock:
var blockScope = newScope(scope)
var lastType = makeVoid()