Skip to content

Commit dd0480e

Browse files
committed
compiler: check call sites of variable-bound annotated lambdas
v0.29.0 added optional `name : type` annotations on `def` and `fn`, but the call-site checker only walked `*ast.FuncDef` — meaning a lambda assigned to a variable square = fn(n : int) : int return n * n end puts(square("oops")) silently compiled and only failed at runtime ("cannot multiply String and String"). Close that gap so Tier 4 (variable-bound annotated lambdas) is checked the same way as Tier 3 (top-level defs).
1 parent 95c58f2 commit dd0480e

6 files changed

Lines changed: 643 additions & 28 deletions

File tree

compiler/check_mismatch_calls.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ func checkCallSites(prog *ast.Program, ti *TypeInfo, sourceFile string) error {
3535
funcs: collectAnnotatedFuncs(prog),
3636
ti: ti,
3737
}
38-
if len(c.funcs) == 0 {
38+
// Tier 4 (variable-bound lambda call sites) lives in TypeInfo.VarFnSigs
39+
// and is independent of whether any top-level def has annotations, so
40+
// only short-circuit when both sources have nothing to check.
41+
if len(c.funcs) == 0 && (ti == nil || len(ti.VarFnSigs) == 0) {
3942
return nil
4043
}
4144
for _, s := range prog.Statements {
@@ -425,20 +428,40 @@ func (c *callChecker) walkExpr(e ast.Expr, line int, file string, currentNS stri
425428
// resolved as `currentNS.f` (sibling call within the same module) before
426429
// falling back to a top-level `f`, matching codegen's resolution order
427430
// in buildCallExpr.
431+
//
432+
// If the identifier does not name a top-level def, fall back to Tier 4:
433+
// look up an annotated lambda binding recorded for the call's IdentExpr
434+
// in TypeInfo.VarFnSigs and validate against its signature.
428435
func (c *callChecker) checkCall(call *ast.CallExpr, line int, file string, currentNS string) error {
429436
ident, ok := call.Func.(*ast.IdentExpr)
430437
if !ok {
431438
return nil
432439
}
433-
fn := c.resolveCallee(ident.Name, currentNS)
434-
if fn == nil {
440+
if fn := c.resolveCallee(ident.Name, currentNS); fn != nil {
441+
return c.checkArgs(call.Args, fn.Params, displayCalleeName(ident.Name, fn.Namespace), line, file)
442+
}
443+
// Tier 4 fallback: variable-bound annotated lambda.
444+
if c.ti == nil {
435445
return nil
436446
}
437-
for i, arg := range call.Args {
438-
if i >= len(fn.Params) {
447+
fnExpr, ok := c.ti.VarFnSigs[ident]
448+
if !ok || fnExpr == nil {
449+
return nil
450+
}
451+
return c.checkArgs(call.Args, fnExpr.Params, ident.Name, line, file)
452+
}
453+
454+
// checkArgs validates a list of call arguments against a parameter list
455+
// (from a def or a tracked annotated lambda). It is the inner loop shared
456+
// by both Tier 3 (direct def call) and Tier 4 (variable-bound lambda
457+
// call) checks. calleeName is the user-facing name to put in error
458+
// messages.
459+
func (c *callChecker) checkArgs(args []ast.Expr, params []ast.Param, calleeName string, line int, file string) error {
460+
for i, arg := range args {
461+
if i >= len(params) {
439462
break
440463
}
441-
p := fn.Params[i]
464+
p := params[i]
442465
if p.TypeAnnot == "" {
443466
continue
444467
}
@@ -453,14 +476,15 @@ func (c *callChecker) checkCall(call *ast.CallExpr, line int, file string, curre
453476
}
454477
return &ast.UserError{Msg: fmt.Sprintf(
455478
"%s:%d: cannot pass %s literal as argument %d to '%s' (parameter '%s' declared as %s)",
456-
file, line, displayTypeName(argType), i+1, displayCalleeName(ident.Name, fn.Namespace),
479+
file, line, displayTypeName(argType), i+1, calleeName,
457480
p.Name, displayTypeName(annot),
458481
)}
459482
}
460-
// Path 2 (Tier 3): variable / non-literal argument — consult the
461-
// flow-sensitive type recorded for that exact expression. We only
462-
// flag when the inferred type is fully resolved AND incompatible;
463-
// dynamic or unknown types pass silently.
483+
// Path 2 (Tier 3 / Tier 4): variable / non-literal argument —
484+
// consult the flow-sensitive type recorded for that exact
485+
// expression. We only flag when the inferred type is fully
486+
// resolved AND incompatible; dynamic or unknown types pass
487+
// silently.
464488
if c.ti == nil {
465489
continue
466490
}
@@ -473,7 +497,7 @@ func (c *callChecker) checkCall(call *ast.CallExpr, line int, file string, curre
473497
}
474498
return &ast.UserError{Msg: fmt.Sprintf(
475499
"%s:%d: cannot pass %s value as argument %d to '%s' (parameter '%s' declared as %s)",
476-
file, line, displayTypeName(argType), i+1, displayCalleeName(ident.Name, fn.Namespace),
500+
file, line, displayTypeName(argType), i+1, calleeName,
477501
p.Name, displayTypeName(annot),
478502
)}
479503
}

0 commit comments

Comments
 (0)