Close out §13.9-13.12: dead token, -t docs, isTruthy unification, semicolon parsing - #170
Merged
Merged
Conversation
§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
marked this pull request as ready for review
August 30, 2026 08:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Bundles the remaining small-to-medium §13 defects (§13.8 skipped, per the
user), at the user's request, since each is independent:
token.PRINTwas dead code (the scanner'skeywordsmapnever mapped
"print"to it), left over from beforeconsole.log(...)replaced a bare
print.-t("display how long the program ran for") was registeredand honored by
cmd/ghost.gobut never listed incmd/help.go's printedhelp, the mirror image of §13.4's
-igap.evaluator/evaluator.goandobject/boolean.goeach definedtheir 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.
;only reliably terminated a plain assignment;after anything else it broke the next statement's parse.
Changes
§13.9 — Fixed
token.PRINTand itstypeNamesentry (token/token.go).§13.10 — Fixed
-ttocmd/help.go's printed flags list, matching its ownflag.BoolVardescription.§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 switchrather than calling either existing
isTruthy, and itsdefaultbranchanswered
falsefor every type butBoolean/Null— silently skippingthe case
Stringhas in the real rule (an empty string is falsy, §8.5).!""answeredfalseinstead oftrue.optimizer/fold.go's constant-folding equivalent (foldPrefix) had theidentical bug, for the identical reason — its own comment said it was
"matching the evaluator," and it did, bug included, folding every string
literal's
!tofalseregardless of content.Deleted
evaluator/evaluator.go's privateisTruthy; its four call sites(
if.go,ternary.go,while.go,for.go) now callobject.IsTruedirectly, so
object/boolean.go'sisTruthy(reached only through theexported
IsTrue/IsFalse) is genuinely the one place this rule isdecided — the same principle
object.ValuesEqualalready established forequality (§13.2).
evaluator/prefix.go'sBANGcase is nowtoBooleanValue(object.IsFalse(right))— no hand-rolled switch.optimizer/fold.go'sfoldPrefixkeeps its own switch (folding happensover
astnodes, before anyobject.Objectexists to callIsFalseon)but its
Stringcase now checksright.Value == ""instead ofunconditionally folding to
false.SPEC.md§8.5 gained a closing note namingobject.IsTrue/IsFalseasthe 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;afteranything 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 sametrailing-
;-consumptionassign()already had (fixes only #2, leaves #1 asdocumented,
;-avoidable behavior) — and the user picked the narrower,lower-risk option.
parser/statement.go'sexpressionStatement()— the path every bareexpression statement, and every
if/while/for/function/class/trait/switch/import/use/break/continuestatement, funnelsthrough — now consumes an optional trailing
;.parser/destructure.go'sdestructuringAssign()special-cases anythingstarting with
[/{beforeexpressionStatement()is ever reached (toread 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 describebug parse prefix expressions #1's now-permanent, documented behavior accurately instead of denying
it exists.
Added/updated tests
evaluator/evaluator_test.go'sTestBangOperator: new!""/!0cases.optimizer/optimizer_test.go'sTestFoldsComparisonsAndBooleans: thesame
!""case, at the constant-folding layer specifically.parser/parser_test.go's newTestSemicolonTerminatesEveryStatementKind:runs 14 statement kinds (plus both destructuring forms) through
parser.Errors()directly — confirmed to fail on all but thealready-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 ./..., andgo test -race ./...all clean.gofmt -l .reports nothing. Reproduced each bug against the built binarybefore its fix (the
!""case both as a constant-folded literal and via acomputed empty string; the
;-breaks-the-next-statement case across everyaffected statement kind) and confirmed the fix in each case, before writing
the automated tests.
Generated by Claude Code