Skip to content

Close out §13.9-13.12: dead token, -t docs, isTruthy unification, semicolon parsing - #170

Merged
kaidesu merged 2 commits into
1.0from
claude/section-12-callouts-1-0-5phxkh
Aug 30, 2026
Merged

kaidesu merged 2 commits into
1.0from
claude/section-12-callouts-1-0-5phxkh

Conversation

@kaidesu

@kaidesu kaidesu commented Aug 30, 2026 •

Copy link
Copy Markdown
Member

Overview

Bundles the remaining small-to-medium §13 defects (§13.8 skipped, per the
user), at the user's request, since each is independent:

  • §13.9 — token.PRINT was dead code (the scanner's keywords map
    never mapped "print" to it), left over from before console.log(...)
    replaced a bare print.
  • §13.10 — -t ("display how long the program ran for") was registered
    and honored by cmd/ghost.go but never listed in cmd/help.go's printed
    help, the mirror image of §13.4's -i gap.
  • §13.11 — evaluator/evaluator.go and object/boolean.go each defined
    their own copy of the truthiness rule. The audit this called for found
    the duplication wasn't just theoretical: it had already produced a real
    bug.
  • §13.12 — a trailing ; only reliably terminated a plain assignment;
    after anything else it broke the next statement's parse.

Changes

§13.9 — Fixed

  • Deleted token.PRINT and its typeNames entry (token/token.go).

§13.10 — Fixed

  • Added -t to cmd/help.go's printed flags list, matching its own
    flag.BoolVar description.

§13.11 — Fixed (found and fixed a real bug along the way)

The audit turned up two more independent copies of the same truthiness
switch beyond the two §13.11 named, and one of them had already drifted:

  • evaluator/prefix.go's ! (BANG) case hand-rolled its own switch
    rather than calling either existing isTruthy, and its default branch
    answered false for every type but Boolean/Null — silently skipping
    the case String has in the real rule (an empty string is falsy, §8.5).
    !"" answered false instead of true.

  • optimizer/fold.go's constant-folding equivalent (foldPrefix) had the
    identical bug, for the identical reason — its own comment said it was
    "matching the evaluator," and it did, bug included, folding every string
    literal's ! to false regardless of content.

    !""      // before: false (wrong - empty string is falsy, so this should
             //   be true)
             // after:  true
    !"abc"   // unchanged: false (correct - non-empty string is truthy)
    !0       // unchanged: false (correct - a number is always truthy)
    
  • Deleted evaluator/evaluator.go's private isTruthy; its four call sites
    (if.go, ternary.go, while.go, for.go) now call object.IsTrue
    directly, so object/boolean.go's isTruthy (reached only through the
    exported IsTrue/IsFalse) is genuinely the one place this rule is
    decided — the same principle object.ValuesEqual already established for
    equality (§13.2).

  • evaluator/prefix.go's BANG case is now
    toBooleanValue(object.IsFalse(right)) — no hand-rolled switch.

  • optimizer/fold.go's foldPrefix keeps its own switch (folding happens
    over ast nodes, before any object.Object exists to call IsFalse on)
    but its String case now checks right.Value == "" instead of
    unconditionally folding to false.

  • SPEC.md §8.5 gained a closing note naming object.IsTrue/IsFalse as
    the one place truthiness is decided, mirroring the equivalent note §8.5
    already has for object.ValuesEqual.

§13.12 — Fixed (the more targeted of two possible fixes, by design)

The original callout found two bugs under one heading: (1) a statement can
swallow the next line's unrelated opener when no ; separates them
(x = 1\n[10, 20, 30] parses as one statement), and (2) a ; after
anything but a plain assignment broke the next statement's parse entirely
(console.log(1);\nconsole.log(2); failed with `;` cannot start an expression).

Asked the user which of the SPEC's two suggested fixes to take — making
newlines significant (fixes both, but is a much larger grammar change with
its own unresolved trade-offs around multi-line fluent method chaining,
foo()\n .bar()) or giving every statement-producing path the same
trailing-;-consumption assign() already had (fixes only #2, leaves #1 as
documented, ;-avoidable behavior) — and the user picked the narrower,
lower-risk option.

  • parser/statement.go's expressionStatement() — the path every bare
    expression statement, and every if/while/for/function/class/
    trait/switch/import/use/break/continue statement, funnels
    through — now consumes an optional trailing ;.
  • parser/destructure.go's destructuringAssign() special-cases anything
    starting with [/{ before expressionStatement() is ever reached (to
    read it first as a possible destructuring pattern, §12); its four "this
    wasn't actually a pattern" fallback branches had the identical gap for a
    bare list/map-literal statement ([1, 2, 3], {"a": 1}). Both functions'
    "wrap this expression as a statement" logic is now the single shared
    expressionStatementFrom.
  • SPEC.md §8.1's "statement separation" bullet is reworded to describe
    bug parse prefix expressions #1's now-permanent, documented behavior accurately instead of denying
    it exists.

Added/updated tests

  • evaluator/evaluator_test.go's TestBangOperator: new !""/!0 cases.
  • optimizer/optimizer_test.go's TestFoldsComparisonsAndBooleans: the
    same !"" case, at the constant-folding layer specifically.
  • parser/parser_test.go's new TestSemicolonTerminatesEveryStatementKind:
    runs 14 statement kinds (plus both destructuring forms) through
    parser.Errors() directly — confirmed to fail on all but the
    already-working assignment/return cases against the pre-fix code.

Related issues

Closes SPEC.md §13.9, §13.10, §13.11, and §13.12.

Additional context

go build ./..., go vet ./..., and go test -race ./... all clean.
gofmt -l . reports nothing. Reproduced each bug against the built binary
before its fix (the !"" case both as a constant-folded literal and via a
computed empty string; the ;-breaks-the-next-statement case across every
affected statement kind) and confirmed the fix in each case, before writing
the automated tests.


Generated by Claude Code

claude added 2 commits August 30, 2026 08:15
§13.9: token.PRINT was never produced by the scanner (keywords never mapped
"print" to it) and nothing else referenced it - deleted, along with its
typeNames entry.

§13.10: -t ("display how long the program ran for") was registered and
honored by cmd/ghost.go but never listed in cmd/help.go's printed help -
added, matching its own flag.BoolVar description.

§13.11: evaluator/evaluator.go and object/boolean.go each defined their own
copy of the language's truthiness rule. The audit this called for found the
duplication had already caused a real bug: evaluator/prefix.go's `!`
operator, and optimizer/fold.go's constant-folding equivalent, both
hand-rolled a third and fourth copy of the same switch, and both answered
false for every string regardless of content - so `!""` answered false
instead of the true the documented rule (§8.5) requires. Fixed by deleting
evaluator's private isTruthy and routing every call site (if/ternary/
while/for, and prefix.go's `!`) through object.IsTrue/IsFalse, the one
place this is now decided; the optimizer's separate AST-level fold keeps
its own switch (there's no object.Object yet to call IsFalse on during
folding) but its String case now checks for emptiness instead of always
folding to false.
…13.12)

`assign()` consumed an optional trailing `;`, but expressionStatement() -
the path every bare call, and every if/while/for/function/class/trait/
switch/import/use/break/continue statement, funnels through - did not, so
`;` after anything but a plain assignment broke the next statement's parse
("`;` cannot start an expression"). destructuringAssign()'s "this wasn't
actually a pattern" fallback branches (for a bare list/map literal
statement) had the identical gap, for the identical reason.

Both now share one function, expressionStatementFrom, for "wrap this
expression as a statement, consuming an optional trailing `;`" - the same
fix as assign()'s, applied symmetrically rather than reintroducing a second
definition of it.

This was one of two bugs the original callout found under one heading: a
statement can also swallow the next line's unrelated opener when no `;`
separates them (`x = 1\n[10, 20, 30]` parses as one statement). That one is
left as documented, existing behavior rather than fixed - making newlines
actually significant is a much larger grammar change with its own
unresolved trade-offs (multi-line fluent method chaining), and terminating
with `;` already avoids it, which this change makes reliable everywhere for
the first time.
@kaidesu kaidesu changed the title Delete dead token.PRINT, document -t, and unify isTruthy (§13.9-13.11) Close out §13.9-13.12: dead token, -t docs, isTruthy unification, semicolon parsing Aug 30, 2026
@kaidesu
kaidesu marked this pull request as ready for review August 30, 2026 08:24
@kaidesu
kaidesu merged commit c4c2a42 into 1.0 Aug 30, 2026
2 checks passed
@kaidesu
kaidesu deleted the claude/section-12-callouts-1-0-5phxkh branch August 30, 2026 08:24
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.

2 participants