close some gaps in the type system

This commit is contained in:
2026-07-19 01:01:11 +02:00
parent c7e3162ecb
commit 95f90cc306
12 changed files with 367 additions and 78 deletions
+4 -4
View File
@@ -41,7 +41,7 @@ roadmap and milestone history.
- 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
- native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids - native sum composition with `A | B` for unbacked enums and tagged unions, optionally grouped as `(A | B)`, using program-global `u16` variant ids
- fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield` - fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition; `void ! E` functions complete successfully on fallthrough, and void-success `catch` handlers may fall through without `yield`
#### native record constraint fields #### native record constraint fields
@@ -98,7 +98,7 @@ fields. `_` is not a keyword member name.
- `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `expand for` specializes a comptime aggregate into one checked body per element - `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures; `expand for` specializes a comptime aggregate into one checked body per element
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block - `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks; `break :label` can cross nested scopes to exit a labeled block
- bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO - bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO
- bare void `return`, same-line `return value`, value blocks, value `if`, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value` - bare void `return`, same-line `return value`, value blocks, value `if` with implicit single-expression branches, value loops, value `match`, and strictly value-producing `yield value` / `yield :label value`
- `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns - `match` statements/expressions over enums, tagged unions, and scalars, including exhaustiveness checks, payload captures, pointer payload captures, multi-pattern arms, and scalar range patterns
- a final `expand |value|:` enum arm or `expand |payload[, tag]|:` tagged-union arm generates one specialized arm for each variant not covered earlier; enum values and optional tags are comptime-known, while union payloads keep their concrete variant type - a final `expand |value|:` enum arm or `expand |payload[, tag]|:` tagged-union arm generates one specialized arm for each variant not covered earlier; enum values and optional tags are comptime-known, while union payloads keep their concrete variant type
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks - fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
@@ -188,7 +188,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and bare function identities; equal structural values and aliases of one function declaration share specializations, while distinct declarations remain distinct and pointers, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity - comptime parameters may appear anywhere, are erased from the runtime ABI, and accept recursively stable booleans, integers, floats, types, immutable bytes, enums, fixed arrays, records/tuples, optionals, tagged unions, and bare function identities; equal structural values and aliases of one function declaration share specializations, while distinct declarations remain distinct and pointers, general slices, fallibles, ranges, untagged unions, and undefined values have no stable comptime identity
- comptime parameters may be omitted when uniquely recoverable from runtime arguments, the immediate expected result, or exact type-factory provenance; `_` is an explicit inference hole - comptime parameters may be omitted when uniquely recoverable from runtime arguments, the immediate expected result, or exact type-factory provenance; `_` is an explicit inference hole
- forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }` - forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }`
- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed - comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, exact type `==`/`!=`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed
- comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected - comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected
- tuple types are unnamed-field structs (`struct { i32, []u8 }`), tuple values use `{1, "bro"}` / `{1,}` / `{}`, and fields use canonical numeric names such as `.0` - tuple types are unnamed-field structs (`struct { i32, []u8 }`), tuple values use `{1, "bro"}` / `{1,}` / `{}`, and fields use canonical numeric names such as `.0`
- `typeinfo!`, `field!`, `compile_error!`, and semantic `expand for` provide compile-time record and enum reflection and heterogeneous static expansion without runtime metadata; enum reflection exposes declaration-ordered fields, reflected aggregates remain persistent compile-time values, and expand-loop `break` / `continue` must be selected entirely at comptime - `typeinfo!`, `field!`, `compile_error!`, and semantic `expand for` provide compile-time record and enum reflection and heterogeneous static expansion without runtime metadata; enum reflection exposes declaration-ordered fields, reflected aggregates remain persistent compile-time values, and expand-loop `break` / `continue` must be selected entirely at comptime
@@ -216,7 +216,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime - `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime
- entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` - entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io`
- `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init` - `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init`
- `std/testing` supplies fallible `expect` and expected-first `expect_equal`; direct calls through - `std/testing` supplies fallible `expect`, expected-first `expect_equal`, and exact compile-time `expect_type`; direct calls through
an alias of exactly `@std/testing` receive compiler-injected source locations an alias of exactly `@std/testing` receive compiler-injected source locations
### compiler behavior ### compiler behavior
+142 -43
View File
@@ -164,6 +164,8 @@ Generated_Type_Entry :: struct {
expr: ast.Expr_Id, expr: ast.Expr_Id,
values: []Comptime_Value, values: []Comptime_Value,
result: types.Type, result: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
} }
Type_Factory_Origin :: struct { Type_Factory_Origin :: struct {
@@ -493,6 +495,10 @@ write_type_label :: proc(checker: ^Checker, builder: ^strings.Builder, value: ty
write_type_label(checker, builder, item.child) write_type_label(checker, builder, item.child)
strings.write_string(builder, " ! ") strings.write_string(builder, " ! ")
write_type_label(checker, builder, item.extra) write_type_label(checker, builder, item.extra)
case .Sum:
write_type_label(checker, builder, item.child)
strings.write_string(builder, " | ")
write_type_label(checker, builder, item.extra)
case .Type_Call: case .Type_Call:
strings.write_string(builder, "<type factory call>") strings.write_string(builder, "<type factory call>")
case .Struct: case .Struct:
@@ -1136,7 +1142,14 @@ type_from_syntax :: proc(
return types.INVALID return types.INVALID
} }
} }
case .Pointer, .Slice, .Optional, .Range, .Distinct, .Enum, .Fallible: return types.intern(store, item)
case .Pointer, .Slice, .Optional, .Range, .Fallible:
child := type_from_syntax(checker, item.child, pkg, file, depth+1)
extra := type_from_syntax(checker, item.extra, pkg, file, depth+1)
item.child = child
item.extra = extra
return types.intern(store, item)
case .Distinct, .Enum:
child := type_from_syntax(checker, item.child, pkg, file, depth+1) child := type_from_syntax(checker, item.child, pkg, file, depth+1)
extra := type_from_syntax(checker, item.extra, pkg, file, depth+1) extra := type_from_syntax(checker, item.extra, pkg, file, depth+1)
changed = child != item.child || extra != item.extra changed = child != item.child || extra != item.extra
@@ -1146,15 +1159,24 @@ type_from_syntax :: proc(
params := types.params_for(store, value) params := types.params_for(store, value)
resolved_params := make([]types.Type, len(params), checker.allocator) resolved_params := make([]types.Type, len(params), checker.allocator)
defer delete(resolved_params, checker.allocator) defer delete(resolved_params, checker.allocator)
params_changed := false
for param, index in params { for param, index in params {
resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1) resolved_params[index] = type_from_syntax(checker, param.type, pkg, file, depth+1)
params_changed = params_changed || resolved_params[index] != param.type
} }
result := type_from_syntax(checker, item.child, pkg, file, depth+1) result := type_from_syntax(checker, item.child, pkg, file, depth+1)
if params_changed || result != item.child {
return types.function(store, resolved_params, result, item.c_abi, item.variadic) return types.function(store, resolved_params, result, item.c_abi, item.variadic)
case .Sum:
left := type_from_syntax(checker, item.child, pkg, file, depth+1)
right := type_from_syntax(checker, item.extra, pkg, file, depth+1)
composed, compose_error := types.compose_sum(store, left, right)
if compose_error == .Unsupported {
source.add(checker.diagnostics, source.Span{}, "only native unbacked enums and tagged unions can be composed with '|'")
return types.INVALID
} }
if compose_error == .Conflict {
source.add(checker.diagnostics, source.Span{}, "sum composition contains the same variant name with different payload types")
return types.INVALID
}
return composed
case .Type_Call: case .Type_Call:
return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file) return resolve_type_factory_call(checker, ast.Expr_Id(item.count_expr), pkg, file)
} }
@@ -1176,6 +1198,31 @@ is_runtime_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)
} }
type_contains_unresolved_named :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool {
if depth > 64 {
return true
}
item, ok := types.node(&checker.module.types, value)
if !ok {
return false
}
if item.kind == .Named && !item.declared {
return true
}
if type_contains_unresolved_named(checker, item.child, depth+1) ||
type_contains_unresolved_named(checker, item.extra, depth+1) {
return true
}
if item.kind == .Function {
for param in types.params_for(&checker.module.types, value) {
if type_contains_unresolved_named(checker, param.type, depth+1) {
return true
}
}
}
return false
}
is_comptime_value_type :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool { is_comptime_value_type :: proc(checker: ^Checker, value: types.Type, depth := 0) -> bool {
if depth > 256 || !types.is_valid(value) { if depth > 256 || !types.is_valid(value) {
return false return false
@@ -1960,6 +2007,9 @@ explicit_comptime_argument_valid :: proc(
if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) { if types.is_concrete_integer(type_from_syntax(checker, param.type, function.pkg, function.file)) {
return eval_integer_constant_in_context(checker, arg, pkg, file).kind == .Value return eval_integer_constant_in_context(checker, arg, pkg, file).kind == .Value
} }
if type_pattern_mentions_comptime(checker, function, comptime_param_count(function), param.type) {
return true
}
declared := type_from_syntax(checker, param.type, function.pkg, function.file) declared := type_from_syntax(checker, param.type, function.pkg, function.file)
_, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file) _, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file)
return ok return ok
@@ -2324,11 +2374,14 @@ match_inferred_type_pattern :: proc(
pattern_item, pattern_ok := types.node(store, pattern) pattern_item, pattern_ok := types.node(store, pattern)
if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 { if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 {
name := symbol.Id(pattern_item.name) name := symbol.Id(pattern_item.name)
if _, is_binding := comptime_binding_index(function, prefix, name); is_binding { if binding_index, is_binding := comptime_binding_index(function, prefix, name); is_binding {
param, param_ok := comptime_param_for_name(function, name) param, param_ok := comptime_param_for_name(function, name)
if !param_ok || !is_comptime_type_param(checker, param) { if !param_ok || !is_comptime_type_param(checker, param) {
return false return false
} }
if bound[binding_index] && can_implicitly_convert_type(checker, actual_type, values[binding_index].type) {
return true
}
return bind_inferred_comptime( return bind_inferred_comptime(
checker, function, prefix, values, bound, name, checker, function, prefix, values, bound, name,
Comptime_Value{type=actual_type, kind=.Type}, span, diagnose, Comptime_Value{type=actual_type, kind=.Type}, span, diagnose,
@@ -2584,7 +2637,6 @@ infer_call_comptime_values :: proc(
values[binding_index] = Comptime_Value{name=param.name, type=declared, value=constant.value, kind=.Integer} values[binding_index] = Comptime_Value{name=param.name, type=declared, value=constant.value, kind=.Integer}
bound[binding_index] = true bound[binding_index] = true
} else { } else {
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
available: [dynamic]Comptime_Value available: [dynamic]Comptime_Value
available.allocator = checker.allocator available.allocator = checker.allocator
append(&available, ..checker.current_comptime_values) append(&available, ..checker.current_comptime_values)
@@ -2593,6 +2645,10 @@ infer_call_comptime_values :: proc(
append(&available, prior) append(&available, prior)
} }
} }
previous := checker.current_comptime_values
checker.current_comptime_values = available[:]
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
checker.current_comptime_values = previous
value, value_ok := eval_static_comptime_value( value, value_ok := eval_static_comptime_value(
checker, param.name, arg_id, declared, pkg, file, available[:], diagnose, checker, param.name, arg_id, declared, pkg, file, available[:], diagnose,
) )
@@ -2880,6 +2936,8 @@ resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, p
expr=expr_id, expr=expr_id,
values=clone_comptime_values(checker.current_comptime_values, checker.allocator), values=clone_comptime_values(checker.current_comptime_values, checker.allocator),
result=result, result=result,
pkg=pkg,
file=file,
}) })
return result return result
} }
@@ -2961,7 +3019,19 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg:
ct_state_destroy(&state) ct_state_destroy(&state)
checker.type_factories[entry_index].result = result checker.type_factories[entry_index].result = result
checker.type_factories[entry_index].resolving = false checker.type_factories[entry_index].resolving = false
if types.is_valid(result) { record_type_factory_origin(checker, result, template, values)
return result
}
record_type_factory_origin :: proc(
checker: ^Checker,
result: types.Type,
template: ast.Function_Id,
values: []Comptime_Value,
) {
if !types.is_valid(result) {
return
}
generated := false generated := false
for entry in checker.generated_types { for entry in checker.generated_types {
if types.equal(entry.result, result) { if types.equal(entry.result, result) {
@@ -2969,24 +3039,20 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg:
break break
} }
} }
if generated { if !generated {
has_origin := false return
}
for origin in checker.type_factory_origins { for origin in checker.type_factory_origins {
if types.equal(origin.result, result) { if origin.template == template && types.equal(origin.result, result) &&
has_origin = true comptime_values_equal(origin.values, values) {
break return
} }
} }
if !has_origin {
append(&checker.type_factory_origins, Type_Factory_Origin{ append(&checker.type_factory_origins, Type_Factory_Origin{
result=result, result=result,
template=template, template=template,
values=clone_comptime_values(values, checker.allocator), values=clone_comptime_values(values, checker.allocator),
}) })
}
}
}
return result
} }
collect_comptime_values :: proc( collect_comptime_values :: proc(
@@ -3056,7 +3122,15 @@ collect_comptime_values :: proc(
}) })
continue continue
} }
resolution_values: [dynamic]Comptime_Value
resolution_values.allocator = checker.allocator
append(&resolution_values, ..extra_values)
append(&resolution_values, ..values[:])
previous := checker.current_comptime_values
checker.current_comptime_values = resolution_values[:]
declared := type_from_syntax(checker, param.type, function.pkg, function.file) declared := type_from_syntax(checker, param.type, function.pkg, function.file)
checker.current_comptime_values = previous
delete(resolution_values)
if types.is_concrete_integer(declared) { if types.is_concrete_integer(declared) {
constant := Constant{kind = .Not_Constant} constant := Constant{kind = .Not_Constant}
if index < len(args) { if index < len(args) {
@@ -4037,6 +4111,13 @@ validate_type_nodes :: proc(checker: ^Checker) {
} }
} }
if item.kind == .Function { if item.kind == .Function {
unresolved := type_contains_unresolved_named(checker, item.child)
for param in types.params_for(&checker.module.types, id) {
unresolved = unresolved || type_contains_unresolved_named(checker, param.type)
}
if unresolved {
continue
}
if item.c_abi { if item.c_abi {
if types.kind(item.child, &checker.module.types) == .Fallible { if types.kind(item.child, &checker.module.types) == .Fallible {
source.add(checker.diagnostics, source.Span{}, "c_func pointer results cannot be fallible") source.add(checker.diagnostics, source.Span{}, "c_func pointer results cannot be fallible")
@@ -4545,11 +4626,14 @@ infer_compound_expr :: proc(
if initialized[index] { if initialized[index] {
continue continue
} }
if field_default, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok { if field_default, default_values, ok := find_struct_field_default(checker, value, symbol.Id(field.name)); ok {
previous := checker.current_comptime_values
checker.current_comptime_values = default_values
_ = infer_nested_expr( _ = infer_nested_expr(
checker, field_default.expr, nil, field_default.pkg, field_default.file, checker, field_default.expr, nil, field_default.pkg, field_default.file,
demanded, expected=field.type, demanded, expected=field.type,
) )
checker.current_comptime_values = previous
} }
} }
} }
@@ -6841,15 +6925,35 @@ find_struct_field_default :: proc(
checker: ^Checker, checker: ^Checker,
struct_type: types.Type, struct_type: types.Type,
name: symbol.Id, name: symbol.Id,
) -> (ast.Struct_Field_Default, bool) { ) -> (ast.Struct_Field_Default, []Comptime_Value, bool) {
resolved := types.resolve_alias(struct_type, &checker.module.types) resolved := types.resolve_alias(struct_type, &checker.module.types)
for field_default in checker.ast_module.struct_field_defaults { for field_default in checker.ast_module.struct_field_defaults {
if types.resolve_alias(field_default.record, &checker.module.types) == resolved && if types.resolve_alias(field_default.record, &checker.module.types) == resolved &&
field_default.field == name { field_default.field == name {
return field_default, true return field_default, nil, true
} }
} }
return {}, false for entry in checker.generated_types {
if !types.equal(entry.result, resolved) || entry.expr == ast.INVALID_EXPR ||
int(entry.expr) >= len(checker.ast_module.exprs) {
continue
}
expr := checker.ast_module.exprs[entry.expr]
fields := types.fields_for(&checker.module.types, resolved)
for field, index in fields {
if field.name != u32(name) || index >= len(expr.args) || expr.args[index] == ast.INVALID_EXPR {
continue
}
return ast.Struct_Field_Default{
record=resolved,
field=name,
expr=expr.args[index],
pkg=entry.pkg,
file=entry.file,
}, entry.values, true
}
}
return {}, nil, false
} }
find_tuple_field :: proc(checker: ^Checker, tuple_type: types.Type, index: u64) -> (int, types.Field, bool) { find_tuple_field :: proc(checker: ^Checker, tuple_type: types.Type, index: u64) -> (int, types.Field, bool) {
@@ -7656,10 +7760,6 @@ build_compound_expr :: proc(
id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function") id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function")
return invalid_hir_expr(checker, expr.span, id, success) return invalid_hir_expr(checker, expr.span, id, success)
} }
if !types.equal(success, enclosing_success) {
id := source.add(checker.diagnostics, expr.span, "'try' success type must match the enclosing fallible result")
return invalid_hir_expr(checker, expr.span, id, success)
}
error_type := types.fallible_error(channel_type, store) error_type := types.fallible_error(channel_type, store)
if !types.equal(error_type, enclosing_error) && if !types.equal(error_type, enclosing_error) &&
!types.can_sum_widen(error_type, enclosing_error, store) { !types.can_sum_widen(error_type, enclosing_error, store) {
@@ -8076,11 +8176,14 @@ build_compound_expr :: proc(
if values[index] != hir.INVALID_EXPR { if values[index] != hir.INVALID_EXPR {
continue continue
} }
if field_default, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok { if field_default, default_values, ok := find_struct_field_default(checker, struct_type, symbol.Id(field.name)); ok {
previous := checker.current_comptime_values
checker.current_comptime_values = default_values
values[index] = build_nested_expr( values[index] = build_nested_expr(
checker, field_default.expr, nil, global_reads, calls, checker, field_default.expr, nil, global_reads, calls,
field.type, field_default.pkg, field_default.file, field.type, field_default.pkg, field_default.file,
) )
checker.current_comptime_values = previous
values[index] = coerce_expr( values[index] = coerce_expr(
checker, values[index], field.type, checker, values[index], field.type,
checker.ast_module.exprs[field_default.expr].span, checker.ast_module.exprs[field_default.expr].span,
@@ -11385,6 +11488,18 @@ emit_value_branch :: proc(
) -> bool { ) -> bool {
checker := ctx.checker checker := ctx.checker
n := len(branch_stmts) n := len(branch_stmts)
if n == 1 && checker.ast_module.statements[branch_stmts[0]].kind == .Expression {
expr_stmt := checker.ast_module.statements[branch_stmts[0]]
expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID
value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file)
if checker.module.exprs[value].kind == .Invalid {
ctx.problematic^ = true
return false
}
value = adopt_value_slot(ctx, slot, slot_type, value, checker.module.exprs[value].type, span)
emit_slot_assign(checker, out, slot^, value, span)
return true
}
ends_in_yield := n > 0 && ends_in_yield := n > 0 &&
checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield && checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield &&
!symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label) !symbol.is_valid(checker.ast_module.statements[branch_stmts[n - 1]].label)
@@ -11992,9 +12107,7 @@ build_match_arm_body :: proc(
return result[:], body_ok return result[:], body_ok
} }
// build_value_arm appends a value-match arm's slot assignment(s) to `out`: a single bare // build_value_arm appends a value-match arm's slot assignment(s) to `out`.
// expression yields implicitly; anything else reuses the value-branch rule (trailing
// `yield`, or exit on every path).
build_value_arm :: proc( build_value_arm :: proc(
ctx: ^Build_Ctx, ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id, out: ^[dynamic]hir.Stmt_Id,
@@ -12003,20 +12116,6 @@ build_value_arm :: proc(
slot_type: ^types.Type, slot_type: ^types.Type,
span: source.Span, span: source.Span,
) -> bool { ) -> bool {
checker := ctx.checker
if len(arm_body) == 1 && checker.ast_module.statements[arm_body[0]].kind == .Expression {
expr_stmt := checker.ast_module.statements[arm_body[0]]
expected := slot_type^ if slot^ != hir.INVALID_LOCAL else types.INVALID
value := build_expr(checker, expr_stmt.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, expected, ctx.pkg, ctx.file)
if checker.module.exprs[value].kind == .Invalid {
ctx.problematic^ = true
return false
}
vtype := checker.module.exprs[value].type
value = adopt_value_slot(ctx, slot, slot_type, value, vtype, span)
emit_slot_assign(checker, out, slot^, value, span)
return true
}
return emit_value_branch(ctx, out, arm_body, slot, slot_type, span) return emit_value_branch(ctx, out, arm_body, slot, slot_type, span)
} }
+27 -3
View File
@@ -625,14 +625,19 @@ ct_coerce_value :: proc(state: ^Ct_State, id: Ct_Value_Id, expected: types.Type,
} }
if value.kind == .Function { if value.kind == .Function {
actual_item, actual_ok := types.node(store, value.type) actual_item, actual_ok := types.node(store, value.type)
_, _, expected_function, expected_ok := types.function_pointer(expected, store) _, _, expected_function, expected_pointer := types.function_pointer(expected, store)
expected_ok := expected_pointer
if expected_item, ok := types.node(store, expected); ok && expected_item.kind == .Function {
expected_function = expected
expected_ok = true
}
if actual_ok && actual_item.kind == .Function && expected_ok && if actual_ok && actual_item.kind == .Function && expected_ok &&
types.equal(value.type, expected_function) { types.equal(value.type, expected_function) {
value.type = expected value.type = expected
return ct_add_value(state, value), true return ct_add_value(state, value), true
} }
_, _, actual_function, actual_pointer := types.function_pointer(value.type, store) _, _, actual_function, actual_pointer := types.function_pointer(value.type, store)
if actual_pointer && expected_ok && types.equal(actual_function, expected_function) { if actual_pointer && expected_pointer && types.equal(actual_function, expected_function) {
value.type = expected value.type = expected
return ct_add_value(state, value), true return ct_add_value(state, value), true
} }
@@ -1522,14 +1527,17 @@ ct_eval_struct_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Ty
if values[index] != INVALID_CT_VALUE { if values[index] != INVALID_CT_VALUE {
continue continue
} }
field_default, has_default := find_struct_field_default(checker, struct_type, symbol.Id(field.name)) field_default, default_values, has_default := find_struct_field_default(checker, struct_type, symbol.Id(field.name))
if !has_default { if !has_default {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name))) return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "missing initializer for struct field '%s'", symbol_text(checker, symbol.Id(field.name)))
} }
previous_pkg, previous_file := state.pkg, state.file previous_pkg, previous_file := state.pkg, state.file
previous_comptime := checker.current_comptime_values
state.pkg, state.file = field_default.pkg, field_default.file state.pkg, state.file = field_default.pkg, field_default.file
checker.current_comptime_values = default_values
value, flow, ok := ct_eval_expr(state, field_default.expr, field.type, depth+1) value, flow, ok := ct_eval_expr(state, field_default.expr, field.type, depth+1)
state.pkg, state.file = previous_pkg, previous_file state.pkg, state.file = previous_pkg, previous_file
checker.current_comptime_values = previous_comptime
if !ok || flow.kind != .Normal { if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok return INVALID_CT_VALUE, flow, ok
} }
@@ -2102,6 +2110,16 @@ ct_eval_binary :: proc(state: ^Ct_State, op: ast.Expr_Kind, left_id, right_id: C
left := state.values[left_id] left := state.values[left_id]
right := state.values[right_id] right := state.values[right_id]
is_compare := op == .Eq || op == .Ne || op == .Lt || op == .Le || op == .Gt || op == .Ge is_compare := op == .Eq || op == .Ne || op == .Lt || op == .Le || op == .Gt || op == .Ge
if left.kind == .Type || right.kind == .Type {
if left.kind != .Type || right.kind != .Type || (op != .Eq && op != .Ne) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "type values only support '==' and '!=' with another type")
}
ok := types.equal(types.Type(left.index), types.Type(right.index))
if op == .Ne {
ok = !ok
}
return ct_add_value(state, Ct_Value{kind=.Bool, type=types.BOOL, integer=1 if ok else 0}), ct_flow(.Normal), true
}
if left.kind == .Bool && right.kind == .Bool { if left.kind == .Bool && right.kind == .Bool {
if op != .Eq && op != .Ne { if op != .Eq && op != .Ne {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "bool values only support '==' and '!='") return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "bool values only support '==' and '!='")
@@ -2897,6 +2915,12 @@ ct_eval_template_call :: proc(
if ct_value_references_dead_storage(state, result) { if ct_value_references_dead_storage(state, result) {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage") return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage")
} }
if is_type_metatype_syntax(checker, function.result) && result != INVALID_CT_VALUE &&
int(result) < len(state.values) && state.values[result].kind == .Type {
record_type_factory_origin(
checker, types.Type(state.values[result].index), template, comptime_values,
)
}
if types.is_valid(expected) { if types.is_valid(expected) {
return ct_coerce_expr_value(state, result, expected, span) return ct_coerce_expr_value(state, result, expected, span)
} }
+13
View File
@@ -1747,6 +1747,19 @@ canonical_type :: proc(
mapping[index] = resolved mapping[index] = resolved
return resolved return resolved
} }
if item.kind == .Sum {
left := canonical_type(module, item.child, mapping, visiting)
right := canonical_type(module, item.extra, mapping, visiting)
if resolved, compose_error := types.compose_sum(&module.type_store, left, right); compose_error == .None {
mapping[index] = resolved
return resolved
}
item.child = left
item.extra = right
resolved := types.intern(&module.type_store, item)
mapping[index] = resolved
return resolved
}
if item.kind == .Function { if item.kind == .Function {
params := make([]types.Type, int(item.field_count), context.temp_allocator) params := make([]types.Type, int(item.field_count), context.temp_allocator)
for param, param_index in types.params_for(&module.type_store, value) { for param, param_index in types.params_for(&module.type_store, value) {
+37 -1
View File
@@ -379,6 +379,13 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Bool: case .Keyword_Bool:
advance(parser) advance(parser)
return types.BOOL return types.BOOL
case .Left_Paren:
advance(parser)
grouped := parse_type(parser)
if _, ok := allow(parser, .Right_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected ')' after grouped type")
}
return grouped
case .Keyword_Func, .Keyword_C_Func: case .Keyword_Func, .Keyword_C_Func:
c_abi := tok.kind == .Keyword_C_Func c_abi := tok.kind == .Keyword_C_Func
advance(parser) advance(parser)
@@ -447,6 +454,24 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
return types.INVALID return types.INVALID
} }
sum_syntax_requires_resolution :: proc(parser: ^Parser, value: types.Type, depth := 0) -> bool {
if depth > 64 {
return true
}
item, ok := types.node(&parser.module.type_store, value)
if !ok {
return false
}
if item.kind == .Named || item.kind == .Type_Call {
return true
}
if item.kind == .Alias || item.kind == .Sum {
return sum_syntax_requires_resolution(parser, item.child, depth+1) ||
sum_syntax_requires_resolution(parser, item.extra, depth+1)
}
return false
}
parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type_Syntax { parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type_Syntax {
result := left result := left
for current(parser).kind == .Pipe { for current(parser).kind == .Pipe {
@@ -454,8 +479,12 @@ parse_type_pipe_tail :: proc(parser: ^Parser, left: ast.Type_Syntax) -> ast.Type
right := parse_type_atom(parser) right := parse_type_atom(parser)
composed, compose_error := types.compose_sum(&parser.module.type_store, result, right) composed, compose_error := types.compose_sum(&parser.module.type_store, result, right)
if compose_error == .Unsupported { if compose_error == .Unsupported {
if sum_syntax_requires_resolution(parser, result) || sum_syntax_requires_resolution(parser, right) {
result = types.sum_syntax(&parser.module.type_store, result, right)
} else {
source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'") source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'")
result = types.INVALID result = types.INVALID
}
} else if compose_error == .Conflict { } else if compose_error == .Conflict {
source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types") source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types")
result = types.INVALID result = types.INVALID
@@ -738,9 +767,15 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id {
start := advance(parser) start := advance(parser)
fields: [dynamic]types.Field fields: [dynamic]types.Field
fields.allocator = parser.module.allocator fields.allocator = parser.module.allocator
defaults: [dynamic]ast.Expr_Id
defaults.allocator = parser.module.allocator
tuple := false tuple := false
if !parse_record_body(parser, &fields, "expected '{' after anonymous struct type", tuple_result=&tuple) { if !parse_record_body(
parser, &fields, "expected '{' after anonymous struct type",
tuple_result=&tuple, defaults=&defaults,
) {
delete(fields) delete(fields)
delete(defaults)
return invalid_expr(parser, start.span, "invalid anonymous struct type") return invalid_expr(parser, start.span, "invalid anonymous struct type")
} }
end := previous(parser) end := previous(parser)
@@ -752,6 +787,7 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id {
kind=.Anonymous_Struct_Type, kind=.Anonymous_Struct_Type,
span=span_from(start.span, end.span), span=span_from(start.span, end.span),
integer=u64(field_start)<<32 | u64(field_count), integer=u64(field_start)<<32 | u64(field_count),
args=defaults[:],
tuple=tuple, tuple=tuple,
left=ast.INVALID_EXPR, left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR, right=ast.INVALID_EXPR,
+2 -1
View File
@@ -122,11 +122,12 @@ inject_assertion_locations :: proc(
) { ) {
expect := symbol.intern(symbols, "expect") expect := symbol.intern(symbols, "expect")
expect_equal := symbol.intern(symbols, "expect_equal") expect_equal := symbol.intern(symbols, "expect_equal")
expect_type := symbol.intern(symbols, "expect_type")
original_count := len(module.exprs) original_count := len(module.exprs)
for index in 0..<original_count { for index in 0..<original_count {
expr := &module.exprs[index] expr := &module.exprs[index]
if expr.kind != .Call || expr.intrinsic || !symbol.is_valid(expr.qualifier) || if expr.kind != .Call || expr.intrinsic || !symbol.is_valid(expr.qualifier) ||
(expr.name != expect && expr.name != expect_equal) { (expr.name != expect && expr.name != expect_equal && expr.name != expect_type) {
continue continue
} }
file := source_file_id(module, expr.span.file) file := source_file_id(module, expr.span.file)
+5
View File
@@ -77,6 +77,7 @@ Kind :: enum u8 {
Struct, Struct,
Union, Union,
Fallible, Fallible,
Sum,
Type_Call, Type_Call,
} }
@@ -411,6 +412,10 @@ fallible_error :: proc(value: Type, store: ^Store) -> Type {
return item.extra if ok && item.kind == .Fallible else INVALID return item.extra if ok && item.kind == .Fallible else INVALID
} }
sum_syntax :: proc(store: ^Store, left, right: Type) -> Type {
return intern(store, Node{kind=.Sum, child=left, extra=right})
}
append_sum_variants :: proc(store: ^Store, value: Type, out: ^[dynamic]Sum_Variant) -> bool { append_sum_variants :: proc(store: ^Store, value: Type, out: ^[dynamic]Sum_Variant) -> bool {
item, ok := node(store, value) item, ok := node(store, value)
if !ok { if !ok {
+98 -10
View File
@@ -5812,7 +5812,7 @@ main func() i32 {
testing.expect(t, found_yield) testing.expect(t, found_yield)
testing.expect(t, found_misplaced_yield) testing.expect(t, found_misplaced_yield)
success_text := `A :: enum { local_success_text := `A :: enum {
a a
} }
B :: enum { B :: enum {
@@ -5822,23 +5822,26 @@ Both :: alias A | B
fs func() i64 ! A { fs func() i64 ! A {
return 1 return 1
} }
bad_success func() i32 ! Both { use_success func() void ! Both {
x :: try fs() x :: try fs()
return x _ = x
} }
main func() i32 { main func() i32 {
return bad_success() catch 0 use_success() catch |_| { return 1 }
return 0
} }
` `
directory := "/tmp/brolang-test-try-success-mismatch" directory := "/tmp/brolang-test-try-local-success"
main_path := "/tmp/brolang-test-try-success-mismatch/main.bro" main_path := "/tmp/brolang-test-try-local-success/main.bro"
output := "/tmp/brolang-test-try-success-mismatch-output" output := "/tmp/brolang-test-try-local-success-output"
_ = os2.remove_all(directory) _ = os2.remove_all(directory)
defer _ = os2.remove_all(directory) defer _ = os2.remove_all(directory)
defer _ = os.remove(output) defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil) testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)success_text)) testing.expect(t, os.write_entire_file(main_path, transmute([]byte)local_success_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 1) 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)
@@ -6032,7 +6035,10 @@ OtherError :: union(enum) {
} }
other void other void
} }
BothError :: alias PayloadError | OtherError BothError :: alias (PayloadError | OtherError)
grouped_error func() void ! (PayloadError | OtherError) {
return .other
}
accept func(value PayloadError) i32 { accept func(value PayloadError) i32 {
match value { match value {
.not_found |info|: return info.path + info.line .not_found |info|: return info.path + info.line
@@ -13609,6 +13615,10 @@ milestone_44_struct_field_defaults_compile_and_run :: proc(t: ^testing.T) {
Lists :: struct { items [][]u8 = &[] } Lists :: struct { items [][]u8 = &[] }
Generated func($T type, $initial i32) type {
return struct { value T = initial }
}
read func(value Config) i32 { read func(value Config) i32 {
return value.required + value.count return value.required + value.count
} }
@@ -13619,10 +13629,12 @@ main func() i32 {
defaults Config = Config{required = 35} defaults Config = Config{required = 35}
overridden Config = Config{count = 1, enabled = false, name = "x", required = 40} overridden Config = Config{count = 1, enabled = false, name = "x", required = 40}
lists Lists = Lists{} lists Lists = Lists{}
generated Generated(i32, 9) = Generated(i32, 9){}
if (ANSWER != 42) return 1 if (ANSWER != 42) return 1
if (defaults.count != 7 or defaults.enabled == false) return 2 if (defaults.count != 7 or defaults.enabled == false) return 2
if (defaults.name.len != 3 or lists.items.len != 0) return 3 if (defaults.name.len != 3 or lists.items.len != 0) return 3
if (overridden.count != 1 or overridden.enabled or overridden.name.len != 1) return 4 if (overridden.count != 1 or overridden.enabled or overridden.name.len != 1) return 4
if (generated.value != 9) return 5
return 0 return 0
} }
` `
@@ -13635,3 +13647,79 @@ main func() i32 {
state := run_executable(output) state := run_executable(output)
testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, state.exit_code, 0)
} }
@(test)
dependent_comptime_callbacks_and_imported_grouped_sums_compile :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-dependent-callbacks"
app := "/tmp/brolang-test-dependent-callbacks/app"
dependency := "/tmp/brolang-test-dependent-callbacks/dependency"
app_path := "/tmp/brolang-test-dependent-callbacks/app/main.bro"
dependency_path := "/tmp/brolang-test-dependent-callbacks/dependency/errors.bro"
output := "/tmp/brolang-test-dependent-callbacks-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os2.make_directory_all(app) == nil)
testing.expect(t, os2.make_directory_all(dependency) == nil)
dependency_text := `AllocError :: enum { out_of_memory }
`
app_text := `dependency :: import "../dependency"
PutError :: enum { key_exists }
Entry func($K, $V type) type {
return struct { key K, value V }
}
Map func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
) type {
return struct { entry Entry(K, V) }
}
hash func(key []u8) usize { return key.len }
eql func(a, b []u8) bool { return a.len == b.len }
StringMap func($V type) type {
return Map([]u8, V, hash, eql)
}
make func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
) Map(K, V, hash_key, keys_eql) {
return Map(K, V, hash_key, keys_eql) {
entry = Entry(K, V) {key = "", value = 0},
}
}
put func(
$K, $V type,
$hash_key func(key K) usize,
$keys_eql func(a, b K) bool,
map @Map(K, V, hash_key, keys_eql),
key K,
) void {
_ = map
_ = key
}
fallible func() void ! (PutError | dependency.AllocError) { return .key_exists }
main func() void {
map StringMap(u32) = make()
put(&map, "key")
_ = map
fallible() catch |_| {}
}
`
testing.expect(t, os.write_entire_file(dependency_path, transmute([]byte)dependency_text))
testing.expect(t, os.write_entire_file(app_path, transmute([]byte)app_text))
testing.expect_value(t, compiler_core.compile_package(app, output), 0)
state := run_executable(output)
testing.expect_value(t, state.exit_code, 0)
}
@@ -38,6 +38,10 @@ fixed_len func($T type, $N usize, value @Fixed(T, N)) usize {
return value.values.len return value.values.len
} }
same_type func($Expected, $Actual type, _ Actual) bool {
return $(Expected == Actual)
}
take_i32 func(value i32) i32 { take_i32 func(value i32) i32 {
return value return value
} }
@@ -87,5 +91,12 @@ main func() i32 {
_ = assigned _ = assigned
_ = take_i32(zero()) _ = take_i32(zero())
_ = return_zero() _ = return_zero()
optional ?u32 = none
if !same_type(?u32, optional) {
return 8
}
if same_type(?u16, optional) {
return 9
}
return 0 return 0
} }
+7
View File
@@ -85,6 +85,11 @@ vif_defer func() i32 {
return r # 5 return r # 5
} }
vif_expression func(old_entries []u8) usize {
new_size :: if (old_entries.len > 0) old_entries.len * 2 else 8
return new_size
}
# --- value loops (milestone 20.5) -------------------------------------------- # --- value loops (milestone 20.5) --------------------------------------------
# Labeled `for` used as a value: `yield :blk i` exits early with a value, the # Labeled `for` used as a value: `yield :blk i` exits early with a value, the
@@ -306,6 +311,8 @@ main func() i32 {
if (vif_reassign(0) != 7) return 110 if (vif_reassign(0) != 7) return 110
if (vif_reassign(9) != 9) return 111 if (vif_reassign(9) != 9) return 111
if (vif_defer() != 5) return 112 if (vif_defer() != 5) return 112
if (vif_expression("") != 8) return 136
if (vif_expression("abc") != 6) return 137
if (loop_search() != 0) return 113 if (loop_search() != 0) return 113
if (loop_none() != 0) return 114 if (loop_none() != 0) return 114
+1
View File
@@ -7,6 +7,7 @@ handles_append test {
try append(&list, 42) try append(&list, 42)
try testing.expect_type(usize, list.items.len)
try testing.expect_equal(1, list.items.len) try testing.expect_equal(1, list.items.len)
try testing.expect_equal(42, list.items[0]) try testing.expect_equal(42, list.items[0])
} }
+4
View File
@@ -24,6 +24,10 @@ expect_equal func($T type, expected, actual T, location SourceLocation) void ! E
} }
} }
expect_type func($Expected, $Actual type, _ Actual, location SourceLocation) void ! Error {
try expect($(Expected == Actual), location)
}
run func(name []u8, callback *func() void ! Error) bool { run func(name []u8, callback *func() void ! Error) bool {
callback() catch |_| { callback() catch |_| {
debug.print("{s} [failed]\n", {name,}) debug.print("{s} [failed]\n", {name,})