diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a2c5c..d13e8c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,31 @@ All notable changes to EigenScript are documented here. being recomputed by render and click separately. ### Fixed +- **`prev of` now binds unary-or-tighter like every other `of` (#634).** + `prev of x + 1` parsed as `prev of (x + 1)` — the operand parser used + `parse_expression` where every other `of` uses `parse_unary` — and + silently returned null. It is now `(prev of x) + 1`, matching the spec's + Application rule. `prev of` also requires a variable name now: a non-name + operand (literal, index/dot, parenthesised expr) has no assignment history + and is a clean parse error instead of a silent null. +- **Source line numbers wrapped at 65536 (#630).** The `OP_LINE` bytecode + operand was 16-bit, so every line the VM tracked was `line % 65536`. Two + assignments 65536 lines apart collapsed onto one stamp and `what is x at L` + returned the wrong value at rc=0; runtime-error/stack-trace lines and the + trace-tape line context were also silently wrong past line 65535 (the + regime generated programs reach). Widened to 32-bit; no opcode number or + on-disk format changed. +- **`relu`/`leaky_relu`/`softmax`/`log_softmax` returned a silent null on a + scalar (#632).** `tensor_to_flat` reports 0 dims for a `VAL_NUM`, and the + four did `if (!flat) return make_null()`, so the null poisoned downstream + arithmetic far from the cause. Each now has a scalar fast path (the + degenerate element-wise case), matching `sqrt`/`exp`/`log`. +- **A modal dialog blocked the mouse but not the keyboard (#631).** Keyboard + focus never consulted `_ui.modal_stack`, so Tab could reach a widget behind + an open dialog and Enter/Space would activate it — a confirmation dialog + could fire the destructive action it guards. `dispatch` now rebinds to the + top modal before routing keys, and `push_modal`/`pop_modal` scope the focus + list to the dialog (which also makes a dialog's own children Tab-able, #575). - **README's `unobserved` example showed the inert half of a real optimisation (#655).** Reported by email with a full repro. The section paired a dict-field example (`game.px is game.px + game.vx * DT`) with diff --git a/docs/SPEC.md b/docs/SPEC.md index 666b429..5dcc9f9 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -1150,6 +1150,12 @@ x` is the value x held before its latest assignment. `what is x at ` reads the value x had at a source line. (History recording turns on automatically when a program contains a temporal query.) +`prev of` takes a **variable name** — it looks back through that binding's +history — and binds like any other `of` (tighter than arithmetic, per the +Application rule above): `prev of x + 1` is `(prev of x) + 1`. A non-name +operand (a literal, an index/dot, or a parenthesised expression) has no +trajectory and is a parse error. + ```eigenscript score is 10 score is 25 diff --git a/src/parser.c b/src/parser.c index 2e5ccbe..028191f 100644 --- a/src/parser.c +++ b/src/parser.c @@ -566,6 +566,7 @@ static void compound_to_op(TokType t, char op[4]) { } static ASTNode* parse_expression(Parser *p); +static ASTNode* parse_unary(Parser *p); /* #634: parse_primary's `prev of` operand */ static ASTNode* parse_statement(Parser *p); static int chain_too_deep(Parser *p); @@ -731,7 +732,24 @@ static ASTNode* parse_primary(Parser *p) { p_advance(p); if (p_cur(p)->type == TOK_OF) { p_advance(p); - ASTNode *expr = parse_expression(p); + /* #634: unary-or-tighter, like every other `of` (parse_unary at + * the OF handler) — so `prev of x + 1` is `(prev of x) + 1`, not + * `prev of (x + 1)` (which silently returned null). The `at` + * suffix below still takes a full expression. */ + ASTNode *expr = parse_unary(p); + /* #634: prev looks back through a variable's assignment history, so + * the operand must be a bare name. A literal, index, dot, or + * parenthesised expression has no trajectory — it used to return + * null silently (the parser's own "only IDENT operands are + * meaningful" comment). Raise instead of tolerating it. */ + if (expr && expr->type != AST_IDENT) { + fprintf(stderr, + "Parse error line %d: 'prev of' requires a variable name\n", + t->line); + eigs_record_first_error(t->line, + "'prev of' requires a variable name"); + g_parse_errors++; + } ASTNode *at_expr = NULL; if (p_cur(p)->type == TOK_AT) { p_advance(p); diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 4d56b74..610f0d0 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -2386,6 +2386,29 @@ else fi echo "" +# [70b] prev-of operand must be a bare name (#634). 'prev of' looks back +# through a variable's assignment history, so a non-name operand (a literal, +# a parenthesised expression, an index/dot) has no trajectory. It used to +# return null silently; now it is a clean parse error, and the program does +# not run. (The precedence half — `prev of x + 1` = `(prev of x) + 1` — is +# asserted in test_soft_keyword_idents.eigs SK19.) +echo "[70b] prev-of requires a variable name (#634)" +PRV_FILE=$(mktemp /tmp/eigs_prev634_XXXX.eigs) +printf 'x is 5\nx is 9\nr is prev of (x + 1)\nprint of "should-not-run"\n' > "$PRV_FILE" +PRV_OUT=$(./eigenscript "$PRV_FILE" &1); PRV_RC=$? +TOTAL=$((TOTAL + 1)) +if [ "$PRV_RC" -ne 0 ] \ + && echo "$PRV_OUT" | grep -q "requires a variable name" \ + && ! echo "$PRV_OUT" | grep -q "should-not-run"; then + PASS=$((PASS + 1)) + echo " PASS: prev of a non-name is a parse error, program does not run" +else + FAIL=$((FAIL + 1)) + echo " FAIL: prev-of non-name (rc=$PRV_RC)" + echo "$PRV_OUT" | head -3 +fi +rm -f "$PRV_FILE" + # Temporal correctness under JIT/OSR: a deep loop's `at`/`state_at` must not # freeze at the OSR point (OP_LINE must stamp g_trace_current_line in the JIT). check_eigs_suite "JIT temporal at/state_at under OSR (g_trace_current_line)" test_jit_temporal_osr.eigs "All tests passed" 2 diff --git a/tests/test_soft_keyword_idents.eigs b/tests/test_soft_keyword_idents.eigs index 60a175d..4e73993 100644 --- a/tests/test_soft_keyword_idents.eigs +++ b/tests/test_soft_keyword_idents.eigs @@ -74,4 +74,14 @@ sk_v is 1 sk_v is 5 assert of [(prev of sk_v) == 1, "SK18 prev of stays temporal with prev bound"] +# SK19 (#634): prev of binds unary-or-tighter like every other `of`, so +# trailing arithmetic is NOT absorbed. Before the fix `prev of sk_w + 1` +# parsed as `prev of (sk_w + 1)` and silently returned null. sk_w's value +# before its last assignment is 10. +sk_w is 10 +sk_w is 20 +assert of [(prev of sk_w + 1) == 11, "SK19 prev of binds tighter than + (unparenthesized)"] +assert of [(prev of sk_w * 2) == 20, "SK19b prev of binds tighter than *"] +assert of [(prev of sk_w - 3) == 7, "SK19c prev of binds tighter than -"] + print of "soft keyword idents: all passed"