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

## [Unreleased]

### Fixed
- **Dot access accepts keyword-named fields (#542).** `d.loop`, `d.in`,
`d.when` — any word keyword now parses as a dot key (read, write,
compound assign, any chain depth, all three parser postfix sites).
The position after `.` admits nothing but a field name, so the old
`TOK_IDENT`-only check rejected programs with no ambiguity to protect
against, leaving keys creatable by literal, `dict_set`, and
`json_decode` reachable only by bracket — and contradicted SYNTAX.md's
soft-keyword promise for `d.prev`/`d.at`. Found porting a DAW whose
data model has `clip.loop` / event `when` fields. Suite [118]
(49 checks); SPEC/SYNTAX/GRAMMAR/COMPARISON updated. The ouroboros
`frontend.eigs` mirror lands with the next `EIGS_REF` bump
(deferred-mirror pattern, #326/#328).

### Added
- **`--bundle`: single-file distribution (#413).** `eigenscript
--bundle app.eigs out [--with-tape tape]` copies the runtime binary
Expand Down
5 changes: 3 additions & 2 deletions docs/COMPARISON.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,9 @@ user.field = "computing";
console.log(user.name, Object.keys(user).length);
```

EigenScript — dot access and bracket access are interchangeable;
assignment creates keys:
EigenScript — dot access and bracket access are interchangeable
(keyword-named fields too: `d.loop`, `d.when` — like JavaScript member
access, the position disambiguates); assignment creates keys:

```eigenscript
user is {"name": "Ada", "year": 1815}
Expand Down
8 changes: 7 additions & 1 deletion docs/GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,17 @@ lambda = '(' [ param_list ] ')' '=>' expression
After any primary expression, zero or more postfix operations:

```
postfix = primary { subscript | '.' IDENT }
postfix = primary { subscript | '.' word }
word = IDENT | any keyword ; #542
subscript = '[' expression ']'
| '[' [ expression ] ':' [ expression ] ']' ; slice
```

The dot-key position accepts any word, keywords included: nothing but a
field name can appear after `.`, so there is no ambiguity to protect
against, and keys creatable by literal, `dict_set`, and `json_decode`
(`"loop"`, `"in"`, `"when"`, …) stay reachable by dot.

Note: which postfix forms a primary accepts depends on the primary.
IDENT, dict literals, parenthesized expressions, f-strings (which
desugar to parenthesized expressions), and the soft-keyword identifier
Expand Down
16 changes: 16 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,22 @@ print of app.users[1].id
2
```

Key names are not restricted by the keyword table: after `.` nothing but
a field name can appear, so any word — including keywords like `loop`,
`in`, or `when` (common in `json_decode` output) — works as a dot key,
read or write, at any chain depth.

```eigenscript
ev is {"when": 3, "loop": 1}
ev.loop is ev.loop + 1
print of ev.when
print of ev.loop
```
```output
3
2
```

## Functions

`define name(params) as:` introduces a function. `return` exits with a
Expand Down
5 changes: 5 additions & 0 deletions docs/SYNTAX.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,11 @@ print of config.host # "localhost"
print of config.port # 8080
```

Any word can follow the dot, including keywords — `d.loop`, `d.in`,
`d.when` all work: the position after `.` is unambiguous, so
keyword-named keys (e.g. from `json_decode` of external data) are
reachable by dot as well as by bracket.

**Bracket access:**
```eigenscript
key is "host"
Expand Down
2 changes: 2 additions & 0 deletions src/eigenscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@

typedef enum {
TOK_NUM, TOK_STR, TOK_IDENT,
/* Word keywords: TOK_IS..TOK_LOCAL must stay contiguous — the parser's
* tok_is_dot_key accepts the whole run as dict field names after `.`. */
TOK_IS, TOK_OF, TOK_DEFINE, TOK_AS,
TOK_IF, TOK_ELSE, TOK_ELIF, TOK_LOOP, TOK_WHILE,
TOK_RETURN, TOK_AND, TOK_OR, TOK_NOT,
Expand Down
24 changes: 21 additions & 3 deletions src/parser.c
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,24 @@ static void p_expect_ident_like(Parser *p) {
p_advance(p);
}

/* Dot keys: after `.` nothing but a field name can appear, so the position is
* unambiguous and ANY word may serve as a key — TOK_IDENT plus every word
* keyword (the contiguous TOK_IS..TOK_LOCAL run; all are lexed through the
* identifier path, so str_val always carries the source word). Keys like
* "loop"/"in"/"when" are creatable via literals, dict_set, and json_decode;
* rejecting them here made those fields reachable only by bracket (#542). */
static int tok_is_dot_key(TokType type) {
return type == TOK_IDENT || (type >= TOK_IS && type <= TOK_LOCAL);
}

static void p_expect_dot_key(Parser *p) {
if (!tok_is_dot_key(p_cur(p)->type)) {
p_expect(p, TOK_IDENT); /* raises + advances */
return;
}
p_advance(p);
}

static void p_skip_newlines(Parser *p) {
while (p_cur(p)->type == TOK_NEWLINE) p_advance(p);
}
Expand Down Expand Up @@ -666,7 +684,7 @@ static ASTNode* parse_postfix_chain(Parser *p, ASTNode *n) {
if (p_cur(p)->type == TOK_DOT) {
p_advance(p);
Token *key_tok = p_cur(p);
p_expect(p, TOK_IDENT);
p_expect_dot_key(p);
ASTNode *dot = make_node_col(AST_DOT, p_cur(p)->line, key_tok->col);
dot->data.dot.target = n;
dot->data.dot.key = xstrdup(key_tok->str_val);
Expand Down Expand Up @@ -849,7 +867,7 @@ static ASTNode* parse_primary(Parser *p) {
if (p_cur(p)->type == TOK_DOT) {
p_advance(p);
Token *key_tok = p_cur(p);
p_expect(p, TOK_IDENT);
p_expect_dot_key(p);
ASTNode *dot = make_node_col(AST_DOT, key_tok->line, key_tok->col);
dot->data.dot.target = expr;
dot->data.dot.key = xstrdup(key_tok->str_val);
Expand Down Expand Up @@ -957,7 +975,7 @@ static ASTNode* parse_primary(Parser *p) {
if (p_cur(p)->type == TOK_DOT) {
p_advance(p);
Token *key_tok = p_cur(p);
p_expect(p, TOK_IDENT);
p_expect_dot_key(p);
ASTNode *dot = make_node_col(AST_DOT, p_cur(p)->line, key_tok->col);
dot->data.dot.target = n;
dot->data.dot.key = xstrdup(key_tok->str_val);
Expand Down
7 changes: 7 additions & 0 deletions tests/run_all_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,13 @@ echo "[117] for-in Length Snapshot (#491, 9 checks)"
check_eigs_suite "for-in snapshots length; body mutation is bounded + safe" \
test_for_in_mutation.eigs "FOR_IN_MUTATION_DONE" 9

# [118] Any keyword works as a dot key (#542): keys creatable by literal/
# dict_set/json_decode were unreachable by `.` — read, write, chains, and
# all three parser postfix sites (IDENT chain, paren, dict literal).
echo "[118] Keyword Dot Keys (#542, 49 checks)"
check_eigs_suite "all 39 keywords + chains/json/paren/literal as dot keys" \
test_dict_keyword_keys.eigs "All tests passed" 49

# [23] Named parameters
echo "[23/27] Named Parameters (9 checks)"
NP_OUTPUT=$(./eigenscript ../tests/test_named_params.eigs 2>&1); NP_OUTPUT_RC=$?
Expand Down
162 changes: 162 additions & 0 deletions tests/test_dict_keyword_keys.eigs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Regression #542: any keyword works as a dot key — the position after `.`
# is unambiguous, so keys creatable by literal/dict_set/json_decode must be
# reachable (read AND write, any chain depth) by dot, not just bracket.
load_file of "lib/test.eigs"

# Every word keyword (TOK_IS..TOK_LOCAL) as a dot key: literal create,
# dot-write (read-modify), dot-read.
d is {"loop": 1}
d.loop is d.loop + 1
assert_eq of [d.loop, 2, "dot key: loop"]
d is {"if": 1}
d.if is d.if + 1
assert_eq of [d.if, 2, "dot key: if"]
d is {"return": 1}
d.return is d.return + 1
assert_eq of [d.return, 2, "dot key: return"]
d is {"of": 1}
d.of is d.of + 1
assert_eq of [d.of, 2, "dot key: of"]
d is {"is": 1}
d.is is d.is + 1
assert_eq of [d.is, 2, "dot key: is"]
d is {"and": 1}
d.and is d.and + 1
assert_eq of [d.and, 2, "dot key: and"]
d is {"or": 1}
d.or is d.or + 1
assert_eq of [d.or, 2, "dot key: or"]
d is {"not": 1}
d.not is d.not + 1
assert_eq of [d.not, 2, "dot key: not"]
d is {"while": 1}
d.while is d.while + 1
assert_eq of [d.while, 2, "dot key: while"]
d is {"for": 1}
d.for is d.for + 1
assert_eq of [d.for, 2, "dot key: for"]
d is {"in": 1}
d.in is d.in + 1
assert_eq of [d.in, 2, "dot key: in"]
d is {"try": 1}
d.try is d.try + 1
assert_eq of [d.try, 2, "dot key: try"]
d is {"catch": 1}
d.catch is d.catch + 1
assert_eq of [d.catch, 2, "dot key: catch"]
d is {"import": 1}
d.import is d.import + 1
assert_eq of [d.import, 2, "dot key: import"]
d is {"local": 1}
d.local is d.local + 1
assert_eq of [d.local, 2, "dot key: local"]
d is {"define": 1}
d.define is d.define + 1
assert_eq of [d.define, 2, "dot key: define"]
d is {"elif": 1}
d.elif is d.elif + 1
assert_eq of [d.elif, 2, "dot key: elif"]
d is {"else": 1}
d.else is d.else + 1
assert_eq of [d.else, 2, "dot key: else"]
d is {"as": 1}
d.as is d.as + 1
assert_eq of [d.as, 2, "dot key: as"]
d is {"null": 1}
d.null is d.null + 1
assert_eq of [d.null, 2, "dot key: null"]
d is {"break": 1}
d.break is d.break + 1
assert_eq of [d.break, 2, "dot key: break"]
d is {"continue": 1}
d.continue is d.continue + 1
assert_eq of [d.continue, 2, "dot key: continue"]
d is {"match": 1}
d.match is d.match + 1
assert_eq of [d.match, 2, "dot key: match"]
d is {"case": 1}
d.case is d.case + 1
assert_eq of [d.case, 2, "dot key: case"]
d is {"unobserved": 1}
d.unobserved is d.unobserved + 1
assert_eq of [d.unobserved, 2, "dot key: unobserved"]
d is {"converged": 1}
d.converged is d.converged + 1
assert_eq of [d.converged, 2, "dot key: converged"]
d is {"stable": 1}
d.stable is d.stable + 1
assert_eq of [d.stable, 2, "dot key: stable"]
d is {"improving": 1}
d.improving is d.improving + 1
assert_eq of [d.improving, 2, "dot key: improving"]
d is {"oscillating": 1}
d.oscillating is d.oscillating + 1
assert_eq of [d.oscillating, 2, "dot key: oscillating"]
d is {"diverging": 1}
d.diverging is d.diverging + 1
assert_eq of [d.diverging, 2, "dot key: diverging"]
d is {"equilibrium": 1}
d.equilibrium is d.equilibrium + 1
assert_eq of [d.equilibrium, 2, "dot key: equilibrium"]

# The documented soft keywords (SYNTAX.md promised these worked already).
d is {"prev": 1}
d.prev is d.prev + 1
assert_eq of [d.prev, 2, "dot key: prev"]
d is {"at": 1}
d.at is d.at + 1
assert_eq of [d.at, 2, "dot key: at"]
d is {"what": 1}
d.what is d.what + 1
assert_eq of [d.what, 2, "dot key: what"]
d is {"who": 1}
d.who is d.who + 1
assert_eq of [d.who, 2, "dot key: who"]
d is {"when": 1}
d.when is d.when + 1
assert_eq of [d.when, 2, "dot key: when"]
d is {"where": 1}
d.where is d.where + 1
assert_eq of [d.where, 2, "dot key: where"]
d is {"why": 1}
d.why is d.why + 1
assert_eq of [d.why, 2, "dot key: why"]
d is {"how": 1}
d.how is d.how + 1
assert_eq of [d.how, 2, "dot key: how"]

# Chained access through a keyword key, read/write/compound.
d2 is {"x": {"when": 7}}
assert_eq of [d2.x.when, 7, "chained keyword key read"]
d2.x.when is 8
assert_eq of [d2.x.when, 8, "chained keyword key write"]
d2.x.when += 2
assert_eq of [d2.x.when, 10, "chained keyword key compound assign"]

# json_decode of external data — the original DeslanStudios report.
j is json_decode of ("{\"in\": 4, \"if\": {\"for\": 5}}")
assert_eq of [j.in, 4, "json_decode keyword key"]
assert_eq of [j.if.for, 5, "json_decode nested keyword chain"]

# The parenthesized-expression and dict-literal postfix chains (separate
# parser sites from the IDENT chain).
p is ({"at": 9})
assert_eq of [p.at, 9, "paren postfix keyword key"]
lit is {"try": 10}.try
assert_eq of [lit, 10, "dict-literal postfix keyword key"]

# dict_set-created key, dot-read; dot-created key, bracket-read (agreement).
e is {}
dict_set of [e, "loop", 5]
assert_eq of [e.loop, 5, "dict_set key dot-read"]
e.while is 6
assert_eq of [e["while"], 6, "dot-created key bracket-read"]

# Keyword still functions as a keyword right next to its key use.
total is 0
src is {"in": [1, 2]}
for x in src.in:
total is total + x
assert_eq of [total, 3, "for-in iterates over d.in"]

test_summary of null
Loading