Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 37 additions & 17 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
28 changes: 16 additions & 12 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}

Expand All @@ -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
Expand All @@ -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 {
Expand Down
36 changes: 18 additions & 18 deletions evaluator/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -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)
}
Loading