Conversation
`END { print "done" }` and every other END-only form declined (exit 3) -- the
simplest awk one-liners, a real surprise for new users. Now they compile and
match gawk: constant/NR/field/NF/length/arithmetic print, printf, multi-statement
END lists, END-`if`, and the empty program `''` (which reads input and prints
nothing, as gawk does).
An END-only program is NOT "skip the loop": an END block sees NR (record count),
$0/$N (LAST record), NF and length, so the record loop still runs -- consuming
all stdin, counting NR, retaining the last record -- with only the per-record
RULE CHAIN empty. On empty input NR is 0, $0 empty, NF 0. So it is the existing
rules+END driver with an empty rule chain, not the BEGIN-only driver (which reads
no input).
Three coordinated changes, each gated on empty rules so no program with rules is
touched (20/20 golden-corpus programs byte-identical):
1. plawk_scalar_state_plan/3 admits an empty plan when Rules == [] -- its guard
otherwise requires some action / print-field / getline, none of which a
rule-less program has.
2. a dedicated plawk_scalar_rule_chain_ir([], ...) clause emits
`br label %continue_loop`, the terminator the empty `lowered_match:` block
needs. Without it the general clause emits an unterminated block -- invalid
LLVM, an exit-4 clang failure. The general clause keeps its `RuleCount > 0`.
3. the END-loop driver clause is guarded `Rules0 \== []`.
Change 3 is the sharp one and the reason for a full exit-4 sweep. Relaxing the
state-plan guard (change 1) made `END { while ... }` -- which the END-loop clause
handles, cutting as soon as it sees a loop -- newly pass state_plan, commit past
its cut, and emit a malformed loop driver: exit 4, a MISCOMPILE, on a program that
used to decline cleanly. The guard keeps END-loop-with-no-rule declining (exit 3)
as the follow-on it is. Lesson recorded in the handoff: relaxing a shared gate can
convert a clause that declined-by-relying-on-that-gate-failing into one that
commits and miscompiles; enumerate every such clause and sweep the whole surface
for exit 4.
Scope: END-only print / printf / if / empty-program compile and match gawk.
Still declining cleanly (pinned, exit 3, never exit 4): END-only loop, END-only
scalar assignment, for-in, getline, record builtins (substr($0,..)/toupper($1)).
Parse-level (exit 2): BEGIN+assignment+END, END assoc assignment.
Tests: tests/test_plawk_end_only.pl (29) -- the target matrix incl. empty input,
the retained-record composition, the empty program, every boundary decline, an
explicit "END-loop declines exit 3 not exit 4" pin, and an all-forms no-miscompile
sweep. Two stale pins in test_plawk_end_if_nr.pl and test_plawk_end_field_reads.pl
that asserted END-only declines are re-attributed (not deleted) to assert the
status flip, output parity covered by the new suite.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g
Strengthens the END-only suite with an explicit enumeration test: one rule-less
program routed at each END-driver clause type -- scalar print/list/if, loop, the
MIXED and ASSOC ends whose RuleCount>0 gates were deliberately NOT relaxed,
BEGIN+end, getline and assignment -- each asserted to build to a real exit code
(0/2/3) and NEVER 4.
The load-bearing cases are the mixed and assoc rule-less shapes (`END { print
c["x"] }`, `END { for (k in c) print k }`, `END { c["x"]++ }`): they confirm the
two unrelaxed gates keep those clauses DECLINING on empty rules rather than
committing past a cut and miscompiling -- the exact "did you enumerate every
clause that relied on the shared gate failing" question the state-plan relaxation
raises. All twelve build without exit 4. Complements the shape-level
no-miscompile sweep already in the suite with a clause-level one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g
…exit-4)
An ultra review found a blocking miscompile the first cut missed: END-only
programs that read/write a scalar VARIABLE -- `END { print x }`,
`END { printf "%d\n", x }`, `END { if (x == 0) ... }`, `END { if (NR>0) print x }`,
`BEGIN{...} END { print x }` incl. BINFMT -- exited 4 (clang: use of undefined
value '%rule_-1_in_slot_0').
Cause: admitting an empty rule chain let plawk_scalar_state_plan collect the
scalar as a slot, so the state plan was NOT empty. The next-slot phi emitter then
computed LastRuleIndex = RuleCount - 1 = -1 and referenced %rule_-1_match /
%rule_-1_in_slot_N -- undefined SSA. My own exit-4 sweep missed it because every
print case it tried used a literal / field / NR / NF / length, never a scalar
variable; the defect lives in the interaction between an empty rule chain and a
NON-empty state plan.
Fix: guard the dedicated empty rule-chain clause to fire only when the state plan
has NO slots (plawk_state_plan_slots(StatePlan, [])). A scalar-var END-only
program therefore declines cleanly (exit 3) instead of miscompiling. Declining
(rather than a pass-through phi) is the right scope: an unset scalar's value in END
is context-dependent -- empty in string context, 0 in numeric, the
uninitialised-scalar representation problem -- so a correct compile needs that
settled first. Scalar-var END-only is now a pinned follow-on.
Every non-scalar END-only form is unaffected and still compiles + matches gawk
(literal/NR/field/NF/length print, printf, multi-statement, END-if, empty program).
20/20 golden-corpus programs remain byte-identical (the guard is inert for
rule-bearing programs). The reviewer's entire blocking list now exits 3, zero
exit-4 across a broad rule-less re-sweep.
Tests: the enumeration test gains the scalar-var forms (the exact blind spot), and
a new pin `end_only_scalar_var_read_declines_not_miscompiles` asserts exit 3 (not 4)
for five scalar-var shapes, so the miscompile cannot silently return. Suite now 31
tests. Handoff records the lesson: an exit-4 sweep must vary the DATA a relaxation
admits (does the END read a literal/field/special/scalar-VAR?), not just program
shapes, because the defect can hide in a data-dependent downstream a clause-level
enumeration never reaches -- and external review earns its keep precisely when the
change already looks polished.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g
Blocking miscompile from the ultra review — fixed (commit
|
test_plawk_begin_only.pl pinned `BEGIN { print "b" } END { print "e" }` and
`END { print "e" }` as clean declines (exit 3) while END-only was a gap. The
END-only driver landed, so both now compile and match gawk. Re-attributed (not
deleted) to assert the new behaviour: they build (exit 0) and print their literal
output deterministically on empty stdin. begin_only now 31/31.
Surfaced by the full sweep -- which also exposed a harness bug (fixed in the local
sweep tooling, not this repo): under `set -e` a suite whose run_tests failed
aborted the whole sweep at that suite instead of recording the failure and
continuing, so these two stale pins had masqueraded as a sweep that "died at
11/195" rather than being reported as failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g
…GES)
The review confirmed the scalar-slot fix is correct (no residual exit-4, guard
covers all scalar dispatch paths, no bypass) and asked only for stronger
regression coverage. Applied:
- The scalar-var decline pin gains the four contexts the review named as
missing: a concatenation (`print "n=" x`), an arithmetic operand
(`print x + 1`), an ELSE branch (`if (NR==0) print "empty"; else print x`),
and a union-BINFMT program. All decline cleanly at exit 3 (verified), never
miscompile.
- The no-miscompile enumeration gains an explicit redirection example
(`print NR > "/dev/stdout"`, a parse error at 2) -- the comment claimed
redirection coverage without a case.
- build_status_not_4/1 now asserts the status is a KNOWN-GOOD value
(memberchk in [0,2,3]) rather than merely `=\= 4`, which would have passed an
unexpected exit 1 (a crash). Renamed intent in the comment.
Suite stays at 31 tests, all passing.
Also documents, in the handoff follow-ons, the EOF-sentinel defect both reviews
surfaced: a literal `end_of_file` input line is mistaken for EOF by a text
comparison in the runtime stream driver
(src/unifyweaver/targets/wam_llvm_target.pl ~23909). It is PRE-EXISTING (the
rule-bearing form is equally wrong) and needs an identity-based EOF check -- a
runtime fix affecting all programs, deliberately out of scope here; END-only
merely made it reachable in an END-only shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g
Second review (Astra, gpt-5.6, clang 14): SHIP-WITH-CHANGES — changes applied (
|
What was wrong
END { print "done" }— and every other END-only form — declined at exit 3. These are among the simplest awk one-liners, so it was a real surprise for new users (the motivation for closing it: reduce surprise). The instant a main rule existed ({ n++ } END { ... }), the same END worked.The key insight: an END-only program still reads all input
An END block sees
NR(total record count),$0/$N(the last record),NFandlength— so the record loop must still run, consuming all of stdin, counting NR and retaining the last record, with only the per-record rule chain empty. On empty inputNRis 0,$0empty,NF0.So an END-only program is the existing rules+END driver with an empty rule chain, not the BEGIN-only driver (which reads no input). Both pieces it depends on — the NR counter and the retained-last-record projection — already existed in the tree.
Three coordinated changes, each gated on empty rules
All three fire only when the rules are empty, so no program with rules is touched — 20/20 golden-corpus programs byte-identical:
plawk_scalar_state_plan/3admits an empty plan whenRules == [](its guard otherwise requires an action / print-field / getline, none of which a rule-less program has).plawk_scalar_rule_chain_ir([], ...)clause emitsbr label %continue_loop— the terminator the emptylowered_match:block needs. Without it the general clause emits an unterminated block: invalid LLVM, an exit-4 clang failure. The general clause keeps itsRuleCount > 0.Rules0 \== [].Total: 5 lines of logic plus comments.
Change 3 is the sharp one — and why a full exit-4 sweep was mandatory
Relaxing the state-plan guard (change 1) made
END { while ... }— which the END-loop clause handles, cutting as soon as it sees a loop — newly passstate_plan, commit past its cut, and emit a malformed loop driver: exit 4, a miscompile, on a program that used to decline cleanly. Change 3 keeps END-loop-with-no-rule declining (exit 3) as the follow-on it is.The transferable lesson (recorded in the handoff): relaxing a shared gate can convert a clause that declined-by-relying-on-that-gate-failing into one that commits and miscompiles. A broad exit-4 sweep of the whole surface the relaxation touches is not optional — here it found exactly one such site, and the campaign's worst outcome (invalid LLVM on a supported-looking program) was one un-run probe away.
Scope
Compiles and matches gawk: END-only constant/NR/field/NF/length/arithmetic print,
printf, multi-statement END lists, END-if(withelse, empty input), and the empty program''(reads input, prints nothing — correct gawk behaviour, pinned as intended).Declines cleanly (exit 3, never exit 4), pinned as follow-ons: END-only loop, END-only scalar assignment, for-in, getline, record builtins (
substr($0,…),toupper($1)).Parse-level (exit 2):
BEGIN { x=5 } END { print x }, END assoc assignment.Verification
tests/test_plawk_end_only.pl— the target matrix (incl. empty input and the retained-record composition), the empty program, every boundary decline, an explicit "END-loop declines exit 3 not exit 4" pin, and an all-forms no-miscompile sweep.test_plawk_end_if_nr.pl,test_plawk_end_field_reads.pl) that asserted END-only declines are re-attributed, not deleted, to assert the status flip; output parity is covered by the new suite. Both suites re-run green (14/14, 86/86).🤖 Generated with Claude Code
https://claude.ai/code/session_01QrQistdoYMNpVtecwims6g