Short-circuit and and or (§13.21, §14 decision 11) - #178
Merged
Merged
Conversation
§8.4 asked for both operands to be evaluated and the evaluator obeyed, so
this was a stance rather than drift. §15 ranked it first: eight defects in
the first library written against Ghost, two of them shipped, every one a
null guard written the way Python, Ruby, JavaScript and PHP all teach it
and crashing on the dereference it existed to prevent.
target = null
target == null or target.hint == ''
// was: property error, cannot read property `hint` of null
// now: true
evaluateInfix routes `and`/`or` to evaluateLogicalInfix as soon as the left
operand is evaluated, before the right one is touched. It requires the left
operand to be a boolean, returns immediately when that settles the answer
(`false and x`, `true or x`), and otherwise evaluates the right operand and
answers with it - once the left has not decided, the result *is* the right
one. The two cases are gone from evaluateBooleanInfix, which is handed both
operands already evaluated and so cannot make this decision; they would be
dead code. foldBooleanInfix still folds two literal booleans, where there is
no evaluation to skip either way, and only its comment changed.
The narrow reversal decision 11 described held: the truth table is
untouched, both operands are still booleans, and the one observable
loosening is that an unreached operand is no longer type-checked, so
`false and 1` is now false where it was a type error.
What the decision did not anticipate is that the error wording had to move
with it. A wrong left operand can no longer be reported as "between null and
boolean" - the right operand was deliberately never evaluated, and naming a
type it might have had would be inventing one. logicalOperandError names the
side at fault instead, on either side, and suggests comparing first when the
operand is null. That reads better than what it replaced: a bare
`if (x and x.foo)` now fails at the `and` with a type error rather than
dying on the null dereference downstream of it.
Tested in evaluator/logical_test.go: the truth table, short-circuiting
proved both by a right operand that raises and by one whose side effect is
counted and must not happen, the reached/unreached error wording with
positions, and the null help line. All 41 programs in examples/ produce
identical output before and after - mud.gs differs only in how many frames
its non-terminating interactive loop renders inside the timeout, byte
identical up to that point. Studio's 132-case suite, the codebase that
reported this, passes unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
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
Closes §15's #1. §8.4 asked for both operands to be evaluated and the evaluator obeyed, so this was a stance rather than drift — which is why §14 decision 11 had to reverse it before this patch could exist. The evidence behind the ranking: eight defects in the first library written against Ghost, two of them shipped, every one a null guard written the way Python, Ruby, JavaScript and PHP all teach it, crashing on the dereference it existed to prevent.
The narrow reversal decision 11 described held: the truth table is untouched, both operands are still booleans, and the one observable loosening is that an unreached operand is no longer type-checked.
Changes
Changed
evaluateInfix(evaluator/infix.go) routesand/orto a newevaluateLogicalInfix(evaluator/boolean.go) as soon as the left operand is evaluated, before the right one is touched. It requires the left operand to be a boolean, returns immediately when that settles the answer (false and x,true or x), and otherwise evaluates the right operand and answers with it — once the left has not decided, the result is the right one, so there is no second truth table to keep in sync.and/orare gone fromevaluateBooleanInfix. It is handed both operands already evaluated and so cannot make this decision; the cases would be dead code.cannot useandwith null on the left, via a newlogicalOperandError. This is the part decision 11 did not anticipate: a wrong left operand can no longer be reported as "between null and boolean", because the right operand was deliberately never evaluated and naming a type it might have had would be inventing one. The null case carrieshelp: compare it first, as inx != null``.§8.4documents short-circuiting as the rule; §13.21 and §14 decision 11 are marked done with what landed; §15's table moves parse prefix expressions #1 to closed and names parse infix operators #2 as next.foldBooleanInfix(optimizer/fold.go) is unchanged apart from its comment — it folds two literal booleans, where there is no evaluation to skip either way.Added
evaluator/logical_test.go.Additional context
The wording change is a real improvement, not just a consequence. A bare truthy guard now fails at the operator instead of at the null dereference downstream of it:
and/orstay boolean-only, so this shape is still an error — it is a truthy guard in a language that has never had truthy operators. It just says so now.Verification.
evaluator/logical_test.gocovers the truth table (unchanged), short-circuiting proved two ways — a right operand that raises, and one whose side effect is counted and must not happen — the reached/unreached error wording with exact positions, and the null help line.examples/produce identical output before and after.mud.gsis the one exception and is not a behaviour difference: it is awhile (true)game loop onconsole.read()that both builds run into the timeout, byte-identical for the first 2.8 MB, differing only in how many frames each rendered inside the window.go build ./...,go vet ./...,gofmt -l .andgo test ./...are all clean.Next on §15 is #2 (§13.22, a method's name shadowing a same-named import), which wants the same class-construction check as #4 (§13.18) and should land with it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01EGsNMRiKwWmvoizxD619zA
Generated by Claude Code