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
|
- 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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user