Skip to content

fix(desugar): a constructor gets its type arguments from its use - #439

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 with type parameters did not check when the other side of its sum had a type parameter.

Program Before Now
fn none(): Option(I32) = None Type mismatch: expected I32, found Var("a") accepted
or(None, 2) with or(o: Option(I32), d: I32) Type mismatch 2
let o = None in match o of ... Type mismatch runs
pick(L(42)) with pick(e: Either(I32, Bool)) Type mismatch runs
fn none<T>(): Option(T) = None Type mismatch (Var("a") against Var("T")) accepted

Some(n) checked before, because the other side of its sum is ().

Cause and change

The annotation of an injection had the type parameter itself (inl[a](())), which is no type at the constructor. It checked only when a generic function gave its own type parameter the same name as the data type did.

The annotation now binds the type parameters of the data type (inl[forall a. a](())), for each data type with type parameters. #438 did this for a data type that refers to itself. The checker gives each bound parameter a type from the use of the value.

Each annotation gets its own type: an injection has each type on its other side. So a side that nothing uses stays free. A use at another type is a type error, as before: g(B(1)) with g(t: T(Bool)) gives "Type mismatch".

No flag: a data type with type parameters is not behind --unstable-core.

Verification

  • cargo test --workspace --no-fail-fast: 637 passed, 0 failed. No program of the corpus changed.
  • cargo fmt --all --check and cargo clippy --workspace --all-targets: clean.
  • scripts/status-gate.sh: 634 all-target tests (1 more than before).
  • scripts/probe-battery.sh: 51 SAME, 7 SKIP (the SKIP rows need the wasmtime CLI).
  • New test a_constructor_gets_its_type_arguments_from_its_use in run_wasm.rs: 7 programs, as wasm and in the interpreter (a result, an argument, no annotation, the 2 branches of an if, a generic function, a constructor in a constructor, 2 type parameters at I64 and F64, 3 constructors), and 1 program that is refused.

Generated by Claude Code

A constructor of a `data` type with type parameters did not check when
the other side of its sum had a type parameter: `None` was not an
`Option(I32)`, and `L(1)` was not an `Either(I32, Bool)`. The
annotation of the injection had the type parameter itself, which is no
type at the constructor. It checked only when a generic function gave
its own type parameter the same name.

The annotation now binds the type parameters of the `data` type, for
each `data` type with type parameters. #438 did this for a `data` type
that refers to itself. The checker gives each bound parameter a type
from the use of the value. Each annotation gets its own type: an
injection has each type on its other side.

A use at another type is a type error, as before. No program of the
corpus changed. New test:
a_constructor_gets_its_type_arguments_from_its_use (7 programs, as wasm
and in the interpreter). 637 tests pass; the all-target count is 634.

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 20:01
@hyperpolymath
hyperpolymath merged commit b8df481 into main Nov 10, 2026
48 checks passed
@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.

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

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

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

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

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T20:07:34.552178Z 352456f 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.

@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: 352456f5e3

ℹ️ 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".

// constructors are names of the module that declares the type.
let value = self.in_scope(&info.home, |this| {
this.wrap_injection(payload, info.index, &ctors, params, span)
this.wrap_injection(payload, info.index, &ctors, &params, 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.

P1 Badge Preserve one instantiation across all constructor summands

For a nonrecursive data T(a) = A | B(a) | C(a), this passes params into every nested injection, but each annotation is quantified separately and check_inl/check_inr therefore instantiates it with a different unification variable. Consequently, if true then B(1) else C(true) now type-checks as Unit + (I32 + Bool), and a subsequent match can use the B payload numerically and the C payload as a boolean even though both occurrences are declared to have the same a. The constructor needs one shared instantiation for all occurrences of each data parameter rather than an independent one per injection annotation.

Useful? React with 👍 / 👎.

// annotation: the type checker finds a type for each one from the
// use of the value. (An annotation had the parameter itself, which
// is no type at the constructor: `None` was not an `Option(I32)`.)
let (params, ty) = self.data_ty(&info)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve linearity after resolving constructor parameters

Giving a nonrecursive constructor a fresh inferred parameter also lets an affine binding be classified before that parameter is solved. For example, with data Option(a) = None | Some(a), let x = None in eat(x) + eat(x) is accepted when eat takes and consumes an Option(String@r): x is stored as Unit + ?a, so its context entry is considered non-linear; the first call solves ?a to the linear String@r, but mark_used still checks the unresolved entry and permits the second use. This bypasses the checker's documented rule that a sum is linear whenever any component is linear, so inferred constructor parameters must be resolved when enforcing reuse (or the binding must be updated when unification solves them).

Useful? React with 👍 / 👎.

hyperpolymath added a commit that referenced this pull request Nov 10, 2026
…#441)

## Summary

1. A top-level constant works: `let K = 40` and `let K: I32 = 40`.
2. A fix for a defect that #439 exposed: a type that a function body
fixes late gave a wasm module that was not valid.

## 1. Top-level constants

The grammar had the declaration (`const_decl`), and only the core parser
read it.

| Step | Before | Now |
|---|---|---|
| Surface parser | "Unexpected declaration: const_decl" |
`SurfaceDecl::Const` |
| Type checker | the value was not checked, and no function body had the
constant in scope | the value is checked, and each function body sees
the constant |
| wasm | no code; a use of the name was a trap | a function with no
parameter; a use is a call |
| Interpreter | no code | the value, from an empty environment |

The rules:

- **Scope.** The value of a constant sees the functions of the module
and the constants before it. Each function sees each constant. A `let`
or a parameter of the same name hides the constant.
- **Type.** The type in the declaration, or else the type of the value.
A linear type is an error (a constant is used many times). A value that
does not fix the type (`let K = None`) is an error that asks for a type
in the declaration.
- **Modules.** A constant is private to its module (the grammar has no
`pub` for it). 2 modules can have a constant of one name: in the merge,
a constant gets a module name as a function does.
- **Evaluation.** The value is computed at each use. A value that is
costly, or that has an effect that must happen 1 time, needs a wasm
global that a start function sets. The code names that step.
- **Upper-case names.** The parser reads a name with an upper-case first
letter as a constructor. Such a name that is no constructor in scope,
and that is a constant of the module, is the constant. A constructor of
the same name comes first.
- **One declaration for a name.** A module declares a name 1 time, as a
function or as a constant. This also refuses 2 functions of one name,
which passed the checker and then gave a wasm module that was not valid
("duplicate export name").

The forms behind `--unstable-core` are behind it in the value of a
constant too.

## 2. Fix: a type that a function body fixes late

```
data Option(a) = None | Some(a)

fn main(): I64 = let o = None in match o of | None => 5000000000 | Some(n) => n end
```

`check` accepted this program, and `run` then stopped: "emitted wasm
failed structural validation: type mismatch: expected i64, found i32".
The validator stopped the module, so no wrong program ran.

Cause: the arms of the `match` fix the type of `o` after the checker
recorded the type of the scrutinee. The solutions of a function body
were dropped at the end of its check, before the type table was
resolved, so the table kept the type as not known and the wasm backend
read the payload as an i32. Before #439 this program did not pass the
checker, so the gap was not reachable.

The types of a body are now resolved when its check ends.

## Verification

- `cargo test --workspace --no-fail-fast`: 642 passed, 0 failed. No
program of the corpus changed.
- `cargo fmt --all --check` and `cargo clippy --workspace
--all-targets`: clean.
- `scripts/status-gate.sh`: 639 all-target tests (4 more than before).
- `scripts/probe-battery.sh`: 51 SAME, 7 SKIP (the SKIP rows need the
wasmtime CLI).
- New file `constants.rs`, each program as wasm and in the interpreter:
- `a_top_level_constant_runs` (10 programs: plain, lower-case with a
type, order, in a lambda, hidden by a local, I64 and F64 and Bool, a
closure and a pair and a list and a `data` value, a generic function, a
constructor of the same name, a function value);
  - `a_constant_is_private_to_its_module`;
  - `a_constant_follows_the_flag`;
  - `a_constant_that_is_not_valid_is_refused_by_name` (8 programs).
- `a_constructor_gets_its_type_arguments_from_its_use`: 2 more programs
for the fix (I64 and F64).

---
_Generated by [Claude
Code](https://claude.ai/code/session_0136P7ZmKrYLDC4WHi2yt11u)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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