diff --git a/.cursor/skills/finding-ion-bugs/references/bug-hotspots.md b/.cursor/skills/finding-ion-bugs/references/bug-hotspots.md index 6d6ae6d..2e4f6d0 100644 --- a/.cursor/skills/finding-ion-bugs/references/bug-hotspots.md +++ b/.cursor/skills/finding-ion-bugs/references/bug-hotspots.md @@ -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>` self-reference stack-overflows at decl time. Representability (`InfiniteSize`) treats only `Box`/`Vec`/`RawPtr` as size boundaries — `Option` is still infinite size; `Option>` 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` vs `Option>` 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` (`test_send_option_none.ion`); `Box::new(Option::None)` infers from an expected `Box>` (`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` vs `Option>` 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` (`test_send_option_none.ion`); `Box::new(Option::None)` infers from an expected `Box>` (`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 @@ -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`. diff --git a/.cursor/skills/ion-integration-tests/SKILL.md b/.cursor/skills/ion-integration-tests/SKILL.md index aa32207..98a09f9 100644 --- a/.cursor/skills/ion-integration-tests/SKILL.md +++ b/.cursor/skills/ion-integration-tests/SKILL.md @@ -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 diff --git a/.cursor/skills/writing-ion-code/references/verified-patterns.md b/.cursor/skills/writing-ion-code/references/verified-patterns.md index 16b9b9d..b803638 100644 --- a/.cursor/skills/writing-ion-code/references/verified-patterns.md +++ b/.cursor/skills/writing-ion-code/references/verified-patterns.md @@ -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` ([tests/test_send_option_none.ion](../../../../tests/test_send_option_none.ion)); `Box::new(Option::None)` infers from an expected `Box>` ([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` ([tests/test_send_option_none.ion](../../../../tests/test_send_option_none.ion)); `Box::new(Option::None)` infers from an expected `Box>` ([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 }`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d30709..158f3c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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; 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`, and `Box::new(Option::None)` infers from an expected `Box>`. 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. diff --git a/Cargo.lock b/Cargo.lock index 56fcfc6..7b30ef2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,7 +323,7 @@ dependencies = [ [[package]] name = "ion-compiler" -version = "0.1.20" +version = "0.1.21" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index b326ec8..de7d0bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ion-compiler" -version = "0.1.20" +version = "0.1.21" edition = "2024" [[bin]] diff --git a/ION_SPEC.md b/ION_SPEC.md index b8b6893..553b95c 100644 --- a/ION_SPEC.md +++ b/ION_SPEC.md @@ -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`, each monomorphized `Wrapper` is `Send` if and only if all of its fields (with `T` replaced by `U`) are `Send`. diff --git a/src/cgen/mod.rs b/src/cgen/mod.rs index d3b5546..e4b7412 100644 --- a/src/cgen/mod.rs +++ b/src/cgen/mod.rs @@ -66,7 +66,9 @@ pub struct Codegen { /// Compilation-wide callee env from TypeInfo (Ion names, alias::name, prefix_name). compilation_param_types: HashMap>, compilation_return_types: HashMap>, - in_unsafe_block: bool, // Track if we're in an unsafe block + in_unsafe_block: bool, // Track if we're in an unsafe block + /// When true, enum literals emit nested designated initializers without a type cast. + nested_designated_init: bool, temp_var_counter: usize, // Counter for unique temporary variable names current_function_params: HashMap, // Track current function parameter types for field access spawn_counter: usize, @@ -114,6 +116,7 @@ impl Codegen { compilation_param_types: HashMap::new(), compilation_return_types: HashMap::new(), in_unsafe_block: false, + nested_designated_init: false, temp_var_counter: 0, current_function_params: HashMap::new(), spawn_counter: 0, @@ -209,6 +212,22 @@ impl Codegen { .position(|v| v.name == variant_name) } + fn monomorphized_payload_type( + payload_ty: &Type, + enum_decl: &EnumDecl, + type_context: Option<&Type>, + ) -> Type { + let Some(Type::Generic { params, .. }) = type_context else { + return payload_ty.clone(); + }; + let names = TypeParam::names(&enum_decl.generics); + if names.len() != params.len() { + return payload_ty.clone(); + } + let subst: HashMap = names.into_iter().zip(params.iter()).collect(); + substitute_type_params(payload_ty, &subst) + } + fn emit_enum_variant_compound_literal( &mut self, c_type_name: &str, @@ -216,6 +235,7 @@ impl Codegen { variant_name: &str, args: &[IREexpr], named_fields: Option<&[(String, IREexpr)]>, + type_context: Option<&Type>, ) { let enum_decl = self .enum_map @@ -227,10 +247,34 @@ impl Codegen { }); let variant = &enum_decl.variants[variant_idx]; let has_payloads = !variant.payload_types.is_empty() || variant.named_fields.is_some(); + let payload_tys: Vec = variant + .payload_types + .iter() + .map(|ty| Self::monomorphized_payload_type(ty, &enum_decl, type_context)) + .collect(); + let named_field_tys: HashMap = variant + .named_fields + .as_ref() + .map(|fields| { + fields + .iter() + .map(|(name, ty)| { + ( + name.clone(), + Self::monomorphized_payload_type(ty, &enum_decl, type_context), + ) + }) + .collect() + }) + .unwrap_or_default(); - self.write(&format!( - "({c_type_name}){{ .tag = {variant_idx}, .data = {{" - )); + if self.nested_designated_init { + self.write(&format!("{{ .tag = {variant_idx}, .data = {{")); + } else { + self.write(&format!( + "({c_type_name}){{ .tag = {variant_idx}, .data = {{" + )); + } if has_payloads { self.write(&format!(" .variant_{variant_idx} = {{")); if let Some(named_fields) = named_fields { @@ -239,7 +283,7 @@ impl Codegen { self.write(", "); } self.write(&format!(" .{field_name} = ")); - self.generate_expr(field_expr); + self.generate_expr_with_type(field_expr, named_field_tys.get(field_name)); } } else { for (i, arg) in args.iter().enumerate() { @@ -247,7 +291,7 @@ impl Codegen { self.write(", "); } self.write(&format!(" .arg{i} = ")); - self.generate_expr(arg); + self.generate_expr_with_type(arg, payload_tys.get(i)); } } self.write(" }"); @@ -361,8 +405,9 @@ impl Codegen { self.generic_instantiations = resolved_instantiations; - // Vec, slice, and tuple typedefs must precede struct fields that reference them. - self.emit_vec_slice_tuple_typedefs(program); + // Vec and slice typedefs must precede struct fields that reference them. + // Tuple typedefs wait until generic enums (e.g. Option_int) are complete types. + self.emit_vec_slice_typedefs(program); // Enums before structs so struct fields can use enum types by value. for e in &program.enums { @@ -479,6 +524,8 @@ impl Codegen { self.generated_types.insert(key, true); } + self.emit_tuple_typedefs(program); + for (decl, params) in struct_instantiations { let key = mangle_type_name(&decl.name, ¶ms); self.generate_monomorphized_struct(&decl, ¶ms); @@ -712,18 +759,17 @@ impl Codegen { collect_generic_instantiations(program, &mut generic_instantiations_map); self.generic_instantiations = generic_instantiations_map.clone(); - self.emit_vec_slice_tuple_typedefs(program); + self.emit_vec_slice_typedefs(program); + self.emit_tuple_typedefs(program); self.emit_monomorphized_enum_forwards(); - // Emit struct type definitions first so functions can use them. for s in &program.structs { if s.generics.is_empty() { self.write(&format!("typedef struct {} {{\n", s.name)); self.indent_level += 1; for field in &s.fields { self.write_indent(); - // Handle arrays specially: in struct fields, arrays must be declared as "type name[size];" let field_decl = match &field.ty { Type::Array { inner, size } => { let base_type = self.type_to_c(inner); @@ -741,7 +787,6 @@ impl Codegen { } } - // Emit enum type definitions for e in &program.enums { if e.generics.is_empty() { self.generate_enum_type(e); @@ -1766,12 +1811,14 @@ impl Codegen { }; if is_array_return { - if let Some(Type::Array { inner, size }) = self.current_return_type.as_ref() { + if let Some(ret_ty) = self.current_return_type.clone() + && let Type::Array { inner, size } = &ret_ty + { let base_type = self.type_to_c(inner); self.writeln(&format!("static {} _ret_array[{}] = ", base_type, size)); self.write_indent(); self.write(" "); - self.generate_expr(value); + self.generate_expr_with_type(value, Some(&ret_ty)); self.writeln(";"); self.write_indent(); self.writeln("ret_val = _ret_array;"); @@ -2946,7 +2993,10 @@ impl Codegen { .find(|f| f.name == field.name) .map(|f| f.ty.clone()) }); + let prev = self.nested_designated_init; + self.nested_designated_init = true; self.generate_expr_with_type(&field.value, field_ty.as_ref()); + self.nested_designated_init = prev; } self.write("}"); } @@ -3076,6 +3126,7 @@ impl Codegen { variant, args, named_fields.as_deref(), + type_context, ); } IREexpr::Match { @@ -3252,6 +3303,12 @@ impl Codegen { continue; } if let Some(ref pty) = param_ty { + if matches!(arg, IREexpr::ArrayLiteral { .. }) + && let Type::Array { inner, size } = pty + { + let elem_c = self.type_to_c(inner); + self.write(&format!("({}[{}])", elem_c, size)); + } self.generate_expr_with_type(arg, Some(pty)); } else { self.generate_expr(arg); @@ -3282,11 +3339,18 @@ impl Codegen { self.write(", "); } self.write(&format!(".f{} = ", i)); - self.generate_expr(elem); + let prev = self.nested_designated_init; + self.nested_designated_init = true; + self.generate_expr_with_type(elem, elem_types.get(i)); + self.nested_designated_init = prev; } self.write("}"); } IREexpr::ArrayLiteral { elements, repeat } => { + let elem_ty = match type_context { + Some(Type::Array { inner, .. }) => Some(inner.as_ref().clone()), + _ => None, + }; if let Some((value_expr, count)) = repeat { // Array repeat: [value; count] // For zero initialization, use {0} syntax @@ -3305,7 +3369,7 @@ impl Codegen { if i > 0 { self.write(", "); } - self.generate_expr(value_expr); + self.generate_expr_with_type(value_expr, elem_ty.as_ref()); } self.write("}"); } @@ -3316,7 +3380,7 @@ impl Codegen { if i > 0 { self.write(", "); } - self.generate_expr(elem); + self.generate_expr_with_type(elem, elem_ty.as_ref()); } self.write("}"); } @@ -3402,27 +3466,35 @@ impl Codegen { self.generate_expr(expr); } IREexpr::Assign { target, value } => { - // Generate assignment: target = value self.write(target); self.write(" = "); - self.generate_expr(value); + let ty = self.lookup_var_type(target); + self.generate_expr_with_type(value, ty.as_ref()); } IREexpr::AssignIndex { target, index, value, } => { - // Generate array element assignment: arr[i] = value self.generate_expr(target); self.write("["); self.generate_expr(index); self.write("] = "); - self.generate_expr(value); + let elem_ty = match self.infer_irexpr_type(target) { + Some(Type::Array { inner, .. }) => Some(*inner), + Some(Type::Ref { inner, .. }) => match *inner { + Type::Array { inner, .. } => Some(*inner), + _ => None, + }, + _ => None, + }; + self.generate_expr_with_type(value, elem_ty.as_ref()); } IREexpr::AssignField { target, value } => { self.generate_expr(target); self.write(" = "); - self.generate_expr(value); + let field_ty = self.infer_irexpr_type(target); + self.generate_expr_with_type(value, field_ty.as_ref()); } IREexpr::FnLiteral(lit) => { self.generate_fn_literal(lit); @@ -5265,7 +5337,7 @@ impl Codegen { self.output.insert_str(insert_at, &forward_decls); } - fn emit_vec_slice_tuple_typedefs(&mut self, program: &IRProgram) { + fn emit_vec_slice_typedefs(&mut self, program: &IRProgram) { let mut vec_types = std::collections::HashSet::new(); collect_vec_types_impl(program, &mut vec_types); for vec_type_name in &vec_types { @@ -5277,7 +5349,9 @@ impl Codegen { for slice_type_name in &slice_types { self.generate_slice_struct(slice_type_name); } + } + fn emit_tuple_typedefs(&mut self, program: &IRProgram) { let mut tuple_types: std::collections::HashMap> = std::collections::HashMap::new(); collect_tuple_types_impl(program, &mut tuple_types); @@ -5335,7 +5409,15 @@ impl Codegen { self.indent_level += 1; for (i, elem) in elements.iter().enumerate() { self.write_indent(); - self.writeln(&format!("{} f{};", self.type_to_c(elem), i)); + match elem { + Type::Array { inner, size } => { + let base_type = self.type_to_c(inner); + self.writeln(&format!("{} f{}[{}];", base_type, i, size)); + } + _ => { + self.writeln(&format!("{} f{};", self.type_to_c(elem), i)); + } + } } self.indent_level -= 1; self.writeln(&format!("}} {};", tuple_name)); @@ -5824,6 +5906,11 @@ fn collect_generic_from_type( Type::Channel { elem_type } => collect_generic_from_type(elem_type, instantiations), Type::Array { inner, .. } => collect_generic_from_type(inner, instantiations), Type::Slice { inner } => collect_generic_from_type(inner, instantiations), + Type::Tuple { elements } => { + for elem in elements { + collect_generic_from_type(elem, instantiations); + } + } Type::Fn { params, return_type, @@ -6030,7 +6117,13 @@ fn collect_generic_from_expr( collect_generic_from_expr(arg, instantiations); } } - IREexpr::TupleLit { elements, .. } => { + IREexpr::TupleLit { + elements, + elem_types, + } => { + for ty in elem_types { + collect_generic_from_type(ty, instantiations); + } for elem in elements { collect_generic_from_expr(elem, instantiations); } diff --git a/src/tc/mod.rs b/src/tc/mod.rs index cd404e4..771728f 100644 --- a/src/tc/mod.rs +++ b/src/tc/mod.rs @@ -434,6 +434,68 @@ impl TypeChecker { None } + fn expected_array_elem(&self) -> Option { + match &self.expr_expected { + Some(Type::Array { inner, .. }) => Some((**inner).clone()), + _ => None, + } + } + + fn expected_tuple_elems(&self) -> Option> { + match &self.expr_expected { + Some(Type::Tuple { elements }) => Some(elements.clone()), + _ => None, + } + } + + fn is_generic_placeholder(ty: &Type, generic_names: &[String]) -> bool { + match ty { + Type::Struct(n) | Type::Enum(n) => generic_names.iter().any(|g| g == n), + Type::Generic { name, params } if params.is_empty() => { + generic_names.iter().any(|g| g == name) + } + _ => false, + } + } + + /// Concrete payload type after substituting adjacent generic params. + /// `None` when the result is still a type-parameter placeholder. + fn expected_enum_payload_type( + &self, + payload_ty: &Type, + enum_name: &str, + generic_names: &[String], + ) -> Option { + if generic_names.is_empty() { + return Some(payload_ty.clone()); + } + let expected_params = self.expected_generic_params(enum_name)?; + if expected_params.len() != generic_names.len() { + return None; + } + let subst: HashMap = + generic_names.iter().cloned().zip(expected_params).collect(); + let substituted = substitute_generic_types_impl(payload_ty, &subst); + if Self::is_generic_placeholder(&substituted, generic_names) { + None + } else { + Some(substituted) + } + } + + fn check_enum_payload_arg( + &mut self, + arg_expr: &Expr, + payload_ty: &Type, + enum_name: &str, + generic_names: &[String], + ) -> Result { + match self.expected_enum_payload_type(payload_ty, enum_name, generic_names) { + Some(expected) => self.check_expr_with_expected(arg_expr, &expected), + None => self.check_expr(arg_expr), + } + } + fn record_hover_doc(&mut self, span: Span, doc: String) { if !self.lsp_recording { return; @@ -1550,9 +1612,15 @@ impl TypeChecker { "tuple destructuring requires an initializer".to_string(), ) })?; - let init_type = self.check_expr(init)?; - let tuple_type = if let Some(ref type_ann) = let_stmt.type_ann { - let resolved = self.resolve_type_name(type_ann)?; + let resolved_ann = match &let_stmt.type_ann { + Some(ann) => Some(self.resolve_type_name(ann)?), + None => None, + }; + let init_type = match &resolved_ann { + Some(expected) => self.check_expr_with_expected(init, expected)?, + None => self.check_expr(init)?, + }; + let tuple_type = if let Some(resolved) = resolved_ann { if !types_equal(&resolved, &init_type) { return Err(TypeCheckError::TypeMismatch { expected: type_to_string(&resolved), @@ -1764,7 +1832,12 @@ impl TypeChecker { Stmt::Return(return_stmt) => { if let Some(ref value) = return_stmt.value { // First check the expression type (this verifies variables are Valid) - let value_type = self.check_expr(value)?; + let expected_return = self.current_return_type.clone(); + let value_type = if let Some(ref expected) = expected_return { + self.check_expr_with_expected(value, expected)? + } else { + self.check_expr(value)? + }; let resolved_value_type = self.resolve_type_name(&value_type)?; // Check no-escape rule: cannot return reference-containing types @@ -3216,9 +3289,9 @@ impl TypeChecker { // Check argument types (now we can call self.check_expr since we've dropped the module_exports borrow) for (arg_expr, param) in enum_lit.args.iter().zip(params.iter()) { - let arg_ty = self.check_expr(arg_expr)?; - let resolved_arg_ty = self.resolve_type_name(&arg_ty)?; let resolved_param_ty = self.resolve_type_name(¶m.ty)?; + let arg_ty = self.check_expr_with_expected(arg_expr, &resolved_param_ty)?; + let resolved_arg_ty = self.resolve_type_name(&arg_ty)?; let numeric_coerced = Self::can_coerce_numeric(&resolved_arg_ty, &resolved_param_ty); if !numeric_coerced && !types_equal(&resolved_arg_ty, &resolved_param_ty) { @@ -3315,7 +3388,12 @@ impl TypeChecker { let mut inferred_params: Vec = Vec::new(); for (arg_expr, expected_ty) in enum_lit.args.iter().zip(payload_types.iter()) { - let arg_ty = self.check_expr(arg_expr)?; + let arg_ty = self.check_enum_payload_arg( + arg_expr, + expected_ty, + &enum_decl.name, + &generic_names, + )?; let resolved_arg_ty = self.resolve_type_name(&arg_ty)?; let resolved_expected_ty = self.resolve_type_name(expected_ty)?; @@ -3389,9 +3467,15 @@ impl TypeChecker { field_name, enum_lit.variant, enum_decl.name ))); }; - let arg_ty = self.check_expr(field_expr)?; + let field_ty = field_ty.clone(); + let arg_ty = self.check_enum_payload_arg( + field_expr, + &field_ty, + &enum_decl.name, + &generic_names, + )?; let resolved_arg_ty = self.resolve_type_name(&arg_ty)?; - let resolved_expected_ty = self.resolve_type_name(field_ty)?; + let resolved_expected_ty = self.resolve_type_name(&field_ty)?; if is_generic { let subs = infer_generic_substitutions( &resolved_expected_ty, @@ -3731,6 +3815,8 @@ impl TypeChecker { })?; let payload_types = variant.payload_types.clone(); + let generic_names = TypeParam::names(&enum_decl.generics); + let enum_name = enum_decl.name.clone(); // Check argument count matches variant if call_expr.args.len() != payload_types.len() { @@ -3745,7 +3831,12 @@ impl TypeChecker { for (arg_expr, expected_ty) in call_expr.args.iter().zip(payload_types.iter()) { - let arg_ty = self.check_expr(arg_expr)?; + let arg_ty = self.check_enum_payload_arg( + arg_expr, + expected_ty, + &enum_name, + &generic_names, + )?; let resolved_arg_ty = self.resolve_type_name(&arg_ty)?; let resolved_expected_ty = self.resolve_type_name(expected_ty)?; @@ -4236,9 +4327,18 @@ impl TypeChecker { "empty tuple literal is not supported".to_string(), )); } + let expected_elems = self.expected_tuple_elems(); let mut elem_types = Vec::with_capacity(tuple_lit.elements.len()); - for elem in &tuple_lit.elements { - elem_types.push(self.check_expr(elem)?); + if let Some(ref expected) = expected_elems + && expected.len() == tuple_lit.elements.len() + { + for (elem, expected_ty) in tuple_lit.elements.iter().zip(expected.iter()) { + elem_types.push(self.check_expr_with_expected(elem, expected_ty)?); + } + } else { + for elem in &tuple_lit.elements { + elem_types.push(self.check_expr(elem)?); + } } Ok(Type::Tuple { elements: elem_types, @@ -4247,8 +4347,11 @@ impl TypeChecker { Expr::ArrayLiteral(arr_lit) => { // Handle [value; count] syntax if let Some((ref value_expr, count)) = arr_lit.repeat { - // Type-check the repeated expression once - let elem_ty = self.check_expr(value_expr)?; + let expected_elem = self.expected_array_elem(); + let elem_ty = match &expected_elem { + Some(expected) => self.check_expr_with_expected(value_expr, expected)?, + None => self.check_expr(value_expr)?, + }; // For integer literals, default to int, but allow coercion later // The actual element type will be determined by the type annotation if present @@ -4272,9 +4375,16 @@ impl TypeChecker { } else { // Regular array literal: [expr, expr, ...] // Check all elements have the same type - let first_elem_ty = self.check_expr(&arr_lit.elements[0])?; + let expected_elem = self.expected_array_elem(); + let first_elem_ty = match &expected_elem { + Some(expected) => { + self.check_expr_with_expected(&arr_lit.elements[0], expected)? + } + None => self.check_expr(&arr_lit.elements[0])?, + }; for elem in arr_lit.elements.iter().skip(1) { - let elem_ty = self.check_expr(elem)?; + let check_against = expected_elem.as_ref().unwrap_or(&first_elem_ty); + let elem_ty = self.check_expr_with_expected(elem, check_against)?; if !types_equal(&elem_ty, &first_elem_ty) { return Err(TypeCheckError::TypeMismatch { expected: type_to_string(&first_elem_ty), @@ -4354,152 +4464,126 @@ impl TypeChecker { // Return the target type Ok(resolved_target_type) } - Expr::Assign(assign_expr) => { - // Check the value expression - let value_ty = self.check_expr(&assign_expr.value)?; - let resolved_value_ty = self.resolve_type_name(&value_ty)?; - - // Handle different assignment targets - match &*assign_expr.target { - Expr::Var(var_expr) => { - // Variable assignment: x = value - // Check that variable exists and is mutable + Expr::Assign(assign_expr) => match &*assign_expr.target { + Expr::Var(var_expr) => { + let var_ty = { let var_info = self.variables.get(&var_expr.name).ok_or_else(|| { TypeCheckError::UndefinedVariable { name: var_expr.name.clone(), span: var_expr.span, } })?; + var_info.ty.clone() + }; - self.check_owner_not_borrowed(&var_expr.name, var_expr.span)?; + self.check_owner_not_borrowed(&var_expr.name, var_expr.span)?; - // Check type compatibility with coercion - let resolved_var_ty = self.resolve_type_name(&var_info.ty)?; - let numeric_coerced = - Self::can_coerce_numeric(&resolved_value_ty, &resolved_var_ty); + let resolved_var_ty = self.resolve_type_name(&var_ty)?; + let value_ty = + self.check_expr_with_expected(&assign_expr.value, &resolved_var_ty)?; + let resolved_value_ty = self.resolve_type_name(&value_ty)?; + let numeric_coerced = + Self::can_coerce_numeric(&resolved_value_ty, &resolved_var_ty); - if !types_equal(&resolved_value_ty, &resolved_var_ty) && !numeric_coerced { - return Err(TypeCheckError::TypeMismatch { - expected: type_to_string(&resolved_var_ty), - got: type_to_string(&resolved_value_ty), - span: assign_expr.value.span(), - }); - } - - // Assignment returns unit type (void) in Ion - Ok(Type::Void) + if !types_equal(&resolved_value_ty, &resolved_var_ty) && !numeric_coerced { + return Err(TypeCheckError::TypeMismatch { + expected: type_to_string(&resolved_var_ty), + got: type_to_string(&resolved_value_ty), + span: assign_expr.value.span(), + }); } - Expr::Index(index_expr) => { - // Array element assignment: arr[i] = value - // Check the base of the index expression to get the array type - if let Some((owner, owner_span)) = - self.borrow_owner_from_expr(&index_expr.target) - { - self.check_owner_not_borrowed(&owner, owner_span)?; - } - let base_ty = self.check_expr(&index_expr.target)?; - let target_ty_str = type_to_string(&base_ty); - match base_ty { - Type::Array { inner, .. } => { - // Check value type matches element type (with coercion) - let resolved_elem_ty = self.resolve_type_name(inner.as_ref())?; - let numeric_coerced = - Self::can_coerce_numeric(&resolved_value_ty, &resolved_elem_ty); - - if !types_equal(&resolved_value_ty, &resolved_elem_ty) - && !numeric_coerced - { - return Err(TypeCheckError::TypeMismatch { - expected: type_to_string(&resolved_elem_ty), - got: type_to_string(&resolved_value_ty), - span: assign_expr.value.span(), - }); - } - - // Assignment returns unit type - Ok(Type::Void) - } - Type::Ref { inner, .. } => { - // Indexing a reference to an array - match inner.as_ref() { - Type::Array { inner: elem_ty, .. } => { - let resolved_elem_ty = - self.resolve_type_name(elem_ty.as_ref())?; - let numeric_coerced = Self::can_coerce_numeric( - &resolved_value_ty, - &resolved_elem_ty, - ); - if !types_equal(&resolved_value_ty, &resolved_elem_ty) - && !numeric_coerced - { - return Err(TypeCheckError::TypeMismatch { - expected: type_to_string(&resolved_elem_ty), - got: type_to_string(&resolved_value_ty), - span: assign_expr.value.span(), - }); - } - - Ok(Type::Void) - } - _ => Err(TypeCheckError::TypeMismatch { - expected: "array type for indexed assignment".to_string(), - got: target_ty_str, - span: assign_expr.span, - }), - } + Ok(Type::Void) + } + Expr::Index(index_expr) => { + if let Some((owner, owner_span)) = + self.borrow_owner_from_expr(&index_expr.target) + { + self.check_owner_not_borrowed(&owner, owner_span)?; + } + let base_ty = self.check_expr(&index_expr.target)?; + let target_ty_str = type_to_string(&base_ty); + let elem_ty = match &base_ty { + Type::Array { inner, .. } => inner.as_ref().clone(), + Type::Ref { inner, .. } => match inner.as_ref() { + Type::Array { inner: elem_ty, .. } => elem_ty.as_ref().clone(), + _ => { + return Err(TypeCheckError::TypeMismatch { + expected: "array type for indexed assignment".to_string(), + got: target_ty_str, + span: assign_expr.span, + }); } - _ => Err(TypeCheckError::TypeMismatch { + }, + _ => { + return Err(TypeCheckError::TypeMismatch { expected: "array type for indexed assignment".to_string(), got: target_ty_str, span: assign_expr.span, - }), - } - } - Expr::FieldAccess(acc) => { - let base_ty = self.check_expr(&acc.base)?; - let assignable = matches!( - &base_ty, - Type::Ref { mutable: true, .. } - | Type::Struct(_) - | Type::Generic { .. } - ); - if !assignable { - return Err(TypeCheckError::Message( - "cannot assign to field: base must be an owned struct or &mut struct" - .to_string(), - )); - } - if let Some((owner, owner_span)) = self.borrow_owner_from_expr(&acc.base) { - self.check_owner_not_borrowed(&owner, owner_span)?; - } - let field_ty = self.check_expr(&Expr::FieldAccess(acc.clone()))?; - let resolved_field_ty = self.resolve_type_name(&field_ty)?; - let assign_expected = match &resolved_field_ty { - Type::Ref { - inner, - mutable: true, - } if Self::is_copy_type(inner) => inner.as_ref().clone(), - other => other.clone(), - }; - let numeric_coerced = - Self::can_coerce_numeric(&resolved_value_ty, &assign_expected); - if !types_equal(&resolved_value_ty, &assign_expected) && !numeric_coerced { - return Err(TypeCheckError::TypeMismatch { - expected: type_to_string(&assign_expected), - got: type_to_string(&resolved_value_ty), - span: assign_expr.value.span(), }); } - Ok(Type::Void) + }; + let resolved_elem_ty = self.resolve_type_name(&elem_ty)?; + let value_ty = + self.check_expr_with_expected(&assign_expr.value, &resolved_elem_ty)?; + let resolved_value_ty = self.resolve_type_name(&value_ty)?; + let numeric_coerced = + Self::can_coerce_numeric(&resolved_value_ty, &resolved_elem_ty); + + if !types_equal(&resolved_value_ty, &resolved_elem_ty) && !numeric_coerced { + return Err(TypeCheckError::TypeMismatch { + expected: type_to_string(&resolved_elem_ty), + got: type_to_string(&resolved_value_ty), + span: assign_expr.value.span(), + }); } - _ => Err(TypeCheckError::TypeMismatch { - expected: "variable, field, or indexed expression".to_string(), - got: "invalid assignment target".to_string(), - span: assign_expr.span, - }), + + Ok(Type::Void) } - } + Expr::FieldAccess(acc) => { + let base_ty = self.check_expr(&acc.base)?; + let assignable = matches!( + &base_ty, + Type::Ref { mutable: true, .. } | Type::Struct(_) | Type::Generic { .. } + ); + if !assignable { + return Err(TypeCheckError::Message( + "cannot assign to field: base must be an owned struct or &mut struct" + .to_string(), + )); + } + if let Some((owner, owner_span)) = self.borrow_owner_from_expr(&acc.base) { + self.check_owner_not_borrowed(&owner, owner_span)?; + } + let field_ty = self.check_expr(&Expr::FieldAccess(acc.clone()))?; + let resolved_field_ty = self.resolve_type_name(&field_ty)?; + let assign_expected = match &resolved_field_ty { + Type::Ref { + inner, + mutable: true, + } if Self::is_copy_type(inner) => inner.as_ref().clone(), + other => other.clone(), + }; + let value_ty = + self.check_expr_with_expected(&assign_expr.value, &assign_expected)?; + let resolved_value_ty = self.resolve_type_name(&value_ty)?; + let numeric_coerced = + Self::can_coerce_numeric(&resolved_value_ty, &assign_expected); + if !types_equal(&resolved_value_ty, &assign_expected) && !numeric_coerced { + return Err(TypeCheckError::TypeMismatch { + expected: type_to_string(&assign_expected), + got: type_to_string(&resolved_value_ty), + span: assign_expr.value.span(), + }); + } + Ok(Type::Void) + } + _ => Err(TypeCheckError::TypeMismatch { + expected: "variable, field, or indexed expression".to_string(), + got: "invalid assignment target".to_string(), + span: assign_expr.span, + }), + }, Expr::FnLiteral(lit) => { let mut exclude = std::collections::HashSet::new(); for param in &lit.params { diff --git a/tests/README.md b/tests/README.md index c000d6c..aaa348f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -20,7 +20,7 @@ Or from the project root: cd tests && ./test_runner.sh ``` -To run a subset, pass stems (with or without `.ion`). Matching is exact or substring; every matching manifest row runs (`run`, `error`, `cgen`): +To run a subset, pass stems. A name ending in `.ion` matches that file only. A bare stem is a substring (`test_enum_unannotated` also runs `test_enum_unannotated_let.ion`). Every matching manifest row runs (`run`, `error`, `cgen`); other rows are skipped: ```bash cd tests && ./test_runner.sh test_enum_unannotated_let.ion @@ -121,6 +121,17 @@ The test runner prints pass/fail counts when it finishes. Do not rely on hardcod - `test_send_option_none.ion` - `send(&tx, Option::None)` infers `T` from `Sender>` (exit 0) - `test_send_result_err.ion` - `send(&tx, Result::Err(4))` infers `T` from `Sender>` (exit 4) - `test_box_new_option_none.ion` - `Box::new(Option::None)` infers from expected `Box>` (exit 0) +- `test_array_option_none.ion` - `[Option::None]` infers `T` from `[Option; 1]` (exit 2) +- `test_tuple_option_none.ion` - `(Option::None, 3)` infers `T` from `(Option, int)` (exit 3) +- `test_array_option_some.ion` - `[Option::Some(4)]` type-checks and emits `Option_int` not bare `(Option)` (exit 4) +- `test_array_repeat_option_none.ion` - `[Option::None; 1]` infers from `[Option; 1]` (exit 5) +- `test_array_option_none_call_arg.ion` - array literal argument infers from the parameter type (exit 6) +- `test_array_option_none_struct_field.ion` - array literal field infers from the struct field type (exit 7) +- `test_array_of_tuples_option_none.ion` - `[(Option::None, 8)]` nested array-of-tuples (exit 8) +- `test_tuple_of_arrays_option_none.ion` - `([Option::None], 9)` nested tuple-of-arrays (exit 9) +- `test_array_result_err.ion` - `[Result::Err(Option::None)]` infers payload from the array element type (exit 10) +- `test_assign_option_none.ion` - `x = Option::None` infers from the left-hand side type (exit 11) +- `test_return_array_option_none.ion` - `return [Option::None]` infers from the function return type (exit 12) - `test_unannotated_let_non_int.ion` - unannotated `let q = p` / `let n = w.p` / `let v = origin()` keep struct types, not default int (exit 5); cgen asserts `Point q =` / `Point n =` / `Point v =` - `test_enum_generic.ion` - Generic enum types - `test_result_custom_enum.ion` - `Result` via `stdlib/result.ion` (Ok and Err, exit 0) diff --git a/tests/test_array_of_tuples_option_none.ion b/tests/test_array_of_tuples_option_none.ion new file mode 100644 index 0000000..8b8115b --- /dev/null +++ b/tests/test_array_of_tuples_option_none.ion @@ -0,0 +1,17 @@ +// Nested array-of-tuples infers Option::None from [(Option, int); N]. +enum Option { + Some(T); + None; +} + +fn main() -> int { + let a: [(Option, int); 1] = [(Option::None, 8)]; + match a[0].0 { + Option::Some(_) => { + return 1; + } + Option::None => { + return a[0].1; + } + } +} diff --git a/tests/test_array_option_none.ion b/tests/test_array_option_none.ion new file mode 100644 index 0000000..818802d --- /dev/null +++ b/tests/test_array_option_none.ion @@ -0,0 +1,17 @@ +// Array elements infer Option::None from the adjacent [Option; N] type. +enum Option { + Some(T); + None; +} + +fn main() -> int { + let a: [Option; 1] = [Option::None]; + match a[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return 2; + } + } +} diff --git a/tests/test_array_option_none_call_arg.ion b/tests/test_array_option_none_call_arg.ion new file mode 100644 index 0000000..dae784d --- /dev/null +++ b/tests/test_array_option_none_call_arg.ion @@ -0,0 +1,20 @@ +// Array literal elements infer Option::None from a function parameter type. +enum Option { + Some(T); + None; +} + +fn take(a: [Option; 1]) -> int { + match a[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return 6; + } + } +} + +fn main() -> int { + return take([Option::None]); +} diff --git a/tests/test_array_option_none_struct_field.ion b/tests/test_array_option_none_struct_field.ion new file mode 100644 index 0000000..781b115 --- /dev/null +++ b/tests/test_array_option_none_struct_field.ion @@ -0,0 +1,21 @@ +// Array literal elements infer Option::None from a struct field type. +enum Option { + Some(T); + None; +} + +struct Holder { + a: [Option; 1]; +} + +fn main() -> int { + let h: Holder = Holder { a: [Option::None] }; + match h.a[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return 7; + } + } +} diff --git a/tests/test_array_option_some.ion b/tests/test_array_option_some.ion new file mode 100644 index 0000000..f69adc7 --- /dev/null +++ b/tests/test_array_option_some.ion @@ -0,0 +1,17 @@ +// Payload-carrying Option in an array must emit Option_int, not bare (Option). +enum Option { + Some(T); + None; +} + +fn main() -> int { + let a: [Option; 1] = [Option::Some(4)]; + match a[0] { + Option::Some(v) => { + return v; + } + Option::None => { + return 1; + } + } +} diff --git a/tests/test_array_repeat_option_none.ion b/tests/test_array_repeat_option_none.ion new file mode 100644 index 0000000..c19b01b --- /dev/null +++ b/tests/test_array_repeat_option_none.ion @@ -0,0 +1,17 @@ +// Repeat arrays infer Option::None from the adjacent [Option; N] type. +enum Option { + Some(T); + None; +} + +fn main() -> int { + let a: [Option; 1] = [Option::None; 1]; + match a[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return 5; + } + } +} diff --git a/tests/test_array_result_err.ion b/tests/test_array_result_err.ion new file mode 100644 index 0000000..0dfba83 --- /dev/null +++ b/tests/test_array_result_err.ion @@ -0,0 +1,26 @@ +// Result::Err payload infers Option::None from the adjacent array element type. +import "stdlib/result.ion" as result; + +enum Option { + Some(T); + None; +} + +fn main() -> int { + let a: [Result>; 1] = [Result::Err(Option::None)]; + match a[0] { + Result::Ok(_) => { + return 1; + } + Result::Err(e) => { + match e { + Option::Some(_) => { + return 2; + } + Option::None => { + return 10; + } + } + } + } +} diff --git a/tests/test_assign_option_none.ion b/tests/test_assign_option_none.ion new file mode 100644 index 0000000..a144bf3 --- /dev/null +++ b/tests/test_assign_option_none.ion @@ -0,0 +1,18 @@ +// Assignment infers Option::None from the already-typed left-hand side. +enum Option { + Some(T); + None; +} + +fn main() -> int { + let mut x: Option = Option::Some(1); + x = Option::None; + match x { + Option::Some(_) => { + return 1; + } + Option::None => { + return 11; + } + } +} diff --git a/tests/test_expectations.tsv b/tests/test_expectations.tsv index 6cf3fcf..38ae677 100644 --- a/tests/test_expectations.tsv +++ b/tests/test_expectations.tsv @@ -67,6 +67,18 @@ test_send_option_none.ion cgen Option_int _send_val test_send_result_err.ion run 4 test_box_new_option_none.ion run 0 test_box_new_option_none.ion cgen (Option_int) +test_array_option_none.ion run 2 +test_tuple_option_none.ion run 3 +test_array_option_some.ion run 4 +test_array_option_some.ion cgen (Option_int){ (Option){ +test_array_repeat_option_none.ion run 5 +test_array_option_none_call_arg.ion run 6 +test_array_option_none_struct_field.ion run 7 +test_array_of_tuples_option_none.ion run 8 +test_tuple_of_arrays_option_none.ion run 9 +test_array_result_err.ion run 10 +test_assign_option_none.ion run 11 +test_return_array_option_none.ion run 12 test_vec_string_scope_drop.ion run 0 test_vec_string_scope_drop.ion cgen ion_string_free(((ion_string_t**)((v)->data)) test_vec_string_scope_drop.ion cgen ion_vec_free((ion_vec_t*)(v)) diff --git a/tests/test_return_array_option_none.ion b/tests/test_return_array_option_none.ion new file mode 100644 index 0000000..cba73ef --- /dev/null +++ b/tests/test_return_array_option_none.ion @@ -0,0 +1,21 @@ +// Returned array elements infer Option::None from the function return type. +enum Option { + Some(T); + None; +} + +fn make() -> [Option; 1] { + return [Option::None]; +} + +fn main() -> int { + let a = make(); + match a[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return 12; + } + } +} diff --git a/tests/test_runner.sh b/tests/test_runner.sh index b2070f6..3101dfa 100755 --- a/tests/test_runner.sh +++ b/tests/test_runner.sh @@ -6,7 +6,8 @@ # Usage: ./test_runner.sh [filter ...] # No args: full manifest plus special cases (multifile, ion-build smokes). # Filters match test stems, with or without .ion / tests/ / path prefix. -# Substring match: test_enum_unannotated runs test_enum_unannotated_let.ion +# A filter that ends in .ion is an exact stem. A bare stem is a substring +# (test_enum_unannotated runs test_enum_unannotated_let.ion). # All matching manifest rows run (run, error, and cgen for the same file). set +e @@ -105,6 +106,8 @@ test_count=0 pass_count=0 fail_count=0 FILTERS=() +FILTER_STEMS=() +FILTER_EXACT=() usage() { cat <<'EOF' @@ -112,8 +115,9 @@ Usage: ./test_runner.sh [filter ...] No arguments runs the full suite. Each filter matches a test stem (test_foo, test_foo.ion, or tests/test_foo.ion). - Matching is by exact stem or substring, so test_enum_unannotated selects - test_enum_unannotated_let.ion. Every matching manifest row runs (run, error, cgen). + A filter ending in .ion is exact. A bare stem is a substring, so + test_enum_unannotated selects test_enum_unannotated_let.ion. + Every matching manifest row runs (run, error, cgen). Non-matching rows are skipped. Special-case stems: test_multifile, test_multi_struct, test_multi_fmt_io, build_hello, worker_pool, build_bad_main. @@ -128,20 +132,37 @@ normalize_test_stem() { printf '%s' "$f" } +prepare_filters() { + FILTER_STEMS=() + FILTER_EXACT=() + local raw exact + for raw in "${FILTERS[@]}"; do + exact=0 + case "$raw" in + *.ion|*.ION) exact=1 ;; + esac + FILTER_STEMS+=("$(normalize_test_stem "$raw")") + FILTER_EXACT+=("$exact") + done +} + should_run() { - local n=${#FILTERS[@]} + local n=${#FILTER_STEMS[@]} if [ "$n" -eq 0 ]; then return 0 fi - local candidate stem raw f + local candidate stem i f for candidate in "$@"; do stem="$(normalize_test_stem "$candidate")" - for raw in "${FILTERS[@]}"; do - f="$(normalize_test_stem "$raw")" + for i in "${!FILTER_STEMS[@]}"; do + f="${FILTER_STEMS[$i]}" [ -z "$f" ] && continue if [ "$stem" = "$f" ]; then return 0 fi + if [ "${FILTER_EXACT[$i]}" = "1" ]; then + continue + fi case "$stem" in *"$f"*) return 0 ;; esac @@ -454,10 +475,6 @@ test_ion_build_bad_main() { return 0 } -strip_cr() { - printf '%s' "$1" | tr -d '\r' -} - resolve_error_pattern() { local pattern="$1" local exit_code="$2" @@ -544,17 +561,17 @@ run_manifest() { fi while IFS="$MANIFEST_FS" read -r file kind exit_code error_pattern must_match must_not_match || [ -n "$file" ]; do - file="$(strip_cr "$file")" - kind="$(strip_cr "$kind")" - exit_code="$(strip_cr "$exit_code")" - error_pattern="$(strip_cr "$error_pattern")" - must_match="$(strip_cr "$must_match")" - must_not_match="$(strip_cr "$must_not_match")" - + # Strip CR in-shell. Do not pipe each field through `tr` (Git Bash spawn cost). + file="${file//$'\r'/}" [ "$file" = "file" ] && continue [ -z "$file" ] && continue - [ ! -f "$file" ] && continue should_run "$file" || continue + [ ! -f "$file" ] && continue + kind="${kind//$'\r'/}" + exit_code="${exit_code//$'\r'/}" + error_pattern="${error_pattern//$'\r'/}" + must_match="${must_match//$'\r'/}" + must_not_match="${must_not_match//$'\r'/}" case "$kind" in run) @@ -592,6 +609,8 @@ for arg in "$@"; do esac done +prepare_filters + echo "Ion Compiler Test Harness" echo "=========================" if [ ${#FILTERS[@]} -gt 0 ]; then diff --git a/tests/test_tuple_of_arrays_option_none.ion b/tests/test_tuple_of_arrays_option_none.ion new file mode 100644 index 0000000..d746dec --- /dev/null +++ b/tests/test_tuple_of_arrays_option_none.ion @@ -0,0 +1,17 @@ +// Nested tuple-of-arrays infers Option::None from ([Option; 1], int). +enum Option { + Some(T); + None; +} + +fn main() -> int { + let t: ([Option; 1], int) = ([Option::None], 9); + match t.0[0] { + Option::Some(_) => { + return 1; + } + Option::None => { + return t.1; + } + } +} diff --git a/tests/test_tuple_option_none.ion b/tests/test_tuple_option_none.ion new file mode 100644 index 0000000..4430d19 --- /dev/null +++ b/tests/test_tuple_option_none.ion @@ -0,0 +1,17 @@ +// Tuple elements infer Option::None from the adjacent (Option, int) type. +enum Option { + Some(T); + None; +} + +fn main() -> int { + let t: (Option, int) = (Option::None, 3); + match t.0 { + Option::Some(_) => { + return 1; + } + Option::None => { + return t.1; + } + } +}