refine distinct construction semantics

This commit is contained in:
2026-08-02 19:13:13 +02:00
parent b9526b5f06
commit f25f76adff
5 changed files with 88 additions and 36 deletions
+11 -8
View File
@@ -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 - 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 - 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 - 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)` - 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 - 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 - 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 #### distinct types
`Name :: distinct T` creates a nominal identity and reuses `T`'s runtime representation. `Name :: distinct T` creates a nominal identity and reuses `T`'s runtime representation. When
Construction accepts exactly one value of the immediate backing type. There is no implicit `T` is a concrete numeric scalar, construction first applies the corresponding explicit scalar
conversion in either direction, and separate distinct declarations never mix. An explicit scalar cast, so `UserID(index)` is sufficient for `UserID :: distinct u32` even when `index` is `usize`.
cast extracts exactly one layer: `u32(id)` works for `UserID :: distinct u32`, while nested There is still no implicit conversion in either direction. Construction with a non-scalar or
distinct values must be peeled one declared layer at a time. 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 Scalar-backed distinct values support the operations of their representation while preserving the
nominal result type: checked integer `+`, `-`, `*`, unary `-`, bitwise operators, shifts, nominal result type: checked integer `+`, `-`, `*`, unary `-`, bitwise operators, shifts,
comparisons, and compound assignments; float arithmetic, unary `-`, comparisons, and compound comparisons, and compound assignments; float arithmetic, unary `-`, comparisons, and compound
assignments; and boolean equality/inequality. Integer literals and float literals are contextual, 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; 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. `minval!` / `maxval!` return the distinct type. Runtime and comptime behavior match.
`typeinfo!(Distinct).backing` reports the immediate declared backing. Standard formatting peels `typeinfo!(Distinct).backing` reports the immediate declared backing. Standard formatting peels
+11 -7
View File
@@ -139,8 +139,9 @@
8. distinct types (implemented; see below) 8. distinct types (implemented; see below)
- nominal declarations preserve identity across packages and reuse the backing runtime representation - 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 - construction of a numeric scalar-backed distinct type applies the backing's explicit scalar
crosses the nominal boundary or mixes separate distinct declarations 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 - explicit scalar casts extract one declared distinct layer at a time
- scalar-backed values support matching runtime/comptime arithmetic, bitwise, shift, comparison, - scalar-backed values support matching runtime/comptime arithmetic, bitwise, shift, comparison,
compound-assignment, bounds, indexing, reflection, and standard formatting behavior 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 ## A word on distinct types
Distinct declarations are nominal even when they share a backing type. Construction requires the Distinct declarations are nominal even when they share a backing type. A constructor for a
exact immediate backing, implicit conversion is forbidden in either direction, and an explicit numeric scalar-backed distinct type first performs the backing's explicit scalar cast, while
scalar cast extracts one layer: implicit conversion remains forbidden in either direction. Explicit scalar casts extract one
layer:
```bro ```bro
UserID :: distinct u32 UserID :: distinct u32
OuterID :: distinct UserID OuterID :: distinct UserID
id UserID :: UserID(u32(42)) index usize = 42
id UserID :: UserID(index)
raw u32 :: u32(id) raw u32 :: u32(id)
outer OuterID :: OuterID(id) outer OuterID :: OuterID(id)
inner UserID :: UserID(outer) 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, backing scalar. Integer forms support checked arithmetic, bitwise operations, shifts, comparisons,
compound assignments, indexing, slicing, and `minval!` / `maxval!`; float forms support arithmetic compound assignments, indexing, slicing, and `minval!` / `maxval!`; float forms support arithmetic
and comparisons; boolean forms support equality and inequality. Separate distinct identities and 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 comptime rules are identical. Reflection reports the immediate backing, while standard formatting
recursively follows nested distinct backings to the final scalar. recursively follows nested distinct backings to the final scalar.
+20 -9
View File
@@ -935,6 +935,14 @@ valid_layout_type :: proc(checker: ^Checker, value: types.Type) -> bool {
return types.is_runtime_value(value, &checker.module.types) 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 { type_builtin_value :: proc(checker: ^Checker, kind: Type_Builtin, value: types.Type) -> i128 {
#partial switch kind { #partial switch kind {
case .Size_Of: 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)) 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) return invalid_hir_expr(checker, expr.span, id, types.USIZE)
} }
bound_representation := target if (kind == .Min_Value || kind == .Max_Value) && !is_integer_bound_type(checker, 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) {
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)) 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) return invalid_hir_expr(checker, expr.span, id, types.USIZE)
} }
@@ -5423,7 +5427,7 @@ infer_expr :: proc(
last = types.USIZE last = types.USIZE
} else if len(expr.args) == 1 { } else if len(expr.args) == 1 {
target, ok := resolve_type_argument(checker, expr.args[0], pkg, file) 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 { } else {
last = types.INVALID last = types.INVALID
} }
@@ -6821,7 +6825,7 @@ infer_all :: proc(checker: ^Checker) {
checker.global_types[index] = types.USIZE checker.global_types[index] = types.USIZE
} else if len(expr.args) == 1 { } else if len(expr.args) == 1 {
target, ok := resolve_type_argument(checker, expr.args[0], global.pkg, global.file) 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 checker.global_types[index] = target
} }
} }
@@ -10313,14 +10317,21 @@ build_expr :: proc(
if frame.stage == 8 { if frame.stage == 8 {
distinct_item, ok := types.node(&checker.module.types, frame.target_type) distinct_item, ok := types.node(&checker.module.types, frame.target_type)
actual := checker.module.exprs[last].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 || if !ok || distinct_item.kind != .Distinct ||
!types.can_retype_distinct(actual, frame.target_type, &checker.module.types) { !types.can_retype_distinct(actual, frame.target_type, &checker.module.types) {
id := source.addf( id := source.addf(
checker.diagnostics, checker.diagnostics,
expr.span, expr.span,
"distinct type '%s' requires an exact %s value, got %s", "cannot construct distinct type '%s' from %s",
symbol_text(checker, expr.name), symbol_text(checker, expr.name),
types.name(distinct_item.child),
types.name(actual), types.name(actual),
) )
last = invalid_hir_expr(checker, expr.span, id, frame.target_type) last = invalid_hir_expr(checker, expr.span, id, frame.target_type)
+12 -7
View File
@@ -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) { 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)) 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 (builtin == .Min_Value || builtin == .Max_Value) && !is_integer_bound_type(checker, 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) {
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)) 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 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 return INVALID_CT_VALUE, flow, ok
} }
actual := state.values[value].type 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) { if !types.can_retype_distinct(actual, target, &checker.module.types) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf( return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(
state, state,
.Not_Comptime, .Not_Comptime,
expr.span, expr.span,
"distinct type '%s' requires an exact %s value, got %s", "cannot construct distinct type '%s' from %s",
symbol_text(checker, expr.name), symbol_text(checker, expr.name),
types.name(target_item.child),
types.name(actual), types.name(actual),
) )
} }
+34 -5
View File
@@ -13323,6 +13323,40 @@ main func() i32 {
testing.expect(t, retype_count >= 5) 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) @(test)
distinct_types_reject_implicit_conversions_and_invalid_backings :: proc(t: ^testing.T) { distinct_types_reject_implicit_conversions_and_invalid_backings :: proc(t: ^testing.T) {
text := `Opaque :: opaque text := `Opaque :: opaque
@@ -13339,8 +13373,6 @@ main func() void {
id UserID = raw id UserID = raw
backing u32 = UserID(2) backing u32 = UserID(2)
other OtherID = UserID(3) other OtherID = UserID(3)
narrow u8 = 4
_ = UserID(narrow)
_ = UserID() _ = UserID()
_ = UserID(1, 2) _ = UserID(1, 2)
left UserID :: UserID(5) left UserID :: UserID(5)
@@ -13368,7 +13400,6 @@ main func() void {
invalid_backing_count := 0 invalid_backing_count := 0
implicit_conversion_count := 0 implicit_conversion_count := 0
found_exact := false
found_arity := false found_arity := false
found_arithmetic := false found_arithmetic := false
found_comparison := false found_comparison := false
@@ -13376,7 +13407,6 @@ main func() void {
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
invalid_backing_count += 1 if strings.contains(diagnostic.message, "requires a concrete runtime backing type") else 0 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 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_arity = found_arity || strings.contains(diagnostic.message, "expects 1 argument")
found_arithmetic = found_arithmetic || strings.contains(diagnostic.message, "arithmetic requires compatible numeric operands") 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") 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_value(t, invalid_backing_count, 4)
testing.expect(t, implicit_conversion_count >= 3) testing.expect(t, implicit_conversion_count >= 3)
testing.expect(t, found_exact)
testing.expect(t, found_arity) testing.expect(t, found_arity)
testing.expect(t, found_arithmetic) testing.expect(t, found_arithmetic)
testing.expect(t, found_comparison) testing.expect(t, found_comparison)