Skip to content

pm-lang: type checking (v1)#45

Merged
sean-parent merged 9 commits into
mainfrom
worktree-language-server-vs-code-extension-3
Jul 20, 2026
Merged

pm-lang: type checking (v1)#45
sean-parent merged 9 commits into
mainfrom
worktree-language-server-vs-code-extension-3

Conversation

@sean-parent

@sean-parent sean-parent commented Jul 19, 2026

Copy link
Copy Markdown
Member

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 per
writing-plans' scope-check guidance rather than one plan spanning the type checker, the new
pm-lsp crate, and the VS Code extension.

  • cel_parser::Ty — a minimal static type model (built-in primitives + Ty::Any), with
    Literal/TypeId conversions and a unifies_with predicate where Any unifies silently with
    everything in both directions.
  • cel_parser::op_table::{OperandTypes, builtin_operand_types} — projects the existing runtime
    operator-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 — 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 to a later phase.
  • pm_lang::typecheck::check_sheet — 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, 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 # Examples doc blocks, a
ParseError::new/new_range inconsistency, the single-output tuple-body gap, and unifying the
operator-routing logic between the checker and the runtime dispatcher).

Test plan

  • cargo build --workspace — zero warnings
  • cargo test --workspace and cargo test --doc --workspace — all passing
  • cargo clippy --workspace --exclude begin --all-targets -- -D warnings
  • cargo clippy -p begin --no-default-features --all-targets -- -D warnings
  • cargo clippy -p begin --all-targets -- -D warnings
  • cargo fmt --all — no diff

🤖 Generated with Claude Code


Note

Medium Risk
New diagnostic semantics can disagree with runtime until LSP integration is designed (ParseError is !Send), but changes are additive and do not alter PmParser execution; incorrect Ty::Any unification 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 PmAstParser ASTs, intended for upcoming pm-lsp diagnostics (not wired here).

In cel-parser, introduces Ty (built-in primitives + Ty::Any with bidirectional silent unification) and ty::check_expr, which validates Expr::Op against builtin_operand_types and Expr::Logical for bool operands while recursing through apply/tuple/if nodes but inferring those as Any. op_table gains OperandTypes / builtin_operand_types and a shared signatures_for helper so runtime BuiltinScope::lookup and the checker read the same overload tables (no duplicated signature knowledge).

In pm-lang, check_sheet maps declared cell types via TypeRegistry, checks cell literal initializers against annotations (aligned with runtime parse_literal_as rules, 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, ParseErrorCELError for async LSP).

Reviewed by Cursor Bugbot for commit 64dbb20. Bugbot is set up for automated code reviews on this repo. Configure here.

sean-parent and others added 7 commits July 19, 2026 00:03
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>
@sean-parent
sean-parent requested a review from Copilot July 19, 2026 16:45

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread pm-lang/src/typecheck.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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::Ty plus cel_parser::ty::check_expr to infer/check types for Expr::Op (builtin overload tables) and Expr::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 via OperandTypes + builtin_operand_types, and shares routing logic between the checker and runtime lookup.
  • Adds pm_lang::check_sheet to 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.

sean-parent and others added 2 commits July 19, 2026 18:12
…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>
@sean-parent
sean-parent merged commit 6bc4700 into main Jul 20, 2026
1 of 2 checks passed
@sean-parent
sean-parent deleted the worktree-language-server-vs-code-extension-3 branch July 20, 2026 21:12
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.

2 participants