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)`
- 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
- 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`
#### 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
- `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 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
- 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
@@ -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 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 }`
- 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
- 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
@@ -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
- 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/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
### compiler behavior
+156 -57
View File
@@ -164,6 +164,8 @@ Generated_Type_Entry :: struct {
expr: ast.Expr_Id,
values: []Comptime_Value,
result: types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
}
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)
strings.write_string(builder, " ! ")
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:
strings.write_string(builder, "<type factory call>")
case .Struct:
@@ -1136,7 +1142,14 @@ type_from_syntax :: proc(
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)
extra := type_from_syntax(checker, item.extra, pkg, file, depth+1)
changed = child != item.child || extra != item.extra
@@ -1146,15 +1159,24 @@ type_from_syntax :: proc(
params := types.params_for(store, value)
resolved_params := make([]types.Type, len(params), checker.allocator)
defer delete(resolved_params, checker.allocator)
params_changed := false
for param, index in params {
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)
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:
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)
}
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 {
if depth > 256 || !types.is_valid(value) {
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)) {
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)
_, ok := eval_static_comptime_value(checker, param.name, arg, declared, pkg, file)
return ok
@@ -2324,11 +2374,14 @@ match_inferred_type_pattern :: proc(
pattern_item, pattern_ok := types.node(store, pattern)
if pattern_ok && pattern_item.qualifier == 0 && pattern_item.name != 0 {
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)
if !param_ok || !is_comptime_type_param(checker, param) {
return false
}
if bound[binding_index] && can_implicitly_convert_type(checker, actual_type, values[binding_index].type) {
return true
}
return bind_inferred_comptime(
checker, function, prefix, values, bound, name,
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}
bound[binding_index] = true
} else {
declared := type_from_syntax(checker, param.type, function.pkg, function.file)
available: [dynamic]Comptime_Value
available.allocator = checker.allocator
append(&available, ..checker.current_comptime_values)
@@ -2593,6 +2645,10 @@ infer_call_comptime_values :: proc(
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(
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,
values=clone_comptime_values(checker.current_comptime_values, checker.allocator),
result=result,
pkg=pkg,
file=file,
})
return result
}
@@ -2961,32 +3019,40 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg:
ct_state_destroy(&state)
checker.type_factories[entry_index].result = result
checker.type_factories[entry_index].resolving = false
if types.is_valid(result) {
generated := false
for entry in checker.generated_types {
if types.equal(entry.result, result) {
generated = true
break
}
}
if generated {
has_origin := false
for origin in checker.type_factory_origins {
if types.equal(origin.result, result) {
has_origin = true
break
}
}
if !has_origin {
append(&checker.type_factory_origins, Type_Factory_Origin{
result=result,
template=template,
values=clone_comptime_values(values, checker.allocator),
})
}
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
for entry in checker.generated_types {
if types.equal(entry.result, result) {
generated = true
break
}
}
return result
if !generated {
return
}
for origin in checker.type_factory_origins {
if origin.template == template && types.equal(origin.result, result) &&
comptime_values_equal(origin.values, values) {
return
}
}
append(&checker.type_factory_origins, Type_Factory_Origin{
result=result,
template=template,
values=clone_comptime_values(values, checker.allocator),
})
}
collect_comptime_values :: proc(
@@ -3056,7 +3122,15 @@ collect_comptime_values :: proc(
})
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)
checker.current_comptime_values = previous
delete(resolution_values)
if types.is_concrete_integer(declared) {
constant := Constant{kind = .Not_Constant}
if index < len(args) {
@@ -4037,6 +4111,13 @@ validate_type_nodes :: proc(checker: ^Checker) {
}
}
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 types.kind(item.child, &checker.module.types) == .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] {
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(
checker, field_default.expr, nil, field_default.pkg, field_default.file,
demanded, expected=field.type,
)
checker.current_comptime_values = previous
}
}
}
@@ -6841,15 +6925,35 @@ find_struct_field_default :: proc(
checker: ^Checker,
struct_type: types.Type,
name: symbol.Id,
) -> (ast.Struct_Field_Default, bool) {
) -> (ast.Struct_Field_Default, []Comptime_Value, bool) {
resolved := types.resolve_alias(struct_type, &checker.module.types)
for field_default in checker.ast_module.struct_field_defaults {
if types.resolve_alias(field_default.record, &checker.module.types) == resolved &&
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) {
@@ -7656,10 +7760,6 @@ build_compound_expr :: proc(
id := source.add(checker.diagnostics, expr.span, "'try' requires an enclosing fallible function")
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)
if !types.equal(error_type, enclosing_error) &&
!types.can_sum_widen(error_type, enclosing_error, store) {
@@ -8076,11 +8176,14 @@ build_compound_expr :: proc(
if values[index] != hir.INVALID_EXPR {
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(
checker, field_default.expr, nil, global_reads, calls,
field.type, field_default.pkg, field_default.file,
)
checker.current_comptime_values = previous
values[index] = coerce_expr(
checker, values[index], field.type,
checker.ast_module.exprs[field_default.expr].span,
@@ -11385,6 +11488,18 @@ emit_value_branch :: proc(
) -> bool {
checker := ctx.checker
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 &&
checker.ast_module.statements[branch_stmts[n - 1]].kind == .Yield &&
!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
}
// build_value_arm appends a value-match arm's slot assignment(s) to `out`: a single bare
// expression yields implicitly; anything else reuses the value-branch rule (trailing
// `yield`, or exit on every path).
// build_value_arm appends a value-match arm's slot assignment(s) to `out`.
build_value_arm :: proc(
ctx: ^Build_Ctx,
out: ^[dynamic]hir.Stmt_Id,
@@ -12003,20 +12116,6 @@ build_value_arm :: proc(
slot_type: ^types.Type,
span: source.Span,
) -> 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)
}
+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 {
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 &&
types.equal(value.type, expected_function) {
value.type = expected
return ct_add_value(state, value), true
}
_, _, 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
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 {
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 {
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_comptime := checker.current_comptime_values
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)
state.pkg, state.file = previous_pkg, previous_file
checker.current_comptime_values = previous_comptime
if !ok || flow.kind != .Normal {
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]
right := state.values[right_id]
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 op != .Eq && op != .Ne {
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) {
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) {
return ct_coerce_expr_value(state, result, expected, span)
}
+13
View File
@@ -1747,6 +1747,19 @@ canonical_type :: proc(
mapping[index] = 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 {
params := make([]types.Type, int(item.field_count), context.temp_allocator)
for param, param_index in types.params_for(&module.type_store, value) {
+39 -3
View File
@@ -379,6 +379,13 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
case .Keyword_Bool:
advance(parser)
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:
c_abi := tok.kind == .Keyword_C_Func
advance(parser)
@@ -447,6 +454,24 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
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 {
result := left
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)
composed, compose_error := types.compose_sum(&parser.module.type_store, result, right)
if compose_error == .Unsupported {
source.add(parser.diagnostics, operator.span, "only native unbacked enums and tagged unions can be composed with '|'")
result = types.INVALID
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 '|'")
result = types.INVALID
}
} else if compose_error == .Conflict {
source.add(parser.diagnostics, operator.span, "sum composition contains the same variant name with different payload types")
result = types.INVALID
@@ -738,9 +767,15 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id {
start := advance(parser)
fields: [dynamic]types.Field
fields.allocator = parser.module.allocator
defaults: [dynamic]ast.Expr_Id
defaults.allocator = parser.module.allocator
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(defaults)
return invalid_expr(parser, start.span, "invalid anonymous struct type")
}
end := previous(parser)
@@ -752,6 +787,7 @@ parse_anonymous_struct_type_expr :: proc(parser: ^Parser) -> ast.Expr_Id {
kind=.Anonymous_Struct_Type,
span=span_from(start.span, end.span),
integer=u64(field_start)<<32 | u64(field_count),
args=defaults[:],
tuple=tuple,
left=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_equal := symbol.intern(symbols, "expect_equal")
expect_type := symbol.intern(symbols, "expect_type")
original_count := len(module.exprs)
for index in 0..<original_count {
expr := &module.exprs[index]
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
}
file := source_file_id(module, expr.span.file)
+5
View File
@@ -77,6 +77,7 @@ Kind :: enum u8 {
Struct,
Union,
Fallible,
Sum,
Type_Call,
}
@@ -411,6 +412,10 @@ fallible_error :: proc(value: Type, store: ^Store) -> Type {
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 {
item, ok := node(store, value)
if !ok {
+98 -10
View File
@@ -5812,7 +5812,7 @@ main func() i32 {
testing.expect(t, found_yield)
testing.expect(t, found_misplaced_yield)
success_text := `A :: enum {
local_success_text := `A :: enum {
a
}
B :: enum {
@@ -5822,23 +5822,26 @@ Both :: alias A | B
fs func() i64 ! A {
return 1
}
bad_success func() i32 ! Both {
use_success func() void ! Both {
x :: try fs()
return x
_ = x
}
main func() i32 {
return bad_success() catch 0
use_success() catch |_| { return 1 }
return 0
}
`
directory := "/tmp/brolang-test-try-success-mismatch"
main_path := "/tmp/brolang-test-try-success-mismatch/main.bro"
output := "/tmp/brolang-test-try-success-mismatch-output"
directory := "/tmp/brolang-test-try-local-success"
main_path := "/tmp/brolang-test-try-local-success/main.bro"
output := "/tmp/brolang-test-try-local-success-output"
_ = os2.remove_all(directory)
defer _ = os2.remove_all(directory)
defer _ = os.remove(output)
testing.expect(t, os.make_directory(directory) == nil)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)success_text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
testing.expect(t, os.write_entire_file(main_path, transmute([]byte)local_success_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)
@@ -6032,7 +6035,10 @@ OtherError :: union(enum) {
}
other void
}
BothError :: alias PayloadError | OtherError
BothError :: alias (PayloadError | OtherError)
grouped_error func() void ! (PayloadError | OtherError) {
return .other
}
accept func(value PayloadError) i32 {
match value {
.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 = &[] }
Generated func($T type, $initial i32) type {
return struct { value T = initial }
}
read func(value Config) i32 {
return value.required + value.count
}
@@ -13619,10 +13629,12 @@ main func() i32 {
defaults Config = Config{required = 35}
overridden Config = Config{count = 1, enabled = false, name = "x", required = 40}
lists Lists = Lists{}
generated Generated(i32, 9) = Generated(i32, 9){}
if (ANSWER != 42) return 1
if (defaults.count != 7 or defaults.enabled == false) return 2
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 (generated.value != 9) return 5
return 0
}
`
@@ -13635,3 +13647,79 @@ main func() i32 {
state := run_executable(output)
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
}
same_type func($Expected, $Actual type, _ Actual) bool {
return $(Expected == Actual)
}
take_i32 func(value i32) i32 {
return value
}
@@ -87,5 +91,12 @@ main func() i32 {
_ = assigned
_ = take_i32(zero())
_ = return_zero()
optional ?u32 = none
if !same_type(?u32, optional) {
return 8
}
if same_type(?u16, optional) {
return 9
}
return 0
}
+7
View File
@@ -85,6 +85,11 @@ vif_defer func() i32 {
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) --------------------------------------------
# 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(9) != 9) return 111
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_none() != 0) return 114
+1
View File
@@ -7,6 +7,7 @@ handles_append test {
try append(&list, 42)
try testing.expect_type(usize, list.items.len)
try testing.expect_equal(1, list.items.len)
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 {
callback() catch |_| {
debug.print("{s} [failed]\n", {name,})