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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,27 @@ All notable changes to EigenScript are documented here.

## [Unreleased]

### Changed
- **BREAKING — one call rule (#405, closes #153): a bare literal list
after `of` is ALWAYS an argument list**, at every element count:
`f of []` is zero arguments, `f of [x]` is ONE argument (the element —
previously it bound the whole 1-element list to the first parameter),
`f of [a, b]` is two. Brackets are an argument list; parentheses are
one argument — `f of ([x])` (#355) still passes a literal list whole.
The obvious recursive form `fib of [n - 1]` now works even after a
defaulted parameter is added (the arity-dependent silent-wrong this
kills). Migration: every behavior-changed site is a bare 1-element
literal arg list — the new **W017** lint flags exactly that shape with
the two unambiguous rewrites (`f of x` / `f of ([x])`), so `--lint`
over a repo is the mechanical audit. SPEC/COMPARISON call sections
rewritten.

### Added
- **W017 — bare 1-element literal arg list** (#405): warns on `f of [x]`
(historically ambiguous; inverted meaning pre-#405) and names both
unambiguous spellings — `f of x` for one argument, `f of ([x])` to
pass a 1-element list. Doubles as the #405 migration audit over a
consumer repo.
- **E003 — undefined-name lint, increment one of the scope-aware
name-resolution pass** (#404): `--lint` (and the LSP, as red squiggles)
now reports a name that is read but bound nowhere — the typo'd name in a
Expand Down
15 changes: 7 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,13 @@ bash tools/embed_stack_soak.sh # embed REPL soak inside a 64 KiB stack rlimit (
- **`tests/test_temporal.eigs` is line-number-sensitive** — its `at`
queries hardcode line numbers. Append only before the final if/else, and
re-verify the `grep -n` markers in the file.
- **`f of [x]` does not spread** — the compiler spreads literal list args
only at `count > 1`, so a 1-element list binds the *whole list* to the
first param. For 1-arg calls to multi-param (incl. defaulted) functions
use `f of (x)`. This breaks the obvious recursive form `fib of [n - 1]`
the moment a defaulted param is added (issue #153). Parenthesising
suppresses spread entirely (#355): `f of ([a, b])` passes the literal
list whole. (More `.eigs`-writing gotchas: the `write-eigenscript`
skill.)
- **Brackets after `of` are an argument list; parentheses are one
argument** (#405, closed #153): a bare literal list is an arg list at
EVERY count — `f of []` zero args, `f of [x]` one arg (the element,
not the list), `f of [a, b]` two. To pass a literal list whole,
parenthesise (#355): `f of ([x])`. Lint W017 flags the 1-element bare
form (pre-#405 it meant the opposite). (More `.eigs`-writing gotchas:
the `write-eigenscript` skill.)
- The Makefile `asan` target compiles with `EIGENSCRIPT_EXT_HTTP=0`; if you
touch `ext_http.c`, compile-check with `make http`. Same for `ext_gfx.c`
— in **no** default build; compile-check with `make gfx`. All variants
Expand Down
7 changes: 5 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ observer/deterministic-replay niche instead of diluting it.**
- [ ] Scope-aware name-resolution lint (E-class, shared with the LSP) —
the highest correctness yield per week available
([#404](https://github.com/InauguralSystems/EigenScript/issues/404))
- [ ] **Language change:** bare literal list after `of` is always an
- [x] **Language change:** bare literal list after `of` is always an
argument list — kill the 1-element spread trap while pre-1.0 makes
it cheap ([#405](https://github.com/InauguralSystems/EigenScript/issues/405), closes #153)
it cheap ([#405](https://github.com/InauguralSystems/EigenScript/issues/405), closes #153).
Landed on main with lint `W017` as the migration audit; the
ouroboros `frontend.eigs` parity leg rides the next release + pin
bump (with the `chr` raise and #411 tape headers)
- [ ] Structured runtime errors `{kind, message, line}` with a closed
kind set ([#406](https://github.com/InauguralSystems/EigenScript/issues/406))
- [~] Column tracking + caret/span diagnostics — parse errors now carry
Expand Down
20 changes: 10 additions & 10 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ const add = (a, b) => a + b;
console.log(add(3, 4));
```

EigenScript — multi-argument calls pass a literal list, which spreads
into the parameters. (One-argument calls to multi-parameter functions
must use parentheses — `f of (x)` — because a 1-element literal list
does not spread.)
EigenScript — a bare literal list after `of` is the argument list, at
every element count: `f of [a, b]` is two arguments, `f of [x]` is one.
(Parentheses always mean one argument: `f of ([x])` passes a literal
1-element list whole.)

```eigenscript
define add(a, b) as:
Expand Down Expand Up @@ -373,8 +373,8 @@ Transformations you will apply constantly when porting Python code:
|---|---|---|
| `x = v` | `x is v` | assignment keyword |
| `f(a)` | `f of a` | application keyword |
| `f(a, b)` | `f of [a, b]` | literal list spreads (2+ elements) |
| `f(a)` where `f` has 2+ params | `f of (a)` | 1-element lists do **not** spread |
| `f(a, b)` | `f of [a, b]` | bare literal list = argument list |
| `f([a])` (pass a 1-element list) | `f of ([a])` | parentheses = one argument |
| `True` / `False` / `None` | `1` / `0` / `null` | no boolean type |
| `x ** y` | `pow of [x, y]` | `^` is XOR |
| `len(x)` | `len of x` | builtin, same name |
Expand Down Expand Up @@ -411,7 +411,7 @@ print of (word_lengths of (["ada", "grace"]))
```

(Note the `of (["ada", "grace"])` — parenthesised: parentheses always
make the argument a single value, so the literal list arrives whole
instead of spreading. A bare `of ["ada", "grace"]` would also bind
whole here — single-parameter functions never spread — but the
parenthesised form works for any arity.)
make the argument a single value, so the literal list arrives whole. A
bare `of ["ada", "grace"]` would also work here — two arguments to a
one-parameter function pack back into a list — but the parenthesised
form says "one list" directly and works for any arity.)
1 change: 1 addition & 0 deletions docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ a code's meaning never changes, and retired codes are not reused.
| `W014` | warning | Bare trajectory predicate in a loop condition reads the last-observed binding, but the body assigns two or more bindings — name it (`<predicate> of <var>`). |
| `W015` | warning | A function assigns (without `local`) over a module-level **function** name, clobbering it via mutate-outward so later `<fn> of ...` calls fail — add `local` or rename. (Scoped to function clobbering; benign module-variable reuse is not flagged — that is #404's dataflow-aware territory. `_`-prefixed names are skipped as intentional module state.) |
| `W016` | warning | Bare trajectory predicate **outside a loop condition** (`if stable:`, `ok is converged`, `return diverging`) reads the last-observed binding — an invisible alias (#247/#262) — write `<predicate> of <var>`. Loop conditions are exempt: the single-assign `loop while not converged` form is the documented idiom, and the ambiguous multi-assign case is `W014`. Any explicit subject counts as named, including `stable of (x + 0.0)`; deliberate bare reads carry `# lint: allow W016`. |
| `W017` | warning | Bare 1-element literal arg list: `f of [x]` passes **one argument** — the element, not the list (#405; the pre-#405 rule meant the opposite, so the form reads ambiguously). Write `f of x` for one argument, or `f of ([x])` (#355) to pass a 1-element list. Doubles as the #405 migration audit: `--lint` over a consumer repo surfaces every behavior-changed call site. |

The human linter output carries the code inline:

Expand Down
37 changes: 19 additions & 18 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ EigenScript is dynamically typed. The runtime types are:
```eigenscript
print of (type of 1)
print of (type of "a")
print of (type of [1])
print of (type of [1, 2])
print of (type of {"k": 1})
print of (type of null)
print of (type of print)
Expand Down Expand Up @@ -576,13 +576,14 @@ print of app.users[1].id
value; falling off the end returns `null`. Calling conventions:

- `f of x` — one argument.
- `f of [a, b, c]` — a **bare literal** list with 2+ elements spreads
into the parameters.
- `f of (x)` — parenthesised single argument (required for one-argument
calls to multi-parameter functions — see the warning below). This
also works for literal lists: `f of ([a, b])` binds the whole list
`[a, b]` as the single argument — parentheses always mean "one
argument", so only a *bare* literal list ever spreads.
- `f of [a, b, c]` — a **bare literal** list after `of` is always an
argument list, at every element count: `f of []` is zero arguments,
`f of [x]` is one argument (x itself, not a 1-element list),
`f of [a, b]` is two.
- `f of (x)` — parenthesised single argument. This also works for
literal lists: `f of ([a, b])` binds the whole list `[a, b]` as the
single argument — parentheses always mean "one argument", so only a
*bare* literal list is ever an argument list.
- `f of null` — call with no meaningful argument.

```eigenscript
Expand All @@ -600,12 +601,12 @@ print of (shout of "hey")
hey!
```

**Spread warning:** a literal list with exactly **one** element does
not spread — `f of [x]` binds the whole list `[x]` to the first
parameter. For one-argument calls to multi-parameter (including
defaulted) functions, write `f of (x)`. To pass a literal 2+-element
list whole to a multi-parameter function, parenthesise it:
`f of ([a, b])`.
**One rule, one sentence:** brackets after `of` are an argument list;
parentheses are one argument. So `f of [x]` and `f of x` are the same
one-argument call, and `f of ([x])` passes a literal 1-element list
whole. (Before #405, `f of [x]` bound the whole list `[x]` to the
first parameter — lint `W017` flags the historically ambiguous
1-element form and names both unambiguous spellings.)

```eigenscript
define first(a, b) as:
Expand All @@ -617,7 +618,7 @@ print of (first of ([10, 20]))
```
```output
10
list
num
[10, 20]
```

Expand Down Expand Up @@ -677,14 +678,14 @@ print of n
5
```

Recursion works as expected. Note the parenthesised recursive call
`fib of (m - 1)`, not `fib of [m - 1]`:
Recursion works as expected, including the bracketed recursive call
`fib of [m - 1]` (one argument — see the call rule above):

```eigenscript
define fib(m) as:
if m < 2:
return m
return (fib of (m - 1)) + (fib of (m - 2))
return (fib of [m - 1]) + (fib of [m - 2])

print of (fib of 10)
```
Expand Down
49 changes: 29 additions & 20 deletions lib/eigen.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# EigenScript implemented in EigenScript. Evaluator VALUES are at parity with
# the C evaluator: and/or short-circuit returns the deciding operand, unbound
# identifiers raise, arithmetic / comparison / truthiness match, and the call
# spread rule matches (#338: only a LITERAL list argument with 2+ elements
# spreads into parameters; a variable list binds whole to the first parameter;
# missing parameters bind to null).
# rule matches (#405: a LITERAL list argument node is always the argument
# list, at every element count; a variable list binds whole to the first
# parameter; missing parameters bind to null).
# UNSUPPORTED SYNTAX FAILS LOUDLY (#338): the tokenizer raises on any
# character it doesn't lex — compound assignment (`+=` family) and bitwise
# operators (`& | ^ ~`) are not implemented here, so they raise a tokenize
Expand Down Expand Up @@ -1309,24 +1309,33 @@ define eigen_eval(node, env) as:
params is fn[2]
body is fn[3]
call_env is _env_new of env
# C-parity spread rule (#338): only a LITERAL list
# argument with 2+ elements spreads into parameters —
# a list arriving via a variable (or a 1-element
# literal) binds whole to the first parameter. The C
# compiler decides this from the call-site AST, so we
# check the arg NODE (node[2]), not the runtime value.
# Missing parameters bind to null, as the C VM does.
spread is 0
if (len of params) > 1:
if node[2][0] == "list":
if (len of (node[2][1])) > 1:
spread is 1
if spread == 1:
for pi in range of (len of params):
if pi < (len of arg):
_env_set_local of [call_env, params[pi], arg[pi]]
# C-parity call rule (#405): a LITERAL list argument
# node is ALWAYS the argument list — argc is its
# element count at every size ([x] passes x itself).
# Binding then mirrors the C VM: each param takes one
# argument (missing -> null); a 1-param callee packs
# 2+ arguments back into a list, and `f of []` keeps
# the #154 1-param empty-list bind. A list arriving
# via a variable (or parenthesized, #355) binds whole
# to the first parameter. The C compiler decides from
# the call-site AST, so we check the arg NODE
# (node[2]), not the runtime value.
if node[2][0] == "list":
argc is len of arg
if (len of params) == 1:
if argc == 1:
_env_set_local of [call_env, params[0], arg[0]]
else:
_env_set_local of [call_env, params[pi], null]
if argc == 0:
_env_set_local of [call_env, params[0], []]
else:
_env_set_local of [call_env, params[0], arg]
else:
for pi in range of (len of params):
if pi < argc:
_env_set_local of [call_env, params[pi], arg[pi]]
else:
_env_set_local of [call_env, params[pi], null]
else:
_env_set_local of [call_env, params[0], arg]
fill is 1
Expand Down
2 changes: 1 addition & 1 deletion lib/validate.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ define is_number(value) as:
if (len of value) == 1:
return 0
start is 1
for i in range of [(len of value) - start]:
for i in range of ((len of value) - start):
c is char_at of [value, i + start]
if c == ".":
if has_dot == 1:
Expand Down
17 changes: 6 additions & 11 deletions src/compiler.c
Original file line number Diff line number Diff line change
Expand Up @@ -2100,17 +2100,12 @@ static void compile_node_inner(Compiler *c, ASTNode *node) {

compile_node(c, fn_node);

if (arg_node && arg_node->type == AST_LIST && !arg_node->parenthesized &&
arg_node->data.list.count == 0) {
/* Zero-arg call: f of [] — required so default-param fns can
* be called with no args and let every default fire. */
emit_call(c, 0, node->line);
} else if (arg_node && arg_node->type == AST_LIST && !arg_node->parenthesized &&
arg_node->data.list.count > 1) {
/* #355: the spread applies to a BARE literal list only — a
* parenthesized list `f of ([a, b])` is one argument, giving
* literals the same escape hatch variables have. */
/* Multi-arg: compile each element as separate stack value */
if (arg_node && arg_node->type == AST_LIST && !arg_node->parenthesized) {
/* #405: a bare literal list after `of` is ALWAYS an argument
* list, at every count — `f of []` is zero args, `f of [x]`
* is one arg (x itself, not a 1-element list), `f of [a, b]`
* is two. #355's parenthesized form `f of ([x])` remains the
* pass-a-literal-list-whole escape hatch. */
for (int i = 0; i < arg_node->data.list.count; i++)
compile_node(c, arg_node->data.list.elems[i]);
emit_call(c, (uint16_t)arg_node->data.list.count, node->line);
Expand Down
Loading
Loading