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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,12 @@ x` is the value x held before its latest assignment. `what is x at
<line>` 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
Expand Down
20 changes: 19 additions & 1 deletion src/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" </dev/null 2>&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
Expand Down
10 changes: 10 additions & 0 deletions tests/test_soft_keyword_idents.eigs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading