feat: and-patterns (conjunctive patterns, dual to or-patterns)#6049
feat: and-patterns (conjunctive patterns, dual to or-patterns)#6049
and-patterns (conjunctive patterns, dual to or-patterns)#6049Conversation
Introduces `AndP (p1, p2)` — conjunctive pattern — as the syntactic
counterpart to the existing disjunctive `AltP`. `(p1 and p2)` matches
a value iff both `p1` and `p2` match it; bindings from both legs are
available in the body.
## Surface syntax
Mirrors the or-pattern production in `parser.mly`:
pat_bin :
| p1=pat_bin OR p2=pat_bin { AltP(p1, p2) }
| p1=pat_bin AND p2=pat_bin { AndP(p1, p2) }
Precedence: `AND` binds tighter than `OR` (matches the expression-level
precedence table), so `#a or #b and #c` parses as `#a or (#b and #c)`.
The `AND`/`OR` tokens were already reserved for expression use.
## Frontend plumbing
Spot-wise additions (no new abstractions):
- `src/mo_def/syntax.ml` — `AndP of pat * pat`; removed the stale
`(* | AsP *)` placeholder comment.
- `src/mo_def/arrange.ml` — pretty-printing arm.
- `src/mo_frontend/typing.ml`:
- `is_explicit_pat`, `combine_pat_srcs`, `vis_pat`, `gather_pat_aux`
— recurse into both legs (bindings are not shared).
- `infer_pat` — error like `AltP` (explicit annotation required).
- `check_pat` — check both legs against expected type, union
bindings, error M0189 on overlap (duplicate binding across legs).
- `check_pat_typ_dec` — analogous for type bindings.
- `src/mo_frontend/definedness.ml` — both legs contribute to
defined/free sets (same as `AltP`).
- `src/mo_frontend/coverage.ml` — stub: approximate as the first leg's
coverage. Unsound for refutability but adequate for a frontend-only
draft; flagged with a TODO.
- `src/mo_interpreter/interpret.ml`:
- `declare_pat` — union of both legs' declared identifiers.
- `match_pat` — both must match; bindings adjoined.
- `src/js/astjs.ml` — AST-to-JSON case.
- `src/docs/namespace.ml` — `idents_in_pattern` dedupes defensively
(typechecker rejects overlap, but mo-doc may traverse pre-typecheck).
## Lowering
`src/lowering/desugar.ml` raises "and-pattern lowering not yet
implemented" — IR does not yet have `AndP` and codegen/IR passes
haven't been extended. This draft PR intentionally scopes to
frontend + typechecking; IR, interpretation of lowered form, and
backend come in follow-up commits.
## Tests
None yet in this commit; `test/run` tests land in a follow-up so the
parser/type-checker fix-ups can be reviewed in isolation first.
- `test/run/and-pattern-binds.mo`: exercises the source interpreter
on four and-pattern shapes — disjoint VarP+VarP, mixed option+VarP,
three-leg and, and an and-inside-or disjunction. Suppresses the
M0145 non-exhaustive-let warning on the option leg. Skips `run-ir`,
`run-low`, and `comp` since IR/lowering aren't wired up yet.
- `test/fail/and-pattern-overlap.mo`: overlapping bindings at top
level trigger M0051 ("duplicate definition") from `gather_pat_aux`.
- `test/fail/and-pattern-overlap-case.mo`: overlapping bindings inside
a switch case trigger M0189 from the `check_pat` AndP arm — the
case-pattern context doesn't go through `gather_pat`, so this is
the code path where the purpose-built error actually fires.
…form
Model the AndP arm after AltP's short-circuit style and express both
failure paths through Option's bind/map rather than an ad-hoc tuple
match. Local open `Option.(...)` keeps it one readable line per step:
| AndP (pat1, pat2) ->
Option.(bind (match_pat pat1 v) (fun ve1 ->
map (V.Env.adjoin ve1) (match_pat pat2 v)))
Reads as 'match p1; if it succeeds, try p2 and merge the envs'.
Observably equivalent to the previous eager-evaluation version
(match_pat has no side effects) — this is a style fix, not a
behaviour change. All existing and-pattern tests still pass.
Extends AND-pattern support from the frontend-only draft to end-to-end compilation. `test/run/and-pattern-binds.mo` now passes [tc] [run] [run-ir] [run-low] [comp] [valid] [wasm-run]. IR core (`src/ir_def/`): - `ir.ml` — new `AndP of pat * pat` constructor. - `ir_utils.ml` — irrefutable iff BOTH legs are (AND semantics). - `freevars.ml` — same `+-` combine as `AltP` (correct: left-biased union coincides with true union when bindings are disjoint, which the frontend enforces for and-patterns). - `arrange_ir.ml` — "AndP" printer. - `rename.ml`, `subst_var.ml` — bindings from both legs chained left-to-right (no assumption of shared bindings, unlike AltP). - `check_ir.ml` — both legs subtype-check against scrutinee; the resulting val_env is the disjoint_union of the legs'. IR interpreter (`src/ir_interpreter/interpret_ir.ml`) — AndP arm uses the same monadic `Option.(bind … map …)` form as the AST interpreter. Both interpreters short-circuit identically. IR passes — AndP arms added to `tailcall`, `erase_typ_field`, `const`, `async`, `await` (three spots: declare_pat / rename_pat / define_pat). All recurse into both legs; `await`'s renames hit the disjoint-union assumption cleanly. Codegen (both backends) — `fill_pat` stashes the scrutinee in a local named `and_scrut`, feeds it to each leg in sequence via `(^^^)` composition; either leg's failure propagates through the pattern-code's fail-continuation and cancels the whole match. `destruct_const_pat` threads the val_env through both legs sequentially, returning `None` if either rejects. Lowering (`src/lowering/desugar.ml`) — the previous `failwith` is replaced by a direct `S.AndP → I.AndP` translation. Tests (`test/run/and-pattern-binds.mo`) — the SKIP directives for `run-ir`, `run-low`, and `comp` are removed; the new phase outputs match the source-interpreter output exactly.
5a9d429 to
b351b22
Compare
- `doc/md/16-language-manual.md`: add 'And pattern' section after
'Or pattern', modelled on the existing 'Or pattern' wording and
noting that sides must bind disjoint identifiers (the key
semantic difference from or-patterns). Include the `and`-binds-
tighter-than-`or` precedence note so `p1 or p2 and p3` parses
unambiguously. Also a tiny wording tweak to the or-pattern
section ('the types assigned to it' rather than 'its type in').
- `doc/md/fundamentals/8-pattern-matching.md`: add a table row
for the and-pattern mirroring the or-pattern row. Also fix a
small typo on the 'refutable/irrefutable' line where the
opening `**` for bold was missing.
Independent FP-savvy review (pre-human-review pass)Ran an independent pass over the branch looking for correctness bugs, style issues, and coverage gaps. Summary below. Overall verdict
Correctness concerns
Missing tests (the biggest gap)The current positive test (
Style nits
Final verdictReady for human review, not ready to merge. Correctness of the core implementation is solid. Before merge I'd want:
🤖 Review performed by an independent agent (not the author). |
… threading Extends `and-pattern-binds.mo` from the earlier irrefutable-only coverage with the test cases flagged in the pre-review pass: - **refutable AndP in switch** (`describeOpt`): `?x4` is refutable, so the switch exercises `(^^^)` composition on `CanFail` codes. - **leg-1 failure falls through** (`leftFails`): `(#Ok 42) and s5` — when the refutable first leg fails the next case must catch it. - **leg-2 failure falls through** (`rightFails`): `(#Ok _) and (#Ok 99)` — leg 1 succeeds on any `#Ok`, leg 2 narrows further; when leg 2 fails the fail-continuation must abandon leg 1's partial match and advance. - **AndP inside a function body** (`addBoth`): wraps the switch-AndP in a function. (Direct func-arg AndP would hit the documented M0184 limitation — infer_pat has no scrutinee type to propagate — so we use a switch to enter check_pat instead.) - **AndP inside TupP**: `let ((a7 : Nat) and b7, c7) = (7, "world")`. - **three-way with refutable middle leg** (`peelOpt`): `?y8 and x8 and z8` — exercises the rho threading in `rename.ml` / `subst_var.ml`, where each leg contributes its own bindings to the accumulating environment. The reviewer-suggested 'type-mismatched legs' case was rejected as not a new failure mode: both legs are `check_pat`'d against the same scrutinee type, so an incompatible annotation on either leg is just the standard M0117 `AnnotP` subtype error — not AndP-specific. The analogous 'same-name bindings with different types' IS distinct for `or`-patterns (drives `lub` + `warn_lossy_bind_type`) and is already covered by `test/run/or-pattern-mismatch.mo`.
or-patterns)
or-patterns)and-patterns
and-patternsand-patterns (conjunctive patterns, dual to or-patterns)
The `test_recovery` expect-test enumerates the syntax-error followup tokens parser.mly emits when recovery kicks in. Adding the `AND` production at `pat_bin` naturally extends that list by one entry (`and <pat_bin> (e.g. 'and x')`); promote the new baseline.
- New error code M0260 (\`lang_utils/error_codes.ml\`): "\`and\`-pattern binds the same variable in both legs". Previously the overlap check emitted M0189 (which covers or-pattern-alternatives binding mismatches) — reusing it conflated two distinct conditions. typing.ml now raises M0260 at the three overlap sites (live \`check_pat\` val-env, live \`check_pat_typ_dec\` typ-env, and the commented-out speculative \`infer_pat\` arm). The fail-baseline for \`test/fail/and-pattern-overlap-case.mo\` is updated. - Codegen style (both backends): collapse two sequential \`let\` bindings in \`fill_pat\` AndP to a single tuple-bind \`let code1, code2 = fill_pat env ae p1, fill_pat env ae p2 in\` and drop the parens on \`let set_i, get_i = new_local env \"and_scrut\"\`. Also refactor \`destruct_const_pat\` AndP to monadic form \`Option.bind (... p1 ...) (fun ae' -> ... ae' p2 ...)\`. All behaviourally equivalent; just reads cleaner.
The inline comment still pointed at M0189; the check in typing.ml's AndP arm now emits M0260 (the dedicated and-pattern overlap code added in the previous commit).
Replaces the previous stub (delegate to leg-1, documented as unsound for refutability) with a proper intersection-semantics check. New `InAnd1 of ctxt * pat * T.typ` ctxt wraps the first-leg match; on succeed, the second leg runs against the narrowed desc under the outer ctxt; on fail, the whole AndP fails for that desc slice. Non- covering content in pat2 (e.g. a narrower tag than pat1 admitted) surfaces as uncovered via the normal flow — no special casing. Also drops one spurious M0146 'this pattern is never matched' that the stub was emitting against a reachable `(#Ok _) and (#Ok 99)` case in and-pattern-binds.mo. Adds test/run/and-pattern-coverage.mo: three switches that each combine a refutable AndP case with later cases so that the ctxt flow must propagate pat1's narrowing correctly to subsequent cases. None emits warnings under the new logic, confirming the fix.
M0184 is registered as 'Cannot infer or-pattern' — reusing it for and-patterns in `infer_pat`'s AndP arm conflated two distinct semantics (users looking up M0184 would see or-pattern docs while hitting the error on an and-pattern). Introduce M0261 dedicated to and-patterns and retarget the infer_pat arm at it.
Direct and-pattern in function-arg position has no scrutinee-type context to check against, so it hits `infer_pat`'s AndP arm and raises M0261. Mirrors the pattern of `test/fail/or-pattern-diff.mo` (which covers M0184 for the or-pattern counterpart).
…ntext
- `check_ir.ml`: add `verify_pair` helper (cheap iter + predicate, no
raise/catch) and use it to enforce disjoint AndP leg bindings in
`check_pat`; rewrite the misleading "(frontend enforces)" comments
at both `gather_pat` and `check_pat` sites so the actual enforcer
is named — matters for separate compilation where IR is read from
disk.
- `typing.ml`: `gather_pat_aux`'s AndP arm now gathers each leg
against the outer scope independently and enforces disjoint
leg-contributions with the AndP-specific M0260 (matching
`check_pat`). Previously, `gather_id` would fire the generic M0051
first in let-context, so M0260 was only reachable from `case`.
- `test/fail/ok/and-pattern-overlap.{tc,tc-human}.ok`: regenerated —
now shows M0260 with the AndP's full region.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`let (x : Nat) and (y : Text) = 5` — expects a clean M0117 subtype error pointing at the offending leg, per the review's missing-test list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 2 Independent FP-savvy re-reviewReferencing: round 1. Scope: the two new commits Overall verdict
Concerns
Things round 1 skipped that I re-checked and cleared
Final verdictReady to merge once the stale M0184 comment in 🤖 Round-2 review by an independent agent (not the author). |
The comment on the switch-lifted AndP test named the wrong error code (M0184 is AltP's; AndP uses its own M0261 since 1b332d7) and framed inference-position rejection as blanket policy. In reality it only bites when FuncE has to synthesise its own domain (top-level `func f` desugars to LetD with no outer type); any checking-mode context — HOF arguments, class members, `let f : T -> U = func(...) = ...` — accepts AndP in func-arg position via check_pat's M0260-enforcing path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the last `disjoint_union env at fmt …` call in `check_ir.ml` (the TupP/ObjP sibling-check) in favour of an explicit verify_pair + T.Env.adjoin, matching the AndP site. Rigorous without raise/catch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: `infer_pat'` rejected every AndP with M0261, forcing both legs to be annotated in inference-position contexts (top-level `func f(pat)`, bare lambda as LetD RHS, etc.). Now: if at least one leg is `is_explicit_pat`, use `infer_pat` on that leg and `check_pat` the other against the inferred type; if both are explicit, take the `glb`. M0261 only fires when *neither* leg carries enough annotation. Disjointness enforcement is unchanged — walks `ve1` and emits M0260 on any key present in `ve2`. - `test/run/and-pattern-infer.mo` — new positive test covering left-only / right-only / both-annotated / let-context / TupP-nested. - `test/fail/and-pattern-infer.mo` — comment clarified; cross-refs the new positive test. `.ok` regenerated via `make accept`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds
p1 and p2— conjunctive pattern, dual to existingp1 or p2. Matches when both legs match; bindings from both are available. Legs must bind disjoint identifiers (or-patterns require the same set);andbinds tighter thanor.Implementation
End-to-end: parser → Syntax → typing/definedness/coverage (stubbed) → desugar → IR → all ir_passes (rename, subst_var, freevars, check_ir, await, async, const, tailcall, erase_typ_field) → both backends (classical + enhanced) → wasm. AST and IR interpreters both use
Option.(bind … map …).Tests
test/run/and-pattern-binds.mo— positive, passes[tc] [run] [run-ir] [run-low] [comp] [valid] [wasm-run]. Covers refutable-leg failures (both positions), func-body, TupP-nested, three-way with refutable middle.test/fail/and-pattern-overlap.mo— let-context overlap (M0051).test/fail/and-pattern-overlap-case.mo— case-context overlap (M0189, bespoke error).Known limitations
coverage.mlAndP arm is a stub delegating to leg 1 — diagnostic-quality regression only, not soundness.Docs
Changelog entry + language-manual section + fundamentals-table row.
See also the independent FP-savvy pre-review pass for concrete correctness/coverage traces.