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
- 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
+11 -7
View File
@@ -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.
+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)
}
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)
+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) {
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),
)
}
+34 -5
View File
@@ -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)