Skip to content

Reject self-referential predicate definitions#61

Merged
rmichaelthomas merged 1 commit into
mainfrom
fix/self-referential-define
Jul 20, 2026
Merged

Reject self-referential predicate definitions#61
rmichaelthomas merged 1 commit into
mainfrom
fix/self-referential-define

Conversation

@rmichaelthomas

Copy link
Copy Markdown
Owner

Summary

Closes the one case PR #60 (fix/cyclic-predicate-rejection, merged dd0d711) doesn't reach: define p: is p on a first-ever definition of p currently returns SUCCESS (canonical define p: each is p), silently meaning "equals the text p" instead of erroring — even though the line reads as self-referential to a human.

Mechanism: when p is not yet a known predicate at parse time, is p parses to a ConditionNode(field=EachPronoun(), op='is', value=BareWord(word='p')) — string equality, not a PredicateApplicationNode. PR #60's _find_predicate_cycle walks predicate references only, so it never sees this case; there's no predicate reference in the AST to walk.

Fix: _check_self_referential_define(name, cond) in analyzer.py, called from _check_define before the existing forward-declaration/cycle checks, unconditionally (regardless of whether name is already in the symbol table). Walks CompoundConditionNode on both branches; for a ConditionNode, checks field, value, and value2 for a BareWord.word or NameRef.name equal to the name being defined.

Message wording: chose "Definition '{name}' can't refer to itself." — distinct from the existing "can't depend on itself" (one-hop redefinition) and "refers back to itself through..." (multi-hop chain), so all three stay distinguishable in tests and terminal output.

Analyzer-only, no parser or interpreter change: execute() gates on analyze() before running, so the analyzer rejects before any interpreter code touches a self-referential define.

Acceptance cases (12 total, verified against this branch)

# Case Result
1 define p: is p (no prior def) ERROR_SEMANTICDefinition 'p' can't refer to itself.
2 define p: is not p ERROR_PARSE (see note below — pre-existing, unrelated)
3 define p: p is above 1 ERROR_SEMANTIC — same new message
4 define p: is above 1 and is p ERROR_SEMANTIC — same new message
5 define p: is p or is above 1 ERROR_SEMANTIC — same new message
6 redefinition one-hop ERROR_SEMANTICDefinition 'p' can't depend on itself.byte-identical to main
7 three-name cycle ERROR_SEMANTIC...through 'q'. A definition can't depend on itself.byte-identical to main
8 three-way cycle ERROR_SEMANTIC...through 'r', 'q'. A definition can't depend on itself.byte-identical to main
9 legal composition SUCCESS
10 legal redefinition SUCCESS
11 undefined bareword, q≠p SUCCESS
12 fact named same as predicate SUCCESS

Case 2 note: define p: is not p never reaches the analyzer — it's ERROR_PARSE ("After 'not' I expected 'above', 'below', or 'equal to'"), for reasons unrelated to self-reference. Confirmed pre-existing and general: define p: is not grownup and define zzz: is not q fail identically on unmodified main. The grammar only accepts is not <word> when <word> is already a known predicate name — an unknown bareword after is not is invalid syntax regardless of what it says. Fixing that is a parser change, explicitly out of scope for this fix. Documented with a test (test_is_not_self_reference_is_a_pre_existing_parse_error_not_semantic) rather than silently left unhandled.

PR #60's messages (cases 6–8) are unchanged, confirmed byte-identical above.

Test plan

  • pytest tests/test_define.py -q — 47 passed (40 existing + 7 new)
  • pytest tests/ -q — 1668 passed (1661 baseline + 7 new), zero regressions, zero skipped
  • len(ALL_RESERVED) - len(TOMBSTONES) == 61 — vocabulary unchanged (pre-existing assertion at tests/test_define.py:342)
  • All 12 acceptance cases run directly against this branch, statuses/messages as above

🤖 Generated with Claude Code

`define p: is p` on a first-ever definition of `p` silently parses to
string-equality (BareWord), not a PredicateApplicationNode, so PR #60's
cycle check never sees it and the line succeeds as "equals the text
'p'" instead of erroring — even though it reads as self-referential.

Adds _check_self_referential_define, run unconditionally in
_check_define before the existing checks, catching a bare-word/name
value identical to the name being defined in field, value, or value2
position, including inside compound and/or branches.
@rmichaelthomas
rmichaelthomas merged commit 92eb657 into main Jul 20, 2026
2 checks passed
@rmichaelthomas
rmichaelthomas deleted the fix/self-referential-define branch July 20, 2026 09:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant