Skip to content

feat: and-patterns (conjunctive patterns, dual to or-patterns)#6049

Open
ggreif wants to merge 21 commits intomasterfrom
gabor/and-patterns
Open

feat: and-patterns (conjunctive patterns, dual to or-patterns)#6049
ggreif wants to merge 21 commits intomasterfrom
gabor/and-patterns

Conversation

@ggreif
Copy link
Copy Markdown
Contributor

@ggreif ggreif commented Apr 23, 2026

Adds p1 and p2 — conjunctive pattern, dual to existing p1 or p2. Matches when both legs match; bindings from both are available. Legs must bind disjoint identifiers (or-patterns require the same set); and binds tighter than or.

switch o {
  case (?x and s) (debug_show {s; x});  // payload + whole value
  case null "none";
};

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

  • Func-arg AndP in inference position errors with M0184 (parity with or-patterns).
  • coverage.ml AndP 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.

ggreif added 2 commits April 23, 2026 10:53
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.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

Comparing from 895c20f to 5c8380c:
In terms of gas, no changes are observed in 5 tests.
In terms of size, no changes are observed in 5 tests.

ggreif added 2 commits April 23, 2026 11:17
…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.
@ggreif ggreif force-pushed the gabor/and-patterns branch from 5a9d429 to b351b22 Compare April 23, 2026 09:33
- `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.
@ggreif ggreif self-assigned this Apr 23, 2026
@ggreif ggreif added the feature New feature or request label Apr 23, 2026
@ggreif ggreif mentioned this pull request Apr 23, 2026
29 tasks
@ggreif
Copy link
Copy Markdown
Contributor Author

ggreif commented Apr 23, 2026

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

  • Frontend plumbing (syntax, parser, definedness, desugar, arrange) — complete and parallel to AltP. Precedence correct: %left OR at parser.mly:268 then %left AND at parser.mly:269 — later-declared = tighter, so and binds tighter than or.
  • Typing (src/mo_frontend/typing.ml) — check_pat, gather_pat_aux, vis_pat, combine_pat_srcs, is_explicit_pat, check_pat_typ_dec all have AndP arms. Overlap rejection via M0189 on both val and typ envs.
  • AST interpreter (src/mo_interpreter/interpret.ml:929) — monadic form bind ... (fun ve1 -> map (V.Env.adjoin ve1) ...) is idiomatic.
  • IR interpreter (src/ir_interpreter/interpret_ir.ml:762) — symmetric; declare_pat adjoins both legs.
  • IR check (src/ir_def/check_ir.ml:1093) — t <: pat1.note; t <: pat2.note + disjoint_union is correct and enforces the invariant IR-side.
  • Freevars / rename / subst_var+- is left-biased union on maps; on disjoint keys it coincides with true union, so the result is correct. Rename threads rho → rho' → rho'' via id_bind which allocates globally-fresh names, so no fresh-rename collision possible.
  • await.mldisjoint_union in rename_pat; declare_pat pat1 (declare_pat pat2 exp) and define_pat concat are correct. AltP's freevars-empty assertion correctly does not extend to AndP.
  • Codegen(^^^) at compile_enhanced.ml:10635 threads the same fail continuation to both legs' CanFail arms, so if either leg fails the whole match aborts. Stack discipline is balanced (set_i consumes scrutinee; each leg starts with get_i and consumes it). Classical and enhanced are identical.
  • ⚠️ Coverage (src/mo_frontend/coverage.ml:272) — explicit TODO stub delegating to pat1 only. Concrete consequences: (a) case (#a _ and #b _) is treated as covering #a _, so an explicitly-unreachable case won't be flagged redundant and a following case (#a _) may be spuriously flagged as redundant; (b) exhaustiveness-wise it's over-approximating coverage of the first leg, so "missing cases" warnings may be missed. Diagnostic-quality regression only, not soundness.Fixed with 7a75752!

Correctness concerns

  1. gather_pat in check_ir.ml:1040 uses go (go ve pat1) pat2 without any collision check, trusting the frontend. check_pat further down does enforce disjoint_union, so any bug that slipped past the frontend would surface there — defensible but worth a one-line assertion or comment noting the redundancy. — Fixed: the "(frontend enforces)" comments at check_ir.ml:1041 and check_ir.ml:1094 have been rewritten to name the actual enforcer (check_pat / verify_pair), so the invariant-holder is explicit for separate-compilation scenarios where IR is loaded from disk. check_pat's AndP arm now uses an explicit verify_pair helper (cheap iter + predicate, names the first clashing key) instead of disjoint_union's raise/catch control flow.
  2. gather_pat_aux in typing.ml:4622 also quietly merges without collision check; the real enforcement happens in check_pat at 3444. Same comment applies: gather runs before the overlap check. — Fixed in 753404cac: gather_pat_aux's AndP arm now gathers each leg independently against the outer scope and enforces disjoint leg-contributions with the AndP-specific M0260, matching check_pat's diagnostic (previously gather_id fired the generic M0051 first in let-context).
  3. infer_pat at 3271 outright errors with M0184. Even when one leg is fully annotated ((a : Nat) and b) we could propagate its type to the other leg; currently this forces users to annotate both. Parity with AltP's behaviour, but surprising in function-arg position: func f ((a : Nat) and b) = … works; func f (a and b) = … is a hard error. OK to leave; document it. — Partially fixed: now uses dedicated M0261 (not M0184) in 1b332d74a; parity with AltP's M0184 preserved, so left undocumented.

Missing tests (the biggest gap)

The current positive test (and-pattern-binds.mo) exercises only irrefutable patterns in let-position, so the interesting codegen path (CanFail is1 ... k; is2 k with actual failure) is untested. Suggested additions:

  • Refutable AndP in switch: case (?x and s) …; case null … over a ?Nat — exercises (^^^) on CanFail codes. — Fixed in bfe6a06aa.
  • Runtime-failing leg: switch where the AndP fails on a value that would have matched the other case — verifies fail-through from leg 1 or leg 2 reaches the next case, not a trap. — Fixed in bfe6a06aa (leftFails / rightFails over Status).
  • AndP in function arg: func f ((x : Nat) and y) = x + y. — Fixed in 1355414ea (test/fail/and-pattern-infer.mo inference-position; positive func-arg covered in and-pattern-binds.mo).
  • AndP inside TupP: let ((a : Nat) and b, c) = (5, 6). — Fixed in bfe6a06aa.
  • Type-mismatched legs: let (x : Nat) and (y : Text) = 5 — expect subtype error, not a confused message. — Fixed in 3e730ff7c (test/fail/and-pattern-typemismatch.mo): produces a clean M0117 "pattern of type Text cannot consume expected type Nat" pointing at the offending leg.
  • Three-way conjunction with one refutable leg (e.g. ((x : Nat) and ?y and z) over ?7) — exercises the threaded rho in rename/subst. — Fixed in bfe6a06aa.

Style nits

  • Both interpreters' monadic form is fine.
  • subst_var's pat (pat rho p1) p2 threading and rename's (p1', rho'); (p2', rho'') are both idiomatic.

Final verdict

Ready for human review, not ready to merge. Correctness of the core implementation is solid. Before merge I'd want:

  1. At least one refutable-AndP switch test exercising (^^^) on CanFail codes. — Fixed
  2. At least one AndP-in-func-arg and one AndP-in-TupP test. — Fixed
  3. Either flesh out coverage.ml's AndP arm or add a FileCheck/warn-expectation test confirming the current stub's behaviour is known. — Fixed
  4. The typing changes include a new use of error code M0184 for AndP; M0189 for overlap — both reuse existing codes, which is fine, but worth a quick grep that no diagnostics index needs updating. — Fixed

🤖 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`.
@ggreif ggreif marked this pull request as ready for review April 23, 2026 10:06
@ggreif ggreif requested a review from a team as a code owner April 23, 2026 10:06
@ggreif ggreif changed the title feat(and-patterns): parser + AST + frontend plumbing feat: and-patterns (conjunctive patterns, dual to or-patterns) Apr 23, 2026
@ggreif ggreif changed the title feat: and-patterns (conjunctive patterns, dual to or-patterns) feat: conjunctive and-patterns Apr 23, 2026
@ggreif ggreif changed the title feat: conjunctive and-patterns feat: and-patterns (conjunctive patterns, dual to or-patterns) Apr 23, 2026
ggreif and others added 10 commits April 23, 2026 15:16
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.
test that `and` has lower precedence than tags
- 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>
@ggreif
Copy link
Copy Markdown
Contributor Author

ggreif commented Apr 24, 2026

Round 2 Independent FP-savvy re-review

Referencing: round 1. Scope: the two new commits 753404ca (check_ir rigour + M0260 let-parity) and 3e730ff7 (type-mismatch test), plus anything round 1 missed.

Overall verdict

  • The two round-1 concerns that remained open are now cleanly resolved. verify_pair in check_ir.ml:155 is a faithful stand-in for disjoint_union's raise/catch — same domain, same diagnostic, no behavioural drift. The new gather_pat_aux AndP arm at typing.ml:4648 genuinely restores M0260 parity between let and case context, and the regenerated and-pattern-overlap.tc{,-human}.ok proves it (region is now the whole AndP 1.5-1.28, code flipped M0051 → M0260).
  • T.Env.adjoin is right-biased (lang_utils/env.ml:35, Some x2). Since verify_pair proves disjointness before adjoin runs, the semantics are identical to the old disjoint_union; if a future regression let overlap through, adjoin would silently drop ve1's binding, but the explicit verify_pair step prevents that. Fine.
  • Scope-component coverage in the new gather_pat_aux arm is complete. gather_pat_aux + gather_pat_field + gather_typ_id only ever touch val_env, typ_env, and con_env. The first two are explicitly checked. con_env uses Cons.fresh per TypPF, so the constructors are always distinct identities; collisions are detected by-name through the typ_env iter before con_env's disjoint_add is asked to swallow them. lib_env, obj_env, mixin_env, fld_src_env are unreachable from patterns — no gap. Verified against scope.ml:43 and typing.ml:4669–4691.
  • Nested AndP ((a and b) and c), AndP-in-TupP (( x and y, z )), AndP-in-AltP-leg, AndP-with-ObjP-leg-containing-TypPF, and duplicate-against-outer-scope (caught earlier by gather_id's M0051) all work as intended — I traced each.
  • Parser precedence %left COLON < %left OR < %left AND (parser.mly:267/269/270) is sound: (a : Nat) and b and ?x and s and #A x and #B y all bind the way round 1 asserted. Nothing to add.

Concerns

  1. Stale comment in test/run/and-pattern-binds.mo:62–63 — still says "infer_pat raises M0184, matching AltP's behaviour", but the round-1 fix in 1b332d74a moved the code to the dedicated M0261. Trivial, one-line fix.
  2. verify_pair is slight over-abstraction. Its only call site (check_ir.ml:1108) passes fun k ve2 -> not (T.Env.mem k ve2). A narrower check_disjoint env at fmt env1 env2 would be equally rigorous and shorter. Not a blocker; ignore if you plan to reuse the predicate form elsewhere.
  3. New M0117 diagnostic for let (x : Nat) and (y : Text) = 5 does not mention and-pattern context. The output reads "pattern of type Text cannot consume expected type Nat" and underlines (y : Text). The underline is helpful, but a first-time AndP user sees a bare AnnotP error and has to reason about why that leg saw Nat. This is shared with AltP's equivalent, and not a new issue introduced by these commits — just worth noting that the test locks in a slightly user-unfriendly diagnostic. I would not block merge on it.

Things round 1 skipped that I re-checked and cleared

  • Coverage residual over-approximation. coverage.ml:272, 345, 374 (InAnd1): traced slices for ((#a _) and (#b _)) — pat1 accepts #a slice, succeed(InAnd1) delegates to pat2 on #a-narrowed desc, pat2's TagP fails → fail(InAnd1) → fail ctxt' → fall-through. Case's at is not added to reached_cases, so a vacuous AndP case is correctly flagged as unreachable. No residual over-approximation.
  • Freevars (freevars.ml:88–89): +- on disjoint keys = union; combined with check_pat/verify_pair's disjointness guarantee, the asymmetric left-biased semantics of +- are irrelevant. Clean.
  • await.ml rename_pat/declare_pat/define_pat all still symmetric between AndP legs. No AltP-style "bindings-empty" assumption leaks into AndP.
  • arrange.ml:172 / arrange_ir.ml:143 — identical to AltP, fine.

Final verdict

Ready to merge once the stale M0184 comment in and-pattern-binds.mo:62–63 is updated. Everything else is either cosmetic (over-general helper) or shared with AltP's long-standing behaviour (AndP-flavour-less M0117). The rigour/parity story is now coherent end-to-end: frontend gathers strictly, check_pat enforces strictly, and check_ir's verify_pair validates disk-loaded IR without trusting the frontend.

🤖 Round-2 review by an independent agent (not the author).

ggreif and others added 3 commits April 24, 2026 12:16
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>
@ggreif ggreif requested review from crusso April 27, 2026 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant