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:
|
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
@@ -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
@@ -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
@@ -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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user