From f06ccd4d1538e312b521be687fd753118d364291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 21:42:10 +0300 Subject: [PATCH 01/15] feat: [US-001] Add TYPE_UNKNOWN sentinel as internal alias of ANY_TYPE Introduce TYPE_UNKNOWN constant in compiler/common.casa, both bound to the string "any". All compiler-internal sentinel uses (stack underflow placeholder, types_match, unify_type, bind_type_var, is_type_variable, bind_fn_param, check_push_var, type_satisfies_trait, check_method_call, signature_matches, and related helpers) now reference TYPE_UNKNOWN. ANY_TYPE remains as a string-equal alias for the duration of the migration so user-visible "any" keyword behaviour is unchanged. Part of #147 --- compiler/common.casa | 9 ++++--- compiler/typechecker.casa | 56 +++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/compiler/common.casa b/compiler/common.casa index 98fed51..e7ce868 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -831,6 +831,7 @@ struct Program { # Constants # ============================================================================ "_start" = GLOBAL_SCOPE_LABEL +"any" = TYPE_UNKNOWN "any" = ANY_TYPE "_" = MATCH_WILDCARD "self" = TRAIT_SELF_TYPE @@ -1057,7 +1058,7 @@ fn signature_matches sig_a:Signature sig_b:Signature -> bool { while index sig_a Signature::parameters.length > do index sig_a Signature::parameters.get Parameter::typ = type_a index sig_b Signature::parameters.get Parameter::typ = type_b - if type_a type_b != ANY_TYPE type_a != && ANY_TYPE type_b != && then + if type_a type_b != TYPE_UNKNOWN type_a != && TYPE_UNKNOWN type_b != && then type_b extract_generic_base = base_b type_a extract_generic_base = base_a if base_b.is_some type_a base_b.unwrap_or "" == && ! then @@ -1073,7 +1074,7 @@ fn signature_matches sig_a:Signature sig_b:Signature -> bool { while index sig_a Signature::return_types.length > do index sig_a Signature::return_types.get = return_a index sig_b Signature::return_types.get = return_b - if return_a return_b != ANY_TYPE return_a != && ANY_TYPE return_b != && then + if return_a return_b != TYPE_UNKNOWN return_a != && TYPE_UNKNOWN return_b != && then return_b extract_generic_base = return_base_b return_a extract_generic_base = return_base_a if return_base_b.is_some return_base_a.is_some && return_base_b.unwrap return_base_a.unwrap == && then @@ -1090,8 +1091,8 @@ fn signature_matches sig_a:Signature sig_b:Signature -> bool { param_index params_b.get = param_b if param_a param_b != - ANY_TYPE param_a != && - ANY_TYPE param_b != && + TYPE_UNKNOWN param_a != && + TYPE_UNKNOWN param_b != && param_a.length 1 != && param_b.length 1 != && then diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 75ffe85..2d372ad 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -177,15 +177,15 @@ fn stack_push_origin tc:TypeChecker typ:str origin:Location { fn stack_peek tc:TypeChecker -> str { if 0 tc TypeChecker::live TypedStack::types.length == then - ANY_TYPE return + TYPE_UNKNOWN return fi tc TypeChecker::live TypedStack::types.length 1 - tc TypeChecker::live TypedStack::types.get } fn stack_pop tc:TypeChecker -> str { if 0 tc TypeChecker::live TypedStack::types.length == then - ANY_TYPE make_param tc TypeChecker::parameters.push - ANY_TYPE return + TYPE_UNKNOWN make_param tc TypeChecker::parameters.push + TYPE_UNKNOWN return fi tc TypeChecker::live TypedStack::origins.pop drop tc TypeChecker::live TypedStack::types.pop @@ -220,8 +220,8 @@ fn expect_type tc:TypeChecker expected:str -> str { fn types_match expected:str actual:str -> bool { if expected actual == then true return fi - if ANY_TYPE expected == then true return fi - if ANY_TYPE actual == then true return fi + if TYPE_UNKNOWN expected == then true return fi + if TYPE_UNKNOWN actual == then true return fi # Check fn type matching if "fn" expected == then if actual is_fn_type then true return fi @@ -262,7 +262,7 @@ fn types_match expected:str actual:str -> bool { fn is_type_variable typ:str -> bool { # Check if the type is a type variable (single uppercase letter or # registered as a type var in a generic enum/struct) - if ANY_TYPE typ == then true return fi + if TYPE_UNKNOWN typ == then true return fi if typ.length 0 == then false return fi # Single uppercase letter is a type variable if typ.length 1 == then @@ -309,8 +309,8 @@ fn types_match_params expected:str actual:str -> bool { index actual_params.get = actual_param if expected_param actual_param != then if - ANY_TYPE expected_param != - ANY_TYPE actual_param != && + TYPE_UNKNOWN expected_param != + TYPE_UNKNOWN actual_param != && expected_param type_vars.has ! && actual_param type_vars.has ! && expected_param is_type_variable ! && @@ -350,8 +350,8 @@ fn unify_generic_params type_a:str type_b:str -> str { fn unify_type type_a:str type_b:str -> str { if type_a type_b == then type_a return fi - if ANY_TYPE type_a == then type_b return fi - if ANY_TYPE type_b == then type_a return fi + if TYPE_UNKNOWN type_a == then type_b return fi + if TYPE_UNKNOWN type_b == then type_a return fi # Enum type variables unify with concrete types if type_a is_enum_type_var then type_b return fi if type_b is_enum_type_var then type_a return fi @@ -448,17 +448,17 @@ fn bind_type_var tc:TypeChecker bindings:Map[str str] type_var:str actual:str -> fi existing.unwrap = bound if - ANY_TYPE bound == + TYPE_UNKNOWN bound == bound is_enum_type_var || bound is_type_variable || - ANY_TYPE actual != && + TYPE_UNKNOWN actual != && then type_var actual bindings.set return fi if actual bound != - ANY_TYPE actual != && - ANY_TYPE bound != && + TYPE_UNKNOWN actual != && + TYPE_UNKNOWN bound != && actual is_enum_type_var ! && then f"Type variable `{type_var}` bound to `{bound}` but got `{actual}`" tc raise_type_mismatch @@ -519,10 +519,10 @@ fn bind_generic_param "Type mismatch" tc raise_type_mismatch fi fi - if base actual == ANY_TYPE actual == || then + if base actual == TYPE_UNKNOWN actual == || then for expected_param in expected_params.iter do if expected_param sig Signature::type_vars.has then - ANY_TYPE expected_param bindings tc bind_type_var = bindings + TYPE_UNKNOWN expected_param bindings tc bind_type_var = bindings fi done true return @@ -547,10 +547,10 @@ fn bind_fn_param tc:TypeChecker bindings:Map[str str] expected:Parameter sig:Sig if has_type_var ! then false return fi tc stack_pop = actual - if ANY_TYPE actual == then + if TYPE_UNKNOWN actual == then for type_var2 in type_var_list.iter do if fn_sig_str type_var2 str::find 0 <= then - ANY_TYPE type_var2 bindings tc bind_type_var = bindings + TYPE_UNKNOWN type_var2 bindings tc bind_type_var = bindings fi done true return @@ -813,7 +813,7 @@ fn unify_variable_assignment op:Op variable:Variable effective_type:str scope:st variable Variable::name = var_name if "" variable Variable::typ == - ANY_TYPE variable Variable::typ == effective_type ANY_TYPE != && || + TYPE_UNKNOWN variable Variable::typ == effective_type TYPE_UNKNOWN != && || then effective_type variable Variable::set_typ fi @@ -907,7 +907,7 @@ fn check_push_var tc:TypeChecker op:Op function_opt:Option[Function] { fi fi fi - ANY_TYPE tc stack_push + TYPE_UNKNOWN tc stack_push return fi @@ -924,7 +924,7 @@ fn check_push_var tc:TypeChecker op:Op function_opt:Option[Function] { if "" local_type != then local_type tc stack_push else - ANY_TYPE tc stack_push + TYPE_UNKNOWN tc stack_push fi if is_trait_exec then var_name function tc simulate_trait_exec @@ -940,7 +940,7 @@ fn check_push_var tc:TypeChecker op:Op function_opt:Option[Function] { if "" global_type != then global_type tc stack_push else - ANY_TYPE tc stack_push + TYPE_UNKNOWN tc stack_push fi return fi @@ -1165,8 +1165,8 @@ fn type_satisfies_trait type_name:str trait_name:str function_opt:Option[Functio param_index resolved Signature::parameters.get Parameter::typ = expected_param if actual_param expected_param != - ANY_TYPE actual_param != && - ANY_TYPE expected_param != && + TYPE_UNKNOWN actual_param != && + TYPE_UNKNOWN expected_param != && then false return fi @@ -1178,8 +1178,8 @@ fn type_satisfies_trait type_name:str trait_name:str function_opt:Option[Functio return_index resolved Signature::return_types.get = expected_return if actual_return expected_return != - ANY_TYPE actual_return != && - ANY_TYPE expected_return != && + TYPE_UNKNOWN actual_return != && + TYPE_UNKNOWN expected_return != && then false return fi @@ -1504,7 +1504,7 @@ fn check_function_ops fi elif fn_value OpValue::FnExec is then tc stack_peek = fn_ptr_type - if ANY_TYPE fn_ptr_type == then "fn" = fn_ptr_type fi + if TYPE_UNKNOWN fn_ptr_type == then "fn" = fn_ptr_type fi fn_ptr_type tc expect_type drop if "fn" fn_ptr_type != then fn_ptr_type extract_fn_signature_str = sig_str_opt @@ -2232,7 +2232,7 @@ fn check_method_call # Try any-typed receiver: search all methods by name if fn_opt.is_none then - if ANY_TYPE receiver == then + if TYPE_UNKNOWN receiver == then method_name find_method_by_name = any_match_opt if any_match_opt.is_some then any_match_opt.unwrap = fn_name From cce60ab8e18befef000687fb53d88bcd6944b3f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 22:26:27 +0300 Subject: [PATCH 02/15] feat: [US-002] Add Eq, Ord, Display, Word traits + primitive impls Declare Eq (eq required, ne default), Ord (lt required, le/gt/ge defaults derived from lt), and Word (empty marker) in lib/std.casa. Display already existed and is left in place. Eq impls: int (already had), bool, char, str (already had), cstr, ptr. Ord impls: int, char. Lexicographic str comparison is intentionally out of scope. Word is satisfied structurally by every type today; bound enforcement for single-slot-only types lands with US-008. Part of #147 --- docs/standard-library.md | 13 ++++++++ docs/traits.md | 40 +++++++++++++++++++++++++ lib/std.casa | 53 +++++++++++++++++++++++++++++++++ tests/compiler/test_traits.casa | 23 ++++++++++++++ 4 files changed, 129 insertions(+) diff --git a/docs/standard-library.md b/docs/standard-library.md index 34fd5ec..1be26f0 100644 --- a/docs/standard-library.md +++ b/docs/standard-library.md @@ -380,6 +380,19 @@ Use `match` with destructuring to handle Result values: end ``` +## Built-in Traits + +The standard library declares traits with primitive implementations. See [Traits](traits.md) for details. + +| Trait | Required | Defaults | Built-in impls | +|-------|----------|----------|----------------| +| `Eq` | `eq self other -> bool` | `ne` | `int`, `bool`, `char`, `str`, `cstr`, `ptr` | +| `Ord` | `lt self other -> bool` | `le`, `gt`, `ge` | `int`, `char` | +| `Display` | `to_str self -> str` | -- | `int`, `bool`, `char`, `str`, `cstr`, `ptr`, `array[T]`, `List[T]`, `Option[T]`, `Result[T E]` | +| `Word` | (marker) | -- | every single-slot type | +| `Hashable` | `hash self -> int`, `eq self other -> bool` | -- | `int`, `str`, payload-free enums (auto-derived) | +| `Iterable[T]` | `next self -> Option[T]` | `collect`, `map`, `filter`, `fold`, `count`, `any`, `all`, `find` | `Iter[T]` | + ## See Also - [Collections](collections.md) -- List, Map, Set, and StringBuilder diff --git a/docs/traits.md b/docs/traits.md index 7bf6e06..8d4a988 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -160,6 +160,46 @@ Map::new (Map[str int]) = m The compiler sees that `Map::new` requires `[K: Hashable, V]`, determines `K=str` from the type cast `(Map[str int])`, verifies that `str` satisfies `Hashable`, and injects `&str::hash` and `&str::eq` behind the scenes. +## Built-in Trait: `Eq` + +Equality comparison. The required method is `eq`; the trait provides a default `ne` implemented as `!eq`. + +```casa +trait Eq { + fn eq self:self other:self -> bool + fn ne self:self other:self -> bool { other self.eq ! } +} +``` + +Built-in implementations: `int`, `bool`, `char`, `str`, `cstr`, `ptr`. + +A type satisfies `Eq` by providing `Type::eq self:Type other:Type -> bool`. The `ne` default is auto-instantiated for any satisfying type, so `x.ne y` works without writing it. + +## Built-in Trait: `Ord` + +Total ordering. The required method is `lt`; the defaults `le`, `gt`, and `ge` are derived from it. + +```casa +trait Ord { + fn lt self:self other:self -> bool + fn le self:self other:self -> bool { self other.lt ! } + fn gt self:self other:self -> bool { self other.lt } + fn ge self:self other:self -> bool { other self.lt ! } +} +``` + +Built-in implementations: `int`, `char`. Lexicographic ordering for `str` is intentionally out of scope. + +## Built-in Trait: `Word` + +Marker trait for register-sized values that fit in one stack slot. It declares no methods: + +```casa +trait Word { } +``` + +It is used as a bound on builtins that require single-slot operands (for example, syscall and `store*` arguments). Primitive types, enums, struct refs, and array refs satisfy `Word`; multi-slot value types do not. + ## Built-in Trait: `Hashable` The standard library defines the `Hashable` trait and provides implementations for `str` and `int`: diff --git a/lib/std.casa b/lib/std.casa index dc6aa75..f78dbec 100644 --- a/lib/std.casa +++ b/lib/std.casa @@ -141,6 +141,8 @@ impl bool { fn to_str b:bool -> str { if b then "true" else "false" fi } + + fn eq self:bool other:bool -> bool { self other == } } impl str { @@ -353,6 +355,25 @@ impl str { } impl cstr { + fn eq self:cstr other:cstr -> bool { + true = result + true = scanning + 0 = scan_index + while scanning do + self (ptr) scan_index + load8 = byte_a + other (ptr) scan_index + load8 = byte_b + if byte_a byte_b != then + false = result + false = scanning + elif 0 byte_a == then + false = scanning + else + 1 += scan_index + fi + done + result + } + fn to_str s:cstr -> str { # Find length by scanning for null byte 0 = source_length @@ -516,10 +537,15 @@ impl char { fn is_space c:char -> bool { c ' ' == c '\t' == || c '\n' == || c '\r' == || } + + fn eq self:char other:char -> bool { self other == } + fn lt self:char other:char -> bool { other self < } } impl ptr { fn to_str p:ptr -> str { p (int).to_str } + + fn eq self:ptr other:ptr -> bool { self other == } } # --------------------------------------------------------------------------- @@ -590,6 +616,30 @@ impl Result { # Traits # --------------------------------------------------------------------------- +trait Eq { + fn eq self:self other:self -> bool + + fn ne self:self other:self -> bool { + other self.eq ! + } +} + +trait Ord { + fn lt self:self other:self -> bool + + fn le self:self other:self -> bool { + self other.lt ! + } + + fn gt self:self other:self -> bool { + self other.lt + } + + fn ge self:self other:self -> bool { + other self.lt ! + } +} + trait Hashable { fn hash self:self -> int @@ -600,6 +650,8 @@ trait Display { fn to_str self:self -> str } +trait Word { } + trait Iterable[T] { fn next self:self -> Option[T] @@ -734,6 +786,7 @@ impl str { impl int { fn hash self:int -> int { self int_hash } fn eq self:int other:int -> bool { self other == } + fn lt self:int other:int -> bool { other self < } } # --------------------------------------------------------------------------- diff --git a/tests/compiler/test_traits.casa b/tests/compiler/test_traits.casa index 5e6ee2e..1e96a2b 100644 --- a/tests/compiler/test_traits.casa +++ b/tests/compiler/test_traits.casa @@ -390,6 +390,26 @@ fn test_undefined_trait_returns_false { clear_global_state "does not satisfy" Option::None(Option[Function]) "Nonexistent" "int" type_satisfies_trait ! assert_true } +# Primitive Eq impls must satisfy a structurally-matching Eq trait. + +fn test_int_satisfies_eq_trait { + "int_satisfies_eq_trait" test_start + "trait Eq { fn eq self:self other:self -> bool }\nimpl int { fn eq self:int other:int -> bool { self other == } }\n" trait_test_resolve drop + "satisfies" Option::None(Option[Function]) "Eq" "int" type_satisfies_trait assert_true +} + +fn test_int_satisfies_ord_trait { + "int_satisfies_ord_trait" test_start + "trait Ord { fn lt self:self other:self -> bool }\nimpl int { fn lt self:int other:int -> bool { other self < } }\n" trait_test_resolve drop + "satisfies" Option::None(Option[Function]) "Ord" "int" type_satisfies_trait assert_true +} + +fn test_empty_word_trait_satisfied_by_any_type { + "empty_word_trait_satisfied_by_any_type" test_start + "trait Word { }\n" trait_test_resolve drop + "int satisfies" Option::None(Option[Function]) "Word" "int" type_satisfies_trait assert_true + "str satisfies" Option::None(Option[Function]) "Word" "str" type_satisfies_trait assert_true +} # ============================================================================ # extract_generic_params @@ -906,6 +926,9 @@ test_partial_impl_does_not_satisfy test_struct_satisfies_generic_trait_concrete_int test_struct_does_not_satisfy_generic_trait_wrong_inner test_undefined_trait_returns_false +test_int_satisfies_eq_trait +test_int_satisfies_ord_trait +test_empty_word_trait_satisfied_by_any_type # Multi-param generics test_map_kv_type_params test_set_k_type_params From 8113ae95ab99dcf8944f5c2ccd640e07ada8611d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 23:10:58 +0300 Subject: [PATCH 03/15] feat: [US-003] Migrate Hashable to extend Eq (supertrait) - Add CasaTrait.supertraits: List[TraitBound] and parser support for trait Sub: Super (+ Super2) syntax - collect_trait_methods walks trait + transitive supertraits - build_hidden_trait_methods, inject_trait_fn_ptrs, type_satisfies_trait, simulate_trait_exec, check_method_call, check_abstract_methods_present all use the expanded list so K::eq dispatches via Eq supertrait - lib/std.casa: trait Hashable: Eq removes duplicated eq method - Fix double-decrement bug in inject_trait_fn_ptrs verify loop that silently skipped the method after a default in the iteration order Closes #150 --- compiler/common.casa | 1 + compiler/syntax.casa | 64 +++++++++++++++++++++++++++++++-- compiler/typechecker.casa | 48 ++++++++++++------------- docs/standard-library.md | 2 +- docs/traits.md | 28 ++++++++++++--- lib/std.casa | 4 +-- tests/compiler/test_traits.casa | 52 +++++++++++++++++++++++++++ 7 files changed, 162 insertions(+), 37 deletions(-) diff --git a/compiler/common.casa b/compiler/common.casa index e7ce868..79ac5b0 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -725,6 +725,7 @@ struct CasaTrait { name: str type_params: List[str] methods: List[TraitMethod] + supertraits: List[TraitBound] location: Location } diff --git a/compiler/syntax.casa b/compiler/syntax.casa index a283b40..71b07ae 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -868,6 +868,37 @@ fn parse_signature cursor:TokenCursor -> Signature { # Bounds whose trait is not registered in the store are silently skipped, # matching the pre-extraction behavior of both call sites. +fn collect_trait_methods_into + seen:Set[str] + result:List[TraitMethod] + parser:Parser + trait_def:CasaTrait +-> Set[str] { + for ctm_method in trait_def CasaTrait::methods.iter do + if ctm_method TraitMethod::name seen.has ! then + ctm_method TraitMethod::name seen.add = seen + ctm_method result.push + fi + done + for ctm_super in trait_def CasaTrait::supertraits.iter do + ctm_super TraitBound::name parser.store.traits.get = ctm_super_opt + if ctm_super_opt.is_some then + ctm_super_opt.unwrap parser result seen collect_trait_methods_into = seen + fi + done + seen +} +# Returns all methods (abstract + default) from `trait_def` and its +# transitive supertraits, deduplicated by name. The trait's own methods +# come first; supertrait methods follow in declaration order. + +fn collect_trait_methods parser:Parser trait_def:CasaTrait -> List[TraitMethod] { + List::new(List[TraitMethod]) = result + Set::new(Set[str]) = seen + trait_def parser result seen collect_trait_methods_into drop + result +} + fn build_hidden_trait_methods parser:Parser trait_bounds:Map[str TraitBound] @@ -891,7 +922,7 @@ fn build_hidden_trait_methods 1 += bhtm_sub_index done fi - for bound_method in bound_trait CasaTrait::methods.iter do + for bound_method in bound_trait parser collect_trait_methods.iter do # Skip default methods — only abstract methods need hidden fn ptrs if 0 bound_method TraitMethod::default_ops.length == then f"__trait_{bound_type_var}_{bound_method TraitMethod::name}" = hidden_name @@ -1288,6 +1319,35 @@ fn parse_trait parser:Parser cursor:TokenCursor -> CasaTrait { "trait" cursor parse_simple_type_vars = type_params fi + List::new(List[TraitBound]) = supertraits + cursor.peek = colon_opt + if colon_opt.is_some ":" colon_opt.unwrap Token::value == && then + cursor.pop drop + true = parse_super_loop + while parse_super_loop do + TokenKind::Identifier cursor expect_token_kind = super_token + super_token Token::value = super_name + super_name parser.store.traits.get = super_def_opt + if super_def_opt.is_none then + super_token Token::location f"Supertrait `{super_name}` is not defined" ErrorKind::UndefinedName raise_error + fi + super_token cursor parse_bound_type_args = super_args + if super_def_opt.is_some then + super_def_opt.unwrap CasaTrait::type_params = super_params + if super_args.length super_params.length != then + super_token Token::location f"Supertrait `{super_name}` expects {super_params.length} type argument(s), got {super_args.length}" ErrorKind::Syntax raise_error + fi + fi + super_args super_name TraitBound supertraits.push + cursor.peek = plus_opt + if plus_opt.is_some "+" plus_opt.unwrap Token::value == && then + cursor.pop drop + else + false = parse_super_loop + fi + done + fi + List::new(List[TraitMethod]) = methods "{" cursor expect_token_value drop @@ -1329,7 +1389,7 @@ fn parse_trait parser:Parser cursor:TokenCursor -> CasaTrait { fi done - name_token Token::location methods type_params name_token Token::value CasaTrait + name_token Token::location supertraits methods type_params name_token Token::value CasaTrait } # ============================================================================ diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 2d372ad..3f42479 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -884,7 +884,7 @@ fn simulate_trait_exec tc:TypeChecker function:Function var_name:str { trait_bound_opt.unwrap TraitBound::name tc.store.traits.get = trait_def_opt if trait_def_opt.is_none then return fi trait_def_opt.unwrap = trait_def - for trait_method_entry in trait_def CasaTrait::methods.iter do + for trait_method_entry in trait_def DEFAULT_PARSER collect_trait_methods.iter do if trait_method_name trait_method_entry TraitMethod::name == then tc stack_pop drop trait_type_var trait_method_entry TraitMethod::signature resolve_trait_sig = resolved_exec_sig @@ -1124,7 +1124,7 @@ fn type_satisfies_trait type_name:str trait_name:str function_opt:Option[Functio if trait_name bound_opt.unwrap trait_bound_to_str == then true return fi fi fi - for method in trait_def CasaTrait::methods.iter do + for method in trait_def DEFAULT_PARSER collect_trait_methods.iter do f"{type_name}::{method TraitMethod::name}" = fn_name fn_name DEFAULT_PARSER.store.functions.get = fn_opt false = via_generic_base @@ -1420,11 +1420,12 @@ fn inject_trait_fn_ptrs fi fi + trait_def DEFAULT_PARSER collect_trait_methods = expanded_methods if is_forwarding then # Forward hidden fn ptrs from calling function (abstract methods only) - trait_def CasaTrait::methods.length 1 - = method_index + expanded_methods.length 1 - = method_index while 0 method_index >= do - method_index trait_def CasaTrait::methods.get = method + method_index expanded_methods.get = method if 0 method TraitMethod::default_ops.length == then f"__trait_{concrete}_{method TraitMethod::name}" = hidden_var location hidden_var OpValue::PushVar make_op = push_op @@ -1443,32 +1444,27 @@ fn inject_trait_fn_ptrs fi # Inject FN_PUSH for each abstract method (reversed) # Skip default methods — they are instantiated on demand - trait_def CasaTrait::methods.length 1 - = method_index + expanded_methods.length 1 - = method_index while 0 method_index >= do - method_index trait_def CasaTrait::methods.get = method - if 0 method TraitMethod::default_ops.length != then - method_index 1 - = method_index - # Use continue-safe pattern (avoid nested loop break/continue bug) - fi + method_index expanded_methods.get = method if 0 method TraitMethod::default_ops.length == then f"{concrete}::{method TraitMethod::name}" = fn_name fn_name tc.store.functions.get = global_fn_opt - if global_fn_opt.is_none then + if global_fn_opt.is_some then + global_fn_opt.unwrap = global_fn + if global_fn Function::is_used ! then + true global_fn Function::set_is_used + global_fn global_fn Function::ops resolve_identifiers global_fn Function::set_ops + fi + global_fn ensure_typechecked + location fn_name OpValue::FnPush make_op = push_op + insert_pos push_op ops.insert + 1 += insert_pos + 1 += inserted + f"fn[{global_fn Function::signature signature_to_str}]" tc stack_push + else location f"Function `{fn_name}` required by trait `{trait_name}` not found" ErrorKind::TypeMismatch raise_error - method_index 1 - = method_index - continue - fi - global_fn_opt.unwrap = global_fn - if global_fn Function::is_used ! then - true global_fn Function::set_is_used - global_fn global_fn Function::ops resolve_identifiers global_fn Function::set_ops fi - global_fn ensure_typechecked - location fn_name OpValue::FnPush make_op = push_op - insert_pos push_op ops.insert - 1 += insert_pos - 1 += inserted - f"fn[{global_fn Function::signature signature_to_str}]" tc stack_push fi method_index 1 - = method_index done @@ -2007,7 +2003,7 @@ fn find_trait_for_method method_name:str -> Option[CasaTrait] { } fn check_abstract_methods_present receiver:str trait_def:CasaTrait -> bool { - for cam_method in trait_def CasaTrait::methods.iter do + for cam_method in trait_def DEFAULT_PARSER collect_trait_methods.iter do if 0 cam_method TraitMethod::default_ops.length == then f"{receiver}::{cam_method TraitMethod::name}" DEFAULT_PARSER.store.functions.get = cam_fn_opt if cam_fn_opt.is_none then @@ -2199,7 +2195,7 @@ fn check_method_call 1 += constraint_index done fi - for trait_method in trait_def CasaTrait::methods.iter do + for trait_method in trait_def DEFAULT_PARSER collect_trait_methods.iter do if method_name trait_method TraitMethod::name == then f"__trait_{receiver}_{method_name}" = hidden_var tc stack_pop drop diff --git a/docs/standard-library.md b/docs/standard-library.md index 1be26f0..d24e4ff 100644 --- a/docs/standard-library.md +++ b/docs/standard-library.md @@ -390,7 +390,7 @@ The standard library declares traits with primitive implementations. See [Traits | `Ord` | `lt self other -> bool` | `le`, `gt`, `ge` | `int`, `char` | | `Display` | `to_str self -> str` | -- | `int`, `bool`, `char`, `str`, `cstr`, `ptr`, `array[T]`, `List[T]`, `Option[T]`, `Result[T E]` | | `Word` | (marker) | -- | every single-slot type | -| `Hashable` | `hash self -> int`, `eq self other -> bool` | -- | `int`, `str`, payload-free enums (auto-derived) | +| `Hashable: Eq` | `hash self -> int` (extends `Eq`) | -- | `int`, `str`, payload-free enums (auto-derived) | | `Iterable[T]` | `next self -> Option[T]` | `collect`, `map`, `filter`, `fold`, `count`, `any`, `all`, `find` | `Iter[T]` | ## See Also diff --git a/docs/traits.md b/docs/traits.md index 8d4a988..bf6136a 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -202,20 +202,18 @@ It is used as a bound on builtins that require single-slot operands (for example ## Built-in Trait: `Hashable` -The standard library defines the `Hashable` trait and provides implementations for `str` and `int`: +The standard library defines the `Hashable` trait as an extension of `Eq`. Any `Hashable` type therefore also satisfies `Eq` (see [Supertraits](#supertraits)). The trait declares only `hash`; equality is reused from `Eq`: ```casa -trait Hashable { +trait Hashable: Eq { fn hash self:self -> int - fn eq self:self other:self -> bool } ``` Built-in implementations: - `str::hash` uses the djb2 hash algorithm (via `str_hash`) -- `str::eq` compares strings by content - `int::hash` returns the absolute value (via `int_hash`) -- `int::eq` compares integers with `==` +- `Eq::eq` for `str` and `int` is provided by their respective `impl` blocks ## Built-in Trait: `Display` @@ -244,6 +242,26 @@ impl Point { f"origin = {origin}\n" print # origin = Point(1, 2) ``` +## Supertraits + +A trait can require its implementors to also satisfy one or more *supertraits*. Declare supertraits with `:` after the trait name (and optional type parameters), separating multiple supertraits with `+`: + +```casa +trait Eq { + fn eq self:self other:self -> bool +} + +trait Hashable: Eq { + fn hash self:self -> int +} +``` + +A type satisfies `Hashable` only if it satisfies every supertrait (here `Eq`) in addition to providing `Hashable`'s own required methods. Concretely, the type's `impl` block must contain `eq` (from `Eq`) and `hash` (from `Hashable`). + +Trait-bounded code may call methods declared by any supertrait directly. For example, inside a function bounded by `K: Hashable`, both `K::hash` and `K::eq` resolve correctly. + +Supertraits are checked at the trait's declaration site: the supertrait must already be defined. + ## Default Methods Traits can provide default method implementations. A default method has a body in the trait definition and is automatically available to any type that satisfies the trait (i.e. implements the required methods). Default methods can call the required methods using `self`. diff --git a/lib/std.casa b/lib/std.casa index f78dbec..bb3a21e 100644 --- a/lib/std.casa +++ b/lib/std.casa @@ -640,10 +640,8 @@ trait Ord { } } -trait Hashable { +trait Hashable: Eq { fn hash self:self -> int - - fn eq self:self other:self -> bool } trait Display { diff --git a/tests/compiler/test_traits.casa b/tests/compiler/test_traits.casa index 1e96a2b..e7f19a1 100644 --- a/tests/compiler/test_traits.casa +++ b/tests/compiler/test_traits.casa @@ -834,6 +834,52 @@ fn test_trait_bounded_fn_with_conflicting_type_var_raises { disable_test_mode } +# ============================================================================ +# Supertraits +# ============================================================================ + +fn test_trait_with_supertrait_parses { + "trait_with_supertrait_parses" test_start + "trait Eq { fn eq self:self other:self -> bool }\ntrait Hashable: Eq { fn hash self:self -> int }\n" trait_test_parse drop + "Hashable" DEFAULT_PARSER.store.traits.get.unwrap = hashable + "supertrait count" 1 hashable CasaTrait::supertraits.length assert_eq + "supertrait name" "Eq" 0 hashable CasaTrait::supertraits.get TraitBound::name assert_str_eq +} + +fn test_trait_with_undefined_supertrait_raises { + "trait_with_undefined_supertrait_raises" test_start + enable_test_mode + clear_errors + "trait Hashable: Bogus { fn hash self:self -> int }\n" trait_test_parse drop + "has errors" assert_has_errors + "undefined supertrait" ErrorKind::UndefinedName assert_error_kind + clear_errors + disable_test_mode +} + +fn test_supertrait_method_required_for_satisfaction { + "supertrait_method_required_for_satisfaction" test_start + "trait Eq { fn eq self:self other:self -> bool }\ntrait Hashable: Eq { fn hash self:self -> int }\nstruct Bare { v: int }\nimpl Bare { fn hash self:Bare -> int { self Bare::v } }\n" trait_test_resolve drop + "Hashable not satisfied without Eq impl" Option::None(Option[Function]) "Hashable" "Bare" type_satisfies_trait ! assert_true +} + +fn test_supertrait_satisfaction_with_full_impl { + "supertrait_satisfaction_with_full_impl" test_start + "trait Eq { fn eq self:self other:self -> bool }\ntrait Hashable: Eq { fn hash self:self -> int }\nstruct Full { v: int }\nimpl Full { fn hash self:Full -> int { self Full::v } fn eq self:Full other:Full -> bool { self Full::v other Full::v == } }\n" trait_test_resolve drop + "Hashable satisfied with Eq impl" Option::None(Option[Function]) "Hashable" "Full" type_satisfies_trait assert_true +} + +fn test_supertrait_negative_typecheck_raises { + "supertrait_negative_typecheck_raises" test_start + enable_test_mode + clear_errors + "trait Eq { fn eq self:self other:self -> bool }\ntrait Hashable: Eq { fn hash self:self -> int }\nstruct Bare { v: int }\nimpl Bare { fn hash self:Bare -> int { self Bare::v } }\nfn use_hash[K: Hashable] k:K -> int { k .hash }\n1 Bare = b\nb use_hash drop\n" trait_test_typecheck drop + "has errors" assert_has_errors + "missing Eq for Hashable bound" ErrorKind::TypeMismatch assert_error_kind + clear_errors + disable_test_mode +} + # ============================================================================ # Parse map type in signature # ============================================================================ @@ -968,6 +1014,12 @@ test_generic_trait_bound_structural_mismatch_raises test_type_var_arg_generic_trait_bound_happy_path test_type_var_arg_bound_conflict_with_param_raises test_type_var_arg_bound_structural_mismatch_raises +# Supertraits +test_trait_with_supertrait_parses +test_trait_with_undefined_supertrait_raises +test_supertrait_method_required_for_satisfaction +test_supertrait_satisfaction_with_full_impl +test_supertrait_negative_typecheck_raises # Parse map type test_parse_map_type_in_signature test_summary From 17ab71df0e03c30c3fa3dadc2d9168a31be70a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 23:20:23 +0300 Subject: [PATCH 04/15] feat: [US-004] Retype typeof to [T] T -> str Surface signature switches from `any -> str` to the explicit generic form `[T] T -> str`. Behaviour is unchanged: typeof pops a value of any type and prints its static type name. The check_typeof body already reads the popped slot's concrete type for the bytecode annotation, so no functional changes were needed there. Updated: - LSP hover string for OpValue::Typeof - docs/intrinsics.md `typeof` signature - tests/compiler/test_typeof.casa: added typecheck and bytecode coverage for typeof on array literals. Part of #147 --- docs/intrinsics.md | 2 +- lsp.casa | 2 +- tests/compiler/test_typeof.casa | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/intrinsics.md b/docs/intrinsics.md index af188cf..f644d1a 100644 --- a/docs/intrinsics.md +++ b/docs/intrinsics.md @@ -58,7 +58,7 @@ true print # true Consumes the top of the stack and prints its type name to stdout. -**Signature:** `typeof a:any` +**Signature:** `[T] T -> str` **Stack effect:** `a -> None` diff --git a/lsp.casa b/lsp.casa index 969bb71..9ae6070 100644 --- a/lsp.casa +++ b/lsp.casa @@ -2134,7 +2134,7 @@ fn op_value_stack_effect op_value:OpValue -> Option[str] { OpValue::PrintChar => "print: any -> None" Option::Some OpValue::PrintCstr => "print: any -> None" Option::Some OpValue::FnExec => "exec: fn[sig] -> ..." Option::Some - OpValue::Typeof => "typeof: any -> str" Option::Some + OpValue::Typeof => "typeof: [T] T -> str" Option::Some OpValue::AssignDec(_) => "-=: int -> None" Option::Some OpValue::AssignInc(_) => "+=: int -> None" Option::Some OpValue::HeapAlloc => "alloc: int -> ptr" Option::Some diff --git a/tests/compiler/test_typeof.casa b/tests/compiler/test_typeof.casa index ef10a70..9a69b1a 100644 --- a/tests/compiler/test_typeof.casa +++ b/tests/compiler/test_typeof.casa @@ -139,6 +139,13 @@ fn test_typecheck_typeof_ptr { "returns str" "str" 0 sig Signature::return_types.get assert_str_eq } +fn test_typecheck_typeof_array { + "typecheck_typeof_array" test_start + "[1 2 3] typeof" tc_typeof = sig + "one return" 1 sig Signature::return_types.length assert_eq + "returns str" "str" 0 sig Signature::return_types.get assert_str_eq +} + # ============================================================================ # Bytecode tests # ============================================================================ @@ -164,6 +171,12 @@ fn test_bytecode_typeof_str { "str in strings" "str" prog Program::strings string_table_contains assert_true } +fn test_bytecode_typeof_array { + "bytecode_typeof_array" test_start + "[1 2 3] typeof" compile_typeof = prog + "array[any] in strings" "array[any]" prog Program::strings string_table_contains assert_true +} + fn test_bytecode_typeof_instruction_sequence { "bytecode_typeof_instruction_sequence" test_start "42 typeof" compile_typeof = prog @@ -221,9 +234,11 @@ test_typecheck_typeof_returns_str_for_char test_typecheck_typeof_no_params test_typecheck_typeof_chained test_typecheck_typeof_ptr +test_typecheck_typeof_array test_bytecode_typeof_int test_bytecode_typeof_bool test_bytecode_typeof_str +test_bytecode_typeof_array test_bytecode_typeof_instruction_sequence test_emit_typeof_int test_emit_typeof_bool From 5df55927b3ea5d7c853cbf29e3a767ab32b0db04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 23:42:27 +0300 Subject: [PATCH 05/15] feat: [US-005] Empty [] literal: lazy element-type inference, error if unresolved --- compiler/typechecker.casa | 46 ++++++++++++++++--- docs/types-and-literals.md | 11 ++++- tests/compiler/test_typechecker.casa | 67 ++++++++++++++++++++++++++++ tests/compiler/test_typeof.casa | 2 +- 4 files changed, 118 insertions(+), 8 deletions(-) diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 3f42479..f2f4967 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -474,6 +474,19 @@ fn is_enum_type_var typ:str -> bool { false fi } +# True when typ has TYPE_UNKNOWN inside any parametric position. Used to +# detect empty array literals whose element type was never constrained by an +# annotation, cast, or surrounding use. + +fn type_has_unresolved typ:str -> bool { + typ extract_generic_params = params_opt + if params_opt.is_none then false return fi + for param in params_opt.unwrap.iter do + if TYPE_UNKNOWN param == then true return fi + if param type_has_unresolved then true return fi + done + false +} fn bind_generic_param tc:TypeChecker @@ -840,6 +853,9 @@ fn check_assignment tc:TypeChecker op:Op function_opt:Option[Function] { if has_annotation then op Op::type_annotation = effective_type fi + if has_annotation ! stack_type type_has_unresolved && then + op Op::location f"Cannot infer element type of empty array literal assigned to `{var_name}`; add a type annotation (e.g. `= {var_name}:array[int]`) or an explicit cast (e.g. `[] (array[int])`)." ErrorKind::TypeMismatch raise_error + fi # Try local variables first if function_opt.is_some then @@ -1540,7 +1556,7 @@ fn check_function_ops # Check literal push operations # ============================================================================ -fn check_literals tc:TypeChecker op:Op { +fn check_literals tc:TypeChecker op:Op function_opt:Option[Function] { op.value = literal_value if literal_value OpValue::PushInt is then "int" tc stack_push @@ -1550,9 +1566,29 @@ fn check_literals tc:TypeChecker op:Op { "bool" tc stack_push elif literal_value OpValue::PushChar is then "char" tc stack_push - elif literal_value OpValue::PushArray is then - # Array literals: pop N items of same type - "array[any]" tc stack_push + elif literal_value OpValue::PushArray(items) is then + if 0 items.length == then + List::new(List[str]) = empty_params + TYPE_UNKNOWN empty_params.push + empty_params "array" build_parameterized_type tc stack_push + else + function_opt items tc run_type_check_ops + TYPE_UNKNOWN = elem_type + 0 = items_idx + while items_idx items.length > do + tc stack_pop = item_type + elem_type item_type unify_type = unified + if "" unified == then + op Op::location f"Heterogeneous array literal: cannot unify `{elem_type}` and `{item_type}`" ErrorKind::TypeMismatch raise_error + else + unified = elem_type + fi + 1 += items_idx + done + List::new(List[str]) = item_params + elem_type item_params.push + item_params "array" build_parameterized_type tc stack_push + fi elif literal_value OpValue::FstringConcat(count) is then 0 = index while index count > do @@ -2456,7 +2492,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct op_value OpValue::PushArray is || op_value OpValue::FstringConcat is || then - current_op checker check_literals + function_opt current_op checker check_literals continue fi diff --git a/docs/types-and-literals.md b/docs/types-and-literals.md index 81e6485..eb333e0 100644 --- a/docs/types-and-literals.md +++ b/docs/types-and-literals.md @@ -201,10 +201,17 @@ All items must have the same type. Heterogeneous arrays are compile-time errors: [1, "hello"] # TYPE_MISMATCH error ``` -An empty array has type `array[any]`: +An empty array literal has an unresolved element type. The compiler infers it from the first constraining use — an explicit cast or a typed binding: ```casa -[] # type: array[any] +[] (array[int]) # explicit cast resolves element type +[] = xs:array[str] # type annotation on the binding resolves it +``` + +If the inference frame closes without resolution, the compiler emits a `TYPE_MISMATCH` error: + +```casa +[] = xs xs drop # error: cannot infer element type of empty array ``` Arrays can be nested. The element type is inferred recursively: diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index d267997..55c4980 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -1702,6 +1702,65 @@ fn test_branch_frame_kind_dispatch { fi "mutation persists through binding" default_present_after assert_true } + +# ============================================================================ +# Tests: Array literal element-type inference +# ============================================================================ + +fn test_array_literal_int_inference { + "array_literal_int_inference" test_start + "[1 2 3]" tc_string = sig + "one return" 1 sig Signature::return_types.length assert_eq + "array[int]" "array[int]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_array_literal_str_inference { + "array_literal_str_inference" test_start + "[\"a\" \"b\"]" tc_string = sig + "array[str]" "array[str]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_array_literal_bool_inference { + "array_literal_bool_inference" test_start + "[true false]" tc_string = sig + "array[bool]" "array[bool]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_array_literal_empty { + "array_literal_empty" test_start + "[] (array[int])" tc_string = sig + "explicit cast resolves" "array[int]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_array_literal_empty_with_annotation { + "array_literal_empty_with_annotation" test_start + "[] = xs:array[int] xs" tc_string = sig + "annotation resolves" "array[int]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_array_literal_empty_with_cast_then_assign { + "array_literal_empty_with_cast_then_assign" test_start + "[] (array[str]) = xs xs" tc_string = sig + "cast then assign" "array[str]" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_error_array_literal_heterogeneous { + "error_array_literal_heterogeneous" test_start + "[1 \"a\"]" tc_string_error drop + "has errors" assert_has_errors + "type mismatch" ErrorKind::TypeMismatch assert_error_kind + clear_errors + disable_test_mode +} + +fn test_error_array_literal_unresolved_assignment { + "error_array_literal_unresolved_assignment" test_start + "[] = xs xs drop" tc_string_error drop + "has errors" assert_has_errors + "type mismatch" ErrorKind::TypeMismatch assert_error_kind + clear_errors + disable_test_mode +} # OpFnReturn inside a branch restores live from the frame's `before` snapshot. fn test_branch_return_inside_if_restores_live { @@ -1860,4 +1919,12 @@ test_branch_if_end_pops_frame test_branch_while_empty_body test_branch_frame_kind_dispatch test_branch_return_inside_if_restores_live +test_array_literal_int_inference +test_array_literal_str_inference +test_array_literal_bool_inference +test_array_literal_empty +test_array_literal_empty_with_annotation +test_array_literal_empty_with_cast_then_assign +test_error_array_literal_heterogeneous +test_error_array_literal_unresolved_assignment test_summary diff --git a/tests/compiler/test_typeof.casa b/tests/compiler/test_typeof.casa index 9a69b1a..eebc683 100644 --- a/tests/compiler/test_typeof.casa +++ b/tests/compiler/test_typeof.casa @@ -174,7 +174,7 @@ fn test_bytecode_typeof_str { fn test_bytecode_typeof_array { "bytecode_typeof_array" test_start "[1 2 3] typeof" compile_typeof = prog - "array[any] in strings" "array[any]" prog Program::strings string_table_contains assert_true + "array[int] in strings" "array[int]" prog Program::strings string_table_contains assert_true } fn test_bytecode_typeof_instruction_sequence { From decd228d27887bd5cae1145c2e13ca527af8d170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 23:53:36 +0300 Subject: [PATCH 06/15] feat: [US-006] Reject `(any)` cast at parse time Migration: drop the cast (the binding annotation already provides the type) or write the concrete type instead. --- compiler/syntax.casa | 5 +++ docs/STYLE.md | 3 -- docs/functions-and-lambdas.md | 3 +- docs/operators.md | 1 - tests/compiler/test_type_annotations.casa | 55 ++++++++++++++--------- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/compiler/syntax.casa b/compiler/syntax.casa index 71b07ae..035d59d 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -285,6 +285,11 @@ fn get_op_type_cast cursor:TokenCursor -> Op { close_token Token::location Location::span Span::offset = close_offset close_offset open_offset - 1 + = length length open_offset open_token Token::location Location::file make_location = loc + + if "any" cast_type == then + loc "type `any` does not exist; use a concrete type or remove the cast" ErrorKind::Syntax raise_error + fi + loc cast_type OpValue::TypeCast make_op } diff --git a/docs/STYLE.md b/docs/STYLE.md index cf39784..91d3f6b 100644 --- a/docs/STYLE.md +++ b/docs/STYLE.md @@ -168,9 +168,6 @@ without a type-name prefix is acceptable. # Required: bare Option needs narrowing Option::None = empty:Option[int] - # Required: narrowing any to a concrete type - 42 (any) = x:int - # MUST NOT: inference works fine, annotation is noise 42 = x:int ``` diff --git a/docs/functions-and-lambdas.md b/docs/functions-and-lambdas.md index fcbc1d2..24da6f4 100644 --- a/docs/functions-and-lambdas.md +++ b/docs/functions-and-lambdas.md @@ -234,10 +234,9 @@ A variable's type is set on first assignment and cannot change: "hi" = x # ERROR: cannot assign str to int variable ``` -The `= name:type` form annotates the variable type explicitly. The type checker verifies the stack value is compatible and uses the annotated type for the variable. This is useful for narrowing `any` or bare `Option` to a concrete type: +The `= name:type` form annotates the variable type explicitly. The type checker verifies the stack value is compatible and uses the annotated type for the variable. This is useful for narrowing a bare `Option` (or other unresolved generic) to a concrete type: ```casa -42 (any) = x:int # narrow any to int Option::None = empty:Option[int] # narrow bare Option to Option[int] ``` diff --git a/docs/operators.md b/docs/operators.md index 568aaab..748162c 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -149,7 +149,6 @@ The `= name:type` form lets you annotate the type of a variable at assignment ti ```casa 42 = x:int # explicit int annotation Option::None = empty:Option[int] # narrow bare Option to Option[int] -42 (any) = val:int # narrow any to int ``` Variables are created on first assignment. See [Functions and Lambdas -- Variables](functions-and-lambdas.md#variables) for scoping rules. diff --git a/tests/compiler/test_type_annotations.casa b/tests/compiler/test_type_annotations.casa index c449553..2f5b03b 100644 --- a/tests/compiler/test_type_annotations.casa +++ b/tests/compiler/test_type_annotations.casa @@ -196,24 +196,38 @@ fn test_tc_annotation_matches_stack { "return type" "int" 0 sig Signature::return_types.get assert_str_eq } -fn test_tc_narrow_any_to_int { - "tc_narrow_any_to_int" test_start - "42 (any) = x:int x" tc_code = sig +fn test_tc_annotation_to_int { + "tc_annotation_to_int" test_start + "42 = x:int x" tc_code = sig "return type" "int" 0 sig Signature::return_types.get assert_str_eq } -fn test_tc_narrow_any_to_str { - "tc_narrow_any_to_str" test_start - "\"hello\" (any) = x:str x" tc_code = sig +fn test_tc_annotation_to_str { + "tc_annotation_to_str" test_start + "\"hello\" = x:str x" tc_code = sig "return type" "str" 0 sig Signature::return_types.get assert_str_eq } -fn test_tc_narrow_any_to_option { - "tc_narrow_any_to_option" test_start - OPTION_ENUM "Option::None (any) = x:Option[T] x" str::concat tc_code = sig +fn test_tc_annotation_to_option { + "tc_annotation_to_option" test_start + OPTION_ENUM "Option::None = x:Option[T] x" str::concat tc_code = sig "return type" "Option[T]" 0 sig Signature::return_types.get assert_str_eq } +fn test_tc_reject_any_cast { + "tc_reject_any_cast" test_start + enable_test_mode + clear_global_state + clear_errors + "test" "42 (any) = x:int x" lex_source = tokens + tokens DEFAULT_PARSER parse_ops drop + "has errors" assert_has_errors + "syntax error" ErrorKind::Syntax assert_error_kind + "mentions any" "any" assert_error_contains + clear_errors + disable_test_mode +} + fn test_tc_reassignment_keeps_annotated_type { "tc_reassignment_keeps_annotated_type" test_start "42 = x:int 99 = x x" tc_code = sig @@ -232,9 +246,9 @@ fn test_tc_annotation_in_function_body { "return type" "int" 0 sig Signature::return_types.get assert_str_eq } -fn test_tc_narrow_any_in_function { - "tc_narrow_any_in_function" test_start - "fn foo -> int { 42 (any) = x:int x } foo" tc_code = sig +fn test_tc_annotation_in_function_explicit { + "tc_annotation_in_function_explicit" test_start + "fn foo -> int { 42 = x:int x } foo" tc_code = sig "return type" "int" 0 sig Signature::return_types.get assert_str_eq } @@ -315,10 +329,10 @@ fn test_warn_no_warn_matching_types { "no warnings" 0 assert_warning_count } -fn test_warn_no_warn_narrowing_any { - "warn_no_warn_narrowing_any" test_start +fn test_warn_no_warn_annotation_match { + "warn_no_warn_annotation_match" test_start clear_warnings - "42 (any) = x:int" tc_code drop + "42 = x:int" tc_code drop "no warnings" 0 assert_warning_count } @@ -344,13 +358,14 @@ test_tc_annotation_int test_tc_annotation_str test_tc_annotation_bool test_tc_annotation_matches_stack -test_tc_narrow_any_to_int -test_tc_narrow_any_to_str -test_tc_narrow_any_to_option +test_tc_annotation_to_int +test_tc_annotation_to_str +test_tc_annotation_to_option +test_tc_reject_any_cast test_tc_reassignment_keeps_annotated_type test_tc_without_annotation test_tc_annotation_in_function_body -test_tc_narrow_any_in_function +test_tc_annotation_in_function_explicit test_tc_annotation_with_struct test_tc_option_int_with_some # Typechecker errors: mismatch (currently no validation in self-hosted) @@ -360,5 +375,5 @@ test_tc_mismatch_bool_as_str test_tc_reassignment_type_mismatch # Warnings: lossy annotation (currently no warnings in self-hosted) test_warn_no_warn_matching_types -test_warn_no_warn_narrowing_any +test_warn_no_warn_annotation_match test_summary From 8ceffbd305266713d31fb97181a3fff90ab2a8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 27 Apr 2026 00:08:43 +0300 Subject: [PATCH 07/15] feat: [US-007] Migrate stack ops to surface generics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace `any`-based hover strings for drop/dup/swap/over/rot with explicit `[T]` / `[T1 T2]` / `[T1 T2 T3]` generic surface forms in LSP, intrinsics docs, and functions-and-lambdas docs. The typechecker side was already polymorphic — `check_stack_ops` pops/pushes the actual stack types — so the compiler change is hover-only. Adds a polymorphic stack-shaped `keep_top[T1 T2] T1 T2 -> T1 { swap drop }` example with two type instantiations, plus LSP hover tests for over and rot. Closes #151 --- docs/functions-and-lambdas.md | 12 ++++++++++++ docs/intrinsics.md | 17 ++++++++++------- examples/generics.casa | 7 +++++++ examples/outputs/generics.out | 2 ++ lsp.casa | 10 +++++----- tests/compiler/test_lsp.casa | 24 +++++++++++++++++++++--- 6 files changed, 57 insertions(+), 15 deletions(-) diff --git a/docs/functions-and-lambdas.md b/docs/functions-and-lambdas.md index 24da6f4..cae297c 100644 --- a/docs/functions-and-lambdas.md +++ b/docs/functions-and-lambdas.md @@ -147,6 +147,18 @@ fn first[T1 T2] a:T1 b:T2 -> T1 { a } fn wrap[T] T -> T int { 42 } ``` +The built-in stack intrinsics use the same generic surface syntax: + +```casa +# drop[T] T -> None +# dup[T] T -> T T +# swap[T1 T2] T1 T2 -> T2 T1 +# over[T1 T2] T1 T2 -> T2 T1 T2 +# rot[T1 T2 T3] T1 T2 T3 -> T3 T1 T2 +fn keep_top[T1 T2] T1 T2 -> T1 { swap drop } +42 "kept" keep_top # "kept" (deeper int dropped, top str kept) +``` + The type checker enforces consistency — if the same type variable appears multiple times in the parameters, all occurrences must bind to the same type: ```casa diff --git a/docs/intrinsics.md b/docs/intrinsics.md index f644d1a..3becba4 100644 --- a/docs/intrinsics.md +++ b/docs/intrinsics.md @@ -6,13 +6,16 @@ Intrinsics are operations built into the compiler. They are available in every C Operations for manipulating the stack directly. -| Intrinsic | Stack Effect | Description | -|-----------|-------------|-------------| -| `drop` | `a ->` | Discard top of stack | -| `dup` | `a -> a a` | Duplicate top of stack | -| `swap` | `a b -> b a` | Swap top two values | -| `over` | `a b -> a b a` | Copy second value to top | -| `rot` | `a b c -> b c a` | Rotate top three values | +| Intrinsic | Generic signature | Description | +|-----------|-------------------|-------------| +| `drop` | `[T] T -> None` | Discard top of stack | +| `dup` | `[T] T -> T T` | Duplicate top of stack | +| `swap` | `[T1 T2] T1 T2 -> T2 T1` | Swap top two values | +| `over` | `[T1 T2] T1 T2 -> T2 T1 T2` | Copy second value to top | +| `rot` | `[T1 T2 T3] T1 T2 T3 -> T3 T1 T2` | Rotate top three values | + +Type variables resolve at the call site, so the same intrinsic works on any value type: +`42 dup` produces two `int`s on the stack, `"hi" dup` produces two `str`s. ### Examples diff --git a/examples/generics.casa b/examples/generics.casa index 96d555f..acc7e46 100644 --- a/examples/generics.casa +++ b/examples/generics.casa @@ -22,6 +22,13 @@ fn second[T1 T2] a:T1 b:T2 -> T2 { b } 42 "hello" first print "\n" print # hello 42 "hello" second print "\n" print # 42 +# stack-shaped polymorphic helper: drop the deeper arg, keep the topmost +# (T1 is the topmost / first RPN arg per the stack convention) + +fn keep_top[T1 T2] T1 T2 -> T1 { swap drop } + +42 "kept" keep_top print "\n" print # kept +"discarded" 99 keep_top print "\n" print # 99 # mixing generic and concrete types fn tag[T] T -> T str { "tagged" } diff --git a/examples/outputs/generics.out b/examples/outputs/generics.out index 6beacca..72fcf72 100644 --- a/examples/outputs/generics.out +++ b/examples/outputs/generics.out @@ -4,6 +4,8 @@ hello 2 hello 42 +kept +99 tagged 99 20 diff --git a/lsp.casa b/lsp.casa index 9ae6070..8665393 100644 --- a/lsp.casa +++ b/lsp.casa @@ -2122,11 +2122,11 @@ fn op_value_stack_effect op_value:OpValue -> Option[str] { OpValue::Ge => ">=: any any -> bool" Option::Some OpValue::And => "&&: bool bool -> bool" Option::Some OpValue::Or => "||: bool bool -> bool" Option::Some - OpValue::Drop => "drop: any -> None" Option::Some - OpValue::Dup => "dup: any -> any any" Option::Some - OpValue::Swap => "swap: any any -> any any" Option::Some - OpValue::Over => "over: any any -> any any any" Option::Some - OpValue::Rot => "rot: any any any -> any any any" Option::Some + OpValue::Drop => "drop[T]: T -> None" Option::Some + OpValue::Dup => "dup[T]: T -> T T" Option::Some + OpValue::Swap => "swap[T1 T2]: T1 T2 -> T2 T1" Option::Some + OpValue::Over => "over[T1 T2]: T1 T2 -> T2 T1 T2" Option::Some + OpValue::Rot => "rot[T1 T2 T3]: T1 T2 T3 -> T3 T1 T2" Option::Some OpValue::Print => "print: any -> None" Option::Some OpValue::PrintInt => "print: any -> None" Option::Some OpValue::PrintStr => "print: any -> None" Option::Some diff --git a/tests/compiler/test_lsp.casa b/tests/compiler/test_lsp.casa index a62ee7b..1ca2904 100644 --- a/tests/compiler/test_lsp.casa +++ b/tests/compiler/test_lsp.casa @@ -565,7 +565,7 @@ fn test_compute_hover_drop { "42 drop" lsp_test_state = state 3 0 state compute_hover = result "is some" result.is_some assert_true - "contains drop effect" 0 result.unwrap HoverContent::content "drop: any -> None" str::find >= assert_true + "contains drop effect" 0 result.unwrap HoverContent::content "drop[T]: T -> None" str::find >= assert_true } fn test_compute_hover_dup { @@ -573,7 +573,7 @@ fn test_compute_hover_dup { "42 dup drop drop" lsp_test_state = state 3 0 state compute_hover = result "is some" result.is_some assert_true - "contains dup effect" 0 result.unwrap HoverContent::content "dup: any -> any any" str::find >= assert_true + "contains dup effect" 0 result.unwrap HoverContent::content "dup[T]: T -> T T" str::find >= assert_true } fn test_compute_hover_swap { @@ -581,7 +581,23 @@ fn test_compute_hover_swap { "1 2 swap drop drop" lsp_test_state = state 4 0 state compute_hover = result "is some" result.is_some assert_true - "contains swap effect" 0 result.unwrap HoverContent::content "swap: any any -> any any" str::find >= assert_true + "contains swap effect" 0 result.unwrap HoverContent::content "swap[T1 T2]: T1 T2 -> T2 T1" str::find >= assert_true +} + +fn test_compute_hover_over { + "compute_hover_over" test_start + "1 2 over drop drop drop" lsp_test_state = state + 4 0 state compute_hover = result + "is some" result.is_some assert_true + "contains over effect" 0 result.unwrap HoverContent::content "over[T1 T2]: T1 T2 -> T2 T1 T2" str::find >= assert_true +} + +fn test_compute_hover_rot { + "compute_hover_rot" test_start + "1 2 3 rot drop drop drop" lsp_test_state = state + 6 0 state compute_hover = result + "is some" result.is_some assert_true + "contains rot effect" 0 result.unwrap HoverContent::content "rot[T1 T2 T3]: T1 T2 T3 -> T3 T1 T2" str::find >= assert_true } fn test_compute_hover_not { @@ -1211,6 +1227,8 @@ test_compute_hover_assign_variable test_compute_hover_drop test_compute_hover_dup test_compute_hover_swap +test_compute_hover_over +test_compute_hover_rot test_compute_hover_not test_compute_hover_eq test_compute_hover_print From 7639db04fa67da4c59e15031b8ab38ca52a8a73e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 27 Apr 2026 00:36:19 +0300 Subject: [PATCH 08/15] feat: [US-008] Migrate store and syscall builtins to [T: Word] Replace `any` value-type slots in store8/16/32/64 and syscall1..6 with a `Word` marker-trait bound. `check_memory` and `check_syscalls` now thread `function_opt` through and validate each popped value/arg type via `assert_word_bound`. Concrete types are accepted (every single-slot value satisfies `Word` today); type variables without an explicit bound are deferred to the call site; type variables whose declared bound does not extend `Word` are rejected with a clear migration message. Hashable and Display now extend `Word` as supertraits so existing stdlib generics (`Map[K V]` with `K: Hashable`, `Display` impls) keep compiling unchanged. Adds `bound_implies_word` to walk supertraits. LSP hover, intrinsics docs, traits docs, and the standard-library trait table reflect the new signatures. Closes #154 --- compiler/typechecker.casa | 45 ++++++++++++--- docs/intrinsics.md | 24 ++++---- docs/standard-library.md | 4 +- docs/traits.md | 18 +++--- lib/std.casa | 8 +-- lsp.casa | 20 +++---- tests/compiler/test_typechecker.casa | 84 +++++++++++++++++++++++++++- 7 files changed, 160 insertions(+), 43 deletions(-) diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index f2f4967..e8d62f6 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -791,8 +791,37 @@ fn check_stack_ops tc:TypeChecker op:Op { # ============================================================================ # Check memory operations # ============================================================================ - -fn check_memory tc:TypeChecker op:Op { +# Returns true if `bound` names `Word` directly or transitively via supertraits. + +fn bound_implies_word bound:TraitBound -> bool { + bound TraitBound::name = bound_name + if "Word" bound_name == then true return fi + bound_name DEFAULT_PARSER.store.traits.get = bound_trait_opt + if bound_trait_opt.is_none then false return fi + for super_bound in bound_trait_opt.unwrap CasaTrait::supertraits.iter do + if super_bound bound_implies_word then true return fi + done + false +} +# Validate that `type_name` satisfies the `Word` marker trait at the given op +# location. Concrete single-slot types always satisfy `Word` in current Casa. +# A type variable from the enclosing function satisfies `Word` only when its +# declared bound is `Word` itself or a trait that extends `Word`. An unbounded +# type variable is allowed; the concrete type bound at the call site will +# satisfy `Word` automatically. + +fn assert_word_bound tc:TypeChecker op:Op type_name:str function_opt:Option[Function] { + if type_name TYPE_UNKNOWN == then return fi + if function_opt.is_none then return fi + function_opt.unwrap Function::signature = word_sig + if type_name word_sig Signature::type_vars.has ! then return fi + type_name word_sig Signature::trait_bounds.get = word_bound_opt + if word_bound_opt.is_none then return fi + if word_bound_opt.unwrap bound_implies_word then return fi + op Op::location f"Type variable `{type_name}` does not satisfy `Word` bound; add `[{type_name}: Word]` (or a trait that extends `Word`) to the function's type parameters" ErrorKind::TypeMismatch raise_error +} + +fn check_memory tc:TypeChecker op:Op function_opt:Option[Function] { op.value = mem_value if mem_value OpValue::Load8 is @@ -809,7 +838,8 @@ fn check_memory tc:TypeChecker op:Op { mem_value OpValue::Store64 is || then "ptr" tc expect_type drop - tc stack_pop drop + tc stack_pop = stored_type + function_opt stored_type op tc assert_word_bound elif mem_value OpValue::HeapAlloc is then "int" tc expect_type drop "ptr" tc stack_push @@ -1632,7 +1662,7 @@ fn check_typeof tc:TypeChecker op:Op { # Check syscalls # ============================================================================ -fn check_syscalls tc:TypeChecker op:Op { +fn check_syscalls tc:TypeChecker op:Op function_opt:Option[Function] { op.value match OpValue::Syscall0 => 0 OpValue::Syscall1 => 1 @@ -1646,7 +1676,8 @@ fn check_syscalls tc:TypeChecker op:Op { "int" tc expect_type drop 0 = index while index arg_count > do - tc stack_pop drop + tc stack_pop = arg_type + function_opt arg_type op tc assert_word_bound 1 += index done "int" tc stack_push @@ -2425,7 +2456,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct op_value OpValue::Store64 is || op_value OpValue::HeapAlloc is || then - current_op checker check_memory + function_opt current_op checker check_memory continue fi @@ -2518,7 +2549,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct op_value OpValue::Syscall5 is || op_value OpValue::Syscall6 is || then - current_op checker check_syscalls + function_opt current_op checker check_syscalls continue fi diff --git a/docs/intrinsics.md b/docs/intrinsics.md index 3becba4..384a4da 100644 --- a/docs/intrinsics.md +++ b/docs/intrinsics.md @@ -84,10 +84,12 @@ Low-level byte-addressed memory access for building data structures. All load/st | `load16` | `ptr -> int` | Load 16-bit value from address (zero-extended) | | `load32` | `ptr -> int` | Load 32-bit value from address (zero-extended) | | `load64` | `ptr -> int` | Load 64-bit value from address | -| `store8` | `any ptr -> None` | Store 8-bit value to address | -| `store16` | `any ptr -> None` | Store 16-bit value to address | -| `store32` | `any ptr -> None` | Store 32-bit value to address | -| `store64` | `any ptr -> None` | Store 64-bit value to address | +| `store8` | `[T: Word] T ptr -> None` | Store 8-bit value to address | +| `store16` | `[T: Word] T ptr -> None` | Store 16-bit value to address | +| `store32` | `[T: Word] T ptr -> None` | Store 32-bit value to address | +| `store64` | `[T: Word] T ptr -> None` | Store 64-bit value to address | + +The value type must satisfy the [`Word`](traits.md#word) marker trait, which constrains it to a single-slot value. Every primitive, enum variant, struct reference, and array reference satisfies `Word` automatically. ### Examples @@ -107,17 +109,17 @@ Values are addressed by byte offset. Use pointer arithmetic (`+`) to access diff ## Syscall Intrinsics -Direct Linux system call access. Each intrinsic pops N+1 values from the stack (the syscall number on top, then arguments in order) and pushes the kernel return value as `int`. The syscall number must be `int`. Arguments have no type constraints. +Direct Linux system call access. Each intrinsic pops N+1 values from the stack (the syscall number on top, then arguments in order) and pushes the kernel return value as `int`. The syscall number must be `int`. Each argument must satisfy the [`Word`](traits.md#word) marker trait so that exactly one register-sized value lands in the corresponding syscall register. | Intrinsic | Stack Effect | Description | |-----------|-------------|-------------| | `syscall0` | `nr -> int` | Syscall with 0 args | -| `syscall1` | `a1 nr -> int` | Syscall with 1 arg | -| `syscall2` | `a2 a1 nr -> int` | Syscall with 2 args | -| `syscall3` | `a3 a2 a1 nr -> int` | Syscall with 3 args | -| `syscall4` | `a4 a3 a2 a1 nr -> int` | Syscall with 4 args | -| `syscall5` | `a5 a4 a3 a2 a1 nr -> int` | Syscall with 5 args | -| `syscall6` | `a6 a5 a4 a3 a2 a1 nr -> int` | Syscall with 6 args | +| `syscall1` | `[A1: Word] A1 nr -> int` | Syscall with 1 arg | +| `syscall2` | `[A1: Word A2: Word] A2 A1 nr -> int` | Syscall with 2 args | +| `syscall3` | `[A1: Word A2: Word A3: Word] A3 A2 A1 nr -> int` | Syscall with 3 args | +| `syscall4` | `[A1: Word ... A4: Word] A4 A3 A2 A1 nr -> int` | Syscall with 4 args | +| `syscall5` | `[A1: Word ... A5: Word] A5 A4 A3 A2 A1 nr -> int` | Syscall with 5 args | +| `syscall6` | `[A1: Word ... A6: Word] A6 A5 A4 A3 A2 A1 nr -> int` | Syscall with 6 args | ### Register Mapping diff --git a/docs/standard-library.md b/docs/standard-library.md index d24e4ff..6e8ae4e 100644 --- a/docs/standard-library.md +++ b/docs/standard-library.md @@ -388,9 +388,9 @@ The standard library declares traits with primitive implementations. See [Traits |-------|----------|----------|----------------| | `Eq` | `eq self other -> bool` | `ne` | `int`, `bool`, `char`, `str`, `cstr`, `ptr` | | `Ord` | `lt self other -> bool` | `le`, `gt`, `ge` | `int`, `char` | -| `Display` | `to_str self -> str` | -- | `int`, `bool`, `char`, `str`, `cstr`, `ptr`, `array[T]`, `List[T]`, `Option[T]`, `Result[T E]` | +| `Display: Word` | `to_str self -> str` (extends `Word`) | -- | `int`, `bool`, `char`, `str`, `cstr`, `ptr`, `array[T]`, `List[T]`, `Option[T]`, `Result[T E]` | | `Word` | (marker) | -- | every single-slot type | -| `Hashable: Eq` | `hash self -> int` (extends `Eq`) | -- | `int`, `str`, payload-free enums (auto-derived) | +| `Hashable: Eq + Word` | `hash self -> int` (extends `Eq`, `Word`) | -- | `int`, `str`, payload-free enums (auto-derived) | | `Iterable[T]` | `next self -> Option[T]` | `collect`, `map`, `filter`, `fold`, `count`, `any`, `all`, `find` | `Iter[T]` | ## See Also diff --git a/docs/traits.md b/docs/traits.md index bf6136a..f89fd4c 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -200,12 +200,14 @@ trait Word { } It is used as a bound on builtins that require single-slot operands (for example, syscall and `store*` arguments). Primitive types, enums, struct refs, and array refs satisfy `Word`; multi-slot value types do not. +`Hashable` and `Display` both extend `Word` as supertraits, so any type that satisfies one of them automatically satisfies `Word`. + ## Built-in Trait: `Hashable` -The standard library defines the `Hashable` trait as an extension of `Eq`. Any `Hashable` type therefore also satisfies `Eq` (see [Supertraits](#supertraits)). The trait declares only `hash`; equality is reused from `Eq`: +The standard library defines the `Hashable` trait as an extension of `Eq` and `Word`. Any `Hashable` type therefore also satisfies both `Eq` and `Word` (see [Supertraits](#supertraits)). The trait declares only `hash`; equality is reused from `Eq`: ```casa -trait Hashable: Eq { +trait Hashable: Eq + Word { fn hash self:self -> int } ``` @@ -217,10 +219,10 @@ Built-in implementations: ## Built-in Trait: `Display` -The standard library defines a `Display` trait used by f-string interpolation to convert values to strings: +The standard library defines a `Display` trait used by f-string interpolation to convert values to strings. `Display` extends `Word`, so any displayable type also satisfies `Word`: ```casa -trait Display { +trait Display: Word { fn to_str self:self -> str } ``` @@ -251,16 +253,18 @@ trait Eq { fn eq self:self other:self -> bool } -trait Hashable: Eq { +trait Word { } + +trait Hashable: Eq + Word { fn hash self:self -> int } ``` -A type satisfies `Hashable` only if it satisfies every supertrait (here `Eq`) in addition to providing `Hashable`'s own required methods. Concretely, the type's `impl` block must contain `eq` (from `Eq`) and `hash` (from `Hashable`). +Multiple supertraits are listed with `+`. A type satisfies `Hashable` only if it satisfies every supertrait (here `Eq` and `Word`) in addition to providing `Hashable`'s own required methods. Concretely, the type's `impl` block must contain `eq` (from `Eq`) and `hash` (from `Hashable`); `Word` is a marker with no required methods, so its satisfaction is automatic. Trait-bounded code may call methods declared by any supertrait directly. For example, inside a function bounded by `K: Hashable`, both `K::hash` and `K::eq` resolve correctly. -Supertraits are checked at the trait's declaration site: the supertrait must already be defined. +Supertraits are checked at the trait's declaration site: each supertrait must already be defined. ## Default Methods diff --git a/lib/std.casa b/lib/std.casa index bb3a21e..8deec74 100644 --- a/lib/std.casa +++ b/lib/std.casa @@ -640,16 +640,16 @@ trait Ord { } } -trait Hashable: Eq { +trait Word { } + +trait Hashable: Eq + Word { fn hash self:self -> int } -trait Display { +trait Display: Word { fn to_str self:self -> str } -trait Word { } - trait Iterable[T] { fn next self:self -> Option[T] diff --git a/lsp.casa b/lsp.casa index 8665393..63b7910 100644 --- a/lsp.casa +++ b/lsp.casa @@ -2142,17 +2142,17 @@ fn op_value_stack_effect op_value:OpValue -> Option[str] { OpValue::Load16 => "load16: ptr -> int" Option::Some OpValue::Load32 => "load32: ptr -> int" Option::Some OpValue::Load64 => "load64: ptr -> int" Option::Some - OpValue::Store8 => "store8: any ptr -> None" Option::Some - OpValue::Store16 => "store16: any ptr -> None" Option::Some - OpValue::Store32 => "store32: any ptr -> None" Option::Some - OpValue::Store64 => "store64: any ptr -> None" Option::Some + OpValue::Store8 => "store8[T: Word]: T ptr -> None" Option::Some + OpValue::Store16 => "store16[T: Word]: T ptr -> None" Option::Some + OpValue::Store32 => "store32[T: Word]: T ptr -> None" Option::Some + OpValue::Store64 => "store64[T: Word]: T ptr -> None" Option::Some OpValue::Syscall0 => "syscall0: int -> int" Option::Some - OpValue::Syscall1 => "syscall1: any int -> int" Option::Some - OpValue::Syscall2 => "syscall2: any any int -> int" Option::Some - OpValue::Syscall3 => "syscall3: any any any int -> int" Option::Some - OpValue::Syscall4 => "syscall4: any any any any int -> int" Option::Some - OpValue::Syscall5 => "syscall5: any any any any any int -> int" Option::Some - OpValue::Syscall6 => "syscall6: any any any any any any int -> int" Option::Some + OpValue::Syscall1 => "syscall1[A1: Word]: A1 int -> int" Option::Some + OpValue::Syscall2 => "syscall2[A1: Word A2: Word]: A1 A2 int -> int" Option::Some + OpValue::Syscall3 => "syscall3[A1: Word A2: Word A3: Word]: A1 A2 A3 int -> int" Option::Some + OpValue::Syscall4 => "syscall4[A1: Word A2: Word A3: Word A4: Word]: A1 A2 A3 A4 int -> int" Option::Some + OpValue::Syscall5 => "syscall5[A1: Word A2: Word A3: Word A4: Word A5: Word]: A1 A2 A3 A4 A5 int -> int" Option::Some + OpValue::Syscall6 => "syscall6[A1: Word A2: Word A3: Word A4: Word A5: Word A6: Word]: A1 A2 A3 A4 A5 A6 int -> int" Option::Some OpValue::Argc => "argc: None -> int" Option::Some OpValue::Argv => "argv: None -> ptr" Option::Some _ => Option::None(Option[str]) diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index 55c4980..ad7985e 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -82,7 +82,7 @@ fn tc_check ops:List[Op] -> TypeChecker { helper_kind OpValue::Store64 is || helper_kind OpValue::HeapAlloc is || then - helper_op helper_tc check_memory + Option::None(Option[Function]) helper_op helper_tc check_memory # Print elif helper_kind OpValue::Print is then helper_op helper_tc check_io @@ -103,7 +103,7 @@ fn tc_check ops:List[Op] -> TypeChecker { helper_kind OpValue::Syscall5 is || helper_kind OpValue::Syscall6 is || then - helper_op helper_tc check_syscalls + Option::None(Option[Function]) helper_op helper_tc check_syscalls # Argc / Argv elif helper_kind OpValue::Argc is then "int" helper_tc stack_push @@ -1204,6 +1204,79 @@ fn test_error_syscall3_non_int_number { disable_test_mode } +# ============================================================================ +# Tests: Word-bound on syscall and store builtins +# ============================================================================ + +fn tc_full code:str { + clear_global_state + enable_test_mode + clear_errors + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops drop + type_check_functions +} + +fn test_error_syscall_non_word_bound_type_var { + "error_syscall_non_word_bound_type_var" test_start + "trait Word { } trait NotWord { } fn fail_syscall[T: NotWord] arg:T -> int { arg 1 syscall1 } 42 fail_syscall drop" tc_full + "has errors" assert_has_errors + "mentions Word" "Word" assert_error_contains + clear_errors + disable_test_mode +} + +fn test_syscall_word_bound_explicit_passes { + "syscall_word_bound_explicit_passes" test_start + "trait Word { } fn ok_syscall[T: Word] arg:T -> int { arg 1 syscall1 } 42 ok_syscall drop" tc_full + "no errors" 0 ERRORS.length assert_eq + clear_errors + disable_test_mode +} + +fn test_syscall_unbound_type_var_passes { + "syscall_unbound_type_var_passes" test_start + "trait Word { } fn deferred_syscall[T] arg:T -> int { arg 1 syscall1 } 42 deferred_syscall drop" tc_full + "no errors" 0 ERRORS.length assert_eq + clear_errors + disable_test_mode +} + +fn test_error_store64_non_word_bound_type_var { + "error_store64_non_word_bound_type_var" test_start + "trait Word { } trait NotWord { } fn fail_store[T: NotWord] destination:ptr value:T -> None { value destination store64 } 42 1 alloc fail_store" tc_full + "has errors" assert_has_errors + "mentions Word" "Word" assert_error_contains + clear_errors + disable_test_mode +} + +fn test_store64_word_bound_explicit_passes { + "store64_word_bound_explicit_passes" test_start + "trait Word { } fn ok_store[T: Word] destination:ptr value:T -> None { value destination store64 } 42 1 alloc ok_store" tc_full + "no errors" 0 ERRORS.length assert_eq + clear_errors + disable_test_mode +} + +fn test_store64_unbound_type_var_passes { + "store64_unbound_type_var_passes" test_start + "trait Word { } fn deferred_store[T] destination:ptr value:T -> None { value destination store64 } 42 1 alloc deferred_store" tc_full + "no errors" 0 ERRORS.length assert_eq + clear_errors + disable_test_mode +} + +fn test_store64_supertrait_word_passes { + "store64_supertrait_word_passes" test_start + "trait Word { } trait Inner: Word { } fn super_store[T: Inner] destination:ptr value:T -> None { value destination store64 } 42 1 alloc super_store" tc_full + "no errors" 0 ERRORS.length assert_eq + clear_errors + disable_test_mode +} + # ============================================================================ # Tests: Syscall op-level stack effects # ============================================================================ @@ -1880,6 +1953,13 @@ test_error_store_non_ptr test_error_syscall_non_int_number test_error_syscall1_non_int_number test_error_syscall3_non_int_number +test_error_syscall_non_word_bound_type_var +test_syscall_word_bound_explicit_passes +test_syscall_unbound_type_var_passes +test_error_store64_non_word_bound_type_var +test_store64_word_bound_explicit_passes +test_store64_unbound_type_var_passes +test_store64_supertrait_word_passes test_store_accepts_any_value_type test_memory_store64 test_syscall_any_arg_type From 3fa0d1ef0202644a1b6fd6bf1f6948d335042b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 27 Apr 2026 01:00:18 +0300 Subject: [PATCH 09/15] feat: [US-009] Migrate print to [T: Display] constraint --- compiler/typechecker.casa | 38 ++++++++++++--- docs/intrinsics.md | 6 +-- docs/types-and-literals.md | 12 ++++- examples/dynamic_list.casa | 2 +- examples/enum.casa | 12 +++++ examples/result.casa | 2 +- examples/vec.casa | 2 +- lsp.casa | 12 ++--- tests/compiler/test_display.casa | 73 ++++++++++++++++++++++++++++ tests/compiler/test_enum.casa | 14 +++--- tests/compiler/test_lsp.casa | 2 +- tests/compiler/test_typechecker.casa | 20 +++++--- 12 files changed, 161 insertions(+), 34 deletions(-) diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index e8d62f6..026882c 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -1633,19 +1633,43 @@ fn check_literals tc:TypeChecker op:Op function_opt:Option[Function] { # Check print dispatch # ============================================================================ -fn check_io tc:TypeChecker op:Op { - tc stack_pop = typ +fn check_io tc:TypeChecker op:Op ops:List[Op] op_index:int function_opt:Option[Function] -> int { + tc stack_peek = typ if "str" typ == then + tc stack_pop drop OpValue::PrintStr op Op::set_value - elif "bool" typ == then + op_index return + fi + if "bool" typ == then + tc stack_pop drop OpValue::PrintBool op Op::set_value - elif "char" typ == then + op_index return + fi + if "char" typ == then + tc stack_pop drop OpValue::PrintChar op Op::set_value - elif "cstr" typ == then + op_index return + fi + if "cstr" typ == then + tc stack_pop drop OpValue::PrintCstr op Op::set_value - else + op_index return + fi + if "int" typ == then + tc stack_pop drop OpValue::PrintInt op Op::set_value + op_index return fi + if function_opt "Display" typ type_satisfies_trait ! then + op Op::location f"Type `{typ}` cannot be printed: does not satisfy trait `Display`" ErrorKind::MissingTraitMethod add_error + tc stack_pop drop + op_index return + fi + "to_str" OpValue::MethodCall op Op::set_value + function_opt op_index ops op tc check_method_call = new_index + op Op::location OpValue::Print make_op = print_op + new_index print_op ops.insert + new_index } # ============================================================================ @@ -2529,7 +2553,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct # IO if op_value OpValue::Print is then - current_op checker check_io + function_opt op_index ops current_op checker check_io = op_index continue fi diff --git a/docs/intrinsics.md b/docs/intrinsics.md index 384a4da..eb48e78 100644 --- a/docs/intrinsics.md +++ b/docs/intrinsics.md @@ -40,13 +40,13 @@ See [`examples/stack_operations.casa`](../examples/stack_operations.casa). ### `print` -Prints the top of the stack to stdout. +Prints the top of the stack to stdout. Requires the value's type to implement the `Display` trait. -**Signature:** `print a:any` +**Signature:** `print[T: Display] a:T` **Stack effect:** `a -> None` -Integers print as decimal numbers. Booleans print as `true` or `false`. Strings, characters, and C strings print as text. +The primitives `int`, `bool`, `char`, `str`, and `cstr` already implement `Display` and are emitted through specialized output instructions. Other types must implement `Display` (a `to_str self -> str` method); the compiler lowers `value print` to `value to_str` followed by a string print. ```casa 42 print # 42 diff --git a/docs/types-and-literals.md b/docs/types-and-literals.md index eb333e0..78b5613 100644 --- a/docs/types-and-literals.md +++ b/docs/types-and-literals.md @@ -433,7 +433,17 @@ The `(TypeName)` syntax casts the top of the stack to the given type. This is a buffer (ptr) load64 (int) # cast int -> int (no-op here, but useful for generic data) ``` -This is useful when working with generic data structures that return `any`. +## Printing Values + +`print` requires the value's type to implement the [`Display` trait](traits.md). The primitives `int`, `bool`, `char`, `str`, and `cstr` are dispatched directly to specialized output instructions; user types must provide a `to_str self -> str` method, which the compiler invokes before printing the resulting string. + +```casa +struct Point { x: int y: int } +impl Point { + fn to_str self:Point -> str { f"({self.x}, {self.y})" } +} +1 2 Point print # (1, 2) +``` ## Comments diff --git a/examples/dynamic_list.casa b/examples/dynamic_list.casa index e1de54f..846a98f 100644 --- a/examples/dynamic_list.casa +++ b/examples/dynamic_list.casa @@ -14,7 +14,7 @@ impl List { fn print_items self:List { 0 = i while i self.length > do - i self.get print "\n" print + i self.get (int) print "\n" print 1 += i done } diff --git a/examples/enum.casa b/examples/enum.casa index 3d05c72..abc277f 100644 --- a/examples/enum.casa +++ b/examples/enum.casa @@ -2,6 +2,7 @@ # # Variants are accessed with EnumName::VariantName syntax. # Match provides exhaustive pattern matching. +import "std" enum Color { Red @@ -9,6 +10,17 @@ enum Color { Blue } +# `print` requires Display; provide `to_str` so we can render variants. + +impl Color { + fn to_str c:Color -> str { + c match + Color::Red => "0" + Color::Green => "1" + Color::Blue => "2" + end + } +} # Variant constructors push an enum value onto the stack Color::Green = color # Print outputs the ordinal (0-based) diff --git a/examples/result.casa b/examples/result.casa index 6d27271..55bbed4 100644 --- a/examples/result.casa +++ b/examples/result.casa @@ -40,7 +40,7 @@ if zero_div.is_error then zero_div.unwrap_error print "\n" print fi # Match on result -5 Result::Ok match +5 Result::Ok(Result[int str]) match Result::Ok(value) => value print "\n" print Result::Error(err) => err print "\n" print end diff --git a/examples/vec.casa b/examples/vec.casa index 49b4e76..1c8ab56 100644 --- a/examples/vec.casa +++ b/examples/vec.casa @@ -6,7 +6,7 @@ import "std" # Create empty list and push elements "=== List::new and push ===" print "\n" print -List::new = v +List::new(List[int]) = v 10 v.push 20 v.push 30 v.push diff --git a/lsp.casa b/lsp.casa index 63b7910..7690a79 100644 --- a/lsp.casa +++ b/lsp.casa @@ -2127,12 +2127,12 @@ fn op_value_stack_effect op_value:OpValue -> Option[str] { OpValue::Swap => "swap[T1 T2]: T1 T2 -> T2 T1" Option::Some OpValue::Over => "over[T1 T2]: T1 T2 -> T2 T1 T2" Option::Some OpValue::Rot => "rot[T1 T2 T3]: T1 T2 T3 -> T3 T1 T2" Option::Some - OpValue::Print => "print: any -> None" Option::Some - OpValue::PrintInt => "print: any -> None" Option::Some - OpValue::PrintStr => "print: any -> None" Option::Some - OpValue::PrintBool => "print: any -> None" Option::Some - OpValue::PrintChar => "print: any -> None" Option::Some - OpValue::PrintCstr => "print: any -> None" Option::Some + OpValue::Print => "print: [T: Display] T -> None" Option::Some + OpValue::PrintInt => "print: [T: Display] T -> None" Option::Some + OpValue::PrintStr => "print: [T: Display] T -> None" Option::Some + OpValue::PrintBool => "print: [T: Display] T -> None" Option::Some + OpValue::PrintChar => "print: [T: Display] T -> None" Option::Some + OpValue::PrintCstr => "print: [T: Display] T -> None" Option::Some OpValue::FnExec => "exec: fn[sig] -> ..." Option::Some OpValue::Typeof => "typeof: [T] T -> str" Option::Some OpValue::AssignDec(_) => "-=: int -> None" Option::Some diff --git a/tests/compiler/test_display.casa b/tests/compiler/test_display.casa index 658a94b..a502902 100644 --- a/tests/compiler/test_display.casa +++ b/tests/compiler/test_display.casa @@ -35,6 +35,15 @@ fn display_test_typecheck code:str -> Signature { Option::None(Option[Function]) ops type_check_ops } +fn display_test_typecheck_ops code:str -> List[Op] { + clear_global_state + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops drop + ops +} + fn display_test_typecheck_error code:str -> Signature { enable_test_mode clear_errors @@ -216,6 +225,65 @@ fn test_fstring_mixed_types { "no errors for mixed primitive f-string" assert_no_errors } +# ============================================================================ +# Typechecker: print routes primitives to existing specialized ops +# ============================================================================ + +fn test_print_int_dispatches_to_print_int { + "print_int_dispatches_to_print_int" test_start + "42 print\n" display_test_typecheck_ops = ops + "no errors for int print" assert_no_errors + "int print resolves to PrintInt" 1 ops.get.value OpValue::PrintInt is assert_true +} + +fn test_print_str_dispatches_to_print_str { + "print_str_dispatches_to_print_str" test_start + "\"hi\" print\n" display_test_typecheck_ops = ops + "no errors for str print" assert_no_errors + "str print resolves to PrintStr" 1 ops.get.value OpValue::PrintStr is assert_true +} + +# ============================================================================ +# Typechecker: print on user struct with Display lowers to a method call +# ============================================================================ + +fn test_print_struct_with_display_lowers_to_method_call { + "print_struct_with_display_lowers_to_method_call" test_start + DISPLAY_TRAIT "struct Tag { val: int }\nimpl Tag { fn to_str self:Tag -> str { \"tag\" } }\n1 Tag = t\nt print\n" str::concat display_test_typecheck_ops = ops + "no errors for Display struct print" assert_no_errors + 0 = lower_index + 0 = fn_call_count + 0 = print_str_count + while lower_index ops.length > do + lower_index ops.get.value = lower_value + if lower_value OpValue::FnCall(callee) is then + if "Tag::to_str" callee == then + 1 += fn_call_count + fi + fi + if lower_value OpValue::PrintStr is then + 1 += print_str_count + fi + 1 += lower_index + done + "exactly one Tag::to_str call" 1 fn_call_count assert_eq + "exactly one PrintStr after lowering" 1 print_str_count assert_eq +} + +# ============================================================================ +# Typechecker: print on user struct without Display raises a clear error +# ============================================================================ + +fn test_print_struct_without_display_raises { + "print_struct_without_display_raises" test_start + DISPLAY_TRAIT "struct NoDisplay { val: int }\n1 NoDisplay = n\nn print\n" str::concat display_test_typecheck_error drop + "has errors" assert_has_errors + "error mentions Display trait" "Display" assert_error_contains + "missing trait method error kind" ErrorKind::MissingTraitMethod assert_error_kind + clear_errors + disable_test_mode +} + # ============================================================================ # Run all tests # ============================================================================ @@ -239,4 +307,9 @@ test_type_satisfies_trait_no_fallback_method test_trait_exec_pushes_return_type # Multi-type-parameter trait bound ordering test_multi_trait_bound_ordering +# print dispatch +test_print_int_dispatches_to_print_int +test_print_str_dispatches_to_print_str +test_print_struct_with_display_lowers_to_method_call +test_print_struct_without_display_raises test_summary diff --git a/tests/compiler/test_enum.casa b/tests/compiler/test_enum.casa index 2096f02..dfd54a8 100644 --- a/tests/compiler/test_enum.casa +++ b/tests/compiler/test_enum.casa @@ -94,6 +94,8 @@ fn is_jump_ne_inst value:InstValue -> bool { value InstValue::JumpNe is } fn is_print_int_inst value:InstValue -> bool { value InstValue::PrintInt is } +fn is_print_str_inst value:InstValue -> bool { value InstValue::PrintStr is } + fn is_heap_alloc_inst value:InstValue -> bool { value InstValue::HeapAlloc is } fn is_store64_inst value:InstValue -> bool { value InstValue::Store64 is } @@ -381,7 +383,7 @@ fn test_enum_as_return_type { fn test_enum_print { "enum_print" test_start - "enum Color { Red Green Blue }\nColor::Red print" tc_enum = sig + "trait Display { fn to_str self:self -> str }\nenum Color { Red Green Blue }\nimpl Color { fn to_str c:Color -> str { \"red\" } }\nColor::Red print" tc_enum = sig "no returns" 0 sig Signature::return_types.length assert_eq } @@ -511,10 +513,10 @@ fn test_match_compiles_to_dup_eq_jump_pattern { "has JUMP_NE" 0 &is_jump_ne_inst prog Program::bytecode count_insts != assert_true } -fn test_enum_print_compiles_to_print_int { - "enum_print_compiles_to_print_int" test_start - "enum Color { Red Green Blue }\nColor::Red print\n" compile_enum = prog - "has PRINT_INT" &is_print_int_inst prog Program::bytecode has_inst_kind assert_true +fn test_enum_print_compiles_via_display { + "enum_print_compiles_via_display" test_start + "trait Display { fn to_str self:self -> str }\nenum Color { Red Green Blue }\nimpl Color { fn to_str c:Color -> str { \"red\" } }\nColor::Red print\n" compile_enum = prog + "has PRINT_STR" &is_print_str_inst prog Program::bytecode has_inst_kind assert_true } fn test_data_enum_variant_compiles_to_heap_alloc { @@ -1122,7 +1124,7 @@ test_enum_variant_compiles_to_push_ordinal_0 test_enum_variant_compiles_to_push_ordinal_1 test_enum_variant_compiles_to_push_ordinal_2 test_match_compiles_to_dup_eq_jump_pattern -test_enum_print_compiles_to_print_int +test_enum_print_compiles_via_display test_data_enum_variant_compiles_to_heap_alloc test_data_enum_stores_ordinal # Match parsing diff --git a/tests/compiler/test_lsp.casa b/tests/compiler/test_lsp.casa index 1ca2904..f0e6e40 100644 --- a/tests/compiler/test_lsp.casa +++ b/tests/compiler/test_lsp.casa @@ -621,7 +621,7 @@ fn test_compute_hover_print { "42 print" lsp_test_state = state 3 0 state compute_hover = result "is some" result.is_some assert_true - "contains print effect" 0 result.unwrap HoverContent::content "print: any -> None" str::find >= assert_true + "contains print effect" 0 result.unwrap HoverContent::content "print: [T: Display] T -> None" str::find >= assert_true } fn test_compute_hover_exec { diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index ad7985e..b5dac63 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -85,7 +85,9 @@ fn tc_check ops:List[Op] -> TypeChecker { Option::None(Option[Function]) helper_op helper_tc check_memory # Print elif helper_kind OpValue::Print is then - helper_op helper_tc check_io + 1 += helper_i + Option::None(Option[Function]) helper_i ops helper_op helper_tc check_io = helper_i + continue # Typeof elif helper_kind OpValue::Typeof is then helper_op helper_tc check_typeof @@ -1582,18 +1584,22 @@ fn test_types_match_array_types { } # ============================================================================ -# Tests: Print dispatch for ptr (falls to print_int) +# Tests: Print dispatch on a non-Display type adds an error # ============================================================================ -fn test_print_dispatch_ptr { - "print_dispatch_ptr" test_start +fn test_print_dispatch_no_display { + "print_dispatch_no_display" test_start + enable_test_mode + clear_errors List::new(List[Op]) = ops empty_location 10 OpValue::PushInt make_op ops.push empty_location OpValue::HeapAlloc make_op ops.push empty_location OpValue::Print make_op ops.push ops tc_check drop - # ptr falls through to print_int - "print ptr resolved" 2 ops.get.value OpValue::PrintInt is assert_true + "ptr without Display impl raises an error" assert_has_errors + "error mentions Display trait" "Display" assert_error_contains + clear_errors + disable_test_mode } # ============================================================================ @@ -1986,7 +1992,7 @@ test_tc_pipeline_print_pops test_stacks_compatible_different_types test_types_match_fn_types test_types_match_array_types -test_print_dispatch_ptr +test_print_dispatch_no_display test_typeof_annotation test_type_cast_str_to_int test_type_cast_bool_to_int From 43ebb38ae65eb80759f12deffb94b8c252dc17e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 27 Apr 2026 01:19:04 +0300 Subject: [PATCH 10/15] feat: [US-010] Migrate ==/!= to [T: Eq] and comparison ops to [T: Ord] --- compiler/typechecker.casa | 95 +++++++++++++++--- docs/operators.md | 16 +-- docs/traits.md | 4 + lsp.casa | 12 +-- tests/compiler/test_compare.casa | 142 +++++++++++++++++++++++++++ tests/compiler/test_lsp.casa | 2 +- tests/compiler/test_typechecker.casa | 4 +- 7 files changed, 245 insertions(+), 30 deletions(-) create mode 100644 tests/compiler/test_compare.casa diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 026882c..1a43ce6 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -684,38 +684,103 @@ fn check_arithmetic tc:TypeChecker op:Op { # ============================================================================ # Check comparison operations # ============================================================================ +# Primitive types whose `==`/`<` etc. are emitted as direct bytecode rather +# than dispatched through Eq/Ord trait methods. `str` is handled separately +# because it only supports `==`/`!=` and carries a type annotation. -fn check_comparison tc:TypeChecker op:Op { +fn is_primitive_compare_type typ:str -> bool { + "int" typ == + "bool" typ == || + "char" typ == || + "cstr" typ == || + "ptr" typ == || +} + +fn comparison_method_name cmp_value:OpValue -> str { + if cmp_value OpValue::Eq is then "eq" return fi + if cmp_value OpValue::Ne is then "ne" return fi + if cmp_value OpValue::Lt is then "lt" return fi + if cmp_value OpValue::Le is then "le" return fi + if cmp_value OpValue::Gt is then "gt" return fi + "ge" +} + +fn comparison_op_str cmp_value:OpValue -> str { + if cmp_value OpValue::Eq is then "==" return fi + if cmp_value OpValue::Ne is then "!=" return fi + if cmp_value OpValue::Lt is then "<" return fi + if cmp_value OpValue::Le is then "<=" return fi + if cmp_value OpValue::Gt is then ">" return fi + ">=" +} + +fn check_comparison + tc:TypeChecker + op:Op + ops:List[Op] + op_index:int + function_opt:Option[Function] +-> int { + op.value = cmp_value + cmp_value OpValue::Eq is cmp_value OpValue::Ne is || = is_eq_op + if is_eq_op then "Eq" else "Ord" fi = trait_name tc stack_pop = type1 tc stack_pop = type2 + # Stack-underflow placeholder on either side: defer to caller-level + # inference (lambda param synthesis, generic body deferral). Skip both + # the same-type rule and the trait-bound check. + if TYPE_UNKNOWN type1 == TYPE_UNKNOWN type2 == || then + "bool" tc stack_push + op_index return + fi type1 resolve_match_type = base1 type2 resolve_match_type = base2 base1 tc.store.enums.get.is_some = is_enum1 base2 tc.store.enums.get.is_some = is_enum2 + # Same-type rule applies before any specialization. + if type1 type2 != then + f"Cannot compare `{type2}` with `{type1}`" tc raise_type_mismatch + "bool" tc stack_push + op_index return + fi + # Enums: tag comparison via bytecode; data-carrying variants need annotation. if is_enum1 is_enum2 || then - if type1 type2 != then - f"Cannot compare `{type2}` with `{type1}`" tc raise_type_mismatch - fi - # Annotate for bytecode: data-carrying enums need heap tag comparison if is_enum1 then base1 tc.store.enums.get.unwrap = casa_enum if casa_enum has_inner_values then base1 op Op::set_type_annotation fi fi - elif "str" type1 == "str" type2 == || then - if type1 type2 != then - f"Cannot compare `{type2}` with `{type1}`" tc raise_type_mismatch - fi - if - op.value OpValue::Eq is ! - op.value OpValue::Ne is ! && - then + "bool" tc stack_push + op_index return + fi + # str: only ==/!=, annotated for the bytecode emitter. + if "str" type1 == then + if is_eq_op ! then "Type `str` only supports `==` and `!=` comparison" tc raise_type_mismatch fi "str" op Op::set_type_annotation + "bool" tc stack_push + op_index return fi - "bool" tc stack_push + # Other primitives: bytecode handles the comparison directly. + if type1 is_primitive_compare_type then + "bool" tc stack_push + op_index return + fi + # User-defined types must satisfy Eq (or Ord) explicitly. + if function_opt trait_name type1 type_satisfies_trait ! then + cmp_value comparison_op_str = cmp_str + op Op::location f"Type `{type1}` cannot be compared with `{cmp_str}`: does not satisfy trait `{trait_name}`" ErrorKind::MissingTraitMethod add_error + "bool" tc stack_push + op_index return + fi + # Lower the operator to the corresponding trait method call. + cmp_value comparison_method_name = method_name + type2 tc stack_push + type1 tc stack_push + method_name OpValue::MethodCall op Op::set_value + function_opt op_index ops op tc check_method_call } # ============================================================================ @@ -2429,7 +2494,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct op_value OpValue::Gt is || op_value OpValue::Ge is || then - current_op checker check_comparison + function_opt op_index ops current_op checker check_comparison = op_index continue fi diff --git a/docs/operators.md b/docs/operators.md index 748162c..b117029 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -83,16 +83,18 @@ buf (ptr) 8 + load64 print # 42 ## Comparison -All comparison operators consume two values and push a `bool`. +All comparison operators consume two values of the same type and push a `bool`. Equality operators require the operand type to satisfy the `Eq` trait; ordering operators require `Ord`. | Operator | Stack Effect | Description | |----------|-------------|-------------| -| `==` | `any any -> bool` | Equal | -| `!=` | `any any -> bool` | Not equal | -| `<` | `any any -> bool` | Less than | -| `<=` | `any any -> bool` | Less than or equal | -| `>` | `any any -> bool` | Greater than | -| `>=` | `any any -> bool` | Greater than or equal | +| `==` | `[T: Eq] T T -> bool` | Equal | +| `!=` | `[T: Eq] T T -> bool` | Not equal | +| `<` | `[T: Ord] T T -> bool` | Less than | +| `<=` | `[T: Ord] T T -> bool` | Less than or equal | +| `>` | `[T: Ord] T T -> bool` | Greater than | +| `>=` | `[T: Ord] T T -> bool` | Greater than or equal | + +Built-in primitives (`int`, `bool`, `char`, `cstr`, `ptr`) and enums get direct bytecode comparison. User-defined types must provide `impl T { fn eq ... }` (and `fn lt ...` for ordering); the operator then lowers to the corresponding trait method call. See [traits.md](traits.md) for `Eq` and `Ord`. ```casa 1 1 == print # 1 (true) diff --git a/docs/traits.md b/docs/traits.md index f89fd4c..5dd3454 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -175,6 +175,8 @@ Built-in implementations: `int`, `bool`, `char`, `str`, `cstr`, `ptr`. A type satisfies `Eq` by providing `Type::eq self:Type other:Type -> bool`. The `ne` default is auto-instantiated for any satisfying type, so `x.ne y` works without writing it. +The `==` and `!=` operators are bounded by `Eq`. Built-in primitives and enums get direct bytecode comparison, but a user-defined struct used with `==` must provide `impl T { fn eq ... }`; the operator then lowers to `T::eq`. Comparing values whose type does not satisfy `Eq` is a compile-time error. + ## Built-in Trait: `Ord` Total ordering. The required method is `lt`; the defaults `le`, `gt`, and `ge` are derived from it. @@ -190,6 +192,8 @@ trait Ord { Built-in implementations: `int`, `char`. Lexicographic ordering for `str` is intentionally out of scope. +The `<`, `<=`, `>`, and `>=` operators are bounded by `Ord`. Built-in primitives (excluding `str`) and enums use direct bytecode ordering; user-defined types must provide `impl T { fn lt ... }` and the operator lowers to the corresponding trait method (`lt`, `le`, `gt`, `ge`). A type with `impl Eq` but no `impl Ord` is rejected at compile time when used with an ordering operator. + ## Built-in Trait: `Word` Marker trait for register-sized values that fit in one stack slot. It declares no methods: diff --git a/lsp.casa b/lsp.casa index 7690a79..685ff24 100644 --- a/lsp.casa +++ b/lsp.casa @@ -2114,12 +2114,12 @@ fn op_value_stack_effect op_value:OpValue -> Option[str] { OpValue::BitXor => "^: int int -> int" Option::Some OpValue::BitNot => "~: int -> int" Option::Some OpValue::Not => "!: bool -> bool" Option::Some - OpValue::Eq => "==: any any -> bool" Option::Some - OpValue::Ne => "!=: any any -> bool" Option::Some - OpValue::Lt => "<: any any -> bool" Option::Some - OpValue::Le => "<=: any any -> bool" Option::Some - OpValue::Gt => ">: any any -> bool" Option::Some - OpValue::Ge => ">=: any any -> bool" Option::Some + OpValue::Eq => "==: [T: Eq] T T -> bool" Option::Some + OpValue::Ne => "!=: [T: Eq] T T -> bool" Option::Some + OpValue::Lt => "<: [T: Ord] T T -> bool" Option::Some + OpValue::Le => "<=: [T: Ord] T T -> bool" Option::Some + OpValue::Gt => ">: [T: Ord] T T -> bool" Option::Some + OpValue::Ge => ">=: [T: Ord] T T -> bool" Option::Some OpValue::And => "&&: bool bool -> bool" Option::Some OpValue::Or => "||: bool bool -> bool" Option::Some OpValue::Drop => "drop[T]: T -> None" Option::Some diff --git a/tests/compiler/test_compare.casa b/tests/compiler/test_compare.casa new file mode 100644 index 0000000..b937bc7 --- /dev/null +++ b/tests/compiler/test_compare.casa @@ -0,0 +1,142 @@ +import "../../compiler/common.casa" +import "../../compiler/error.casa" +import "../../compiler/lexer.casa" +import "../../compiler/syntax.casa" +import "../../compiler/typechecker.casa" +import "../../lib/test.casa" + +# ============================================================================ +# Constants +# ============================================================================ +"trait Eq {\n fn eq self:self other:self -> bool\n\n fn ne self:self other:self -> bool {\n other self.eq !\n }\n}\n\ntrait Ord {\n fn lt self:self other:self -> bool\n\n fn le self:self other:self -> bool {\n self other.lt !\n }\n\n fn gt self:self other:self -> bool {\n self other.lt\n }\n\n fn ge self:self other:self -> bool {\n other self.lt !\n }\n}\n" = EQ_ORD_TRAITS + +# ============================================================================ +# Helpers +# ============================================================================ + +fn compare_test_typecheck code:str -> Signature { + clear_global_state + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops +} + +fn compare_test_typecheck_ops code:str -> List[Op] { + clear_global_state + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops drop + ops +} + +fn compare_test_typecheck_error code:str -> Signature { + enable_test_mode + clear_errors + clear_global_state + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops +} + +# ============================================================================ +# Primitives: bytecode equality keeps working without trait declarations +# ============================================================================ + +fn test_eq_primitive_int_passes { + "eq_primitive_int_passes" test_start + "1 2 == drop" compare_test_typecheck drop + "no errors for int ==" assert_no_errors +} + +fn test_lt_primitive_int_passes { + "lt_primitive_int_passes" test_start + "1 2 < drop" compare_test_typecheck drop + "no errors for int <" assert_no_errors +} + +# ============================================================================ +# Enums: tag comparison keeps working +# ============================================================================ + +fn test_enum_with_eq_compares { + "enum_with_eq_compares" test_start + EQ_ORD_TRAITS "enum Color { Red Green Blue }\nimpl Color { fn eq self:Color other:Color -> bool { self other == } }\nColor::Red Color::Green == drop\n" str::concat compare_test_typecheck drop + "no errors for enum with impl Eq ==" assert_no_errors +} + +# ============================================================================ +# User struct: == lowers to MethodCall when impl Eq exists +# ============================================================================ + +fn test_user_struct_with_eq_lowers_to_method_call { + "user_struct_with_eq_lowers_to_method_call" test_start + EQ_ORD_TRAITS "struct Tag { val: int }\nimpl Tag { fn eq self:Tag other:Tag -> bool { self.val other.val == } }\n1 Tag = a\n2 Tag = b\na b == drop\n" str::concat compare_test_typecheck_ops = ops + "no errors for struct with impl Eq" assert_no_errors + 0 = i + 0 = fn_call_count + while i ops.length > do + if i ops.get.value OpValue::FnCall(callee) is then + if "Tag::eq" callee == then + 1 += fn_call_count + fi + fi + 1 += i + done + "exactly one Tag::eq call" 1 fn_call_count assert_eq +} + +# ============================================================================ +# User struct without impl Eq: == fails with Eq-bound error +# ============================================================================ + +fn test_user_struct_without_eq_fails { + "user_struct_without_eq_fails" test_start + EQ_ORD_TRAITS "struct NoEq { val: int }\n1 NoEq = a\n2 NoEq = b\na b == drop\n" str::concat compare_test_typecheck_error drop + "has errors" assert_has_errors + "error mentions Eq trait" "Eq" assert_error_contains + "missing trait method error kind" ErrorKind::MissingTraitMethod assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# User struct with impl Eq but no impl Ord: < fails with Ord-bound error +# ============================================================================ + +fn test_user_struct_with_eq_but_no_ord_fails_lt { + "user_struct_with_eq_but_no_ord_fails_lt" test_start + EQ_ORD_TRAITS "struct Tag { val: int }\nimpl Tag { fn eq self:Tag other:Tag -> bool { self.val other.val == } }\n1 Tag = a\n2 Tag = b\na b < drop\n" str::concat compare_test_typecheck_error drop + "has errors" assert_has_errors + "error mentions Ord trait" "Ord" assert_error_contains + "missing trait method error kind" ErrorKind::MissingTraitMethod assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Same-type rule preserved: comparing different types still fails +# ============================================================================ + +fn test_eq_different_types_fails { + "eq_different_types_fails" test_start + "42 \"hello\" == drop" compare_test_typecheck_error drop + "has errors" assert_has_errors + "type mismatch error kind" ErrorKind::TypeMismatch assert_error_kind + clear_errors + disable_test_mode +} + +# ============================================================================ +# Run all tests +# ============================================================================ +test_eq_primitive_int_passes +test_lt_primitive_int_passes +test_enum_with_eq_compares +test_user_struct_with_eq_lowers_to_method_call +test_user_struct_without_eq_fails +test_user_struct_with_eq_but_no_ord_fails_lt +test_eq_different_types_fails +test_summary diff --git a/tests/compiler/test_lsp.casa b/tests/compiler/test_lsp.casa index f0e6e40..bb6adbb 100644 --- a/tests/compiler/test_lsp.casa +++ b/tests/compiler/test_lsp.casa @@ -613,7 +613,7 @@ fn test_compute_hover_eq { "1 2 == drop" lsp_test_state = state 4 0 state compute_hover = result "is some" result.is_some assert_true - "contains eq effect" 0 result.unwrap HoverContent::content "==: any any -> bool" str::find >= assert_true + "contains eq effect" 0 result.unwrap HoverContent::content "==: [T: Eq] T T -> bool" str::find >= assert_true } fn test_compute_hover_print { diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index b5dac63..a3effac 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -43,7 +43,9 @@ fn tc_check ops:List[Op] -> TypeChecker { helper_kind OpValue::Gt is || helper_kind OpValue::Ge is || then - helper_op helper_tc check_comparison + 1 += helper_i + Option::None(Option[Function]) helper_i ops helper_op helper_tc check_comparison = helper_i + continue # Boolean elif helper_kind OpValue::And is From 5aa702971aa99cbf2fceead8eb370b32d1d88199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Mon, 27 Apr 2026 09:07:47 +0300 Subject: [PATCH 11/15] feat: [US-011] Delete `any` keyword, ANY_TYPE alias, and method-dispatch fallback - parse_type now raises a Syntax error for bare `any`; the redundant guard in get_op_type_cast is gone (parse_type covers casts too). - Removed the ANY_TYPE constant alias and the `any` entries in is_builtin_type and the builtins set. - Removed find_method_by_name and the TYPE_UNKNOWN-receiver fallback in check_method_call. Method dispatch on an unresolved type now produces a real "method not found" error. - Removed the TYPE_UNKNOWN early-return in is_type_variable; the two call sites already guard against the sentinel. - Replaced dot-method lambdas `{ .is_alpha }` etc. in lib/parser.casa and examples/parser.casa with explicit function references like `&char::is_alpha`, since those lambdas relied on the removed dispatch fallback. - Test assertions and headers swapped from the literal "any" string to the TYPE_UNKNOWN constant. - Docs pruned of `any` references; `fn[any -> str]` rewritten as `fn[T -> str]`, `drop: any -> None` as `drop: [T] T -> None`, etc. Closes #158, #147. --- compiler/common.casa | 5 +-- compiler/syntax.casa | 11 ++++--- compiler/typechecker.casa | 47 ---------------------------- docs/control-flow.md | 2 +- docs/errors.md | 6 ++-- docs/functions-and-lambdas.md | 2 +- docs/language-server.md | 2 +- docs/operators.md | 6 ++-- docs/types-and-literals.md | 30 ++++++------------ examples/parser.casa | 6 ++-- examples/type_annotations.casa | 2 +- lib/parser.casa | 4 +-- tests/compiler/test_common.casa | 4 +-- tests/compiler/test_typechecker.casa | 24 +++++++------- 14 files changed, 46 insertions(+), 105 deletions(-) diff --git a/compiler/common.casa b/compiler/common.casa index 79ac5b0..af0659a 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -832,8 +832,7 @@ struct Program { # Constants # ============================================================================ "_start" = GLOBAL_SCOPE_LABEL -"any" = TYPE_UNKNOWN -"any" = ANY_TYPE +"?" = TYPE_UNKNOWN "_" = MATCH_WILDCARD "self" = TRAIT_SELF_TYPE 8 = WORD_SIZE @@ -968,7 +967,6 @@ fn is_builtin_type typ:str -> bool { "str" typ == || "ptr" typ == || "array" typ == || - "any" typ == || } # ============================================================================ @@ -1495,7 +1493,6 @@ fn symbol_store_new -> SymbolStore { "cstr" builtins.add = builtins "ptr" builtins.add = builtins "array" builtins.add = builtins - "any" builtins.add = builtins builtins Map::new(Map[str CasaTrait]) diff --git a/compiler/syntax.casa b/compiler/syntax.casa index 035d59d..574baf3 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -192,7 +192,7 @@ fn parse_type cursor:TokenCursor -> str { cursor.peek = peek_opt if peek_opt.is_none then empty_location "Expected type" ErrorKind::UnexpectedToken raise_error - "any" return + "" return fi peek_opt.unwrap = peek @@ -208,6 +208,11 @@ fn parse_type cursor:TokenCursor -> str { base_token Token::value = base fi + if "any" base == then + peek Token::location "type `any` does not exist; use a concrete type or a generic parameter" ErrorKind::Syntax raise_error + "" return + fi + # Check for parameterized type cursor.peek = next_opt if next_opt.is_none then base return fi @@ -286,10 +291,6 @@ fn get_op_type_cast cursor:TokenCursor -> Op { close_offset open_offset - 1 + = length length open_offset open_token Token::location Location::file make_location = loc - if "any" cast_type == then - loc "type `any` does not exist; use a concrete type or remove the cast" ErrorKind::Syntax raise_error - fi - loc cast_type OpValue::TypeCast make_op } diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 1a43ce6..bd04ca2 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -262,7 +262,6 @@ fn types_match expected:str actual:str -> bool { fn is_type_variable typ:str -> bool { # Check if the type is a type variable (single uppercase letter or # registered as a type var in a generic enum/struct) - if TYPE_UNKNOWN typ == then true return fi if typ.length 0 == then false return fi # Single uppercase letter is a type variable if typ.length 1 == then @@ -2093,41 +2092,6 @@ fn check_struct_literal tc:TypeChecker op:Op { name tc stack_push } -# ============================================================================ -# Find method by name (for any-typed receivers) -# ============================================================================ - -fn find_method_by_name method_name:str -> Option[str] { - f"::{method_name}" = suffix - DEFAULT_PARSER.store.functions.keys = keys - "" = found - "" = found_with_traits - for key in keys.iter do - if key suffix str::ends_with then - if key "lambda__" str::starts_with ! then - key DEFAULT_PARSER.store.functions.get.unwrap = function - if 0 function Function::signature Signature::trait_bounds.keys.length == then - if "" found == then - key = found - fi - else - if "" found_with_traits == then - key = found_with_traits - fi - fi - fi - fi - done - # Prefer non-trait-bounded matches - if "" found != then - found Option::Some - elif "" found_with_traits != then - found_with_traits Option::Some - else - Option::None - fi -} - # ============================================================================ # Instantiate default trait method for a concrete type # ============================================================================ @@ -2382,17 +2346,6 @@ fn check_method_call fi fi - # Try any-typed receiver: search all methods by name - if fn_opt.is_none then - if TYPE_UNKNOWN receiver == then - method_name find_method_by_name = any_match_opt - if any_match_opt.is_some then - any_match_opt.unwrap = fn_name - fn_name tc.store.functions.get = fn_opt - fi - fi - fi - # Try instantiating a default trait method if fn_opt.is_none then receiver method_name function_opt instantiate_default_trait_method = default_inst_opt diff --git a/docs/control-flow.md b/docs/control-flow.md index b6ce2ad..9f3714b 100644 --- a/docs/control-flow.md +++ b/docs/control-flow.md @@ -80,7 +80,7 @@ fi # result type: Option[int] ``` -The same applies to bare `array` and `array[T]`, and to `any` with any concrete type. +The same applies to bare `array` and `array[T]`. If there is no `else` branch, the `if`/`elif` branches must not change the stack at all (since the "no match" path leaves the stack unchanged). diff --git a/docs/errors.md b/docs/errors.md index e057a3a..567667d 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -157,12 +157,12 @@ error[STACK_MISMATCH]: Branches have incompatible stack effects | 27 | fi | ^^ - Note: `if` branch has signature `any -> any int int` + Note: `if` branch has signature `? -> ? int int` --> examples/multi_error.casa:23:5 | 23 | if dup then | ^^ - Note: `else` branch has signature `any -> any int` + Note: `else` branch has signature `? -> ? int` --> examples/multi_error.casa:25:5 | 25 | else @@ -181,7 +181,7 @@ bad ``` error[SIGNATURE_MISMATCH]: Invalid signature for function `bad` Expected: int -> str - Inferred: any -> int + Inferred: ? -> int ``` ### `INVALID_VARIABLE` diff --git a/docs/functions-and-lambdas.md b/docs/functions-and-lambdas.md index cae297c..9dbd5bc 100644 --- a/docs/functions-and-lambdas.md +++ b/docs/functions-and-lambdas.md @@ -190,7 +190,7 @@ Multiple type variables are separated by commas. Variables without a `:` have no Every type variable must appear in at least one parameter (return-only type variables are not allowed). -Type variable names must not collide with built-in types (`int`, `bool`, `char`, `cstr`, `str`, `ptr`, `array`, `any`) or user-defined struct names: +Type variable names must not collide with built-in types (`int`, `bool`, `char`, `cstr`, `str`, `ptr`, `array`) or user-defined struct names: ```casa fn bad[int] int -> int { } # ERROR: shadows built-in type diff --git a/docs/language-server.md b/docs/language-server.md index 7c739be..6f823f9 100644 --- a/docs/language-server.md +++ b/docs/language-server.md @@ -78,7 +78,7 @@ Hover over a symbol to see type and signature information in a code block. | Integer, string, bool, or char literal | Type and value (e.g. `(int) 42`) | | Assignment | `= name: type` (with inferred type) | | Operator | Name and stack effect (e.g. `+: int int -> int`) | -| Intrinsic | Name and stack effect (e.g. `drop: any -> None`) | +| Intrinsic | Name and stack effect (e.g. `drop: [T] T -> None`) | All operators and intrinsics show their stack effects on hover, including arithmetic, comparison, boolean, bitwise, stack manipulation, memory, syscall, and IO operations. diff --git a/docs/operators.md b/docs/operators.md index b117029..39b65fb 100644 --- a/docs/operators.md +++ b/docs/operators.md @@ -117,9 +117,9 @@ String `==` and `!=` compare by content (byte-by-byte), not by pointer identity. | Operator | Stack Effect | Description | |----------|-------------|-------------| -| `&&` | `any any -> bool` | Logical AND | -| `\|\|` | `any any -> bool` | Logical OR | -| `!` | `any -> bool` | Logical NOT | +| `&&` | `bool bool -> bool` | Logical AND | +| `\|\|` | `bool bool -> bool` | Logical OR | +| `!` | `bool -> bool` | Logical NOT | ```casa true true && print # 1 diff --git a/docs/types-and-literals.md b/docs/types-and-literals.md index 78b5613..d64c4ff 100644 --- a/docs/types-and-literals.md +++ b/docs/types-and-literals.md @@ -233,7 +233,7 @@ Function type representing a lambda or function reference. The signature inside ```casa { 2 * } # type: fn[int -> int] { 1 + } # type: fn[int -> int] -{ drop "hi" } # type: fn[any -> str] +{ drop "hi" } # type: fn[T -> str] ``` Call a function value with `exec`: @@ -315,17 +315,17 @@ Result type representing either a success value (`Result::Ok`) or an error value enum Result[T E] { Error(E) Ok(T) } ``` -`Result::Ok` wraps the top-of-stack value into a result. The resulting type is `Result[T any]` where `T` is the type of the wrapped value. +`Result::Ok` wraps the top-of-stack value into a result. The resulting type is `Result[T E]` where `T` is the type of the wrapped value and `E` remains an unbound type variable until constrained by a use site or annotation. -**Stack effect:** `T -> Result[T any]` +**Stack effect:** `T -> Result[T E]` -`Result::Error` wraps the top-of-stack value into an error result. The resulting type is `Result[any E]` where `E` is the type of the error value. +`Result::Error` wraps the top-of-stack value into an error result. The resulting type is `Result[T E]` where `E` is the type of the error value and `T` remains unbound. -**Stack effect:** `E -> Result[any E]` +**Stack effect:** `E -> Result[T E]` ```casa -42 Result::Ok # type: Result[int any] -"not found" Result::Error # type: Result[any str] +42 Result::Ok # type: Result[int E] +"not found" Result::Error # type: Result[T str] ``` At runtime, a result is heap-allocated as 16 bytes: `[tag, value]` where each field is 8 bytes. The tag is `1` for `Ok` and `0` for `Error`. @@ -333,8 +333,8 @@ At runtime, a result is heap-allocated as 16 bytes: `[tag, value]` where each fi Results stored in variables retain their type: ```casa -42 Result::Ok = x # x has type Result[int any] -"not found" Result::Error = y # y has type Result[any str] +42 Result::Ok = x # x has type Result[int E] +"not found" Result::Error = y # y has type Result[T str] ``` Type annotations can narrow the type to specify both type parameters: @@ -347,7 +347,7 @@ A bare `Result` type matches any `Result[T E]` in function signatures, similar t ```casa fn check res:Result -> bool { true } -42 Result::Ok check # works: Result[int any] matches bare Result +42 Result::Ok check # works: Result[int E] matches bare Result ``` `Result::Ok` and `Result::Error` can appear in different branches of a conditional. The type checker unifies them to the more specific `Result[T E]`: @@ -413,16 +413,6 @@ fn hash_key[K: Hashable] key:K -> int { Type variables are purely compile-time, including trait bounds. See [Functions and Lambdas — Generic Functions](functions-and-lambdas.md#generic-functions) and [Traits](traits.md) for details. -## The `any` Type - -`any` is a special wildcard type that matches any other type. It is used as an escape hatch when the type system cannot determine a precise type. - -```casa -32 alloc = buffer -42 buffer (ptr) store64 -buffer (ptr) load64 # type: int -``` - ## Type Casting The `(TypeName)` syntax casts the top of the stack to the given type. This is a compile-time annotation only — no runtime check is performed. diff --git a/examples/parser.casa b/examples/parser.casa index d4d9331..e07d688 100644 --- a/examples/parser.casa +++ b/examples/parser.casa @@ -12,8 +12,8 @@ cursor.advance drop cursor.peek.unwrap print "\n" print # take_while: collect matching characters "abc123" Cursor::new = tw_cursor -{ .is_alpha } tw_cursor.take_while = letters -{ .is_digit } tw_cursor.take_while = digits +&char::is_alpha tw_cursor.take_while = letters +&char::is_digit tw_cursor.take_while = digits "letters: " print letters print "\n" print "digits: " print digits print "\n" print # save and restore for backtracking @@ -22,7 +22,7 @@ sr_cursor.save = saved_pos &is_ident_char sr_cursor.take_while = full_token "full: " print full_token print "\n" print saved_pos sr_cursor.restore -{ .is_alpha } sr_cursor.take_while = alpha_part +&char::is_alpha sr_cursor.take_while = alpha_part "alpha only: " print alpha_part print "\n" print # skip_whitespace " hello" Cursor::new = ws_cursor diff --git a/examples/type_annotations.casa b/examples/type_annotations.casa index 8462e9c..0b48755 100644 --- a/examples/type_annotations.casa +++ b/examples/type_annotations.casa @@ -1,7 +1,7 @@ # Type annotations on variable assignments # # The = name:type syntax allows explicit type annotation when assigning variables. -# This is useful for narrowing 'any' or bare 'option' types to concrete types. +# This is useful for narrowing inferred or bare 'option' types to concrete types. import "std" # Basic type annotations diff --git a/lib/parser.casa b/lib/parser.casa index c45e3d2..a1f2176 100644 --- a/lib/parser.casa +++ b/lib/parser.casa @@ -162,7 +162,7 @@ fn is_ident_char character:char -> bool { # --------------------------------------------------------------------------- fn skip_whitespace cursor:Cursor { - { .is_space } cursor.skip_while + &char::is_space cursor.skip_while } fn parse_int cursor:Cursor -> Result[int ParseError] { @@ -174,7 +174,7 @@ fn parse_int cursor:Cursor -> Result[int ParseError] { cursor.advance drop fi fi - { .is_digit } cursor.take_while = digits + &char::is_digit cursor.take_while = digits if digits.length 0 == then start cursor.restore start "expected integer" ParseError Result::Error diff --git a/tests/compiler/test_common.casa b/tests/compiler/test_common.casa index 4e6c56d..477e8fe 100644 --- a/tests/compiler/test_common.casa +++ b/tests/compiler/test_common.casa @@ -114,9 +114,9 @@ fn test_signature_matches { "str -> bool" signature_from_str = sig4 "different params don't match" sig4 sig3 signature_matches ! assert_true - "any -> bool" signature_from_str = sig5 + f"{TYPE_UNKNOWN} -> bool" signature_from_str = sig5 "int -> bool" signature_from_str = sig6 - "any matches int" sig6 sig5 signature_matches assert_true + "TYPE_UNKNOWN matches int" sig6 sig5 signature_matches assert_true } fn test_is_builtin_type { diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index a3effac..dbc6015 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -397,7 +397,7 @@ fn test_print_dispatch { } # ============================================================================ -# Tests: Typeof pops any, pushes str +# Tests: Typeof pops a generic value, pushes str # ============================================================================ fn test_typeof_effect { @@ -478,8 +478,8 @@ fn test_argc_argv { fn test_types_match { "types_match" test_start "same types" "int" "int" types_match assert_true - "any matches int" "int" "any" types_match assert_true - "int matches any" "any" "int" types_match assert_true + "TYPE_UNKNOWN matches int" "int" TYPE_UNKNOWN types_match assert_true + "int matches TYPE_UNKNOWN" TYPE_UNKNOWN "int" types_match assert_true "different types" "str" "int" types_match ! assert_true "fn base matches fn type" "fn[int -> int]" "fn" types_match assert_true "generic base matches" "List[int]" "List" types_match assert_true @@ -492,8 +492,8 @@ fn test_types_match { fn test_unify_type { "unify_type" test_start "same" "int" "int" "int" unify_type assert_str_eq - "any + int" "int" "int" "any" unify_type assert_str_eq - "int + any" "int" "any" "int" unify_type assert_str_eq + "unknown + int" "int" "int" TYPE_UNKNOWN unify_type assert_str_eq + "int + unknown" "int" TYPE_UNKNOWN "int" unify_type assert_str_eq "incompatible" "" "str" "int" unify_type assert_str_eq } @@ -1011,7 +1011,7 @@ fn test_tc_pipeline_div_mod { fn test_types_match_extended { "types_match_extended" test_start - "both any" "any" "any" types_match assert_true + "both unknown" TYPE_UNKNOWN TYPE_UNKNOWN types_match assert_true "str str" "str" "str" types_match assert_true "bool bool" "bool" "bool" types_match assert_true "ptr ptr" "ptr" "ptr" types_match assert_true @@ -1020,17 +1020,17 @@ fn test_types_match_extended { } # ============================================================================ -# Tests: stacks_compatible with any type +# Tests: stacks_compatible with TYPE_UNKNOWN sentinel # ============================================================================ -fn test_stacks_compatible_with_any { - "stacks_compatible_with_any" test_start +fn test_stacks_compatible_with_unknown { + "stacks_compatible_with_unknown" test_start List::new(List[str]) = stack_a "int" stack_a.push List::new(List[str]) = stack_b - "any" stack_b.push + TYPE_UNKNOWN stack_b.push stack_b stack_a stacks_compatible = result - "any matches int" 1 result.length assert_eq + "TYPE_UNKNOWN matches int" 1 result.length assert_eq } fn test_stacks_compatible_empty { @@ -1948,7 +1948,7 @@ test_tc_pipeline_bitshift test_tc_pipeline_syscall_remaining test_tc_pipeline_div_mod test_types_match_extended -test_stacks_compatible_with_any +test_stacks_compatible_with_unknown test_stacks_compatible_empty test_error_arithmetic_str_int test_error_arithmetic_bool_int From e1106cff7c741cc8ca25b06bea25e7602e917e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Tue, 28 Apr 2026 22:17:18 +0300 Subject: [PATCH 12/15] feat: report stack underflow as error outside lambdas Non-lambda functions previously synthesized an inferred parameter on stack underflow, masking real bugs. Now report STACK_UNDERFLOW once the declared parameter budget is consumed; lambdas still infer on demand. Cascaded slots are tagged with a `TYPE_ERROR` placeholder that `types_match` treats as a wildcard to prevent spurious follow-on diagnostics within the same op. --- compiler/common.casa | 1 + compiler/typechecker.casa | 102 ++++++++++++++++++---- tests/compiler/test_generic_structs.casa | 2 +- tests/compiler/test_match_underflow.casa | 12 +-- tests/compiler/test_type_annotations.casa | 4 +- tests/compiler/test_typechecker.casa | 6 +- 6 files changed, 97 insertions(+), 30 deletions(-) diff --git a/compiler/common.casa b/compiler/common.casa index af0659a..7e63d4c 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -833,6 +833,7 @@ struct Program { # ============================================================================ "_start" = GLOBAL_SCOPE_LABEL "?" = TYPE_UNKNOWN +"" = TYPE_ERROR "_" = MATCH_WILDCARD "self" = TRAIT_SELF_TYPE 8 = WORD_SIZE diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index bd04ca2..4864f70 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -130,19 +130,23 @@ fn make_match_context match_type:str -> MatchContext { # ============================================================================ struct TypeChecker { - live: TypedStack - parameters: List[Parameter] - return_types: TypedStack - has_return_types: bool - branched_stacks: List[BranchFrame] - current_location: Location - current_op_context: str - current_expect_ctx: str - in_branch_condition: bool - store: SymbolStore + live: TypedStack + parameters: List[Parameter] + return_types: TypedStack + has_return_types: bool + branched_stacks: List[BranchFrame] + current_location: Location + current_op_context: str + current_expect_ctx: str + in_branch_condition: bool + store: SymbolStore + param_cap: int + underflow_reported_this_op: bool } fn make_typechecker store:SymbolStore -> TypeChecker { + false # underflow_reported_this_op + -1 # param_cap (-1 = unlimited until type_check_ops sets a real cap) store # store false # in_branch_condition "" # current_expect_ctx @@ -155,6 +159,11 @@ fn make_typechecker store:SymbolStore -> TypeChecker { TypedStack::new # live TypeChecker } + +fn within_param_cap tc:TypeChecker -> bool { + if -1 tc TypeChecker::param_cap == then true return fi + tc TypeChecker::param_cap tc TypeChecker::parameters.length < +} # Raise a TypeMismatch error at the type checker's current location. fn raise_type_mismatch tc:TypeChecker message:str { @@ -175,17 +184,39 @@ fn stack_push_origin tc:TypeChecker typ:str origin:Location { origin tc TypeChecker::live TypedStack::origins.push } +fn report_stack_underflow tc:TypeChecker message:str { + if tc TypeChecker::underflow_reported_this_op then return fi + tc TypeChecker::current_location message ErrorKind::StackUnderflow add_error + true tc TypeChecker::set_underflow_reported_this_op +} + +fn stack_underflow_message tc:TypeChecker -> str { + tc TypeChecker::current_expect_ctx = expect_what + if "" expect_what == then + "Stack underflow: expected a value but stack is empty" return + fi + f"Stack underflow: expected {expect_what} but stack is empty" +} + fn stack_peek tc:TypeChecker -> str { if 0 tc TypeChecker::live TypedStack::types.length == then - TYPE_UNKNOWN return + if tc within_param_cap then + TYPE_UNKNOWN return + fi + tc stack_underflow_message tc report_stack_underflow + TYPE_ERROR return fi tc TypeChecker::live TypedStack::types.length 1 - tc TypeChecker::live TypedStack::types.get } fn stack_pop tc:TypeChecker -> str { if 0 tc TypeChecker::live TypedStack::types.length == then - TYPE_UNKNOWN make_param tc TypeChecker::parameters.push - TYPE_UNKNOWN return + if tc within_param_cap then + TYPE_UNKNOWN make_param tc TypeChecker::parameters.push + TYPE_UNKNOWN return + fi + tc stack_underflow_message tc report_stack_underflow + TYPE_ERROR return fi tc TypeChecker::live TypedStack::origins.pop drop tc TypeChecker::live TypedStack::types.pop @@ -193,8 +224,12 @@ fn stack_pop tc:TypeChecker -> str { fn expect_type tc:TypeChecker expected:str -> str { if 0 tc TypeChecker::live TypedStack::types.length == then - expected make_param tc TypeChecker::parameters.push - expected return + if tc within_param_cap then + expected make_param tc TypeChecker::parameters.push + expected return + fi + f"Stack underflow: expected `{expected}` but stack is empty" tc report_stack_underflow + TYPE_ERROR return fi tc TypeChecker::live TypedStack::origins.pop = et_origin tc TypeChecker::live TypedStack::types.pop = et_actual @@ -222,6 +257,8 @@ fn types_match expected:str actual:str -> bool { if expected actual == then true return fi if TYPE_UNKNOWN expected == then true return fi if TYPE_UNKNOWN actual == then true return fi + if TYPE_ERROR expected == then true return fi + if TYPE_ERROR actual == then true return fi # Check fn type matching if "fn" expected == then if actual is_fn_type then true return fi @@ -723,12 +760,18 @@ fn check_comparison op.value = cmp_value cmp_value OpValue::Eq is cmp_value OpValue::Ne is || = is_eq_op if is_eq_op then "Eq" else "Ord" fi = trait_name + f"trait `{trait_name}`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type1 tc stack_pop = type2 # Stack-underflow placeholder on either side: defer to caller-level # inference (lambda param synthesis, generic body deferral). Skip both # the same-type rule and the trait-bound check. - if TYPE_UNKNOWN type1 == TYPE_UNKNOWN type2 == || then + if + TYPE_UNKNOWN type1 == + TYPE_UNKNOWN type2 == || + TYPE_ERROR type1 == || + TYPE_ERROR type2 == || + then "bool" tc stack_push op_index return fi @@ -1698,6 +1741,7 @@ fn check_literals tc:TypeChecker op:Op function_opt:Option[Function] { # ============================================================================ fn check_io tc:TypeChecker op:Op ops:List[Op] op_index:int function_opt:Option[Function] -> int { + "trait `Display`" tc TypeChecker::set_current_expect_ctx tc stack_peek = typ if "str" typ == then tc stack_pop drop @@ -1724,6 +1768,10 @@ fn check_io tc:TypeChecker op:Op ops:List[Op] op_index:int function_opt:Option[F OpValue::PrintInt op Op::set_value op_index return fi + if TYPE_ERROR typ == then + tc stack_pop drop + op_index return + fi if function_opt "Display" typ type_satisfies_trait ! then op Op::location f"Type `{typ}` cannot be printed: does not satisfy trait `Display`" ErrorKind::MissingTraitMethod add_error tc stack_pop drop @@ -1905,14 +1953,17 @@ fn check_match tc:TypeChecker op:Op function_opt:Option[Function] { op.value = match_value if match_value OpValue::MatchStart is then if 0 tc TypeChecker::live TypedStack::types.length == then - true = inferring_signature + tc within_param_cap = inferring_signature if function_opt.is_some then - "None -> None" function_opt.unwrap Function::signature signature_to_str == = inferring_signature + if function_opt.unwrap Function::name "lambda__" str::starts_with ! then + false = inferring_signature + fi else false = inferring_signature fi if inferring_signature ! then op Op::location "`match` requires a subject value on the stack" ErrorKind::StackUnderflow add_error + true tc TypeChecker::set_underflow_reported_this_op fi fi tc stack_pop = match_type @@ -2385,10 +2436,16 @@ fn check_fstring_to_str op_index:int function_opt:Option[Function] -> int { + "trait `Display`" tc TypeChecker::set_current_expect_ctx tc stack_peek = fstring_type if "str" fstring_type == then op_index return fi + if TYPE_ERROR fstring_type == then + tc stack_pop drop + "str" tc stack_push + op_index return + fi if function_opt "Display" fstring_type type_satisfies_trait ! then op Op::location f"Type `{fstring_type}` cannot be used in f-string: does not satisfy trait `Display`" ErrorKind::MissingTraitMethod add_error tc stack_pop drop @@ -2425,6 +2482,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct 1 += op_index current_op Op::location checker TypeChecker::set_current_location "" checker TypeChecker::set_current_expect_ctx + false checker TypeChecker::set_underflow_reported_this_op current_op.value = op_value # Arithmetic if @@ -2683,6 +2741,14 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct fn type_check_ops ops:List[Op] function_opt:Option[Function] -> Signature { DEFAULT_PARSER.store make_typechecker = checker + -1 = budget + if function_opt.is_some then + function_opt.unwrap = the_function + if the_function Function::name "lambda__" str::starts_with ! then + the_function Function::signature Signature::parameters.length = budget + fi + fi + budget checker TypeChecker::set_param_cap ERRORS.length = errors_before_ops function_opt ops checker run_type_check_ops diff --git a/tests/compiler/test_generic_structs.casa b/tests/compiler/test_generic_structs.casa index 76e95d4..9e6d55b 100644 --- a/tests/compiler/test_generic_structs.casa +++ b/tests/compiler/test_generic_structs.casa @@ -185,7 +185,7 @@ fn test_typecheck_generic_struct_in_function { fn test_resolve_generic_struct_new { "resolve_generic_struct_new" test_start "struct Box[T] { value: T }\n42 Box" resolve_gs = ops - OpValue::StructNew ops find_ops_by_kind = struct_new_ops + List::new(List[str]) empty_location List::new(List[Member]) "" CasaStruct OpValue::StructNew ops find_ops_by_kind = struct_new_ops "has struct new op" 1 struct_new_ops.length assert_eq } diff --git a/tests/compiler/test_match_underflow.casa b/tests/compiler/test_match_underflow.casa index de242e3..b696b0c 100644 --- a/tests/compiler/test_match_underflow.casa +++ b/tests/compiler/test_match_underflow.casa @@ -65,8 +65,7 @@ fn test_match_without_subject_in_declared_fn_body_errors { } # ============================================================================ -# Inference contexts (undeclared fn, lambda) keep pre-fix behavior: -# empty stack at `match` synthesizes an inferred parameter, no error. +# Lambda still infers from caller-pushed values; non-lambda functions never do. # ============================================================================ fn test_lambda_match_subject_inferred { @@ -80,13 +79,14 @@ fn test_lambda_match_subject_inferred { disable_test_mode } -fn test_undeclared_fn_match_subject_inferred { - "undeclared_fn_match_subject_inferred" test_start +fn test_undeclared_fn_match_subject_errors { + "undeclared_fn_match_subject_errors" test_start enable_test_mode clear_global_state clear_errors "fn f { match 1 => 1 _ => 2 end drop }\n42 f\n" tc_mu drop - "no errors" assert_no_errors + "has errors" assert_has_errors + "stack underflow kind" ErrorKind::StackUnderflow assert_error_kind clear_errors disable_test_mode } @@ -113,6 +113,6 @@ test_top_level_match_without_subject_errors test_match_underflow_message_names_construct test_match_without_subject_in_declared_fn_body_errors test_lambda_match_subject_inferred -test_undeclared_fn_match_subject_inferred +test_undeclared_fn_match_subject_errors test_well_formed_match_still_passes test_summary diff --git a/tests/compiler/test_type_annotations.casa b/tests/compiler/test_type_annotations.casa index 2f5b03b..2b99528 100644 --- a/tests/compiler/test_type_annotations.casa +++ b/tests/compiler/test_type_annotations.casa @@ -155,7 +155,7 @@ fn test_annotation_char { fn test_no_annotation_on_increment { "no_annotation_on_increment" test_start "42 = x 1 += x" test_parse = ops - OpValue::AssignInc ops find_ops_by_kind = inc_ops + "" OpValue::AssignInc ops find_ops_by_kind = inc_ops "count" 1 inc_ops.length assert_eq "annotation empty" "" 0 inc_ops.get Op::type_annotation assert_str_eq } @@ -163,7 +163,7 @@ fn test_no_annotation_on_increment { fn test_no_annotation_on_decrement { "no_annotation_on_decrement" test_start "42 = x 1 -= x" test_parse = ops - OpValue::AssignDec ops find_ops_by_kind = dec_ops + "" OpValue::AssignDec ops find_ops_by_kind = dec_ops "count" 1 dec_ops.length assert_eq "annotation empty" "" 0 dec_ops.get Op::type_annotation assert_str_eq } diff --git a/tests/compiler/test_typechecker.casa b/tests/compiler/test_typechecker.casa index dbc6015..3c78406 100644 --- a/tests/compiler/test_typechecker.casa +++ b/tests/compiler/test_typechecker.casa @@ -1748,7 +1748,7 @@ fn test_branch_while_empty_body { fn test_branch_frame_kind_dispatch { "branch_frame_kind_dispatch" test_start - make_typechecker = tc + DEFAULT_PARSER.store make_typechecker = tc List::new(List[Op]) = ops empty_location OpValue::IfStart make_op ops.push 0 = bd_i @@ -1846,7 +1846,7 @@ fn test_error_array_literal_unresolved_assignment { fn test_branch_return_inside_if_restores_live { "branch_return_inside_if_restores_live" test_start - make_typechecker = tc + DEFAULT_PARSER.store make_typechecker = tc # Seed the live stack with a single `int`. "int" tc stack_push # Begin an `if true then` block. @@ -1863,7 +1863,7 @@ fn test_branch_return_inside_if_restores_live { OpValue::IfCondition br_kind == || then br_op tc check_if_block - elif OpValue::PushBool br_kind == then + elif 0 OpValue::PushBool br_kind == then "bool" tc stack_push fi 1 += br_i From 0f812fc4fb67e35b16cc3c6860f39cf0600c140e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Tue, 28 Apr 2026 22:18:25 +0300 Subject: [PATCH 13/15] feat: clarify stack underflow messages and cascade token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stack underflow diagnostics now name the slot the operation needed instead of generic "expected a value": - Trait dispatch (`print`, `==`, `<`): "value with trait `T`" — was "trait `T`", which read as if the trait itself was on the stack. - Stack intrinsics (`drop`, `dup`, `swap`, `over`, `rot`): "value for `op`" or "argument N of `op`" with N counting from the top. Rename the cascade placeholder from `` (TYPE_ERROR) to `` (TYPE_MISSING) so user-facing messages communicate "this slot was tainted by an earlier error, fix that first" rather than the internal sentinel. Document `STACK_UNDERFLOW` and the new `Cascade Errors` section in docs/errors.md. Cross-referenced from `TYPE_MISMATCH` so a user seeing `got ` knows to look upstream. --- compiler/common.casa | 2 +- compiler/typechecker.casa | 33 ++++--- docs/errors.md | 44 ++++++++++ tests/compiler/test_underflow_messages.casa | 95 +++++++++++++++++++++ 4 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 tests/compiler/test_underflow_messages.casa diff --git a/compiler/common.casa b/compiler/common.casa index 7e63d4c..5ac8500 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -833,7 +833,7 @@ struct Program { # ============================================================================ "_start" = GLOBAL_SCOPE_LABEL "?" = TYPE_UNKNOWN -"" = TYPE_ERROR +"" = TYPE_MISSING "_" = MATCH_WILDCARD "self" = TRAIT_SELF_TYPE 8 = WORD_SIZE diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index 4864f70..f667f80 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -204,7 +204,7 @@ fn stack_peek tc:TypeChecker -> str { TYPE_UNKNOWN return fi tc stack_underflow_message tc report_stack_underflow - TYPE_ERROR return + TYPE_MISSING return fi tc TypeChecker::live TypedStack::types.length 1 - tc TypeChecker::live TypedStack::types.get } @@ -216,7 +216,7 @@ fn stack_pop tc:TypeChecker -> str { TYPE_UNKNOWN return fi tc stack_underflow_message tc report_stack_underflow - TYPE_ERROR return + TYPE_MISSING return fi tc TypeChecker::live TypedStack::origins.pop drop tc TypeChecker::live TypedStack::types.pop @@ -229,7 +229,7 @@ fn expect_type tc:TypeChecker expected:str -> str { expected return fi f"Stack underflow: expected `{expected}` but stack is empty" tc report_stack_underflow - TYPE_ERROR return + TYPE_MISSING return fi tc TypeChecker::live TypedStack::origins.pop = et_origin tc TypeChecker::live TypedStack::types.pop = et_actual @@ -257,8 +257,8 @@ fn types_match expected:str actual:str -> bool { if expected actual == then true return fi if TYPE_UNKNOWN expected == then true return fi if TYPE_UNKNOWN actual == then true return fi - if TYPE_ERROR expected == then true return fi - if TYPE_ERROR actual == then true return fi + if TYPE_MISSING expected == then true return fi + if TYPE_MISSING actual == then true return fi # Check fn type matching if "fn" expected == then if actual is_fn_type then true return fi @@ -760,7 +760,7 @@ fn check_comparison op.value = cmp_value cmp_value OpValue::Eq is cmp_value OpValue::Ne is || = is_eq_op if is_eq_op then "Eq" else "Ord" fi = trait_name - f"trait `{trait_name}`" tc TypeChecker::set_current_expect_ctx + f"value with trait `{trait_name}`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type1 tc stack_pop = type2 # Stack-underflow placeholder on either side: defer to caller-level @@ -769,8 +769,8 @@ fn check_comparison if TYPE_UNKNOWN type1 == TYPE_UNKNOWN type2 == || - TYPE_ERROR type1 == || - TYPE_ERROR type2 == || + TYPE_MISSING type1 == || + TYPE_MISSING type2 == || then "bool" tc stack_push op_index return @@ -869,25 +869,34 @@ fn check_bitwise tc:TypeChecker op:Op { fn check_stack_ops tc:TypeChecker op:Op { op.value = stack_value if stack_value OpValue::Drop is then + "value for `drop`" tc TypeChecker::set_current_expect_ctx tc stack_pop drop elif stack_value OpValue::Dup is then + "value for `dup`" tc TypeChecker::set_current_expect_ctx tc stack_pop = typ typ tc stack_push typ tc stack_push elif stack_value OpValue::Swap is then + "argument 1 of `swap`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type1 + "argument 2 of `swap`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type2 type1 tc stack_push type2 tc stack_push elif stack_value OpValue::Over is then + "argument 1 of `over`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type1 + "argument 2 of `over`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type2 type2 tc stack_push type1 tc stack_push type2 tc stack_push elif stack_value OpValue::Rot is then + "argument 1 of `rot`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type1 + "argument 2 of `rot`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type2 + "argument 3 of `rot`" tc TypeChecker::set_current_expect_ctx tc stack_pop = type3 type2 tc stack_push type1 tc stack_push @@ -1741,7 +1750,7 @@ fn check_literals tc:TypeChecker op:Op function_opt:Option[Function] { # ============================================================================ fn check_io tc:TypeChecker op:Op ops:List[Op] op_index:int function_opt:Option[Function] -> int { - "trait `Display`" tc TypeChecker::set_current_expect_ctx + "value with trait `Display`" tc TypeChecker::set_current_expect_ctx tc stack_peek = typ if "str" typ == then tc stack_pop drop @@ -1768,7 +1777,7 @@ fn check_io tc:TypeChecker op:Op ops:List[Op] op_index:int function_opt:Option[F OpValue::PrintInt op Op::set_value op_index return fi - if TYPE_ERROR typ == then + if TYPE_MISSING typ == then tc stack_pop drop op_index return fi @@ -2436,12 +2445,12 @@ fn check_fstring_to_str op_index:int function_opt:Option[Function] -> int { - "trait `Display`" tc TypeChecker::set_current_expect_ctx + "value with trait `Display`" tc TypeChecker::set_current_expect_ctx tc stack_peek = fstring_type if "str" fstring_type == then op_index return fi - if TYPE_ERROR fstring_type == then + if TYPE_MISSING fstring_type == then tc stack_pop drop "str" tc stack_push op_index return diff --git a/docs/errors.md b/docs/errors.md index 567667d..013f7c4 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -124,6 +124,29 @@ fn outer { error[INVALID_SCOPE]: Implementation blocks should be defined in the global scope ``` +### `STACK_UNDERFLOW` + +An operation tried to consume a value (or values) from the stack but the stack was empty. + +```casa +fn foo { print rot } +``` + +``` +error[STACK_UNDERFLOW]: Stack underflow: expected value with trait `Display` but stack is empty +error[STACK_UNDERFLOW]: Stack underflow: expected argument 1 of `rot` but stack is empty +``` + +The expectation phrase identifies what the operation needed: + +- `value with trait `T`` — operations that dispatch on a trait (e.g. `print`, `==`, `<`) +- `argument N of `op`` — stack intrinsics that consume more than one value (`swap`, `over`, `rot`); `N` counts from the top of the stack (1 = topmost) +- `value for `op`` — single-value stack intrinsics (`drop`, `dup`) +- `parameter N of `name`` — function call missing arguments +- `value` — generic fallback when no operation-specific context is available + +When a slot cannot be filled, downstream operations may show `` for the absent value. See [Cascade Errors](#cascade-errors). + ### `TYPE_MISMATCH` A type does not match what was expected. @@ -137,6 +160,8 @@ fn bad[T] T T -> T T { } error[TYPE_MISMATCH]: Type variable `T` bound to `str` but got `int` ``` +If `got` shows ``, an earlier error left the slot without a real type — fix the earlier error first. See [Cascade Errors](#cascade-errors). + ### `STACK_MISMATCH` Branches of a conditional or loop leave the stack in inconsistent states. The error shows each branch's stack signature so you can see which branch diverges. @@ -210,6 +235,25 @@ if true then error[UNMATCHED_BLOCK]: `if` without matching `fi` ``` +## Cascade Errors + +When the compiler reports an error that consumes a value from the stack — most commonly `STACK_UNDERFLOW` — the affected slot is tagged with the placeholder type `` so type checking can keep going. If a later operation reads that slot, you may see `` in its diagnostic: + +```casa +fn two_generics[T] a:T b:T { } +fn foo { + print # underflow: nothing to print + 2 two_generics # this also fails because the underflow tainted the stack +} +``` + +``` +error[STACK_UNDERFLOW]: Stack underflow: expected value with trait `Display` but stack is empty +error[TYPE_MISMATCH]: Type variable `T` bound to `int` but got `` +``` + +The second error is a *cascade* — its real cause is the underflow above. Fix the upstream error first; the cascade error usually disappears on its own. `` is distinct from `?` (an unconstrained type that the compiler is still inferring). + ## Multi-Error Collection The compiler collects as many errors as possible within each compilation phase before stopping. For example, the identifier resolution phase will report all undefined names at once rather than stopping at the first one. diff --git a/tests/compiler/test_underflow_messages.casa b/tests/compiler/test_underflow_messages.casa new file mode 100644 index 0000000..1c31b60 --- /dev/null +++ b/tests/compiler/test_underflow_messages.casa @@ -0,0 +1,95 @@ +import "../../compiler/common.casa" +import "../../compiler/error.casa" +import "../../compiler/lexer.casa" +import "../../compiler/syntax.casa" +import "../../compiler/typechecker.casa" +import "../../lib/test.casa" + +# ============================================================================ +# Helper: lex + parse + resolve + typecheck a source string +# ============================================================================ + +fn tc_um code:str -> Signature { + clear_global_state + "test" code lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops +} + +# ============================================================================ +# `print` underflow names the trait expectation as a value, not the trait +# itself. +# ============================================================================ + +fn test_print_underflow_message_names_value_with_trait { + "print_underflow_message_names_value_with_trait" test_start + enable_test_mode + clear_global_state + clear_errors + "fn foo { print }\nfoo\n" tc_um drop + "has errors" assert_has_errors + "stack underflow kind" ErrorKind::StackUnderflow assert_error_kind + "names value with trait" "value with trait `Display`" assert_error_contains + clear_errors + disable_test_mode +} + +# ============================================================================ +# `rot` underflow names the slot index of the missing argument. +# ============================================================================ + +fn test_rot_underflow_message_names_argument_slot { + "rot_underflow_message_names_argument_slot" test_start + enable_test_mode + clear_global_state + clear_errors + "fn foo { rot }\nfoo\n" tc_um drop + "has errors" assert_has_errors + "stack underflow kind" ErrorKind::StackUnderflow assert_error_kind + "names argument slot" "argument 1 of `rot`" assert_error_contains + clear_errors + disable_test_mode +} + +# ============================================================================ +# `drop` underflow uses the singular "value for" phrasing. +# ============================================================================ + +fn test_drop_underflow_message_uses_singular_phrase { + "drop_underflow_message_uses_singular_phrase" test_start + enable_test_mode + clear_global_state + clear_errors + "fn foo { drop }\nfoo\n" tc_um drop + "has errors" assert_has_errors + "stack underflow kind" ErrorKind::StackUnderflow assert_error_kind + "names value for drop" "value for `drop`" assert_error_contains + clear_errors + disable_test_mode +} + +# ============================================================================ +# Cascade slots show as ``, never the internal `` token. +# ============================================================================ + +fn test_cascade_slot_renders_as_missing_token { + "cascade_slot_renders_as_missing_token" test_start + enable_test_mode + clear_global_state + clear_errors + "fn two_generics[T] a:T b:T {}\nfn foo { print 2 two_generics }\nfoo\n" tc_um drop + "has errors" assert_has_errors + "renders missing token" "" assert_error_contains + clear_errors + disable_test_mode +} + +# ============================================================================ +# Run all tests +# ============================================================================ +test_print_underflow_message_names_value_with_trait +test_rot_underflow_message_names_argument_slot +test_drop_underflow_message_uses_singular_phrase +test_cascade_slot_renders_as_missing_token +test_summary From 421b9e4706cb9ff160f8d64cacbf603a81fbf9f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Tue, 28 Apr 2026 22:46:40 +0300 Subject: [PATCH 14/15] feat: validate type cast targets exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the parse-time `any`-specific syntax error with a general type-existence check at the TypeCast op in run_type_check_ops. The validator covers builtins, registered structs/enums, declared signature type variables, parameterized types (recurses into params), and `fn` types. Unknown identifiers — `foo`, single-letter `T` outside a generic function, or nested unknowns like `List[Foo]` — now raise a uniform `type \`X\` does not exist` syntax error. --- compiler/syntax.casa | 5 --- compiler/typechecker.casa | 45 +++++++++++++++++++++++ tests/compiler/test_type_annotations.casa | 30 ++++++++++++--- 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/compiler/syntax.casa b/compiler/syntax.casa index 574baf3..7dc0989 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -208,11 +208,6 @@ fn parse_type cursor:TokenCursor -> str { base_token Token::value = base fi - if "any" base == then - peek Token::location "type `any` does not exist; use a concrete type or a generic parameter" ErrorKind::Syntax raise_error - "" return - fi - # Check for parameterized type cursor.peek = next_opt if next_opt.is_none then base return fi diff --git a/compiler/typechecker.casa b/compiler/typechecker.casa index f667f80..209e4a2 100644 --- a/compiler/typechecker.casa +++ b/compiler/typechecker.casa @@ -296,6 +296,50 @@ fn types_match expected:str actual:str -> bool { false } +fn base_type_exists base:str function_opt:Option[Function] -> bool { + if base is_builtin_type then true return fi + if base DEFAULT_PARSER.store.structs.get.is_some then true return fi + if base DEFAULT_PARSER.store.enums.get.is_some then true return fi + if function_opt Option::Some(check_fn) is then + if base check_fn Function::signature Signature::type_vars.has then true return fi + fi + false +} + +fn type_exists_recursive typ:str function_opt:Option[Function] -> bool { + if typ.length 0 == then false return fi + + if typ is_fn_type then + typ extract_fn_signature_str = function_inner_opt + if function_inner_opt Option::Some(function_inner) is then + for function_token in function_inner split_type_tokens.iter do + if "->" function_token != then + if function_opt function_token type_exists_recursive ! then false return fi + fi + done + fi + true return + fi + + typ extract_generic_base = base_opt + if base_opt Option::Some(generic_base) is then + generic_base.length = base_length + typ.length base_length - 2 - = inner_length + typ base_length 1 + inner_length str::substring split_type_tokens = generic_params + for generic_param in generic_params.iter do + if function_opt generic_param type_exists_recursive ! then false return fi + done + function_opt generic_base base_type_exists return + fi + + function_opt typ base_type_exists +} + +fn validate_type_exists typ:str function_opt:Option[Function] location:Location { + if function_opt typ type_exists_recursive then return fi + location f"type `{typ}` does not exist" ErrorKind::Syntax raise_error +} + fn is_type_variable typ:str -> bool { # Check if the type is a type variable (single uppercase letter or # registered as a type var in a generic enum/struct) @@ -2720,6 +2764,7 @@ fn run_type_check_ops checker:TypeChecker ops:List[Op] function_opt:Option[Funct # Type cast if op_value OpValue::TypeCast(cast_type) is then + current_op Op::location function_opt cast_type validate_type_exists checker stack_pop drop cast_type checker stack_push continue diff --git a/tests/compiler/test_type_annotations.casa b/tests/compiler/test_type_annotations.casa index 2b99528..9ede822 100644 --- a/tests/compiler/test_type_annotations.casa +++ b/tests/compiler/test_type_annotations.casa @@ -214,20 +214,36 @@ fn test_tc_annotation_to_option { "return type" "Option[T]" 0 sig Signature::return_types.get assert_str_eq } -fn test_tc_reject_any_cast { - "tc_reject_any_cast" test_start +fn assert_cast_rejected_as_unknown_type source:str expected_type:str { enable_test_mode clear_global_state clear_errors - "test" "42 (any) = x:int x" lex_source = tokens - tokens DEFAULT_PARSER parse_ops drop + "test" source lex_source = tokens + tokens DEFAULT_PARSER parse_ops = ops + ops DEFAULT_PARSER resolve_identifiers_global = ops + Option::None(Option[Function]) ops type_check_ops drop "has errors" assert_has_errors "syntax error" ErrorKind::Syntax assert_error_kind - "mentions any" "any" assert_error_contains + "type message" f"type `{expected_type}` does not exist" assert_error_contains clear_errors disable_test_mode } +fn test_tc_reject_unknown_cast { + "tc_reject_unknown_cast" test_start + "foo" "42 (foo)" assert_cast_rejected_as_unknown_type +} + +fn test_tc_reject_unknown_uppercase_cast { + "tc_reject_unknown_uppercase_cast" test_start + "T" "42 (T)" assert_cast_rejected_as_unknown_type +} + +fn test_tc_reject_unknown_generic_param_cast { + "tc_reject_unknown_generic_param_cast" test_start + "List[Foo]" "42 (List[Foo])" assert_cast_rejected_as_unknown_type +} + fn test_tc_reassignment_keeps_annotated_type { "tc_reassignment_keeps_annotated_type" test_start "42 = x:int 99 = x x" tc_code = sig @@ -361,7 +377,9 @@ test_tc_annotation_matches_stack test_tc_annotation_to_int test_tc_annotation_to_str test_tc_annotation_to_option -test_tc_reject_any_cast +test_tc_reject_unknown_cast +test_tc_reject_unknown_uppercase_cast +test_tc_reject_unknown_generic_param_cast test_tc_reassignment_keeps_annotated_type test_tc_without_annotation test_tc_annotation_in_function_body From 949d7d9f602ff104ab561bb2c9773ff8f93c235d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Tue, 28 Apr 2026 23:01:04 +0300 Subject: [PATCH 15/15] ci: bootstrap stage1 directly from release binary Skip the main-source intermediate compile so the workflow accepts syntax not yet on main (supertraits in lib/std.casa). Restore the two-stage flow after merge. --- .github/workflows/test-compiler.yml | 10 +--------- .github/workflows/test-examples.yml | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test-compiler.yml b/.github/workflows/test-compiler.yml index ea44e80..4a89833 100644 --- a/.github/workflows/test-compiler.yml +++ b/.github/workflows/test-compiler.yml @@ -14,21 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/checkout@v5 - with: - ref: main - path: main-src - name: Download casac release binary run: | curl --silent --location --fail \ --output casac-release \ "https://github.com/frendsick/casa/releases/latest/download/casac" chmod +x casac-release - - name: Bootstrap stage0 compiler from main - run: | - cd main-src - ../casac-release -L lib casa.casa -o ../casac-main - name: Bootstrap stage1 compiler from branch - run: ./casac-main -L lib casa.casa -o casac-stage1 + run: ./casac-release -L lib casa.casa -o casac-stage1 - name: Run compiler tests run: ./tests/test_compiler.sh ./casac-stage1 diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index 22263e0..b9e0d8f 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -14,21 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/checkout@v5 - with: - ref: main - path: main-src - name: Download casac release binary run: | curl --silent --location --fail \ --output casac-release \ "https://github.com/frendsick/casa/releases/latest/download/casac" chmod +x casac-release - - name: Bootstrap stage0 compiler from main - run: | - cd main-src - ../casac-release -L lib casa.casa -o ../casac-main - name: Bootstrap stage1 compiler from branch - run: ./casac-main -L lib casa.casa -o casac-stage1 + run: ./casac-release -L lib casa.casa -o casac-stage1 - name: Test example programs run: ./tests/test_examples.sh ./casac-stage1