Skip to content

Emit lane: empty then_/else_ arms emit bash no-op ':' instead of invalid syntax - #7282

Merged
briansrls merged 6 commits into
mainfrom
session/stern-cat-330
Jul 26, 2026
Merged

briansrls merged 6 commits into
mainfrom
session/stern-cat-330

Conversation

@briansrls

@briansrls briansrls commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #7277. Pipeline.steps (v2.std.orchestration) is a plain FreeMonoid<PipelineStep> with no NonEmpty variant and no smart constructor, so a zero-step Pipeline (steps: Empty) is legally constructible today. Nested as an If's then_/else_ arm, orch_emit_if_step threaded that arm's emitted "" straight into the inline bash row's body slot, producing "if COND; then ; fi" — invalid bash (a then clause needs ≥1 command before the closing separator; real bash raises "syntax error near unexpected token `fi'"). This is a fabricated-invalid-output shape, not a refusal.

Zero production occurrences of an empty arm exist in the tree today (verified via Pipeline { steps: ... } census across dag/gunbc and src/v2/workflow) — this is a priced, cheap fix ahead of any real trigger, not a live incident.

Fix (uniform, no special-casing): an empty arm's body always emits the bash no-op : (orch_if_arm_body_or_noop in 05_emit_orchestration.dag), applied identically in the inline and block dispatch and regardless of whether the other arm is also empty. This covers four shapes with one rule:

  • empty then_, else_ Absent
  • empty then_, non-empty else_ present ("act only on the else")
  • non-empty then_, empty else_ present ("act only on the then")
  • both arms empty

Why no refusal for the both-empty case (reconsidered mid-review): Predicate (ExitZero/LogMatches/StrEq/etc.) executes a command — so even a both-empty If runs the condition and discards the outcome, a real "run cmd, ignore result" intent, not dead code. Refusing it would forbid working intent. This also matches the existing convention already in the codebase: a top-level empty Pipeline (not nested in an If) already emits "" cleanly via orch_emit_pipeline's HeadAbsent arm, with no error — so "empty pipeline is a legitimate no-op" was already this codebase's convention before this PR; the fix makes that convention hold uniformly whether or not the Pipeline is nested inside an If.

Option (3) declined, named on the carrier: a NonEmpty<PipelineStep> carrier on Pipeline.steps was considered (construction-first, §5-preferred shape in the abstract) and declined — zero occurrences in tree today, and it would touch FreeMonoid<PipelineStep> plus every Pipeline constructor for a currently-unexercised case (the purity trap, DESIGN.md §6). This is recorded as a typed note directly on the carrier: pipeline_steps_empty_arm_note beside Pipeline's declaration in src/v2/std/orchestration.dag, plus orch_if_empty_arm_note beside the fix in 05_emit_orchestration.dag — not just in this PR body, per DESIGN §6 "let the mark on the carrier be the authority."

Test plan

Four new discriminating RED witnesses in src/v2/workflow/orchestration_emit_test.dag, all asserting valid emitted bash (none asserting a refusal):

  • orch_if_empty_then_no_else_noop_holds → "if [ -z \"$1\" ]; then :; fi"
  • orch_if_empty_then_with_nonempty_else_noop_holds → "if [ -z \"$1\" ]; then :; else verdict=present; fi"
  • orch_if_nonempty_then_with_empty_else_noop_holds → "if [ -z \"$1\" ]; then verdict=absent; else :; fi"
  • orch_if_both_arms_empty_noop_holds → "if [ -z \"$1\" ]; then :; else :; fi"

Run via:

cargo run --release --bin claim_batch -- \
  --source-root src/v2 --source-root dag \
  --entry src/v2/workflow/orchestration_emit_test.dag \
  --function orch_if_empty_then_no_else_noop_holds \
  --function orch_if_empty_then_with_nonempty_else_noop_holds \
  --function orch_if_nonempty_then_with_empty_else_noop_holds \
  --function orch_if_both_arms_empty_noop_holds

All 4 PASS.

Byte-identity receipt (post-#7277 main)

Re-ran against main post-#7277-merge (this branch merged main via git merge, not rebase) to confirm the fix doesn't disturb any existing emitted bytes:

  • All 5 pre-existing real If call-site witnesses (the 3 ci_floor_peak_emit.dag single-step sites covered by ci_floor_peak_emit_band_holds, and the 2 live_deploy/emit.dag no-else sites covered by live_deploy_emit_holds) — PASS, byte-identical.
  • All 5 pre-existing If shape tests in orchestration_emit_test.dag (orch_if_single_step_no_else_byte_identical_holds, orch_if_single_step_with_else_byte_identical_holds, and the 3 multi-step block-form tests from Emit lane: block-bodied if_else row for multi-step then_/else_ bind #7277) — PASS, byte-identical.
  • The 4 new empty-arm witnesses above — PASS.

Combined run (11 witnesses total), exit code 0, all PASS:

cargo run --release --bin claim_batch -- \
  --source-root src/v2 --source-root dag \
  --entry src/v2/workflow/orchestration_emit_test.dag \
  --function orch_if_empty_then_no_else_noop_holds \
  --function orch_if_empty_then_with_nonempty_else_noop_holds \
  --function orch_if_nonempty_then_with_empty_else_noop_holds \
  --function orch_if_both_arms_empty_noop_holds \
  --function orch_if_single_step_no_else_byte_identical_holds \
  --function orch_if_single_step_with_else_byte_identical_holds \
  --function orch_if_multi_step_then_no_else_block_form_holds \
  --function orch_if_multi_step_then_and_else_block_form_holds \
  --function orch_if_multi_step_else_only_block_form_holds \
  --entry src/v2/workflow/ci_floor_peak_emit_test.dag --function ci_floor_peak_emit_band_holds \
  --entry dag/test/claim/live_deploy/emit_test.dag --function live_deploy_emit_holds

@gunbai-bot gunbai-bot Bot changed the title Emit lane: refuse multi-step then_ at inline if_else bind Emit lane: empty then_/else_ arms emit bash no-op ':' instead of invalid syntax Jul 26, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review July 26, 2026 13:10
@briansrls
briansrls merged commit b9d5121 into main Jul 26, 2026
5 checks passed
@briansrls
briansrls deleted the session/stern-cat-330 branch July 26, 2026 16:50
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