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:
@@ -174,6 +174,7 @@ type
|
||||
of ekCall:
|
||||
exprCallCallee*: Expr
|
||||
exprCallArgs*: seq[Expr]
|
||||
exprCallArgNames*: seq[string] ## empty = positional, non-empty = named arg
|
||||
exprCallInferredTypeArgs*: seq[TypeExpr] ## filled by sema for inferred generic calls
|
||||
of ekGenericCall:
|
||||
exprGenericCallee*: string
|
||||
|
||||
+11
-1
@@ -589,17 +589,27 @@ proc parsePostfix(p: var Parser): Expr =
|
||||
# Call expression
|
||||
discard p.advance()
|
||||
var args: seq[Expr] = @[]
|
||||
var argNames: seq[string] = @[]
|
||||
while not p.check(tkRParen) and not p.isAtEnd:
|
||||
if p.check(tkDotDotDot):
|
||||
discard p.advance()
|
||||
let operand = p.parseExpr()
|
||||
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:
|
||||
args.add(p.parseExpr())
|
||||
argNames.add("")
|
||||
if p.check(tkComma):
|
||||
discard p.advance()
|
||||
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:
|
||||
# Generic type arguments: Max<int>(10, 20)
|
||||
# Only treat '<' as generic args if lookahead confirms a matching '>'
|
||||
|
||||
+61
-2
@@ -725,6 +725,63 @@ proc checkExprList(sema: var Sema, exprs: seq[Expr], scope: Scope): seq[Type] =
|
||||
for e in exprs:
|
||||
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 =
|
||||
if expr == nil:
|
||||
return makeUnknown()
|
||||
@@ -983,8 +1040,7 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
|
||||
# Regular function call
|
||||
let calleeType = sema.checkExpr(expr.exprCallCallee, scope)
|
||||
var argTypes = sema.checkExprList(expr.exprCallArgs, scope)
|
||||
# Look up callee declaration early (needed for borrow checking)
|
||||
# Look up callee declaration early (needed for borrow checking and defaults)
|
||||
var calleeDecl: Decl = nil
|
||||
case expr.exprCallCallee.kind
|
||||
of ekIdent:
|
||||
@@ -995,6 +1051,9 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
|
||||
let sym = scope.lookup(fullName)
|
||||
if sym != nil: calleeDecl = sym.decl
|
||||
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:
|
||||
discard # will be handled later
|
||||
if calleeType.kind == tkFunc:
|
||||
|
||||
+9
-9
@@ -81,21 +81,21 @@ let msg2: String = "Count: {num}";
|
||||
|
||||
---
|
||||
|
||||
### 5. Named / Default Parameters
|
||||
### 5. Named / Default Parameters ✅ DONE
|
||||
**Why:** API ergonomics.
|
||||
|
||||
**Syntax:**
|
||||
```bux
|
||||
func HttpResponse(code: int = 200, contentType: String = "text/plain", body: String = "") -> Response { ... }
|
||||
let r: Response = HttpResponse(body: "hello"); // code=200, contentType=default
|
||||
func HttpResponse(code: int = 200, body: String = "") -> Response { ... }
|
||||
let r: Response = HttpResponse(body: "hello"); // code=200
|
||||
let s = HttpResponse(404, body: "err"); // positional + named mixed
|
||||
```
|
||||
|
||||
**Implementation Steps:**
|
||||
1. Parser: allow `param: Type = defaultExpr`
|
||||
2. Sema: fill missing args at call sites
|
||||
3. C backend: emit args in correct order with defaults
|
||||
|
||||
**Complexity:** Medium — sema changes for call resolution.
|
||||
**Status:** Implemented in both bootstrap and selfhost.
|
||||
- Bootstrap parser already parsed defaults; added named-arg parsing and sema injection.
|
||||
- Selfhost parser now parses `= defaultExpr` in params and `name: value` at call sites.
|
||||
- Sema injects default expressions and reorders named args into param order.
|
||||
- HIR lowerer unchanged — desugaring happens in sema.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ const ekStringInterp: int = 26;
|
||||
struct ExprList {
|
||||
expr: *Expr,
|
||||
next: *ExprList,
|
||||
argName: String,
|
||||
}
|
||||
|
||||
struct Expr {
|
||||
@@ -212,6 +213,7 @@ struct Param {
|
||||
name: String;
|
||||
refParamType: *TypeExpr;
|
||||
isVariadic: bool;
|
||||
defaultExpr: *Expr;
|
||||
}
|
||||
|
||||
struct StructField {
|
||||
|
||||
+26
-1
@@ -353,10 +353,22 @@ func parserParsePostfixExpr(p: *Parser) -> *Expr {
|
||||
var firstArg: *ExprList = null as *ExprList;
|
||||
var lastArg: *ExprList = null as *ExprList;
|
||||
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;
|
||||
argNode.expr = argExpr;
|
||||
argNode.next = null as *ExprList;
|
||||
argNode.argName = argName;
|
||||
if firstArg == null as *ExprList {
|
||||
firstArg = argNode;
|
||||
lastArg = argNode;
|
||||
@@ -1016,34 +1028,47 @@ func parserParseParamList(p: *Parser) -> *Decl {
|
||||
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name");
|
||||
discard parserExpect(p, tkColon, "expected ':' in parameter");
|
||||
let ptype: *TypeExpr = parserParseType(p);
|
||||
var defExpr: *Expr = null as *Expr;
|
||||
if parserMatch(p, tkAssign) {
|
||||
defExpr = parserParseExpr(p);
|
||||
}
|
||||
|
||||
if d.paramCount == 0 {
|
||||
d.param0.name = nameTok.text;
|
||||
d.param0.refParamType = ptype;
|
||||
d.param0.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 1 {
|
||||
d.param1.name = nameTok.text;
|
||||
d.param1.refParamType = ptype;
|
||||
d.param1.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 2 {
|
||||
d.param2.name = nameTok.text;
|
||||
d.param2.refParamType = ptype;
|
||||
d.param2.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 3 {
|
||||
d.param3.name = nameTok.text;
|
||||
d.param3.refParamType = ptype;
|
||||
d.param3.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 4 {
|
||||
d.param4.name = nameTok.text;
|
||||
d.param4.refParamType = ptype;
|
||||
d.param4.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 5 {
|
||||
d.param5.name = nameTok.text;
|
||||
d.param5.refParamType = ptype;
|
||||
d.param5.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 6 {
|
||||
d.param6.name = nameTok.text;
|
||||
d.param6.refParamType = ptype;
|
||||
d.param6.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 7 {
|
||||
d.param7.name = nameTok.text;
|
||||
d.param7.refParamType = ptype;
|
||||
d.param7.defaultExpr = defExpr;
|
||||
} else if d.paramCount == 8 {
|
||||
d.param8.name = nameTok.text;
|
||||
d.param8.refParamType = ptype;
|
||||
d.param8.defaultExpr = defExpr;
|
||||
}
|
||||
d.paramCount = d.paramCount + 1;
|
||||
|
||||
|
||||
+100
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -234,6 +333,7 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
|
||||
// Call
|
||||
if kind == ekCall {
|
||||
discard Sema_CheckExpr(sema, expr.child1);
|
||||
Sema_ResolveCallArgs(sema, expr);
|
||||
var arg: *ExprList = expr.callArgs;
|
||||
while arg != null as *ExprList {
|
||||
discard Sema_CheckExpr(sema, arg.expr);
|
||||
|
||||
Reference in New Issue
Block a user