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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions casa.casa
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 2 additions & 31 deletions compiler/common.casa
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ============================================================================
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions compiler/syntax.casa
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ============================================================================
Expand Down
4 changes: 4 additions & 0 deletions docs/enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions docs/traits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions examples/enum_hashable_derive.casa
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions examples/outputs/enum_hashable_derive.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
10
20
30
true
false
true
1 change: 1 addition & 0 deletions lsp.casa
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 67 additions & 1 deletion tests/compiler/test_enum.casa
Original file line number Diff line number Diff line change
Expand Up @@ -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] {
Expand Down Expand Up @@ -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
# ============================================================================
Expand Down Expand Up @@ -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