From 3f0a3901ca8210ac3a7ea531e6f0ba21b7e9f0fa Mon Sep 17 00:00:00 2001 From: dimgigov Date: Mon, 8 Jun 2026 22:02:19 +0300 Subject: [PATCH] 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. --- bootstrap/ast.nim | 1 + bootstrap/parser.nim | 12 +++++- bootstrap/sema.nim | 63 ++++++++++++++++++++++++++- docs/ROADMAP.md | 18 ++++---- src/ast.bux | 2 + src/parser.bux | 27 +++++++++++- src/sema.bux | 100 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 210 insertions(+), 13 deletions(-) diff --git a/bootstrap/ast.nim b/bootstrap/ast.nim index 09811e0..5003fae 100644 --- a/bootstrap/ast.nim +++ b/bootstrap/ast.nim @@ -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 diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index b15e70f..223ffb7 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -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(10, 20) # Only treat '<' as generic args if lookahead confirms a matching '>' diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 367f7ae..8cec0fc 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -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: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 7f3f9d5..a5bf14b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -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. --- diff --git a/src/ast.bux b/src/ast.bux index cf309a4..a976e13 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -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 { diff --git a/src/parser.bux b/src/parser.bux index 5bb2620..fc7e340 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -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; diff --git a/src/sema.bux b/src/sema.bux index 72ac1ae..3c65b0e 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -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);