From 7c761cc64eaf75ef837d87693d5ff6d5258fd763 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 30 Aug 2026 09:08:43 +0000 Subject: [PATCH] =?UTF-8?q?Drop=20the=20upper=20bound=20on=20user-defined?= =?UTF-8?q?=20function=20arity=20(=C2=A714=20decision=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit object.Function.Evaluate (the path list.map/filter/reduce/each/sort call a callback through) never enforced a maximum, so a callback like (item) => item * 2 already worked as a map argument even though map also passes an index - only because that path skipped arity checking entirely. Requiring a direct call to declare every trailing parameter a function doesn't use fought that existing convention instead of matching it. Extra arguments are now dropped for every user-defined function, the same way Evaluate already dropped them; the minimum-argument check stays. --- SPEC.md | 54 +++++++++++++++++++++++++------------ evaluator/evaluator_test.go | 28 ++++++++++--------- evaluator/function.go | 36 ++++++++++++------------- 3 files changed, 71 insertions(+), 47 deletions(-) diff --git a/SPEC.md b/SPEC.md index 118a002..d446421 100644 --- a/SPEC.md +++ b/SPEC.md @@ -583,13 +583,18 @@ sum(...[1, 2, 3]) // spread at a call site list; spreading anything else is a type error. Written anywhere else (`x = ...list`), `...` is a syntax error rather than a value — it is only meaningful as an argument or a list-literal element. -- User-defined functions and methods get the same strict **arity checking** - every library function already has (§14 decision 1): a call with the - wrong number of arguments is an `Argument` fault naming the call - (`` `foo()` expects 2 arguments, got 1 ``), the same as a library call — - no frame is added, since the call never got the chance to start running. - A parameter with a default is optional; a rest parameter has no upper - bound and doesn't count toward the minimum. +- User-defined functions and methods get the same **missing-argument + checking** every library function already has (§14 decision 1, revised): a + call that leaves a required parameter unbound is an `Argument` fault + naming the call (`` `foo()` expects at least 2 arguments, got 1 ``), the + same as a library call — no frame is added, since the call never got the + chance to start running. A parameter with a default is optional; a rest + parameter has no upper bound and doesn't count toward the minimum. There + is no maximum for any function: a call may pass more arguments than a + function declares parameters for, and the extras are silently dropped, so + a function only has to name the parameters its body actually uses (a + `list.map` callback can be `(item) => ...` without also declaring the + index and list every call passes). - Recursion is bounded at **4096** call frames (`evaluator/call.go`, `maxCallDepth`), reported as an ordinary value error rather than a Go stack overflow, and tracked per-`Scope` (not a shared global counter) so @@ -1564,7 +1569,7 @@ already meets — except where flagged. - [x] `if`/`else if`/`else`, `while`, C-style `for`, `for ... in` - [x] `switch`/`case`/`default` as a match-expression (no fallthrough) - [x] `break`/`continue`/`return` -- [x] First-class functions, closures, default parameters, rest parameters, spread (call sites and list literals), strict arity checking (§14 decision 1) +- [x] First-class functions, closures, default parameters, rest parameters, spread (call sites and list literals), missing-argument checking (§14 decision 1) - [x] Destructuring assignment (`[a, b] = list`, `{x, y} = map`, `{x: a} = map`) - [x] Classes, single inheritance, traits/mixins, `this`/`super` - [x] Per-instance field initialization (no shared-mutable-default bug) @@ -2107,15 +2112,30 @@ made here so 1.0 ships with an answer rather than an asterisk. Revisit any of them only if real usage argues otherwise; until then, this is what 1.0 targets. -1. **User-defined function arity — done.** Functions and methods defined in - Ghost get the same strict arity checking every library function already - has (§8.7, §12). A call with the wrong number of arguments becomes an - `Argument` fault naming the call, exactly as it already does for a - library method — closing the largest remaining behavioral gap between - user code and library code, and the one most in tension with the "no - silent gaps" goal in §3. Implemented in `evaluator/function.go` - (`checkArity`, `createFunctionEnvironment`), tested in - `evaluator/evaluator_test.go`'s `TestFunctionArity`. +1. **User-defined function arity — done, revised.** Functions and methods + defined in Ghost get the same missing-argument checking every library + function already has (§8.7, §12): a call that leaves a required + parameter unbound becomes an `Argument` fault naming the call, exactly as + it already does for a library method — closing the largest remaining + behavioral gap between user code and library code, and the one most in + tension with the "no silent gaps" goal in §3. + + The original version of this decision also rejected calls with *too + many* arguments. That half was reverted: it put user-defined functions + and `object.Function.Evaluate` (the path `list.map`/`filter`/`reduce`/ + `each`/`sort` call a callback through) at odds with each other - + `Evaluate` never enforced an upper bound, so a callback like + `(item) => item * 2` already worked as `list.map`'s argument even though + `map` also passes an index, only because that path skipped arity + checking entirely. Requiring every trailing parameter a callback doesn't + use to be declared anyway (`(item, index, list) => item * 2`) fought that + existing convention rather than matching it, for no benefit worth the + friction. The maximum is gone for every user-defined function now, not + only callbacks: extra arguments are dropped, the same way `Evaluate` + already dropped them. The minimum is unchanged and still enforced. + Implemented in `evaluator/function.go` (`checkArity`, + `createFunctionEnvironment`), tested in `evaluator/evaluator_test.go`'s + `TestFunctionArity` and `TestFunctionArityAllowsDefaultsAndVariety`. 2. **`Map` iteration order — done.** `Map` guarantees insertion order for `keys()`, `values()`, `entries()`, `for ... in`, and `String()` (§13.5), matching the predictability JS objects and PHP associative arrays already diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index 13f55a3..34a2f16 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -35,25 +35,24 @@ func TestErrorHandling(t *testing.T) { } } -// TestFunctionArity confirms user-defined functions and methods are strictly -// arity-checked the same way every library call already is (§14 decision 1): -// a call with the wrong number of arguments is an Argument fault, rather -// than silently dropping extras or leaving a missing parameter undefined. +// TestFunctionArity confirms user-defined functions and methods are checked +// for missing arguments the same way every library call already is (§14 +// decision 1, revised): a call that leaves a required parameter unbound is +// an Argument fault, rather than leaving that parameter undefined. func TestFunctionArity(t *testing.T) { tests := []struct { input string expectedMessage string }{ - {"function foo(a, b) { return a + b } foo(1)", "test.gs:1:37: argument error: `foo()` expects 2 arguments, got 1"}, - {"function foo(a, b) { return a + b } foo(1, 2, 3)", "test.gs:1:37: argument error: `foo()` expects 2 arguments, got 3"}, - {"function foo(a, b = 1) { return a + b } foo()", "test.gs:1:41: argument error: `foo()` expects between 1 and 2 arguments, got 0"}, + {"function foo(a, b) { return a + b } foo(1)", "test.gs:1:37: argument error: `foo()` expects at least 2 arguments, got 1"}, + {"function foo(a, b = 1) { return a + b } foo()", "test.gs:1:41: argument error: `foo()` expects at least 1 argument, got 0"}, { "class Point { constructor(x, y) { this.x = x } add(other) { return this.x + other.x } } p = new Point(1, 2) p.add()", - "test.gs:1:111: argument error: `Point.add()` expects 1 argument, got 0", + "test.gs:1:111: argument error: `Point.add()` expects at least 1 argument, got 0", }, { "class Point { constructor(x, y) { this.x = x } } new Point(1)", - "test.gs:1:50: argument error: `Point()` expects 2 arguments, got 1", + "test.gs:1:50: argument error: `Point()` expects at least 2 arguments, got 1", }, } @@ -64,9 +63,12 @@ func TestFunctionArity(t *testing.T) { } } -// TestFunctionArityAllowsDefaultsAndVariety confirms a call within the -// declared parameter's range still works, and that the check doesn't reject -// a valid call - only too few or too many arguments do. +// TestFunctionArityAllowsDefaultsAndVariety confirms a call that supplies at +// least the required arguments still works, whether it lands within the +// declared parameters (using defaults where they're omitted) or passes more +// arguments than the function declared parameters for - the extras are +// dropped rather than rejected, so a function only has to name the +// parameters its body actually uses. func TestFunctionArityAllowsDefaultsAndVariety(t *testing.T) { tests := []struct { input string @@ -75,6 +77,8 @@ func TestFunctionArityAllowsDefaultsAndVariety(t *testing.T) { {"function greet(name, greeting = 1) { return greeting } greet(\"a\")", 1}, {"function greet(name, greeting = 2) { return greeting } greet(\"a\", 5)", 5}, {"function noArgs() { return 3 } noArgs()", 3}, + {"function foo(a, b) { return a + b } foo(1, 2, 3)", 3}, + {"function first(a) { return a } first(1, 2, 3)", 1}, } for _, tt := range tests { diff --git a/evaluator/function.go b/evaluator/function.go index 89ddeba..ca4f853 100644 --- a/evaluator/function.go +++ b/evaluator/function.go @@ -28,8 +28,9 @@ func evaluateFunction(node *ast.Function, scope *object.Scope) object.Object { } // createFunctionEnvironment binds arguments to a user-defined function's -// parameters. name and tok name and locate the call for an arity error - see -// checkArity - the same way every library call already reports one. +// parameters, dropping any beyond the last declared one - see checkArity. +// name and tok name and locate the call for an arity error the same way +// every library call already reports one. func createFunctionEnvironment(function *object.Function, arguments []object.Object, name string, tok token.Token) (*object.Environment, *object.Error) { if err := checkArity(function, arguments, name, tok); err != nil { return nil, err @@ -69,14 +70,21 @@ func createFunctionEnvironment(function *object.Function, arguments []object.Obj return env, nil } -// checkArity gives a user-defined function or method the same strict arity -// checking every library call already has (§14 decision 1): a call with the -// wrong number of arguments is an Argument fault naming the call, rather -// than silently dropping extras or leaving a missing parameter undefined. A +// checkArity gives a user-defined function or method the same missing- +// argument checking every library call already has (§14 decision 1, +// revised): a call that leaves a required parameter unbound is an Argument +// fault naming the call, rather than leaving that parameter undefined. A // parameter with a default is optional, whichever position it is declared -// in, so the minimum is however many fixed parameters have none; a rest -// parameter (§12) never counts toward the minimum and removes the maximum -// entirely, the same as a library method's own `arityAtLeast`. +// in, so the minimum is however many fixed parameters have none. +// +// There is deliberately no maximum: a caller may pass more arguments than a +// function declares parameters for, and the extras are dropped rather than +// rejected, the same as `object.Function.Evaluate` (used for callbacks +// passed to `list.map`/`filter`/`reduce`/`each`/`sort`) has always allowed. +// This lets a function declare only the parameters its body actually uses - +// a map callback can be `(item) => ...` without also naming the index and +// list every call site provides - instead of forcing every unused trailing +// parameter to be spelled out. func checkArity(function *object.Function, arguments []object.Object, name string, tok token.Token) *object.Error { fixed := len(function.Parameters) @@ -86,13 +94,5 @@ func checkArity(function *object.Function, arguments []object.Object, name strin min := fixed - len(function.Defaults) - if function.Rest { - return object.ArityAtLeast(name, tok, arguments, min) - } - - if min == fixed { - return object.Arity(name, tok, arguments, min) - } - - return object.ArityRange(name, tok, arguments, min, fixed) + return object.ArityAtLeast(name, tok, arguments, min) }