pm-lang: type checking (v1)#45
Conversation
First of three Phase 3 sub-plans (type checking / pm-lsp / VS Code extension), split out per writing-plans' scope-check guidance instead of one plan spanning all three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
I8..String plus Any, with Literal/TypeId conversions and a unifies_with predicate where Any unifies silently with everything in both directions. First piece of the design doc's Type checking (v1); Task 2 adds the op_table lookup this depends on transitively via lib.rs's export list (this commit alone does not build cel-parser standalone — see the plan for why that's expected here).
…ecker Projects the existing runtime operator-dispatch tables (BUILTINS, LEFT_SHIFT_SIGNATURES, RIGHT_SHIFT_SIGNATURES) into a checker-usable (arity, lhs TypeId, rhs TypeId) shape, never exposing the execution-only op_fn. The static checker (next commit) and the runtime dispatcher now read one shared source of truth instead of a second, hand-duplicated table that could drift from it.
…c comment cargo doc flagged an unresolvable intra-doc link to a private item. Plain-text reference instead, matching how op_fn is referenced two lines above in the same doc comment.
Checks Expr::Op (via builtin_operand_types) and Expr::Logical (fixed &&/|| bool semantics) directly; recurses into Apply/Tuple/TupleIndex/If so a nested Op error still surfaces, but infers those nodes themselves as Ty::Any — checking call/tuple/if shapes is deferred. Ty::Any silently unifies with everything, so unresolved identifiers and unregistered custom operators never produce false-positive diagnostics.
Checks each cell's literal initializer against its : type_name annotation, and each relationship/conditional method's body against its declared outputs (arity: single value vs n-tuple; and per-output type), built on cel_parser::ty::check_expr for the CEL expression bodies. Completes the design doc's Type checking (v1) section; the follow-on pm-lsp plan wires this and PmAstParser's existing syntax-error recovery into textDocument/publishDiagnostics.
- Add # Examples doctests to Ty's methods, check_expr, and check_sheet (CLAUDE.md mandates # Examples for all public APIs). - check_cell_initializer now uses ParseError::new_range for consistency with every other diagnostic in the file. - check_method's single-output branch now detects a tuple-shaped body (arity over-production) instead of silently passing since Tuple infers Any. - Extract signatures_for, shared by builtin_operand_types and BuiltinScope::lookup, so operator routing lives in exactly one place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 949032e. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds pm-lang type checking (v1) across pm-lang sheets and CEL expression ASTs, providing best-effort static diagnostics (intended groundwork for a future pm-lsp integration) while preserving CEL/pm-lang extensibility via Ty::Any.
Changes:
- Introduces
cel_parser::Typluscel_parser::ty::check_exprto infer/check types forExpr::Op(builtin overload tables) andExpr::Logical(&&/||bool semantics), while recursing through other nodes to surface nested operator errors. - Exposes builtin operand type signatures from
cel-parser’s runtime dispatch tables viaOperandTypes+builtin_operand_types, and shares routing logic between the checker and runtime lookup. - Adds
pm_lang::check_sheetto validate cell literal initializers vs annotations and method bodies vs declared outputs (arity + per-output type), plus a detailed implementation plan doc.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pm-lang/src/typecheck.rs | New check_sheet entry point + tests for pm-lang sheet-level type diagnostics. |
| pm-lang/src/lib.rs | Wires typecheck module and re-exports check_sheet. |
| cel-parser/src/ty.rs | Adds Ty model and check_expr checker over Expr trees (with doctests + unit tests). |
| cel-parser/src/op_table.rs | Exposes OperandTypes/builtin_operand_types and factors shared operator→signature routing for checker + runtime. |
| cel-parser/src/lib.rs | Exports ty module and re-exports Ty, OperandTypes, builtin_operand_types. |
| docs/superpowers/plans/2026-07-18-pm-lang-type-checking-v1.md | Adds the Phase 3 sub-plan writeup for implementing type checking v1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ics, not suffix-inferred type check_cell_initializer only runs when a cell has a `: type_name` annotation, but pm_lang::parser's real cell_decl grammar parses that case via parse_literal_as (digits parsed directly as the declared type, suffix ignored) — infer_and_parse_literal's unsuffixed-int->i32/ unsuffixed-float->f64 defaulting only applies when no annotation is present. Reusing that defaulting for the annotated case produced false positives (`cell x: u32 = 1;`, valid pm-lang, was flagged as i32-vs-u32) and false negatives (a char/byte-string/C-string literal, always rejected by parse_literal_as, was silently treated as Ty::Any). Replace ty_of_lex_literal (infers an independent Ty from the literal) with literal_matches_declared_ty (checks compatibility against the already-known declared Ty, mirroring parse_literal_as's real per-kind acceptance rules) and adjust check_cell_initializer accordingly. Reported by Cursor Bugbot on PR #45. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Summary
First of three Phase 3 sub-plans for the pm-lang language server design
(
docs/superpowers/specs/2026-07-17-pm-lang-language-server-design.md), split out perwriting-plans' scope-check guidance rather than one plan spanning the type checker, the new
pm-lspcrate, and the VS Code extension.cel_parser::Ty— a minimal static type model (built-in primitives +Ty::Any), withLiteral/TypeIdconversions and aunifies_withpredicate whereAnyunifies silently witheverything in both directions.
cel_parser::op_table::{OperandTypes, builtin_operand_types}— projects the existing runtimeoperator-dispatch tables into a checker-usable shape, so the static checker and the runtime
dispatcher read one shared source of truth instead of a second, hand-duplicated table.
cel_parser::ty::check_expr— checksExpr::Op(viabuiltin_operand_types) andExpr::Logical(fixed
&&/||bool semantics) directly; recurses intoApply/Tuple/TupleIndex/Ifso anested
Operror still surfaces, but infers those nodes themselves asTy::Any— checkingcall/tuple/if shapes is deferred to a later phase.
pm_lang::typecheck::check_sheet— checks eachcell's literal initializer against its: type_nameannotation, and eachrelationship/conditionalmethod's body against itsdeclared outputs (arity: single value vs. n-tuple, including a tuple-shaped body for a
single-output method; and per-output type).
Plan doc:
docs/superpowers/plans/2026-07-18-pm-lang-type-checking-v1.md.Built via subagent-driven development: each of the 4 tasks was implemented and independently
reviewed (spec compliance + code quality), followed by a final whole-branch review. One Minor
polish commit addresses that review's findings (missing
# Examplesdoc blocks, aParseError::new/new_rangeinconsistency, the single-output tuple-body gap, and unifying theoperator-routing logic between the checker and the runtime dispatcher).
Test plan
cargo build --workspace— zero warningscargo test --workspaceandcargo test --doc --workspace— all passingcargo clippy --workspace --exclude begin --all-targets -- -D warningscargo clippy -p begin --no-default-features --all-targets -- -D warningscargo clippy -p begin --all-targets -- -D warningscargo fmt --all— no diff🤖 Generated with Claude Code
Note
Medium Risk
New diagnostic semantics can disagree with runtime until LSP integration is designed (
ParseErroris!Send), but changes are additive and do not alterPmParserexecution; incorrectTy::Anyunification would cause false negatives on operator/cell checks.Overview
Adds Phase 3 sub-plan 1: a best-effort static type checker on top of existing
PmAstParserASTs, intended for upcomingpm-lspdiagnostics (not wired here).In
cel-parser, introducesTy(built-in primitives +Ty::Anywith bidirectional silent unification) andty::check_expr, which validatesExpr::Opagainstbuiltin_operand_typesandExpr::Logicalforbooloperands while recursing through apply/tuple/if nodes but inferring those asAny.op_tablegainsOperandTypes/builtin_operand_typesand a sharedsignatures_forhelper so runtimeBuiltinScope::lookupand the checker read the same overload tables (no duplicated signature knowledge).In
pm-lang,check_sheetmaps declared cell types viaTypeRegistry, checks cell literal initializers against annotations (aligned with runtimeparse_literal_asrules, including unsuffixed ints on unsigned types), and checks relationship/conditional method bodies against declared output arity and per-output types—including rejecting a tuple body when only one output is declared.Docs/handoff and the implementation plan are updated to mark this sub-plan complete and note follow-ons (
pm-lsp, VS Code extension,ParseError→CELErrorfor async LSP).Reviewed by Cursor Bugbot for commit 64dbb20. Bugbot is set up for automated code reviews on this repo. Configure here.