From f25f76adffeddd0c209f17da6050731b3a40ff84 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 2 Aug 2026 19:13:13 +0200 Subject: [PATCH] refine distinct construction semantics --- LANGUAGE.md | 19 ++++++++++------- TODO.md | 18 ++++++++++------ compiler/checker/checker.odin | 29 +++++++++++++++++-------- compiler/checker/comptime.odin | 19 +++++++++++------ compiler_tests.odin | 39 +++++++++++++++++++++++++++++----- 5 files changed, 88 insertions(+), 36 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 1c0950d..2423e3a 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -38,7 +38,7 @@ roadmap and milestone history. - UTF-8 string literals as immutable pointers to static zero-terminated byte arrays, plus raw backtick multiline strings - narrow immutable zero-terminated byte pointer/slice conversion to `*c_char` / `?*c_char` without general `u8`/`c_char` interchange - optionals with `null`, `orelse`, postfix `?`, conditional unwraps, guarded unwraps, and left-to-right short-circuiting multi-unwraps -- nominal distinct types with exact backing construction and explicit scalar backing extraction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases +- nominal distinct types with explicit scalar backing conversion during construction and explicit scalar backing extraction, native enums with optional explicit integer backing and explicit backing-to-scalar casts, contextual enum literals, and imported C enums as target-backed integer aliases - source-order native structs, opaque nominal records with `Name :: opaque`, complete `c_struct { ... }`, keyed record literals, native untagged unions, and native tagged unions `union(Enum)` / `union(enum)` - named native struct fields may declare defaults with `field T = expression`; keyed literals use defaults for omitted fields and explicit initializers override them - void-payload tagged-union variants, anonymous struct payloads, contextual `.variant`, `.variant{payload}`, and `.variant{field = value}` construction @@ -47,17 +47,20 @@ roadmap and milestone history. #### distinct types -`Name :: distinct T` creates a nominal identity and reuses `T`'s runtime representation. -Construction accepts exactly one value of the immediate backing type. There is no implicit -conversion in either direction, and separate distinct declarations never mix. An explicit scalar -cast extracts exactly one layer: `u32(id)` works for `UserID :: distinct u32`, while nested -distinct values must be peeled one declared layer at a time. +`Name :: distinct T` creates a nominal identity and reuses `T`'s runtime representation. When +`T` is a concrete numeric scalar, construction first applies the corresponding explicit scalar +cast, so `UserID(index)` is sufficient for `UserID :: distinct u32` even when `index` is `usize`. +There is still no implicit conversion in either direction. Construction with a non-scalar or +distinct immediate backing requires that exact backing type. An explicit scalar cast extracts one +layer: `u32(id)` works for `UserID`, while nested distinct values must be peeled one declared layer +at a time. Scalar-backed distinct values support the operations of their representation while preserving the nominal result type: checked integer `+`, `-`, `*`, unary `-`, bitwise operators, shifts, comparisons, and compound assignments; float arithmetic, unary `-`, comparisons, and compound -assignments; and boolean equality/inequality. Integer literals and float literals are contextual, -but typed backing values remain barred. Distinct integers also work as indices and slice bounds; +assignments; and boolean equality/inequality. Integer literals and float literals are contextual. +Typed backing values and separate distinct identities remain incompatible in ordinary operations; +an explicit constructor is required to cross that boundary. Distinct integers also work as indices and slice bounds; `minval!` / `maxval!` return the distinct type. Runtime and comptime behavior match. `typeinfo!(Distinct).backing` reports the immediate declared backing. Standard formatting peels diff --git a/TODO.md b/TODO.md index e8c5973..309028c 100644 --- a/TODO.md +++ b/TODO.md @@ -139,8 +139,9 @@ 8. distinct types (implemented; see below) - nominal declarations preserve identity across packages and reuse the backing runtime representation - - construction accepts exactly one value of the immediate backing type; no implicit conversion - crosses the nominal boundary or mixes separate distinct declarations + - construction of a numeric scalar-backed distinct type applies the backing's explicit scalar + cast before wrapping; non-scalar and nested-distinct backings still require the exact immediate type + - no implicit conversion crosses the nominal boundary or mixes separate distinct declarations - explicit scalar casts extract one declared distinct layer at a time - scalar-backed values support matching runtime/comptime arithmetic, bitwise, shift, comparison, compound-assignment, bounds, indexing, reflection, and standard formatting behavior @@ -1061,15 +1062,17 @@ For-loop captures are immutable and scoped to the loop body. Sequence index capt ## A word on distinct types -Distinct declarations are nominal even when they share a backing type. Construction requires the -exact immediate backing, implicit conversion is forbidden in either direction, and an explicit -scalar cast extracts one layer: +Distinct declarations are nominal even when they share a backing type. A constructor for a +numeric scalar-backed distinct type first performs the backing's explicit scalar cast, while +implicit conversion remains forbidden in either direction. Explicit scalar casts extract one +layer: ```bro UserID :: distinct u32 OuterID :: distinct UserID -id UserID :: UserID(u32(42)) +index usize = 42 +id UserID :: UserID(index) raw u32 :: u32(id) outer OuterID :: OuterID(id) inner UserID :: UserID(outer) @@ -1079,7 +1082,8 @@ Scalar-backed distinct values retain their nominal type across the operations su backing scalar. Integer forms support checked arithmetic, bitwise operations, shifts, comparisons, compound assignments, indexing, slicing, and `minval!` / `maxval!`; float forms support arithmetic and comparisons; boolean forms support equality and inequality. Separate distinct identities and -typed backing operands never mix, though literals receive the distinct context. Runtime and +typed backing operands never mix implicitly or in ordinary operations; crossing between numeric +representations requires an explicit constructor or scalar cast. Runtime and comptime rules are identical. Reflection reports the immediate backing, while standard formatting recursively follows nested distinct backings to the final scalar. diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 4f22483..884ca0a 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -935,6 +935,14 @@ valid_layout_type :: proc(checker: ^Checker, value: types.Type) -> bool { return types.is_runtime_value(value, &checker.module.types) } +is_integer_bound_type :: proc(checker: ^Checker, value: types.Type) -> bool { + representation := value + if backing, ok := types.distinct_scalar_backing(value, &checker.module.types); ok { + representation = backing + } + return types.is_concrete_integer(representation) +} + type_builtin_value :: proc(checker: ^Checker, kind: Type_Builtin, value: types.Type) -> i128 { #partial switch kind { case .Size_Of: @@ -984,11 +992,7 @@ build_type_builtin :: proc( id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target)) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } - bound_representation := target - if backing, ok := types.distinct_scalar_backing(target, &checker.module.types); ok { - bound_representation = backing - } - if (kind == .Min_Value || kind == .Max_Value) && !types.is_concrete_integer(bound_representation) { + if (kind == .Min_Value || kind == .Max_Value) && !is_integer_bound_type(checker, target) { id := source.addf(checker.diagnostics, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target)) return invalid_hir_expr(checker, expr.span, id, types.USIZE) } @@ -5423,7 +5427,7 @@ infer_expr :: proc( last = types.USIZE } else if len(expr.args) == 1 { target, ok := resolve_type_argument(checker, expr.args[0], pkg, file) - last = target if ok && types.is_concrete_integer(target) else types.INVALID + last = target if ok && is_integer_bound_type(checker, target) else types.INVALID } else { last = types.INVALID } @@ -6821,7 +6825,7 @@ infer_all :: proc(checker: ^Checker) { checker.global_types[index] = types.USIZE } else if len(expr.args) == 1 { target, ok := resolve_type_argument(checker, expr.args[0], global.pkg, global.file) - if ok && types.is_concrete_integer(target) { + if ok && is_integer_bound_type(checker, target) { checker.global_types[index] = target } } @@ -10313,14 +10317,21 @@ build_expr :: proc( if frame.stage == 8 { distinct_item, ok := types.node(&checker.module.types, frame.target_type) actual := checker.module.exprs[last].type + if ok && distinct_item.kind == .Distinct && + !types.can_retype_distinct(actual, frame.target_type, &checker.module.types) { + backing := types.resolve_alias(distinct_item.child, &checker.module.types) + if types.is_concrete_scalar(backing) && !types.is_bool(backing) { + last = build_scalar_cast(checker, last, backing, expr.span) + actual = checker.module.exprs[last].type + } + } if !ok || distinct_item.kind != .Distinct || !types.can_retype_distinct(actual, frame.target_type, &checker.module.types) { id := source.addf( checker.diagnostics, expr.span, - "distinct type '%s' requires an exact %s value, got %s", + "cannot construct distinct type '%s' from %s", symbol_text(checker, expr.name), - types.name(distinct_item.child), types.name(actual), ) last = invalid_hir_expr(checker, expr.span, id, frame.target_type) diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index ef8fa91..96864e3 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -3651,11 +3651,7 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type if (builtin == .Size_Of || builtin == .Align_Of) && !valid_layout_type(checker, target) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "layout target must be a sized runtime value type, got %s", type_label(checker, target)) } - bound_representation := target - if backing, ok := types.distinct_scalar_backing(target, &checker.module.types); ok { - bound_representation = backing - } - if (builtin == .Min_Value || builtin == .Max_Value) && !types.is_concrete_integer(bound_representation) { + if (builtin == .Min_Value || builtin == .Max_Value) && !is_integer_bound_type(checker, target) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, checker.ast_module.exprs[expr.args[0]].span, "integer bound target must be a concrete integer type, got %s", type_label(checker, target)) } result_type := types.USIZE if builtin == .Size_Of || builtin == .Align_Of else target @@ -3743,14 +3739,23 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type return INVALID_CT_VALUE, flow, ok } actual := state.values[value].type + if !types.can_retype_distinct(actual, target, &checker.module.types) { + backing := types.resolve_alias(target_item.child, &checker.module.types) + if types.is_concrete_scalar(backing) && !types.is_bool(backing) { + value, flow, ok = ct_scalar_cast(state, value, backing, expr.span) + if !ok || flow.kind != .Normal { + return INVALID_CT_VALUE, flow, ok + } + actual = state.values[value].type + } + } if !types.can_retype_distinct(actual, target, &checker.module.types) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( state, .Not_Comptime, expr.span, - "distinct type '%s' requires an exact %s value, got %s", + "cannot construct distinct type '%s' from %s", symbol_text(checker, expr.name), - types.name(target_item.child), types.name(actual), ) } diff --git a/compiler_tests.odin b/compiler_tests.odin index 8f25c6b..e140614 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -13323,6 +13323,40 @@ main func() i32 { testing.expect(t, retype_count >= 5) } +@(test) +distinct_construction_casts_to_scalar_backing_and_infers_bound_global :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-distinct-construction" + main_path := "/tmp/brolang-test-distinct-construction/main.bro" + output := "/tmp/brolang-test-distinct-construction-output" + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + defer _ = os.remove(output) + testing.expect(t, os2.make_directory_all(directory) == nil) + text := `UserID :: distinct u32 +OtherID :: distinct u32 +NO_ID :: maxval!(UserID) +Data :: union { id UserID } +Record :: struct { data Data = Data{ id = NO_ID } } +main func() i32 { + small u8 = 7 + wide usize = 8 + a UserID = UserID(small) + b UserID = UserID(wide) + c UserID = UserID(OtherID(9)) + d UserID :: $UserID(usize(10)) + record Record = {} + _ = record + if u32(a) != 7 or u32(b) != 8 or u32(c) != 9 or u32(d) != 10 { return 1 } + return 0 +} +` + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + testing.expect_value(t, compiler_core.compile_package(directory, output), 0) + state := run_executable(output) + testing.expect_value(t, state.exit_code, 0) +} + + @(test) distinct_types_reject_implicit_conversions_and_invalid_backings :: proc(t: ^testing.T) { text := `Opaque :: opaque @@ -13339,8 +13373,6 @@ main func() void { id UserID = raw backing u32 = UserID(2) other OtherID = UserID(3) - narrow u8 = 4 - _ = UserID(narrow) _ = UserID() _ = UserID(1, 2) left UserID :: UserID(5) @@ -13368,7 +13400,6 @@ main func() void { invalid_backing_count := 0 implicit_conversion_count := 0 - found_exact := false found_arity := false found_arithmetic := false found_comparison := false @@ -13376,7 +13407,6 @@ main func() void { for diagnostic in diagnostics.items { invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0 implicit_conversion_count += 1 if strings.contains(diagnostic.message, "cannot implicitly convert") else 0 - found_exact = found_exact || strings.contains(diagnostic.message, "requires an exact u32 value, got u8") found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument") found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") found_comparison = found_comparison || strings.contains(diagnostic.message, "comparison requires compatible numeric operands") @@ -13384,7 +13414,6 @@ main func() void { } testing.expect_value(t, invalid_backing_count, 4) testing.expect(t, implicit_conversion_count >= 3) - testing.expect(t, found_exact) testing.expect(t, found_arity) testing.expect(t, found_arithmetic) testing.expect(t, found_comparison)