feat(nexus): production modular HTTP/1.1 server + compiler fixes

- Rewrite apps/nexus with modular architecture:
  Config, Http, Errors, Parser, Router, Handlers, Server, Main
- Use algebraic enums for ParseResult/FileResult/HttpError
- Thread-pool server via Channel<ConnectionTask> and spawn
- Fix C backend type ordering for generic struct instances
  (Array_T, Iter_T) and algebraic enum struct payloads
- Collect Slice_T types from struct fields and enum payloads
- Fix match lowering for simple enums (direct value compare)
- Resolve match expression return type from first arm
- Infer element type for for-in over Array<UserStruct>
- Preserve generic type args in field access resolution
- Add fflush to PrintLine/Print for immediate server logs
- Add modern_features golden regression test
- Regenerate golden expected.c files
This commit is contained in:
2026-06-15 00:54:03 +03:00
parent fc0a560e60
commit aaeb01e518
40 changed files with 28530 additions and 21790 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
import std/[os, strutils, terminal, strformat, osproc, sets]
import lexer, parser, ast, sema, manifest, hir_lower, lir, lir_lower, lir_c_backend
import lexer, parser, ast, sema, manifest, hir_lower, lir_lower, lir_c_backend
type
ColorMode* = enum
+66 -16
View File
@@ -42,22 +42,37 @@ proc flushPending(ctx: var LowerCtx, node: HirNode): HirNode =
return hirBlock(stmts, nil, makeVoid(), node.loc)
return node
proc enumHasDataVariants(ctx: var LowerCtx, enumName: string): bool =
let sym = ctx.globalScope.lookup(enumName)
if sym != nil and sym.decl != nil and sym.decl.kind == dkEnum:
for v in sym.decl.declEnumVariants:
if v.fields.len > 0 or v.namedFields.len > 0:
return true
return false
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.
# For now, supports enum tag matching and wildcard/ident fallbacks.
let resultName = ctx.freshName()
var stmts: seq[HirNode] = @[]
# Allocate result variable
stmts.add(hirAlloca(resultName, typ, 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)
# 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 body = arm.body
case arm.pattern.kind
of pkEnum:
let path = arm.pattern.patEnumPath
@@ -65,19 +80,25 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
let enumName = path[0]
let variantName = path[^1]
let tagName = enumName & "_" & variantName
# condition: subject.tag == EnumName_VariantName
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
let cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
var cond: HirNode
if subjectHasData and enumName == subjectEnumName:
# Algebraic enum: compare subject.tag
let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag",
typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc)
let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc)
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc)
cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc)
else:
# Simple enum or cross-enum match: compare subject directly
let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName), loc)
cond = hirBinary(tkEq, subject, tagConst, makeBool(), loc)
# body: result = arm_body
var armStmts: seq[HirNode] = @[]
armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc))
let armBlock = hirBlock(armStmts, nil, makeVoid(), loc)
if ifChain == nil:
ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil,
typ: makeVoid(), loc: loc)
@@ -115,9 +136,9 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ
ifChain = HirNode(kind: hIf,
ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc),
ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc)
stmts.add(ifChain)
# Return the result variable as the block expression
return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc)
@@ -397,6 +418,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if f.ftype != nil:
case f.ftype.kind
of tekNamed:
if f.ftype.typeArgs.len > 0:
return substituteType(ctx, f.ftype, subst)
case f.ftype.typeName
of "int", "int32", "int64": return makeInt()
of "float64": return makeFloat64()
@@ -417,6 +440,8 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if f.ftype != nil:
case f.ftype.kind
of tekNamed:
if f.ftype.typeArgs.len > 0:
return ctx.resolveTypeExpr(f.ftype)
case f.ftype.typeName
of "int", "int32", "int64": return makeInt()
of "float64": return makeFloat64()
@@ -478,6 +503,10 @@ proc resolveExprType(ctx: var LowerCtx, expr: Expr): Type =
if baseType.isPointer and baseType.inner.len > 0:
return baseType.inner[0]
return makeUnknown()
of ekMatch:
if expr.exprMatchArms.len > 0:
return ctx.resolveExprType(expr.exprMatchArms[0].body)
return makeUnknown()
of ekBlock:
if expr.exprBlock.stmts.len > 0:
let last = expr.exprBlock.stmts[^1]
@@ -524,6 +553,23 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
return te.typeArgs[0]
if te.kind in {tekPointer, tekRef, tekMutRef} and te.pointerPointee.kind == tekNamed and te.pointerPointee.typeArgs.len > 0:
return te.pointerPointee.typeArgs[0]
of ekField:
# Try to resolve the field's declared TypeExpr directly.
let objType = ctx.resolveExprType(expr.exprFieldObj)
if objType.kind == tkNamed:
var decl = ctx.globalScope.lookup(objType.name).decl
if decl == nil and ctx.structInstMap.hasKey(objType.name):
let (baseName, _) = ctx.structInstMap[objType.name]
let baseSym = ctx.globalScope.lookup(baseName)
if baseSym != nil and baseSym.decl != nil and baseSym.decl.kind == dkStruct:
decl = baseSym.decl
if decl != nil and decl.kind == dkStruct:
for f in decl.declStructFields:
if f.name == expr.exprFieldName and f.ftype != nil:
let fte = f.ftype
if fte.kind == tekNamed and fte.typeArgs.len > 0 and (fte.typeName == "Array" or fte.typeName == "Iter" or fte.typeName == "Channel"):
return fte.typeArgs[0]
return fte
else:
discard
let t = ctx.resolveExprType(expr)
@@ -531,6 +577,11 @@ proc getCollectionElementTypeExpr(ctx: var LowerCtx, expr: Expr): TypeExpr =
return typeToTypeExpr(t.inner[0])
if t.isPointer and t.inner.len > 0 and t.inner[0].kind == tkNamed and t.inner[0].inner.len > 0:
return typeToTypeExpr(t.inner[0].inner[0])
# Generic struct instances (e.g. Array_HeaderEntry) store their type args in structInstMap.
if t.kind == tkNamed and ctx.structInstMap.hasKey(t.name):
let (baseName, concreteArgs) = ctx.structInstMap[t.name]
if concreteArgs.len > 0 and (baseName == "Array" or baseName == "Iter" or baseName == "Channel"):
return typeToTypeExpr(concreteArgs[0])
return TypeExpr(kind: tekNamed, typeName: "unknown")
proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs: seq[TypeExpr]): string
@@ -1660,7 +1711,6 @@ proc generateMethodInstance(ctx: var LowerCtx, baseMethodName: string, typeArgs:
return mangledName
proc lowerClosureFunc(ctx: var LowerCtx, expr: Expr): HirFunc =
let loc = expr.loc
let name = "__closure_" & $ctx.varCounter
inc ctx.varCounter
var f = HirFunc(name: name, isPublic: false)
-22
View File
@@ -61,12 +61,6 @@ proc advance(lex: var Lexer): char =
else:
inc lex.col
proc match(lex: var Lexer, expected: char): bool =
if lex.isAtEnd(): return false
if lex.peek() != expected: return false
discard lex.advance()
return true
proc matchStr(lex: var Lexer, s: string): bool =
for i, c in s:
if lex.peek(i) != c:
@@ -81,9 +75,6 @@ proc currentLocation(lex: Lexer): SourceLocation =
proc emitError(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsError, loc: loc, message: message))
proc emitWarning(lex: var Lexer, loc: SourceLocation, message: string) =
lex.diagnostics.add(LexerDiagnostic(severity: ldsWarning, loc: loc, message: message))
proc makeToken(lex: Lexer, kind: TokenKind, startLoc: SourceLocation, startPos: int): Token =
let text = lex.source[startPos ..< lex.pos]
result = Token(kind: kind, text: text, loc: startLoc)
@@ -337,19 +328,6 @@ proc scanSymbol(lex: var Lexer, startLoc: SourceLocation): Token =
else:
return lex.makeToken(kind1, startLoc, startPos)
template check3(c2: char, kind2: TokenKind, c3: char, kind3: TokenKind, kind1: TokenKind) =
if lex.peek() == c2:
discard lex.advance()
if lex.peek() == c3:
discard lex.advance()
return lex.makeToken(kind3, startLoc, startPos)
return lex.makeToken(kind2, startLoc, startPos)
else:
return lex.makeToken(kind1, startLoc, startPos)
template checkEq(c2: char, kind2: TokenKind, kind1: TokenKind) =
check2(c2, kind2, kind1)
case c1
of '(': return lex.makeToken(tkLParen, startLoc, startPos)
of ')': return lex.makeToken(tkRParen, startLoc, startPos)
-2
View File
@@ -2,8 +2,6 @@
## Linear 3-address code IR, designed for straightforward C emission.
## Each HIR construct lowers to 5-30 LIR instructions.
import types
type
LirKind* = enum
# ── Data movement ──
+119 -44
View File
@@ -17,9 +17,6 @@ proc initLirCBackend*(): LirCBackend =
tempTypes: initTable[string, string](),
)
proc emit(be: var LirCBackend, s: string) =
be.output.add(s)
proc emitIndent(be: var LirCBackend) =
for i in 0 ..< be.indent:
be.output.add(" ")
@@ -43,21 +40,6 @@ proc valToC(be: var LirCBackend, v: LirValue): string =
of lvkField: v.strVal
of lvkType: v.strVal
proc typeFromValue(be: var LirCBackend, v: LirValue): string =
## Infer a C type for a value. Temps are tracked; named vars use lookup.
case v.kind
of lvkTemp:
if be.tempTypes.hasKey(v.strVal):
return be.tempTypes[v.strVal]
return "int" # Default
of lvkString: return "const char*"
of lvkInt: return "int"
of lvkFloat: return "double"
else: return ""
proc setTempType(be: var LirCBackend, temp: string, cType: string) =
be.tempTypes[temp] = cType
proc cParamDecl(cType, name: string): string =
## Emit a C parameter declaration, handling function-pointer syntax.
if cType.contains("(*)"):
@@ -487,6 +469,26 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant
be.emitLine(&"}} {name};")
be.emitLine("")
# ── Type dependency ordering ──
proc collectValueDeps(typ: Type): seq[string] =
## Return type names that must be fully defined before a value of `typ`
## can be declared. Pointers/refs only need a forward declaration, so
## they do not introduce a dependency.
if typ == nil: return @[]
case typ.kind
of tkNamed:
return @[typ.name]
of tkSlice:
return @[typeToCStr(typ)]
of tkPointer, tkRef, tkMutRef, tkTuple, tkFunc:
return @[]
else:
return @[]
proc emitSliceTypeDef(be: var LirCBackend, name: string, elem: string) =
be.emitLine(&"typedef struct {{ {elem}* data; size_t len; }} {name};")
# ── Module emission ──
proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): string =
@@ -557,36 +559,109 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
else: discard
be.emitLine("")
# Enum definitions
# Collect local type names (structs and enums defined in this module).
var localTypeNames: HashSet[string]
for s in module.structs:
localTypeNames.incl(s.name)
for e in module.enums:
be.emitEnumDef(e.name, e.variants)
if module.enums.len > 0:
be.emitLine("")
localTypeNames.incl(e.name)
# Struct definitions
for s in module.structs:
be.emitStructDef(s.name, s.fields)
# Slice types (collect from functions/structs)
# Simple: scan function params/returns for slice types
# Collect slice types used in struct fields and enum payloads.
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
var structNames: HashSet[string]
var sliceNames: HashSet[string]
proc registerSlice(t: Type) =
if t == nil or t.kind != tkSlice: return
let name = typeToCStr(t)
if sliceNames.contains(name): return
sliceNames.incl(name)
let elem = if t.inner.len > 0: typeToCStr(t.inner[0]) else: "void"
sliceTypes.add((name, elem))
for s in module.structs:
structNames.incl(s.name)
for f in module.funcs:
for p in f.params:
var ct = typeToCStr(p.typ)
# Strip pointer/reference suffix to find the base slice type.
while ct.endsWith("*"):
ct = ct[0..^2]
if ct.startsWith("Slice_"):
let elem = ct[6 .. ^1]
if not sliceTypes.anyIt(it.name == ct) and not structNames.contains(ct):
sliceTypes.add((ct, elem))
if sliceTypes.len > 0:
for st in sliceTypes:
be.emitLine(&"typedef struct {{ {st.elem}* data; size_t len; }} {st.name};")
be.emitLine("")
for f in s.fields:
registerSlice(f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
registerSlice(ft)
for nf in v.namedFields:
registerSlice(nf.typ)
# Build dependency graph among structs, enums, and slice types.
# Edge A -> B means "A depends on B, so B must be emitted before A".
var deps: Table[string, seq[string]]
for s in module.structs:
deps[s.name] = @[]
for e in module.enums:
deps[e.name] = @[]
for st in sliceTypes:
deps[st.name] = @[]
proc addDeps(node: string, t: Type) =
for dep in collectValueDeps(t):
if dep == node: continue
if localTypeNames.contains(dep) or sliceNames.contains(dep):
if dep notin deps[node]:
deps[node].add(dep)
for s in module.structs:
for f in s.fields:
addDeps(s.name, f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
addDeps(e.name, ft)
for nf in v.namedFields:
addDeps(e.name, nf.typ)
# Topological sort (Kahn's algorithm).
var inDegree: Table[string, int]
var dependents: Table[string, seq[string]]
for node in deps.keys:
inDegree[node] = 0
for node, nodeDeps in deps:
for d in nodeDeps:
if not inDegree.hasKey(d): inDegree[d] = 0
inDegree[node] += 1
dependents.mgetOrPut(d, @[]).add(node)
var queue: seq[string] = @[]
for node, deg in inDegree:
if deg == 0:
queue.add(node)
var sorted: seq[string] = @[]
while queue.len > 0:
let node = queue.pop()
sorted.add(node)
for depNode in dependents.getOrDefault(node):
inDegree[depNode] -= 1
if inDegree[depNode] == 0:
queue.add(depNode)
if sorted.len < deps.len:
# Cycle detected; fall back to a safe deterministic order.
sorted = @[]
for s in module.structs: sorted.add(s.name)
for e in module.enums: sorted.add(e.name)
for st in sliceTypes: sorted.add(st.name)
# Map type names back to their definitions.
var structMap: Table[string, seq[tuple[name: string, typ: Type]]]
for s in module.structs: structMap[s.name] = s.fields
var enumMap: Table[string, seq[HirEnumVariant]]
for e in module.enums: enumMap[e.name] = e.variants
var sliceMap: Table[string, string]
for st in sliceTypes: sliceMap[st.name] = st.elem
# Emit type definitions in dependency order.
for name in sorted:
if structMap.hasKey(name):
be.emitStructDef(name, structMap[name])
elif enumMap.hasKey(name):
be.emitEnumDef(name, enumMap[name])
elif sliceMap.hasKey(name):
be.emitSliceTypeDef(name, sliceMap[name])
# Forward function declarations
for f in module.funcs:
+4 -8
View File
@@ -3,7 +3,7 @@
## Each HIR node kind lowers to 1-20 LIR instructions.
import std/[strutils, strformat, tables, sequtils]
import ast, types, token, hir, lir
import types, token, hir, lir
## Convert LirValue to C expression string (no % prefix)
proc lirValToC(v: LirValue): string =
@@ -429,7 +429,6 @@ proc lowerExpr(ctx: var LowerToLirCtx, node: HirNode): LirValue =
# ── SizeOf ──
of hSizeOf:
let ctype = typeToCStr(node.sizeOfType)
let t = b.freshTemp()
b.emit(LirInstr(kind: lirRawC, src: lirStr(&"/* sizeof({ctype}) */")))
return lirVar(&"sizeof({ctype})")
@@ -608,7 +607,6 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
# ── While statement ──
of hWhile:
let startLbl = b.freshLabel("while")
let bodyLbl = b.freshLabel("wbody")
let endLbl = b.freshLabel("wend")
ctx.loopStartLabels.add(startLbl.strVal)
@@ -750,9 +748,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
for stmt in node.blockStmts:
lowerStmt(ctx, stmt)
if node.blockExpr != nil:
let exprVal = lowerExpr(ctx, node.blockExpr)
# If block is an expression, store result
discard
# If block is an expression, result is unused at statement level
discard lowerExpr(ctx, node.blockExpr)
if node.isScope:
b.emitRawC("}")
@@ -762,9 +759,8 @@ proc lowerStmt(ctx: var LowerToLirCtx, node: HirNode) =
# ── Expression statement ──
else:
let exprVal = lowerExpr(ctx, node)
# Expression evaluated for side effects; temp is unused
discard
discard lowerExpr(ctx, node)
# ── Module-level lowering ──
-1
View File
@@ -90,7 +90,6 @@ proc parseInlineTable(s: string): OrderedTableRef[string, TomlValue] =
if content[i] == '{': inc braceCount
elif content[i] == '}': dec braceCount
inc i
let val = content[valStart ..< i]
result[key] = TomlValue(kind: tvkInlineTable, inlineVal: newOrderedTable[string, TomlValue]())
else:
while i < content.len and content[i] notin {',', ' ', '\t'}:
-8
View File
@@ -62,12 +62,6 @@ proc checkAny(p: Parser, kinds: openArray[TokenKind]): bool =
if p.peek() == k: return true
return false
proc match(p: var Parser, kind: TokenKind): bool =
if p.check(kind):
discard p.advance()
return true
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 ';'.
@@ -788,7 +782,6 @@ proc parseShift(p: var Parser): Expr =
return left
proc parseCast(p: var Parser): Expr =
let loc = p.currentLoc
var left = p.parseShift()
# 'as' and 'is' are handled in postfix for chaining
return left
@@ -1171,7 +1164,6 @@ proc parseParamList(p: var Parser, allowVariadic: bool = false): seq[Param] =
if p.check(tkRParen) or p.isAtEnd:
break
let loc = p.currentLoc
var isVar = false
if allowVariadic and p.check(tkDotDotDot):
discard p.advance()
let nameTok = p.at
+4 -7
View File
@@ -95,9 +95,6 @@ proc unescapeStringLiteral*(s: string): string =
proc emitError(sema: var Sema, loc: SourceLocation, message: string) =
sema.diagnostics.add(SemaDiagnostic(severity: sdsError, loc: loc, message: message))
proc emitWarning(sema: var Sema, loc: SourceLocation, message: string) =
sema.diagnostics.add(SemaDiagnostic(severity: sdsWarning, loc: loc, message: message))
proc hasErrors*(res: SemaResult): bool =
for d in res.diagnostics:
if d.severity == sdsError:
@@ -1348,12 +1345,12 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
discard sema.checkExpr(expr.exprIsOperand, scope)
return makeBool()
of ekTry:
let operandType = sema.checkExpr(expr.exprTryOperand, scope)
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()
of ekUnwrap:
let operandType = sema.checkExpr(expr.exprUnwrapOperand, scope)
discard sema.checkExpr(expr.exprUnwrapOperand, scope)
# Unwrap: extract Ok value or panic on Err
return makeInt()
of ekBlock:
@@ -1363,7 +1360,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
lastType = sema.checkStmt(stmt, blockScope)
return lastType
of ekMatch:
let subjectType = sema.checkExpr(expr.exprMatchSubject, scope)
discard sema.checkExpr(expr.exprMatchSubject, scope)
var resultType = makeUnknown()
for arm in expr.exprMatchArms:
var armScope = newScope(scope)
@@ -1398,7 +1395,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
expr.exprSpawnAsync = true
return makePointer(makeVoid())
of ekAwait:
let operand = sema.checkExpr(expr.exprAwaitOperand, scope)
discard sema.checkExpr(expr.exprAwaitOperand, scope)
# await on a task handle returns *void (result pointer)
return makePointer(makeVoid())
of ekBorrow: