refine distinct construction semantics
This commit is contained in:
+11
-8
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user