Skip to content

fix: a constructor checks its arguments against the declaration - #442

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/e2e-language-pass-ow72tj
Nov 10, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
claude/e2e-language-pass-ow72tj

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Summary

A constructor of a data type that is not recursive left no core node that carries the declared type. MkBox(true) passed for data Box = MkBox(I32) until a use of the value as a Box. This pull request adds the check.

data Box = MkBox(I32)

fn main(): I32 =
    match MkBox(true) of
    | MkBox(x) => if x then 1 else 2
    end

Before: check accepts the program. After:

box.eph:4:11: Type error: constructor `MkBox`: the declaration gives Base(I32), but the arguments give Base(Bool)

Changes

  • Desugarer. A constructor of a data type that is not recursive becomes let #<Ctor>: T = <value> in #<Ctor>. T is the declared type and binds the type parameters of the data type.
  • Type checker. check_let instantiates the annotation. A mismatch on a #<Ctor> binder is the new error ConstructorMismatch, which names the constructor. The check is on the full value, so the arguments of 1 constructor fix the type parameters for the other constructors also (P2(1, 2) for data P(a) = P2(a, a) | Q(a) makes Q hold an I32).
  • Wasm codegen. compile_let reads the type of such a let from the type table, because the annotation binds type parameters. (With the annotation, a value of 64 bits got a local of 32 bits.)
  • Plan document. Decision D14 records the type ascription for the P4 core. It stays open, to be decided with TMu, fold and unfold.

Owner decisions of 2026-11-10: the annotated let is the interim form, and the error names the constructor.

No file in formal/ changes. The calculus has the term already (ELet v (EVar 0)).

Not in this pull request

  • A constructor of a recursive type has its check from fold, as before. Its error text stays the general "Type mismatch".
  • data types stay structural: a value that no constructor made passes as the data type (f(5) for fn f(b: Box)). D14 lists the nominal form that closes this.

Test plan

  • New test a_constructor_checks_its_arguments_against_the_declaration (src/ephapax-cli/tests/run_wasm.rs): 4 programs that check refuses, and 2 programs that run in wasm and in the interpreter (a generic function whose type parameter has the name of the parameter of the data type, with a value of 64 bits; a linear argument).
  • data_types_that_refer_to_each_other_run: a Tree as a Forest is now refused at the constructor Node.
  • 4 unit tests of the desugarer follow the new shape.
  • cargo test --workspace: 643 pass, 0 fail. cargo fmt --check and cargo clippy --workspace --all-targets -- -D warnings are clean.
  • Probe battery: 51 SAME, 0 CHANGED, 7 SKIP (no wasmtime CLI in the session).
  • check on each of the 149 .eph files of the repository gives the same result before and after.

Generated by Claude Code

A constructor of a `data` type that is not recursive left no core node
that carries the declared type. `MkBox(true)` passed for
`data Box = MkBox(I32)` until a use of the value as a `Box`.

The desugarer now emits `let #<Ctor>: T = <value> in #<Ctor>`. `T` is
the declared type and binds the type parameters of the `data` type. The
type checker instantiates the annotation, and a mismatch is the new
error `ConstructorMismatch`, which names the constructor. The check is
on the full value, so the arguments of 1 constructor fix the type
parameters for the other constructors also.

Wasm codegen reads the type of such a `let` from the type table,
because the annotation binds type parameters.

Owner decisions 2026-11-10: the annotated `let` is the interim form;
decision D14 in the plan document records the type ascription for the
P4 core, to be decided with `TMu`, `fold` and `unfold`.

A constructor of a recursive type has the check from its `fold`, as
before.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u
@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Caution

Review failed

The pull request is closed.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath marked this pull request as ready for review November 10, 2026 21:55
@hyperpolymath
hyperpolymath merged commit 59e0343 into main Nov 10, 2026
37 of 38 checks passed
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ❌ Failed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T22:01:57.068145Z b6bad20 Draft marked ready
🔒 Security Review ❌ Failed 2026-11-10T21:56:02.004361Z
We couldn’t complete this review. Comment @codex security review to try again.
b6bad20 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@hyperpolymath
hyperpolymath deleted the claude/e2e-language-pass-ow72tj branch November 10, 2026 21:56
@chatgpt-codex-connector

Copy link
Copy Markdown

This security review couldn’t start because the paying account has exhausted its Codex allowance and has insufficient available credits. The payer can check their balance and add credits in the Codex usage dashboard, then try again. If you do not manage the paying account, contact this repository's admins.

@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: b6bad20a78

ℹ️ 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 on lines +1399 to +1403
ExprKind::Let {
name: name.clone(),
ty: Some(quantify(ty, &params)),
value: Box::new(value),
body: Box::new(Expr::new(ExprKind::Var(name), span)),

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 Preserve the constructor check across IR serialization

The new safety check exists only in this annotated let, but ephapax_ir::expr_to_sexpr serializes Let without its ty and decode_expr restores it as None. Consequently, serializing a desugared module and compiling it through the public compile_sexpr_module/CLI S-expression path silently removes the check, so invalid terms such as data Box = MkBox(I32) with MkBox(true) are accepted again. Either the IR codec must preserve this annotation or the constructor invariant must be represented by a core form that survives the round trip.

Useful? React with 👍 / 👎.

Comment on lines 1434 to +1438
if let Some(declared) = ty_ann {
// The desugarer gives a constructor the binder `#<name>` and
// the declared type of its `data` type, which binds the type
// parameters. A written annotation binds none.
let declared = &self.instantiate(declared.clone()).0;

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 Restrict instantiation to synthetic constructor lets

This now instantiates every annotated let, not just the synthetic #Ctor bindings. For callers constructing the public core AST, a declaration such as let id: forall a. a -> a = (fn (x: I32) -> x) in ... was previously rejected because the value is monomorphic, but is now accepted by choosing a = I32, even though Ty::ForAll is documented as universal polymorphism. Gate this instantiation on the synthetic constructor binding (or use a separate constructor-ascription mechanism) so ordinary core annotations continue to check the declared universal type rather than an arbitrary instance.

Useful? React with 👍 / 👎.

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.

3 participants