Skip to content

[codex] Add parser-only pipe operator sugar - #462

Merged
briansrls merged 3 commits into
mainfrom
mild-hen-538
Apr 16, 2026
Merged

briansrls merged 3 commits into
mainfrom
mild-hen-538

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

What changed

  • add |> tokenization to the v3 lexer
  • add a parser-level pipe production and desugar x |> f to f(x)
  • add first-arg injection for multi-arg calls so x |> f(y, z) parses as f(x, y, z)
  • add focused regression tests for unary pipe, multi-arg injection, and left-to-right chaining
  • trim stale comments that still listed pipe as an unparsed block-body gap

Why it changed

docs/substrate-reflection-design.md §4.3 specifies pipe as pure parser sugar, not a substrate or lowering feature. The parser previously had no |> token or desugar step, so prereq 5 was still missing.

Impact

  • v3 now accepts parser-level pipe syntax without any lowering or substrate changes
  • call sites using pipe are lowered as ordinary Call expressions after parse-time rewriting
  • the change stays scoped to prereq 5 and does not alter runtime/lowering semantics

Root cause

The tokenizer only recognized plain |, and the expression parser stopped at comparison/additive/term/primary without a pipe stage. That left |> unavailable even though the design already committed to first-arg injection.

Validation

  • cargo test -p v3-compiler

Notes

  • the design doc's [1,2,3] |> length example still depends on the separate list-literal parser work; this PR keeps scope to parser-only pipe sugar and validates the desugar structurally with the current grammar

@briansrls
briansrls marked this pull request as ready for review April 16, 2026 00:05
@briansrls

briansrls commented Apr 16, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Principle audit.

FAIL-CLOSED. This diff mostly satisfies fail-closed. The new parse_pipe_target path rejects bad RHS forms through Diagnostic::ParseError instead of silently accepting them, and the lower-stage scaffold rejection text is updated so user-range block bodies no longer falsely blame |> once pipe parsing lands. That matches both the modeling-discipline fail-closed rule and the scaffold-boundary invariant. The check is behavioral, not structural: it depends on the parser continuing to return diagnostics from this code path rather than a type making silent failure impossible. chatgpt-review-7d5f6174-9ff3-49…

chatgpt-review-555f4e69-f689-42…

ILLEGAL STATES UNREPRESENTABLE. This mostly satisfies the principle. The good call here is what the PR did not add: there is no new SurfaceExpr::Pipe or DAG-level pipe node, only immediate normalization to the existing SurfaceExpr::Call shape in the new parse_pipe / parse_pipe_target block. That avoids carrying two equivalent downstream states for the same computation. The structural part is good (no new IR state); the parse-time normalization itself is still behavioral. chatgpt-review-8bfd33ee-ee6b-40…

FACTS FLOW FORWARD. Partially satisfied. The new parse-stage facts that matter downstream do flow forward: callee name, argument order with the piped lhs injected as arg0, left-to-right nesting, and full expression span are all preserved in the desugared Call, and the new tests assert exactly those downstream facts. But the original surface fact “the user wrote pipe syntax” is discarded by convention, and because parse_pipe_target manually rebuilds a call instead of reusing a single call-target parser, any future richer call-target fact can be added in one path and silently forgotten in the other. So this is only behaviorally safe. chatgpt-review-7d5f6174-9ff3-49…

chatgpt-review-555f4e69-f689-42…

COPRODUCT DISSOLUTION. At the important boundary, this is the right direction. The PR closes a surface grammar gap without growing SurfaceExpr, and it updates the existing scaffold commentary to say pipe is no longer part of the “remaining surface grammar” backlog. That is consistent with the thesis’s decompression rule: do not add another enum arm when the existing structure already carries the meaning. Structural for SurfaceExpr; documentary only for the new terminal token variant unless TokenKind already has a ledger outside this diff. chatgpt-review-8bfd33ee-ee6b-40…

chatgpt-review-8bfd33ee-ee6b-40…

SINGLE AUTHORITY. This is the blocking problem. src/v3/compiler/src/parse.rs now has a second authority for call-target syntax: the new grammar pipe_target := ident | ident(args) and the handwritten parse_pipe_target helper manually parse a subset of call syntax and manually construct SurfaceExpr::Call, instead of routing through the same authority ordinary calls use. The project is explicit that duplicate authorities and parallel implementations drift as the language grows; that risk is sharper here because pipe is already a real surface concept elsewhere (items |> count exists in tests, and the modeling docs already describe a closed PipeMethod vocabulary). This is behavioral only, and it is BLOCKING. chatgpt-review-555f4e69-f689-42…

chatgpt-review-555f4e69-f689-42…

chatgpt-review-31b85e04-383d-4b…

chatgpt-review-7e00b910-daa0-49…

API-LEVEL ENFORCEMENT. Also violated, for the same root reason. Nothing in the API forces pipe targets to stay in lockstep with ordinary call targets; a future contributor can widen normal call syntax and forget to widen pipe_target, and the compiler will happily carry the divergence. The discipline explicitly says invariants should be enforced by the API/type system, not by “remember to edit both parsers” convention. That makes this BLOCKING rather than cleanup. chatgpt-review-7d5f6174-9ff3-49…

chatgpt-review-555f4e69-f689-42…

Design question. Is |> in v3 truly just syntax sugar for the language’s single callable surface, or is this PR creating a second, narrower parser grammar for calls? If that answer is wrong, the PR bakes in two authorities for the same concept now: ordinary call syntax on one side, pipe-call syntax on the other. From there every future callable feature—qualified callees, richer argument syntax, diagnostics on callee spans, or reconciliation with the existing pipe-method vocabulary—either drifts or forces a rework.

Verdict. REQUEST_CHANGES.

The direction is mostly right: parser-only normalization and no new IR node are exactly the right instincts. But the implementation sets the pattern at the wrong level by hand-parsing a second call-target grammar in parse_pipe_target; that is a single-authority / API-enforcement problem that is cheaper to fix now than after more call-syntax features land.

LOOP HEALTH: 📈 CONVERGING — this round closes a surface gap without adding a new IR variant, but it should not lock in a duplicate parser authority for calls.


View conversation

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87c47b8a99

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/v3/compiler/src/parse.rs Outdated
Comment on lines +1248 to +1252
fn parse_pipe(&mut self) -> Result<SurfaceExpr, Diagnostic> {
let mut lhs = self.parse_comparison()?;
while matches!(self.peek().kind, TokenKind::PipeArrow) {
self.bump();
lhs = self.parse_pipe_target(lhs)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Let piped expressions compose with later operators

parse_expr now routes through parse_pipe, and parse_pipe only consumes |> stages after an initial comparison parse; it never lets the resulting expression flow back into additive/comparison parsing. As a result, inputs like 5 |> negate + 1 or x |> f == y leave +/== unconsumed and fail to parse, even though pipe desugars to an ordinary call. This is a functional limitation introduced with the pipe grammar and is especially problematic because this parser currently has no parenthesized-expression fallback.

Useful? React with 👍 / 👎.

@briansrls

Copy link
Copy Markdown
Contributor Author

Review — APPROVE

Clean Prereq 5 implementation. Parser-only, no substrate or lowering changes, exactly per §4.3.

What's right:

  • Tokenizer: PipeArrow (|>) added as a two-char token alongside -> and =>. Correctly ordered before the single-char | fallback — no disambiguation issue with lambda/match |.
  • Parser: parse_pipe wraps parse_comparison, giving pipe the lowest operator precedence. Left-associative via while loop. parse_pipe_target accepts ident or ident(args) — matches §4.3 spec.
  • First-arg injection: injected_args.push(lhs) then append(args) — correct. The piped value becomes arg 0, existing args shift right.
  • Comment cleanups: "pipes" removed from scaffold gap lists in lower.rs and parse.rs dissolution ledger. Accurate — pipes are no longer a gap.
  • Tests: 3 tests covering unary pipe, multi-arg injection, and left-to-right chaining. All assert the desugared Dag shape directly (Transform target name, input count, literal values, return type). Good structural coverage.

One test gap (non-blocking):

No test for the error case — e.g., 5 |> 42 where the pipe target is not an identifier. The parser has a fail-closed diagnostic for this ("expected function name after |>, got ..."), but there's no test pinning it. Worth adding as a follow-up to prevent the diagnostic from silently disappearing.

Scope note from the PR body is accurate: [1,2,3] |> length depends on list-literal parsing (Prereq 4 scope), not on the pipe operator itself. The pipe desugar is complete without it.

Verdict: ship it. Prereq 5 done.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

codex · gpt-5.4 · 87c47b8a

⚠️ Review (blocking: 1, non-blocking: 1+/0-)

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/parse.rs Pipe was added as a bespoke outer grammar stage instead of through the same precedence path that the syntax spec owns → integrate |> into the existing operator-precedence parser so the parser and dag_syntax_spec cannot drift.

Non-blocking — Strengths

  • src/v3/compiler/tests/pipe_desugar.rs The tests exercise unary injection, multi-arg injection, and left-to-right chaining at the DAG boundary, which is the right real consumer for a parser-only feature.

ROADMAP — Verified

  • Prereq 5 parser-only desugar shape: The diff keeps pipe out of lowering and rewrites it to ordinary Call nodes during parsing, which matches docs/substrate-reflection-design.md §4.3.

ROADMAP — Incomplete

  • Prereq 5 precedence fidelity: The prerequisite is not complete until the parser matches the authoritative |> binding power already declared in dsl/extdeps/languages/dag/syntax.dag.

⚠️ The substrate shape is right, but the committed |> precedence is wrong in the new grammar and that is meaningfully harder to unwind after more pipe syntax lands.

Comment thread src/v3/compiler/src/parse.rs Outdated
}

fn parse_pipe(&mut self) -> Result<SurfaceExpr, Diagnostic> {
let mut lhs = self.parse_comparison()?;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKING: Parsing the pipe left-hand side as a full comparison makes |> weaker than ==, +, and *, which contradicts the single syntax authority in dsl/extdeps/languages/dag/syntax.dag and violates INVARIANTS "No duplicate representations".

@briansrls

Copy link
Copy Markdown
Contributor Author

codex · gpt-5.4 · 87c47b8a

⚠️ Review (blocking: 1, non-blocking: 1+/0-)

BLOCKING (1)

Root Cause

  • src/v3/compiler/src/parse.rs Pipe was added as a bespoke outer grammar stage instead of through the same precedence path that the syntax spec owns → integrate |> into the existing operator-precedence parser so the parser and dag_syntax_spec cannot drift.

Non-blocking — Strengths

  • src/v3/compiler/tests/pipe_desugar.rs The tests exercise unary injection, multi-arg injection, and left-to-right chaining at the DAG boundary, which is the right real consumer for a parser-only feature.

ROADMAP — Verified

  • Prereq 5 parser-only desugar shape: The diff keeps pipe out of lowering and rewrites it to ordinary Call nodes during parsing, which matches docs/substrate-reflection-design.md §4.3.

ROADMAP — Incomplete

  • Prereq 5 precedence fidelity: The prerequisite is not complete until the parser matches the authoritative |> binding power already declared in dsl/extdeps/languages/dag/syntax.dag.

⚠️ The substrate shape is right, but the committed |> precedence is wrong in the new grammar and that is meaningfully harder to unwind after more pipe syntax lands.

@briansrls

Copy link
Copy Markdown
Contributor Author

ChatGPT review in progress... (view conversation)

Check back in ~30 minutes for the full review.

@briansrls

Copy link
Copy Markdown
Contributor Author

Director review — #462 (Prereq 5, pipe sugar)

LGTM. Exactly matches §4.3's specification: pure parser sugar, first-arg injection, desugars during parsing, no substrate or lowering change.

Tokenizer: PipeArrow token added cleanly.

Parser: parse_pipe slots between parse_term and parse_primary in the precedence chain. parse_pipe_target handles unary (x |> f) and multi-arg injection (x |> f(y, z) → f(x, y, z)). Fail-closed on dotted paths after |>. Good bonus: parse_ident_expr extracted as shared helper between pipe target and primary expression — clean refactor, no behavior change.

Lowering: Comment-only updates removing "pipes" from scaffold gap lists. No functional changes.

Tests: 5+ tests covering unary pipe, multi-arg injection, left-to-right chaining, precedence with addition, precedence with comparison. Structural assertions on the desugared Call shape confirm the desugar is happening at parse time.

Design coherence: §4.3 says "by the time lowering runs, there is no pipe — just a normal function call." The implementation matches — SurfaceExpr::Pipe doesn't exist; the desugar produces SurfaceExpr::Call directly. Cleanest possible approach.

No issues. Ship-ready.

@briansrls

Copy link
Copy Markdown
Contributor Author

Meta-review in progress... (view conversation)

Loop-health check: is this review cycle making forward progress, or shifting debt? Posts in ~5-15 minutes.

@briansrls
briansrls merged commit 1633c1f into main Apr 16, 2026
2 checks passed
@briansrls
briansrls deleted the mild-hen-538 branch June 1, 2026 18:41
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