Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .cursor/skills/finding-ion-bugs/references/bug-hotspots.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- **Reference escape**: `&` stored in struct, returned, sent on channel, captured by `spawn`. Locals and params must also reject off-stack stores (`Box<&T>`, `Vec<&T>`); `is_reference_containing` on decls/returns is not enough (`test_box_ref_let_error.ion`). `Option<&T>` stack temporaries from `get_ref` stay legal.
- **Send**: non-Send types on channels or in spawn closures; `Box` and channel element variance
- **Recursive types**: `is_reference_containing` / `is_send` / `is_eq_type` / `type_needs_drop` need a visiting set; without one, `Box`/`Vec`/`Option<Box<…>>` self-reference stack-overflows at decl time. Representability (`InfiniteSize`) treats only `Box`/`Vec`/`RawPtr` as size boundaries — `Option<Node>` is still infinite size; `Option<Box<Node>>` is not. Do not “stop at Box” inside the no-escape walker or `Box<&T>` silently passes.
- **Generic enum `None`**: `Option::None` has no payload, so `T` is inferred from `expr_expected` / return type (let annotation, struct field, call argument, built-in value parameter, or return). Unannotated `let empty = Option::None` must error with cannot-infer, not a later `Option<T>` vs `Option<Box<Node>>` mismatch (`test_option_none_unannotated_error.ion`). Same-expression `Node { next: Option::None }` is fine (`test_option_none_struct_field.ion`). Direct call arguments `take(Option::None)` are fine (`test_option_none_call_arg.ion`). `send(&tx, Option::None)` infers from `Sender<T>` (`test_send_option_none.ion`); `Box::new(Option::None)` infers from an expected `Box<Option<...>>` (`test_box_new_option_none.ion`). `send` is `Expr::Send`, not a user `Call`, so call-arg expected-type plumbing does not cover it.
- **Generic enum `None`**: `Option::None` has no payload, so `T` is inferred from `expr_expected` / return type (let annotation, struct field, call argument, built-in value parameter, return, assignment target, or array/tuple/repeat/enum-payload element). Unannotated `let empty = Option::None` must error with cannot-infer, not a later `Option<T>` vs `Option<Box<Node>>` mismatch (`test_option_none_unannotated_error.ion`). Same-expression `Node { next: Option::None }` is fine (`test_option_none_struct_field.ion`). Direct call arguments `take(Option::None)` are fine (`test_option_none_call_arg.ion`). `send(&tx, Option::None)` infers from `Sender<T>` (`test_send_option_none.ion`); `Box::new(Option::None)` infers from an expected `Box<Option<...>>` (`test_box_new_option_none.ion`). Array/tuple elements, `[value; N]`, assignment, and `return [Option::None]` use the same helper (`test_array_option_none.ion`, `test_tuple_option_none.ion`, `test_array_option_some.ion`). `send` is `Expr::Send`, not a user `Call`, so call-arg expected-type plumbing does not cover it.
- **Match-arm result types**: `infer_block_result_type` reads recorded `TypeInfo` expr types plus control-flow shape (diverge vs value). It must not call `check_expr` again after `check_stmt` (`test_vec_get_putback_named.ion`).
- **Match on `&GenericEnum`**: peel `Ref` before building the type-param subst map in `add_pattern_bindings` (see `test_match_ref_generic_enum_arith.ion`); bare `if let Type::Generic` misses `Ref { Generic { … } }` and leaves bindings as `&T`
- **`resolve_type_name` and `&Enum` params**: must recurse into `Ref` so `&Flag` becomes `Ref { Enum }` (parser stores enum names as `Struct`); otherwise calls get `expected &Flag, got &Flag` from Struct vs Enum mismatch
Expand All @@ -21,7 +21,8 @@ CLI errors use `TypeCheckError` Debug form (`UseAfterMove { ... }`). LSP reforma
- Drop order and `ion_drop_*` for moved fields
- **Struct field move-out**: owned fields null after partial move on the next statement (`board.items = NULL`; deferred when the move is a call argument)
- **Vec::push lvalues**: struct variables and field paths use `&item`, not compound literal (`vec_push_struct_var_uses_address_of_lvalue`)
- **Enum emission order**: non-generic enums before structs in single-file C output
- **Enum emission order**: non-generic enums before structs in single-file C output. Generic enum instantiations (`Option_int`) must be complete types before tuple typedefs that store them by value (`test_tuple_option_none.ion`). `collect_generic_from_type` must walk `Type::Tuple` (and TupleLit `elem_types`); walking only nested exprs misses `Option::None` which has no payload expr. Multi-file `generate_module_source` still emits user structs before generic enum bodies (`examples/data_lib`).
- **Nested enum compound literals**: GCC rejects `(Option_int){...}` as a designated field of a tuple or struct. Emit brace-only `{ .tag = N, .data = { } }` there. Call-site and let inits stay `(Option_int){...}` (`test_option_none_call_arg.ion` cgen `take((Option_int)`).
- **Tuple mangle**: `tuple_type_name` sanitizes `*` and brackets when names include `Vec` types
- **Match scrutinee move-out**: pattern payload bindings null `match_val_N.data.variant_*` fields when ownership transfers (`statement_match_payload_move_neutralizes_scrutinee`); whole-enum binding arms clear active variant payloads via `emit_match_scrutinee_whole_enum_moved_out` (`whole_enum_binding_neutralizes_scrutinee_payloads`). IR infers `enum_type` from the scrutinee when arms use binding/wildcard only (`infer_match_enum_name`).
- **Return unwind**: all function exits use `emit_function_return` (`ret_val`, `scope_emit_return_unwind`, `goto epilogue`), including diverging `return` inside rvalue `match` arms (`rvalue_match_divergent_return_unwinds_owned`). Value-producing rvalue arms still assign and `break` from the `switch`.
Expand Down
2 changes: 1 addition & 1 deletion .cursor/skills/ion-integration-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ cargo build --release --bin ion-compiler --bin ion-build
cd tests && ./test_runner.sh
```

One test or a substring (runs every matching `run` / `error` / `cgen` row):
One test: `test_foo.ion` is an exact stem. A bare stem `test_foo` is a substring (runs every matching `run` / `error` / `cgen` row):

```bash
cd tests && ./test_runner.sh test_foo.ion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ let p: Point = Point { x: 1, y: 2 };

## Enum variants

Tuple: `Option::Some(42)`, `Option::None`. `take(Option::None)` infers `T` from the parameter type ([tests/test_option_none_call_arg.ion](../../../../tests/test_option_none_call_arg.ion)); `send(&tx, Option::None)` infers from `Sender<T>` ([tests/test_send_option_none.ion](../../../../tests/test_send_option_none.ion)); `Box::new(Option::None)` infers from an expected `Box<Option<...>>` ([tests/test_box_new_option_none.ion](../../../../tests/test_box_new_option_none.ion)); unannotated `let empty = Option::None` still needs an annotation.
Tuple: `Option::Some(42)`, `Option::None`. `take(Option::None)` infers `T` from the parameter type ([tests/test_option_none_call_arg.ion](../../../../tests/test_option_none_call_arg.ion)); `send(&tx, Option::None)` infers from `Sender<T>` ([tests/test_send_option_none.ion](../../../../tests/test_send_option_none.ion)); `Box::new(Option::None)` infers from an expected `Box<Option<...>>` ([tests/test_box_new_option_none.ion](../../../../tests/test_box_new_option_none.ion)); `[Option::None]` and `(Option::None, 1)` infer from an adjacent array or tuple type ([tests/test_array_option_none.ion](../../../../tests/test_array_option_none.ion), [tests/test_tuple_option_none.ion](../../../../tests/test_tuple_option_none.ion)); unannotated `let empty = Option::None` still needs an annotation.

Struct: `Status::Ok { value: 10 }`.

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.1.21 - 2026-08-14

- **Type checker / Codegen**: array, tuple, and `[value; N]` elements, assignment, returned compounds, and enum variant payloads now check against the adjacent expected type (same `expr_expected` helper as struct fields, call arguments, `send`, and `Box::new`). This impacts `let a: [Option<int>; 1] = [Option::None]`, `(Option::None, 1)`, `[Option::Some(4)]` (emits `Option_int`, not bare `(Option)`), `x = Option::None`, `return [Option::None]`, and `Result::Err(Option::None)` as a compound element. Unannotated `let empty = Option::None` still requires an annotation.
- **Tests**: `test_array_option_none.ion`, `test_tuple_option_none.ion`, `test_array_option_some.ion`, `test_array_repeat_option_none.ion`, `test_array_option_none_call_arg.ion`, `test_array_option_none_struct_field.ion`, `test_array_of_tuples_option_none.ion`, `test_tuple_of_arrays_option_none.ion`, `test_array_result_err.ion`, `test_assign_option_none.ion`, `test_return_array_option_none.ion`. Filtered `test_runner.sh` skips non-matching TSV rows without a `tr` process per field; a `.ion` filter is exact and a bare stem stays a substring.
- **Docs**: ION_SPEC §4.4, bug hotspots, verified patterns.

## 0.1.20 - 2026-08-14

- **Type checker**: `send(&tx, Option::None)` infers `T` from `Sender<T>`, and `Box::new(Option::None)` infers from an expected `Box<Option<...>>`. This impacts passing unannotated no-payload generic variants into `send` or `Box::new` (user-function call arguments already inferred in 0.1.19). Unannotated `let empty = Option::None` still requires an annotation.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ion-compiler"
version = "0.1.20"
version = "0.1.21"
edition = "2024"

[[bin]]
Expand Down
2 changes: 1 addition & 1 deletion ION_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ Ion supports a **local, Hindley–Milner-inspired inference**:
The inference engine is intentionally limited:

- No higher-rank polymorphism.
- Generic enum variants with no payload (`Option::None`) infer type arguments only from an adjacent expected type (a `let` annotation, a struct field, a function parameter / call argument including built-in value parameters such as `send` and `Box::new`, or a return type). They do not take `T` from a later statement; without that context the compiler requires an annotation.
- Generic enum variants with no payload (`Option::None`) infer type arguments only from an adjacent expected type (a `let` annotation, a struct field, a function parameter / call argument including built-in value parameters such as `send` and `Box::new`, a return type, an assignment target, or an element of an array or tuple literal including `[value; N]` repeat arrays and enum variant payloads). They do not take `T` from a later statement; without that context the compiler requires an annotation.
- Generic type parameters may declare optional **trait bounds** (`Copy`, `Eq`, `Send`). Bounds are checked at monomorphization: each concrete instantiation must satisfy every bound on the corresponding parameter. There are no user-defined traits; bounds name structural capabilities checked by the compiler (see Section 4.8).
- Structural `Send` still applies per instantiation even without an explicit bound: for a generic type `Wrapper<T>`, each monomorphized `Wrapper<U>` is `Send` if and only if all of its fields (with `T` replaced by `U`) are `Send`.

Expand Down
Loading