From 187997073d6673a1c3ad4e4abe0af860ddc89f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20P=C3=A4tsi?= Date: Sun, 26 Apr 2026 19:21:18 +0300 Subject: [PATCH] feat(syntax): auto-derive Hashable for payload-free enums (#141) Synthesize {Name}::hash and {Name}::eq for enums whose variants carry no inner values, so they satisfy the Hashable trait without manual impl blocks. Synth runs once per compile after resolve_identifiers_global, skips any name the user already defined (silent override), and produces inline bodies (cast-to-int for hash, == for eq) that need no std import. Removes 5 manual impl blocks in compiler/common.casa now provided by the synth. --- casa.casa | 1 + compiler/common.casa | 33 +---------- compiler/syntax.casa | 44 +++++++++++++++ docs/enums.md | 4 ++ docs/traits.md | 16 ++++++ examples/enum_hashable_derive.casa | 28 ++++++++++ examples/outputs/enum_hashable_derive.out | 6 ++ lsp.casa | 1 + tests/compiler/test_enum.casa | 68 ++++++++++++++++++++++- 9 files changed, 169 insertions(+), 32 deletions(-) create mode 100644 examples/enum_hashable_derive.casa create mode 100644 examples/outputs/enum_hashable_derive.out diff --git a/casa.casa b/casa.casa index 5b99ca3..b12a67b 100644 --- a/casa.casa +++ b/casa.casa @@ -67,6 +67,7 @@ fn main { f"{timer} Parsing ops" log_info tokens DEFAULT_PARSER parse_ops = ops ops DEFAULT_PARSER resolve_identifiers_global = ops + DEFAULT_PARSER derive_hashable_for_enums # Type check f"{timer} Type checking ops" log_info diff --git a/compiler/common.casa b/compiler/common.casa index e09fa19..98fed51 100644 --- a/compiler/common.casa +++ b/compiler/common.casa @@ -267,35 +267,6 @@ enum WarningKind { LossyTypeAnnotation } -# ============================================================================ -# Hashable impls for enums (needed for Map keys) -# ============================================================================ - -impl TokenKind { - fn hash self:TokenKind -> int { self (int) int_hash } - fn eq self:TokenKind other:TokenKind -> bool { self other == } -} - -impl IntrinsicKind { - fn hash self:IntrinsicKind -> int { self (int) int_hash } - fn eq self:IntrinsicKind other:IntrinsicKind -> bool { self other == } -} - -impl KeywordKind { - fn hash self:KeywordKind -> int { self (int) int_hash } - fn eq self:KeywordKind other:KeywordKind -> bool { self other == } -} - -impl DelimiterKind { - fn hash self:DelimiterKind -> int { self (int) int_hash } - fn eq self:DelimiterKind other:DelimiterKind -> bool { self other == } -} - -impl OperatorKind { - fn hash self:OperatorKind -> int { self (int) int_hash } - fn eq self:OperatorKind other:OperatorKind -> bool { self other == } -} - # ============================================================================ # Core data structures # ============================================================================ @@ -720,8 +691,8 @@ struct CasaEnum { } fn has_inner_values casa_enum:CasaEnum -> bool { - for key in casa_enum CasaEnum::variant_types.keys.iter do - if key casa_enum CasaEnum::variant_types.get.unwrap.length 0 != then + for inner_types in casa_enum CasaEnum::variant_types.values.iter do + if 0 inner_types.length != then true return fi done diff --git a/compiler/syntax.casa b/compiler/syntax.casa index b1be5f7..a283b40 100644 --- a/compiler/syntax.casa +++ b/compiler/syntax.casa @@ -1040,6 +1040,50 @@ fn parse_enum parser:Parser cursor:TokenCursor -> CasaEnum { variant_locations type_vars variant_types name_token Token::location variants name_token Token::value CasaEnum } +# ============================================================================ +# Auto-derive Hashable for payload-free enums (#141). Skips enums with inner +# values and any name the user already defined (silent override). +# ============================================================================ + +fn synthesize_hashable_for_enum parser:Parser casa_enum:CasaEnum { + if casa_enum has_inner_values then return fi + casa_enum CasaEnum::name = enum_name + casa_enum CasaEnum::location = location + + f"{enum_name}::hash" = hash_name + if hash_name parser.store.functions.get.is_none then + List::new(List[Op]) = hash_ops + location "int" OpValue::TypeCast make_op hash_ops.push + List::new(List[Parameter]) = hash_params + enum_name make_param hash_params.push + List::new(List[str]) = hash_returns + "int" hash_returns.push + hash_returns hash_params make_signature = hash_sig + hash_sig location hash_ops hash_name make_function_with_sig = hash_fn + hash_name hash_fn parser.store.functions.set drop + fi + + f"{enum_name}::eq" = eq_name + if eq_name parser.store.functions.get.is_none then + List::new(List[Op]) = eq_ops + location OpValue::Eq make_op eq_ops.push + List::new(List[Parameter]) = eq_params + enum_name make_param eq_params.push + enum_name make_param eq_params.push + List::new(List[str]) = eq_returns + "bool" eq_returns.push + eq_returns eq_params make_signature = eq_sig + eq_sig location eq_ops eq_name make_function_with_sig = eq_fn + eq_name eq_fn parser.store.functions.set drop + fi +} + +fn derive_hashable_for_enums parser:Parser { + for casa_enum in parser.store.enums.values.iter do + casa_enum parser synthesize_hashable_for_enum + done +} + # ============================================================================ # _create_member_accessor - create getter/setter for struct members # ============================================================================ diff --git a/docs/enums.md b/docs/enums.md index 1fa3d26..a828c3a 100644 --- a/docs/enums.md +++ b/docs/enums.md @@ -248,6 +248,10 @@ This is resolved at compile time. See [`examples/enum.casa`](../examples/enum.casa). +## Auto-derived `Hashable` + +Enums whose variants carry no inner values are automatically `Hashable`, so they can be used as `Map` keys or `Set` elements without writing an `impl` block. See [Traits -- Auto-derived `Hashable` for Payload-Free Enums](traits.md#auto-derived-hashable-for-payload-free-enums). + ## See Also - [Control Flow -- Match](control-flow.md#match) -- full match syntax, destructuring, and exhaustiveness rules diff --git a/docs/traits.md b/docs/traits.md index 000226e..7bf6e06 100644 --- a/docs/traits.md +++ b/docs/traits.md @@ -55,6 +55,22 @@ impl Point { `Point` now satisfies `Hashable` and can be used as a `Map` key or `Set` element. +## Auto-derived `Hashable` for Payload-Free Enums + +Enums whose variants carry no inner values automatically satisfy `Hashable` — no manual `impl` block is needed. The compiler synthesizes `hash` from the variant discriminant and `eq` from `==`: + +```casa +enum Color { Red Green Blue } + +# Works without writing impl Color { fn hash ... fn eq ... } +Map::new(Map[Color int]) = scores +Color::Red 10 scores.set = scores +``` + +Enums with payload-bearing variants (`Some(T)`, `Circle(int)`, etc.) are not auto-derived; for those, write an explicit `impl` if needed. + +A user-written `impl` always wins. If you define `Color::hash` or `Color::eq` manually, the synthesized version is suppressed and your implementation is used. + ## Trait Bounds Functions and `impl` blocks declare trait bounds on type variables using the `K: TraitName` syntax inside square brackets. Multiple type variables are separated by commas. diff --git a/examples/enum_hashable_derive.casa b/examples/enum_hashable_derive.casa new file mode 100644 index 0000000..12ee565 --- /dev/null +++ b/examples/enum_hashable_derive.casa @@ -0,0 +1,28 @@ +# Auto-derived Hashable for payload-free enums. +# +# Payload-free enums can be used as Map / Set keys without writing a manual +# `impl Color { fn hash ... fn eq ... }` block — the compiler synthesizes +# both methods from the variant discriminant. +import "std" + +enum Color { + Red + Green + Blue +} + +# Used as a Map key with no manual impl +Map::new(Map[Color int]) = scores +Color::Red 10 scores.set = scores +Color::Green 20 scores.set = scores +Color::Blue 30 scores.set = scores +Color::Red scores.get.unwrap print "\n" print +Color::Green scores.get.unwrap print "\n" print +Color::Blue scores.get.unwrap print "\n" print +# Used as a Set member +Set::new(Set[Color]) = seen +Color::Red seen.add = seen +Color::Blue seen.add = seen +Color::Red seen.has print "\n" print +Color::Green seen.has print "\n" print +Color::Blue seen.has print "\n" print diff --git a/examples/outputs/enum_hashable_derive.out b/examples/outputs/enum_hashable_derive.out new file mode 100644 index 0000000..2897987 --- /dev/null +++ b/examples/outputs/enum_hashable_derive.out @@ -0,0 +1,6 @@ +10 +20 +30 +true +false +true diff --git a/lsp.casa b/lsp.casa index a9efe11..969bb71 100644 --- a/lsp.casa +++ b/lsp.casa @@ -316,6 +316,7 @@ fn compile_document file_path:str source:str open_docs:Map[str DocumentState] -> # Parse tokens DEFAULT_PARSER parse_ops = ops ops DEFAULT_PARSER resolve_identifiers_global = ops + DEFAULT_PARSER derive_hashable_for_enums # Type check if 0 ops.length != then diff --git a/tests/compiler/test_enum.casa b/tests/compiler/test_enum.casa index f67259f..2096f02 100644 --- a/tests/compiler/test_enum.casa +++ b/tests/compiler/test_enum.casa @@ -23,7 +23,9 @@ import "../../lib/test.casa" fn test_parse_code code:str -> List[Op] { clear_global_state "test" code lex_source = tokens - tokens DEFAULT_PARSER parse_ops + tokens DEFAULT_PARSER parse_ops = ops + DEFAULT_PARSER derive_hashable_for_enums + ops } fn resolve_enum code:str -> List[Op] { @@ -990,6 +992,62 @@ fn test_braced_arm_as_expression_typechecks { "passes" true assert_true } +# ============================================================================ +# Auto-derive Hashable for payload-free enums (#141) +# ============================================================================ + +fn test_payload_free_enum_synthesizes_hash { + "payload_free_enum_synthesizes_hash" test_start + COLOR_ENUM test_parse_code drop + "Color::hash registered" "Color::hash" DEFAULT_PARSER.store.functions.get.is_some assert_true +} + +fn test_payload_free_enum_synthesizes_eq { + "payload_free_enum_synthesizes_eq" test_start + COLOR_ENUM test_parse_code drop + "Color::eq registered" "Color::eq" DEFAULT_PARSER.store.functions.get.is_some assert_true +} + +fn test_payload_free_enum_hash_signature { + "payload_free_enum_hash_signature" test_start + COLOR_ENUM test_parse_code drop + "Color::hash" DEFAULT_PARSER.store.functions.get.unwrap = hash_fn + hash_fn Function::signature = sig + "param count" 1 sig Signature::parameters.length assert_eq + "param type" "Color" 0 sig Signature::parameters.get Parameter::typ assert_str_eq + "return count" 1 sig Signature::return_types.length assert_eq + "return type" "int" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_payload_free_enum_eq_signature { + "payload_free_enum_eq_signature" test_start + COLOR_ENUM test_parse_code drop + "Color::eq" DEFAULT_PARSER.store.functions.get.unwrap = eq_fn + eq_fn Function::signature = sig + "param count" 2 sig Signature::parameters.length assert_eq + "param0 type" "Color" 0 sig Signature::parameters.get Parameter::typ assert_str_eq + "param1 type" "Color" 1 sig Signature::parameters.get Parameter::typ assert_str_eq + "return type" "bool" 0 sig Signature::return_types.get assert_str_eq +} + +fn test_payload_bearing_enum_skips_hash_synth { + "payload_bearing_enum_skips_hash_synth" test_start + SHAPE_ENUM test_parse_code drop + "Shape::hash NOT registered" "Shape::hash" DEFAULT_PARSER.store.functions.get.is_some assert_false +} + +fn test_payload_bearing_enum_skips_eq_synth { + "payload_bearing_enum_skips_eq_synth" test_start + SHAPE_ENUM test_parse_code drop + "Shape::eq NOT registered" "Shape::eq" DEFAULT_PARSER.store.functions.get.is_some assert_false +} + +fn test_manual_impl_overrides_synth_hash { + "manual_impl_overrides_synth_hash" test_start + "enum Color { Red Green Blue }\nimpl Color {\n fn hash self:Color -> int { 42 }\n fn eq self:Color other:Color -> bool { self other == }\n}\n" test_parse_code drop + "Color::hash registered" "Color::hash" DEFAULT_PARSER.store.functions.get.is_some assert_true +} + # ============================================================================ # Run all tests # ============================================================================ @@ -1103,4 +1161,12 @@ test_is_check_multiple_bindings_parsed # Additional bytecode test_data_enum_match_loads_ordinal test_braced_arm_as_expression_typechecks +# Auto-derive Hashable for payload-free enums (#141) +test_payload_free_enum_synthesizes_hash +test_payload_free_enum_synthesizes_eq +test_payload_free_enum_hash_signature +test_payload_free_enum_eq_signature +test_payload_bearing_enum_skips_hash_synth +test_payload_bearing_enum_skips_eq_synth +test_manual_impl_overrides_synth_hash test_summary