diff --git a/CHANGELOG.md b/CHANGELOG.md index 66499b3..4e99bca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,41 @@ All notable changes to EigenScript are documented here. ## [Unreleased] +### Added + +- **Lint `W022`: over-arity literal calls (#733).** With + `define two(a, b)`, `two of [1, 2, 99]` binds `a=1, b=2` and silently + DISCARDS `99` — rc=0, no diagnostic at any stage (under-arity at + least null-fills and tends to fail later on a type error). W022 flags + a bare literal argument list with more elements than the callee's + parameters, conservatively: only when the callee name has exactly one + `define` in the file and no other binding anywhere (assignment, + param, loop/comprehension var, catch name, list-pattern name, import, + or a match-pattern identifier all poison it). One-parameter callees + never fire — a 2+-element bare list binds WHOLE to a single param by + the #405 arity-1 carve-out (nothing is dropped; it's the variadic + idiom), and `define f()`/`define f` both carry the implicit `n`, so + every define has arity ≥ 1. Built on a new `LINT_FOR_EACH_CHILD` + generic AST child iterator whose switch has no `default:` arm, so + `-Werror=switch` forces new node kinds to update it. + ### Fixed +- **The AI-facing spread-rule docs now state the arity-sensitive rule + the runtime actually has (#733).** `docs/llms.txt`'s "#1 TRAP" + paragraph still described the PRE-#405 semantics (claimed + `f of [x]` binds the whole list — the opposite of today's runtime), + and its "which spreads" framing predicted the wrong binding for every + 2+-element call to a 1-parameter function (`one of [5, 6]` binds + `a = [5, 6]`, not `a = 5`). The call section now states both halves — + bare-list-is-an-arg-list at the call site, arity-1 re-collection at + the binding — plus the silent over-arity behavior and W022. + `CLAUDE.md`'s always-on rule gains the same carve-out sentence + (docs/SPEC.md already had it). llms.txt also gains the four gaps the + #733 experiment hit: `lib/` is NOT ambient (`import list` namespaced + vs `load_file of "lib/list.eigs"` bare), lambda syntax, `sort_by` + (a builtin, no import), and the comprehension `if` clause. + - **The leftover-token parse error diagnoses the actual mistake instead of always saying "one statement per line" (#732).** The four most likely model/newcomer mistakes — `print("hello")`, `def f(a):`, diff --git a/CLAUDE.md b/CLAUDE.md index fa658f8..ad8182e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,12 @@ Always-on: 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: + form (pre-#405 it meant the opposite). **Arity-1 carve-out (#733)**: + that rule describes the call site, not the binding — a 1-parameter + callee re-collects a 2+-element arg list WHOLE (`one of [5, 6]` binds + `a = [5, 6]`, not `a = 5`; this is what keeps `len of [1, 2]` + working). Over-arity on 2+-param callees is silently dropped — W022 + flags it for same-file callees. (More `.eigs`-writing gotchas: the `write-eigenscript` skill.) - **A semantics change must update `docs/SPEC.md` + `docs/COMPARISON.md` in the same PR** — `tests/test_doc_examples.py` runs their example/output diff --git a/docs/DIAGNOSTICS.md b/docs/DIAGNOSTICS.md index 62c8c8f..a04bf3a 100644 --- a/docs/DIAGNOSTICS.md +++ b/docs/DIAGNOSTICS.md @@ -241,6 +241,7 @@ a code's meaning never changes, and retired codes are not reused. | `W019` | warning | An interrogative used as a **bare statement** — `why is "..."`, `what is x`, `prev of y` at statement level — evaluates and **discards** its result: a silent no-op (#583). Question words (`what/who/when/where/why/how`) cannot be assigned with `is` — the "assignment" is the interrogative expression form — so when a same-named binding exists in scope the statement is almost certainly a mistaken assignment (the real hit: a catch handler "reassigning" a `local why` that silently kept its stale value). An interrogative inside an expression (`print of (why is x)`, `r is prev of y`) is never flagged. Extended by #736 to the observer **query** forms over an ident — `report of x`, `report_value of x`, `observe of x`, `trajectory of x` at statement level — which are the same silent no-op through the other door: they print nothing and raise nothing, so silence is indistinguishable from "the observer had nothing to say". Zero false positives by construction: over an ident these are compiler-resolved special forms that never reach a user function even when one shadows the name (#459, see `W013`), and a non-ident argument (`report of (x + 0.0)`) is an ordinary call the check never sees. | | `W020` | warning | An `unobserved:` block in which **every** assignment targets a dict field or list element (`d.k is ...`, `xs[i] is ...`) — a provable no-op (#655). Observer bookkeeping is gated on the named env path, so a dict field is never observed and there is nothing to skip; the in-place numeric mutation the block used to enable became unconditional with NaN-boxing B-3a (`dict_set_cached_immediate`), so it costs nothing to drop the block. This shape was true when written and expired silently, which is why it needs a lint — our own README shipped the dead form as its headline example for two months. Conservative by construction: `g_unobserved_depth` is a global, so a **call** inside the block runs the callee unobserved too and suppresses the warning; any plain-variable assignment, or a name-binding form (`for` / listcomp / `catch` / `match`), also suppresses it. Only a provably inert block fires. | | `W021` | hint | Function definition shadows a **public stdlib function** from a module the file never imported (`define 'median' shadows lib/stats.eigs 'median' (import stats to use it)`) — a discoverability nudge toward `lib/*.eigs` (#591), sibling of `W013` (which covers compiled-in builtins; a name that is both stays `W013`-only). The name table is scraped from the public top-level defines of the same `lib/` directories the import resolver searches; the hint stays silent when the module is imported, and when the linted file *is* the module that ships the name. Name-only matching has false positives (a deliberately-different local `mean`), so this is hint-severity: advisory, **never fails `--lint`** under either `--lint-level`, and suppressible like any other code. | +| `W022` | warning | A bare literal argument list with **more elements than the callee's parameters** — with `define two(a, b)`, `two of [1, 2, 99]` binds `a=1, b=2` and silently DISCARDS `99` (rc=0, no diagnostic at any stage; under-arity at least null-fills and tends to fail later) (#733). Conservative by construction: fires only when the callee name provably has one meaning in the file — exactly one `define` of it anywhere and no other binding (assignment, param, lambda param, loop/comprehension var, catch name, list-pattern name, import, or any identifier inside a `match` pattern poisons the name). One-parameter callees are exempt by the #405 semantics themselves: a 2+-element bare list binds WHOLE to a single parameter — nothing is dropped, and that shape is the deliberate variadic idiom (`reverse of [1, 2, 3]`). Parenthesized lists (`f of ([...])`) are a single argument and never fire. | The human linter output carries the code inline: diff --git a/docs/llms.txt b/docs/llms.txt index b562f79..3e2da8c 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -11,14 +11,24 @@ ## Calls use `of`, never `f(x)` - Apply a function or builtin with `of`: `print of x`, `sqrt of n`, `len of xs`. -- Multiple arguments are passed as a list, which spreads: `f of [a, b]` calls `f(a, b)`. -- **THE #1 TRAP: `f of [x]` (a ONE-element list) does NOT spread** — it binds the - whole list to the first parameter. For a single argument to a multi-parameter - (or defaulted) function, write `f of (x)`. A bare single value `f of x` is - also fine. This silently produces wrong results, not an error. +- A **bare literal list** after `of` is an argument list at every element + count: `f of []` zero arguments, `f of [x]` ONE argument (x itself, not a + 1-element list), `f of [a, b]` two. +- **THE #1 TRAP — the receiving side is arity-sensitive.** A 1-parameter + callee has only one slot, so a 2+-element argument list does NOT + distribute into it: the whole list re-collects and binds to that one + parameter. With `define one(a)`: `one of [5]` binds `a = 5`, but + `one of [5, 6]` binds `a = [5, 6]` (not `a = 5` with `6` dropped). This + carve-out is what makes `len of [1, 2]` return `2` and + `reverse of [1, 2, 3]` reverse the list — but it means you cannot predict + a binding from the call site alone; you need the callee's arity. +- **Over-arity is SILENT** on 2+-parameter callees: with `define two(a, b)`, + `two of [1, 2, 99]` drops `99` with no error at any stage (lint `W022` + catches it when the callee is defined in the same file). Under-arity + null-fills, also silently. - **Parentheses always mean exactly one argument**: `f of ([a, b])` passes the - literal 2-element list as one argument (the escape hatch). `f of []` is the - zero-argument form. + literal 2-element list as one argument (the escape hatch), and `f of (x)` + is an unambiguous single argument. - `of` binds tighter than infix arithmetic: `sqrt of x + 1` means `(sqrt of x) + 1`. Parenthesize the operand when you mean otherwise: `sqrt of (x + 1)`. @@ -117,6 +127,10 @@ print of ("sqrt(2) = " + (str of (newton_sqrt of 2))) # sqrt(2) = 1.4142135623 xs is [1, 2, 3] append of [xs, 4] # in-place doubled is [v * 2 for v in xs] # comprehension (its var leaks!) +evens is [v for v in xs if v % 2 == 0] # comprehension filter clause +inc is (x) => x + 1 # lambda: params NEED the parens +add is (a, b) => a + b +srt is sort_by of [xs, (v) => 0 - v] # sort by key fn (builtin, no import) d is {"a": 1, "b": 2} print of d["a"] @@ -149,10 +163,19 @@ match x: ## Before hand-rolling a helper The runtime has ~255 builtins and `lib/` covers a lot. `ord`/`chr`/`str_lower`/ -`str_upper`/`char_at`/`index_of`/`hex of [n, nibbles]` are builtins (there is no -bare `lower` — it's `str_lower`). `lib/` has `string.pad_left`, `format.fmt_*`, -`checksum.crc32`, and civil-date math in `datetime`. See docs/BUILTINS.md and -docs/STDLIB.md before writing your own. +`str_upper`/`char_at`/`index_of`/`hex of [n, nibbles]`/`sort_by` are builtins +(there is no bare `lower` — it's `str_lower`; `sort_by of [items, key_fn]` is +the record-shaped sort and needs no import). `lib/` has `string.pad_left`, +`format.fmt_*`, `checksum.crc32`, civil-date math in `datetime`, and +`list.filter`/`list.reduce`. **`lib/` functions are NOT ambient** — reach them +one of two ways: + +```eigenscript +import list # namespaced: list.filter of [xs, fn] +load_file of "lib/list.eigs" # bare names: filter of [xs, fn] +``` + +See docs/BUILTINS.md and docs/STDLIB.md before writing your own. ## The generation-validation ladder (always run what you generate) diff --git a/src/lint.c b/src/lint.c index e68185c..d9c4681 100644 --- a/src/lint.c +++ b/src/lint.c @@ -2098,6 +2098,267 @@ static void check_one_element_arg_list(ASTNode *ast, LintContext *ctx) { w017_scan(ast, ctx); } +/* ---- W022 (#733): literal arg list longer than the callee's params ---- */ + +/* Generic direct-child iterator: expands STMT once per non-NULL child of + * n, with `childvar` bound to it. The switch has NO default arm on + * purpose — -Werror=switch then forces a new ASTType to be added here, + * instead of silently pruning that subtree from every walk using this. */ +#define LINT_FOR_EACH_CHILD(n, childvar, STMT) do { \ + ASTNode *childvar; \ + int _lfe_i, _lfe_j; \ + (void)_lfe_i; (void)_lfe_j; \ + switch ((n)->type) { \ + case AST_NUM: case AST_STR: case AST_IDENT: case AST_NULL: \ + case AST_PREDICATE: case AST_BREAK: case AST_CONTINUE: case AST_IMPORT: \ + break; \ + case AST_BINOP: \ + if ((childvar = (n)->data.binop.left)) { STMT; } \ + if ((childvar = (n)->data.binop.right)) { STMT; } \ + break; \ + case AST_UNARY: \ + if ((childvar = (n)->data.unary.operand)) { STMT; } \ + break; \ + case AST_ASSIGN: \ + if ((childvar = (n)->data.assign.expr)) { STMT; } \ + break; \ + case AST_RELATION: \ + if ((childvar = (n)->data.relation.left)) { STMT; } \ + if ((childvar = (n)->data.relation.right)) { STMT; } \ + break; \ + case AST_IF: \ + if ((childvar = (n)->data.cond.cond)) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.cond.if_count; _lfe_i++) \ + if ((childvar = (n)->data.cond.if_body[_lfe_i])) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.cond.else_count; _lfe_i++) \ + if ((childvar = (n)->data.cond.else_body[_lfe_i])) { STMT; } \ + break; \ + case AST_LOOP: \ + if ((childvar = (n)->data.loop.cond)) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.loop.body_count; _lfe_i++) \ + if ((childvar = (n)->data.loop.body[_lfe_i])) { STMT; } \ + break; \ + case AST_FUNC: \ + if ((n)->data.func.param_defaults) \ + for (_lfe_i = 0; _lfe_i < (n)->data.func.param_count; _lfe_i++) \ + if ((childvar = (n)->data.func.param_defaults[_lfe_i])) \ + { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.func.body_count; _lfe_i++) \ + if ((childvar = (n)->data.func.body[_lfe_i])) { STMT; } \ + break; \ + case AST_RETURN: \ + if ((childvar = (n)->data.ret.expr)) { STMT; } \ + break; \ + case AST_BLOCK: case AST_UNOBSERVED: \ + for (_lfe_i = 0; _lfe_i < (n)->data.block.count; _lfe_i++) \ + if ((childvar = (n)->data.block.stmts[_lfe_i])) { STMT; } \ + break; \ + case AST_LIST: \ + for (_lfe_i = 0; _lfe_i < (n)->data.list.count; _lfe_i++) \ + if ((childvar = (n)->data.list.elems[_lfe_i])) { STMT; } \ + break; \ + case AST_INDEX: \ + if ((childvar = (n)->data.index.target)) { STMT; } \ + if ((childvar = (n)->data.index.index)) { STMT; } \ + break; \ + case AST_LISTCOMP: \ + if ((childvar = (n)->data.listcomp.expr)) { STMT; } \ + if ((childvar = (n)->data.listcomp.iter)) { STMT; } \ + if ((childvar = (n)->data.listcomp.filter)) { STMT; } \ + break; \ + case AST_FOR: \ + if ((childvar = (n)->data.forloop.iter)) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.forloop.body_count; _lfe_i++) \ + if ((childvar = (n)->data.forloop.body[_lfe_i])) { STMT; } \ + break; \ + case AST_PROGRAM: \ + for (_lfe_i = 0; _lfe_i < (n)->data.program.count; _lfe_i++) \ + if ((childvar = (n)->data.program.stmts[_lfe_i])) { STMT; } \ + break; \ + case AST_INTERROGATE: \ + if ((childvar = (n)->data.interrogate.expr)) { STMT; } \ + if ((childvar = (n)->data.interrogate.at_expr)) { STMT; } \ + break; \ + case AST_TRY: \ + for (_lfe_i = 0; _lfe_i < (n)->data.trycatch.try_count; _lfe_i++) \ + if ((childvar = (n)->data.trycatch.try_body[_lfe_i])) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.trycatch.catch_count; _lfe_i++) \ + if ((childvar = (n)->data.trycatch.catch_body[_lfe_i])) { STMT; }\ + break; \ + case AST_DICT: \ + for (_lfe_i = 0; _lfe_i < (n)->data.dict.count; _lfe_i++) { \ + if ((childvar = (n)->data.dict.keys[_lfe_i])) { STMT; } \ + if ((childvar = (n)->data.dict.vals[_lfe_i])) { STMT; } \ + } \ + break; \ + case AST_DOT: \ + if ((childvar = (n)->data.dot.target)) { STMT; } \ + break; \ + case AST_DOT_ASSIGN: \ + if ((childvar = (n)->data.dot_assign.target)) { STMT; } \ + if ((childvar = (n)->data.dot_assign.expr)) { STMT; } \ + break; \ + case AST_INDEX_ASSIGN: \ + if ((childvar = (n)->data.index_assign.target)) { STMT; } \ + if ((childvar = (n)->data.index_assign.index)) { STMT; } \ + if ((childvar = (n)->data.index_assign.expr)) { STMT; } \ + break; \ + case AST_MATCH: \ + if ((childvar = (n)->data.match.expr)) { STMT; } \ + for (_lfe_i = 0; _lfe_i < (n)->data.match.case_count; _lfe_i++) { \ + if ((childvar = (n)->data.match.patterns[_lfe_i])) { STMT; } \ + for (_lfe_j = 0; _lfe_j < (n)->data.match.body_counts[_lfe_i]; \ + _lfe_j++) \ + if ((childvar = (n)->data.match.bodies[_lfe_i][_lfe_j])) \ + { STMT; } \ + } \ + break; \ + case AST_LAMBDA: \ + if ((childvar = (n)->data.lambda.body)) { STMT; } \ + break; \ + case AST_LIST_PATTERN_ASSIGN: \ + if ((childvar = (n)->data.list_pattern_assign.expr)) { STMT; } \ + break; \ + case AST_SLICE: \ + if ((childvar = (n)->data.slice.target)) { STMT; } \ + if ((childvar = (n)->data.slice.start)) { STMT; } \ + if ((childvar = (n)->data.slice.end)) { STMT; } \ + break; \ + } \ +} while (0) + +/* Over-arity is silent at runtime: `two of [1, 2, 99]` binds a=1, b=2 and + * DISCARDS 99 — rc=0, no diagnostic at any stage (under-arity at least + * null-fills, which tends to fail later; over-arity returns a plausible + * answer). This is the silent-wrong-answer class, so it gets a lint. + * + * Conservative by construction — the warning fires only when the callee + * name provably has one meaning in this file: exactly one `define` of the + * name anywhere, and NO other binding of it (assignment, param, lambda + * param, loop/comprehension var, catch name, list-pattern name, import, + * or an identifier inside a match pattern). Anything else drops the name + * from the table (false negatives are fine for a lint; a false positive + * is not). One-parameter callees are exempt by the #405 semantics + * themselves: a 2+-element bare list binds WHOLE to a single param — + * nothing is dropped, and that shape is the deliberate variadic idiom + * (`reverse of [1, 2, 3]`). */ + +typedef struct { char *name; int param_count; int defs; int poisoned; } W022Fn; +typedef struct { W022Fn *fns; int count; int cap; } W022Table; + +static W022Fn* w022_find(W022Table *t, const char *name) { + for (int i = 0; i < t->count; i++) + if (strcmp(t->fns[i].name, name) == 0) return &t->fns[i]; + return NULL; +} + +static void w022_poison(W022Table *t, const char *name) { + W022Fn *f = name ? w022_find(t, name) : NULL; + if (f) f->poisoned = 1; +} + +static void w022_collect_defines(ASTNode *n, W022Table *t) { + if (!n) return; + if (n->type == AST_FUNC && n->data.func.name) { + W022Fn *f = w022_find(t, n->data.func.name); + if (f) { + f->defs++; + } else { + if (t->count == t->cap) { + t->cap = t->cap ? t->cap * 2 : 16; + t->fns = realloc(t->fns, (size_t)t->cap * sizeof(W022Fn)); + } + t->fns[t->count].name = strdup(n->data.func.name); + t->fns[t->count].param_count = n->data.func.param_count; + t->fns[t->count].defs = 1; + t->fns[t->count].poisoned = 0; + t->count++; + } + } + LINT_FOR_EACH_CHILD(n, child, w022_collect_defines(child, t)); +} + +/* Poison every NON-define binding of a collected name. Match patterns are + * walked as opaque expressions: any identifier inside one poisons (a + * pattern ident may bind depending on shape — conservative either way). */ +static void w022_poison_idents(ASTNode *n, W022Table *t) { + if (!n) return; + if (n->type == AST_IDENT) w022_poison(t, n->data.ident.name); + LINT_FOR_EACH_CHILD(n, child, w022_poison_idents(child, t)); +} + +static void w022_collect_bindings(ASTNode *n, W022Table *t) { + if (!n) return; + switch (n->type) { + case AST_ASSIGN: + w022_poison(t, n->data.assign.name); + break; + case AST_LIST_PATTERN_ASSIGN: + for (int i = 0; i < n->data.list_pattern_assign.name_count; i++) + w022_poison(t, n->data.list_pattern_assign.names[i]); + break; + case AST_FUNC: + for (int i = 0; i < n->data.func.param_count; i++) + w022_poison(t, n->data.func.params[i]); + break; + case AST_LAMBDA: + for (int i = 0; i < n->data.lambda.param_count; i++) + w022_poison(t, n->data.lambda.params[i]); + break; + case AST_FOR: + w022_poison(t, n->data.forloop.var); + break; + case AST_LISTCOMP: + w022_poison(t, n->data.listcomp.var); + break; + case AST_TRY: + w022_poison(t, n->data.trycatch.err_name); + break; + case AST_IMPORT: + w022_poison(t, n->data.import.module_name); + break; + case AST_MATCH: + for (int c = 0; c < n->data.match.case_count; c++) + w022_poison_idents(n->data.match.patterns[c], t); + break; + default: + break; + } + LINT_FOR_EACH_CHILD(n, child, w022_collect_bindings(child, t)); +} + +static void w022_scan(ASTNode *n, W022Table *t, LintContext *ctx) { + if (!n) return; + if (n->type == AST_RELATION) { + ASTNode *fn = n->data.relation.left; + ASTNode *arg = n->data.relation.right; + if (fn && fn->type == AST_IDENT && + arg && arg->type == AST_LIST && !arg->parenthesized) { + W022Fn *f = w022_find(t, fn->data.ident.name); + if (f && f->defs == 1 && !f->poisoned && + f->param_count != 1 && + arg->data.list.count > f->param_count) { + lint_warn(ctx, arg->line, "W022", + "'%s of [...]' passes %d arguments but '%s' takes %d — " + "the extras are silently dropped (write '%s of ([...])' " + "to pass the list whole)", + f->name, arg->data.list.count, f->name, + f->param_count, f->name); + } + } + } + LINT_FOR_EACH_CHILD(n, child, w022_scan(child, t, ctx)); +} + +static void check_over_arity(ASTNode *ast, LintContext *ctx) { + W022Table t = {NULL, 0, 0}; + w022_collect_defines(ast, &t); + w022_collect_bindings(ast, &t); + w022_scan(ast, &t, ctx); + for (int i = 0; i < t.count; i++) free(t.fns[i].name); + free(t.fns); +} + /* ---- W020 (#655): unobserved: block that provably does nothing ---- */ /* `unobserved:` skips observer bookkeeping, which is gated on the NAMED env @@ -3058,6 +3319,7 @@ static void lint_run_checks(ASTNode *ast, const char *path, check_outer_mutation(ast, ctx); check_bare_predicate_alias(ast, ctx); check_one_element_arg_list(ast, ctx); + check_over_arity(ast, ctx); check_dead_unobserved(ast, ctx); check_error_kind_typo(ast, ctx); #if !EIGENSCRIPT_FREESTANDING diff --git a/tests/test_lint.sh b/tests/test_lint.sh index 1c7bbe2..9546416 100644 --- a/tests/test_lint.sh +++ b/tests/test_lint.sh @@ -1353,6 +1353,73 @@ check_contains "#780 W001 fires in the fallback match arm" "$OUTPUT" "W001.*'w00 rm -f "$TMPFILE" rm -f "$TMPFILE" +# --- W022 (#733): literal arg list longer than the callee's params --- +# Over-arity is silent at runtime (two of [1, 2, 99] drops 99, rc=0); +# W022 fires only when the callee name provably has one meaning in the +# file (one define, no other binding). + +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +define two(a, b) as: + return a + b +print of (two of [1, 2, 99]) +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "W022 fires on over-arity to a unique define" "$OUTPUT" "W022.*passes 3 arguments but 'two' takes 2" +rm -f "$TMPFILE" + +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +define one(a) as: + return a +define two(a, b) as: + return a + b +print of (one of [5, 6]) +print of (two of [1, 2]) +print of (two of ([1, 2, 3])) +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_not_contains "W022 silent: 1-param variadic, exact arity, parenthesized" "$OUTPUT" "W022" +rm -f "$TMPFILE" + +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +define two(a, b) as: + return a + b +two is 7 +x is two of [1, 2, 99] +print of x +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_not_contains "W022 silent when the name is rebound (poisoned)" "$OUTPUT" "W022" +rm -f "$TMPFILE" + +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +define dup(a, b) as: + return a +define dup(a, b, c) as: + return a +y is dup of [1, 2, 3, 4] +print of y +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_not_contains "W022 silent when the name is defined twice" "$OUTPUT" "W022" +rm -f "$TMPFILE" + +# NOTE: there is no 0-param case — `define f()` / `define f` both get the +# implicit single param `n` (parser.c), so every define has arity >= 1 and +# the arity-1 whole-list exemption covers the "no params" shape too. +TMPFILE=$(mktemp /tmp/lint_test_XXXXXX.eigs) +cat > "$TMPFILE" << 'EIGS' +define three(a, b, c is 9) as: + return a + b + c +print of (three of [1, 2, 3, 4]) +EIGS +OUTPUT=$($EIGS --lint "$TMPFILE" 2>&1 || true) +check_contains "W022 counts defaulted params in the arity" "$OUTPUT" "W022.*passes 4 arguments but 'three' takes 3" +rm -f "$TMPFILE" + echo "" echo "Results: $PASS passed, $FAIL failed, $TOTAL total" exit $FAIL