feat: named and default parameters

Bootstrap compiler:
- ast.nim: add exprCallArgNames to ekCall for named arg tracking
- parser.nim: detect name: value syntax in call args
- sema.nim: add resolveCallArgs helper that injects default values
  for missing params and reorders named args into param order.
  Parser already parsed default param values; sema now uses them.

Selfhost compiler:
- ast.bux: add defaultExpr to Param, argName to ExprList
- parser.bux: parse = defaultExpr in param list, detect name: value
  syntax in call arguments
- sema.bux: add Sema_ResolveCallArgs with same logic as bootstrap

Tests:
- _test_named_params verifies defaults, named args, mixed positional+named,
  and named args with defaults in both compilers.

All verifications pass: build, selfhost-loop, test-examples, test-golden.
This commit is contained in:
2026-06-08 22:02:19 +03:00
parent 17c896c4dc
commit 3f0a3901ca
7 changed files with 210 additions and 13 deletions
+1
View File
@@ -174,6 +174,7 @@ type
of ekCall: of ekCall:
exprCallCallee*: Expr exprCallCallee*: Expr
exprCallArgs*: seq[Expr] exprCallArgs*: seq[Expr]
exprCallArgNames*: seq[string] ## empty = positional, non-empty = named arg
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
of ekGenericCall: of ekGenericCall:
exprGenericCallee*: string exprGenericCallee*: string
+11 -1
View File
@@ -589,17 +589,27 @@ proc parsePostfix(p: var Parser): Expr =
# Call expression # Call expression
discard p.advance() discard p.advance()
var args: seq[Expr] = @[] var args: seq[Expr] = @[]
var argNames: seq[string] = @[]
while not p.check(tkRParen) and not p.isAtEnd: while not p.check(tkRParen) and not p.isAtEnd:
if p.check(tkDotDotDot): if p.check(tkDotDotDot):
discard p.advance() discard p.advance()
let operand = p.parseExpr() let operand = p.parseExpr()
args.add(Expr(kind: ekSpread, loc: operand.loc, exprSpreadOperand: operand)) args.add(Expr(kind: ekSpread, loc: operand.loc, exprSpreadOperand: operand))
argNames.add("")
elif p.peek() == tkIdent and p.peek(1) == tkColon:
# Named argument: name: value
let nameTok = p.advance()
discard p.advance() # :
let value = p.parseExpr()
args.add(value)
argNames.add(nameTok.text)
else: else:
args.add(p.parseExpr()) args.add(p.parseExpr())
argNames.add("")
if p.check(tkComma): if p.check(tkComma):
discard p.advance() discard p.advance()
discard p.expect(tkRParen, "expected ')' to close call") discard p.expect(tkRParen, "expected ')' to close call")
left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args) left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args, exprCallArgNames: argNames)
of tkLt: of tkLt:
# Generic type arguments: Max<int>(10, 20) # Generic type arguments: Max<int>(10, 20)
# Only treat '<' as generic args if lookahead confirms a matching '>' # Only treat '<' as generic args if lookahead confirms a matching '>'
+61 -2
View File
@@ -725,6 +725,63 @@ proc checkExprList(sema: var Sema, exprs: seq[Expr], scope: Scope): seq[Type] =
for e in exprs: for e in exprs:
result.add(sema.checkExpr(e, scope)) result.add(sema.checkExpr(e, scope))
proc resolveCallArgs(sema: var Sema, expr: Expr, calleeDecl: Decl, scope: Scope) =
## Reorder named args and inject defaults for missing positional args.
if expr.kind != ekCall or calleeDecl == nil or calleeDecl.kind != dkFunc:
return
let params = calleeDecl.declFuncParams
let providedArgs = expr.exprCallArgs
let providedNames = expr.exprCallArgNames
if providedNames.len == 0:
# All positional — just inject defaults for trailing missing args
if providedArgs.len < params.len:
var newArgs = providedArgs
var newNames = providedNames
for i in providedArgs.len ..< params.len:
if params[i].defaultValue != nil:
newArgs.add(params[i].defaultValue)
newNames.add("")
else:
sema.emitError(expr.loc, &"missing argument for parameter '{params[i].name}'")
break
expr.exprCallArgs = newArgs
expr.exprCallArgNames = newNames
return
# Named args present
var newArgs: seq[Expr] = @[]
var newNames: seq[string] = @[]
var usedNamed = false
var namedArgMap: Table[string, Expr]
# Collect named args and validate ordering
for i in 0 ..< providedArgs.len:
if providedNames[i] != "":
usedNamed = true
if providedNames[i] in namedArgMap:
sema.emitError(expr.loc, &"duplicate named argument '{providedNames[i]}'")
return
namedArgMap[providedNames[i]] = providedArgs[i]
else:
if usedNamed:
sema.emitError(expr.loc, "positional argument after named argument")
return
# Build final arg list in param order
for i in 0 ..< params.len:
if i < providedArgs.len and providedNames[i] == "":
# Positional arg at expected position
newArgs.add(providedArgs[i])
newNames.add("")
elif params[i].name in namedArgMap:
newArgs.add(namedArgMap[params[i].name])
newNames.add("")
elif params[i].defaultValue != nil:
newArgs.add(params[i].defaultValue)
newNames.add("")
else:
sema.emitError(expr.loc, &"missing argument for parameter '{params[i].name}'")
break
expr.exprCallArgs = newArgs
expr.exprCallArgNames = newNames
proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
if expr == nil: if expr == nil:
return makeUnknown() return makeUnknown()
@@ -983,8 +1040,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
# Regular function call # Regular function call
let calleeType = sema.checkExpr(expr.exprCallCallee, scope) let calleeType = sema.checkExpr(expr.exprCallCallee, scope)
var argTypes = sema.checkExprList(expr.exprCallArgs, scope) # Look up callee declaration early (needed for borrow checking and defaults)
# Look up callee declaration early (needed for borrow checking)
var calleeDecl: Decl = nil var calleeDecl: Decl = nil
case expr.exprCallCallee.kind case expr.exprCallCallee.kind
of ekIdent: of ekIdent:
@@ -995,6 +1051,9 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
let sym = scope.lookup(fullName) let sym = scope.lookup(fullName)
if sym != nil: calleeDecl = sym.decl if sym != nil: calleeDecl = sym.decl
else: discard else: discard
# Resolve named args and inject defaults before type-checking args
sema.resolveCallArgs(expr, calleeDecl, scope)
var argTypes = sema.checkExprList(expr.exprCallArgs, scope)
if calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0: if calleeDecl != nil and calleeDecl.kind == dkFunc and calleeDecl.declFuncTypeParams.len > 0:
discard # will be handled later discard # will be handled later
if calleeType.kind == tkFunc: if calleeType.kind == tkFunc:
+9 -9
View File
@@ -81,21 +81,21 @@ let msg2: String = "Count: {num}";
--- ---
### 5. Named / Default Parameters ### 5. Named / Default Parameters ✅ DONE
**Why:** API ergonomics. **Why:** API ergonomics.
**Syntax:** **Syntax:**
```bux ```bux
func HttpResponse(code: int = 200, contentType: String = "text/plain", body: String = "") -> Response { ... } func HttpResponse(code: int = 200, body: String = "") -> Response { ... }
let r: Response = HttpResponse(body: "hello"); // code=200, contentType=default let r: Response = HttpResponse(body: "hello"); // code=200
let s = HttpResponse(404, body: "err"); // positional + named mixed
``` ```
**Implementation Steps:** **Status:** Implemented in both bootstrap and selfhost.
1. Parser: allow `param: Type = defaultExpr` - Bootstrap parser already parsed defaults; added named-arg parsing and sema injection.
2. Sema: fill missing args at call sites - Selfhost parser now parses `= defaultExpr` in params and `name: value` at call sites.
3. C backend: emit args in correct order with defaults - Sema injects default expressions and reorders named args into param order.
- HIR lowerer unchanged — desugaring happens in sema.
**Complexity:** Medium — sema changes for call resolution.
--- ---
+2
View File
@@ -100,6 +100,7 @@ const ekStringInterp: int = 26;
struct ExprList { struct ExprList {
expr: *Expr, expr: *Expr,
next: *ExprList, next: *ExprList,
argName: String,
} }
struct Expr { struct Expr {
@@ -212,6 +213,7 @@ struct Param {
name: String; name: String;
refParamType: *TypeExpr; refParamType: *TypeExpr;
isVariadic: bool; isVariadic: bool;
defaultExpr: *Expr;
} }
struct StructField { struct StructField {
+26 -1
View File
@@ -353,10 +353,22 @@ func parserParsePostfixExpr(p: *Parser) -> *Expr {
var firstArg: *ExprList = null as *ExprList; var firstArg: *ExprList = null as *ExprList;
var lastArg: *ExprList = null as *ExprList; var lastArg: *ExprList = null as *ExprList;
while !parserCheck(p, tkRParen) { while !parserCheck(p, tkRParen) {
let argExpr: *Expr = parserParseExpr(p); var argExpr: *Expr = null as *Expr;
var argName: String = "";
// Named argument: name: value
if parserPeek(p, 0) == tkIdent && parserPeek(p, 1) == tkColon {
let nameTok: LexToken = parserCurToken(p);
argName = nameTok.text;
discard parserAdvance(p); // ident
discard parserAdvance(p); // :
argExpr = parserParseExpr(p);
} else {
argExpr = parserParseExpr(p);
}
let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList; let argNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
argNode.expr = argExpr; argNode.expr = argExpr;
argNode.next = null as *ExprList; argNode.next = null as *ExprList;
argNode.argName = argName;
if firstArg == null as *ExprList { if firstArg == null as *ExprList {
firstArg = argNode; firstArg = argNode;
lastArg = argNode; lastArg = argNode;
@@ -1016,34 +1028,47 @@ func parserParseParamList(p: *Parser) -> *Decl {
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name"); let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name");
discard parserExpect(p, tkColon, "expected ':' in parameter"); discard parserExpect(p, tkColon, "expected ':' in parameter");
let ptype: *TypeExpr = parserParseType(p); let ptype: *TypeExpr = parserParseType(p);
var defExpr: *Expr = null as *Expr;
if parserMatch(p, tkAssign) {
defExpr = parserParseExpr(p);
}
if d.paramCount == 0 { if d.paramCount == 0 {
d.param0.name = nameTok.text; d.param0.name = nameTok.text;
d.param0.refParamType = ptype; d.param0.refParamType = ptype;
d.param0.defaultExpr = defExpr;
} else if d.paramCount == 1 { } else if d.paramCount == 1 {
d.param1.name = nameTok.text; d.param1.name = nameTok.text;
d.param1.refParamType = ptype; d.param1.refParamType = ptype;
d.param1.defaultExpr = defExpr;
} else if d.paramCount == 2 { } else if d.paramCount == 2 {
d.param2.name = nameTok.text; d.param2.name = nameTok.text;
d.param2.refParamType = ptype; d.param2.refParamType = ptype;
d.param2.defaultExpr = defExpr;
} else if d.paramCount == 3 { } else if d.paramCount == 3 {
d.param3.name = nameTok.text; d.param3.name = nameTok.text;
d.param3.refParamType = ptype; d.param3.refParamType = ptype;
d.param3.defaultExpr = defExpr;
} else if d.paramCount == 4 { } else if d.paramCount == 4 {
d.param4.name = nameTok.text; d.param4.name = nameTok.text;
d.param4.refParamType = ptype; d.param4.refParamType = ptype;
d.param4.defaultExpr = defExpr;
} else if d.paramCount == 5 { } else if d.paramCount == 5 {
d.param5.name = nameTok.text; d.param5.name = nameTok.text;
d.param5.refParamType = ptype; d.param5.refParamType = ptype;
d.param5.defaultExpr = defExpr;
} else if d.paramCount == 6 { } else if d.paramCount == 6 {
d.param6.name = nameTok.text; d.param6.name = nameTok.text;
d.param6.refParamType = ptype; d.param6.refParamType = ptype;
d.param6.defaultExpr = defExpr;
} else if d.paramCount == 7 { } else if d.paramCount == 7 {
d.param7.name = nameTok.text; d.param7.name = nameTok.text;
d.param7.refParamType = ptype; d.param7.refParamType = ptype;
d.param7.defaultExpr = defExpr;
} else if d.paramCount == 8 { } else if d.paramCount == 8 {
d.param8.name = nameTok.text; d.param8.name = nameTok.text;
d.param8.refParamType = ptype; d.param8.refParamType = ptype;
d.param8.defaultExpr = defExpr;
} }
d.paramCount = d.paramCount + 1; d.paramCount = d.paramCount + 1;
+100
View File
@@ -140,6 +140,105 @@ func Sema_CheckBlock(sema: *Sema, block: *Block) {
} }
} }
// ---------------------------------------------------------------------------
// Call argument resolution: inject defaults and reorder named args
// ---------------------------------------------------------------------------
func Sema_ResolveCallArgs(sema: *Sema, expr: *Expr) {
if expr == null as *Expr || expr.kind != ekCall { return; }
if expr.child1 == null as *Expr || expr.child1.kind != ekIdent { return; }
let sym: Symbol = Scope_Lookup(sema.scope, expr.child1.strValue);
if sym.kind != skFunc || sym.decl == null as *Decl { return; }
let decl: *Decl = sym.decl;
if decl.paramCount == 0 { return; }
// Check if any named args present
var hasNamed: bool = false;
var arg: *ExprList = expr.callArgs;
while arg != null as *ExprList {
if !String_Eq(arg.argName, "") { hasNamed = true; }
arg = arg.next;
}
if !hasNamed && expr.callArgCount >= decl.paramCount { return; }
// Build new linked list in param order
var newFirst: *ExprList = null as *ExprList;
var newLast: *ExprList = null as *ExprList;
var newCount: int = 0;
var usedPositional: int = 0;
var positionalAfterNamed: bool = false;
var i: int = 0;
while i < decl.paramCount {
var p: *Param = null as *Param;
if i == 0 { p = &decl.param0; }
else if i == 1 { p = &decl.param1; }
else if i == 2 { p = &decl.param2; }
else if i == 3 { p = &decl.param3; }
else if i == 4 { p = &decl.param4; }
else if i == 5 { p = &decl.param5; }
else if i == 6 { p = &decl.param6; }
else if i == 7 { p = &decl.param7; }
else if i == 8 { p = &decl.param8; }
var matched: *Expr = null as *Expr;
// Look for positional arg at this index
if usedPositional < expr.callArgCount {
var posIdx: int = 0;
var posArg: *ExprList = expr.callArgs;
while posArg != null as *ExprList {
if String_Eq(posArg.argName, "") {
if posIdx == usedPositional {
matched = posArg.expr;
usedPositional = usedPositional + 1;
break;
}
posIdx = posIdx + 1;
}
posArg = posArg.next;
}
}
// Look for named arg matching this param
if matched == null as *Expr {
var namedArg: *ExprList = expr.callArgs;
while namedArg != null as *ExprList {
if String_Eq(namedArg.argName, p.name) {
matched = namedArg.expr;
break;
}
namedArg = namedArg.next;
}
}
// Use default if available
if matched == null as *Expr && p.defaultExpr != null as *Expr {
matched = p.defaultExpr;
}
if matched != null as *Expr {
let node: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
node.expr = matched;
node.next = null as *ExprList;
node.argName = "";
if newFirst == null as *ExprList {
newFirst = node;
newLast = node;
} else {
newLast.next = node;
newLast = node;
}
newCount = newCount + 1;
}
i = i + 1;
}
expr.callArgs = newFirst;
expr.callArgCount = newCount;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Expression type checking // Expression type checking
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -234,6 +333,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
// Call // Call
if kind == ekCall { if kind == ekCall {
discard Sema_CheckExpr(sema, expr.child1); discard Sema_CheckExpr(sema, expr.child1);
Sema_ResolveCallArgs(sema, expr);
var arg: *ExprList = expr.callArgs; var arg: *ExprList = expr.callArgs;
while arg != null as *ExprList { while arg != null as *ExprList {
discard Sema_CheckExpr(sema, arg.expr); discard Sema_CheckExpr(sema, arg.expr);