comptime expandable match statements

This commit is contained in:
2026-07-17 14:29:50 +02:00
parent 1f25e6cd1d
commit 97f1c06057
22 changed files with 120821 additions and 119318 deletions
+4 -2
View File
@@ -89,11 +89,12 @@ fields. `_` is not a keyword member name.
- field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support - field access through struct values and pointers, index/slice bounds contextually coerced to `usize`, and unsigned narrower index support
- boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call - boolean `if` / `else if` / `else` and `for` loops with braceless single-statement bodies when the preceding expression is parenthesized or a function call
- `while` loops with optional post-iteration update clauses - `while` loops with optional post-iteration update clauses
- `for` loops over ranges, arrays, slices, and pointers-to-arrays with copy captures, pointer captures `|@item|`, and optional `usize` index captures - `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`, 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
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks - fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
- direct `return match ...` and `yield match ...` value-control-flow operands - direct `return match ...` and `yield match ...` value-control-flow operands
@@ -156,7 +157,8 @@ as `math.divfloor(a, b)` resolve to ordinary functions.
- 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`, 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 `inline 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 inline-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
- `tag!(value)` reads a tagged union's active discriminant and folds when the value is comptime-known; `tagname!(enum_value)` requires a comptime-known enum value and returns its immutable declaration name
- bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names - bodyful `c_func` definitions and bodyless `c_func` declarations with exact external symbol names
- concrete-only C signatures, C variadic declarations/calls, and C default argument promotions - concrete-only C signatures, C variadic declarations/calls, and C default argument promotions
- native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, and non-variadic native indirect calls - native function pointer values and types with `@func(...) R`, fallible `@func(...) R ! E`, optional `?@func(...) R`, and non-variadic native indirect calls
+12 -2
View File
@@ -851,8 +851,8 @@
- tuples are unnamed-field structs with structural anonymous values, nominal named declarations, - tuples are unnamed-field structs with structural anonymous values, nominal named declarations,
brace literals, numeric fields, and no runtime metadata brace literals, numeric fields, and no runtime metadata
- `@std/meta`, `typeinfo!`, `field!`, `compile_error!`, specialization-time branches, and semantic - `@std/meta`, `typeinfo!`, `field!`, `compile_error!`, specialization-time branches, and semantic
`inline for` use checker-owned persistent compile-time values for aggregate-first reflection and `expand for` use checker-owned persistent compile-time values for aggregate-first reflection and
heterogeneous static expansion; inline-loop control is recursively resolved at comptime heterogeneous static expansion; expand-loop control is recursively resolved at comptime
- interleaved comptime parameters use semantic candidate resolution, immutable byte values specialize - interleaved comptime parameters use semantic candidate resolution, immutable byte values specialize
by contents, and all comptime parameters remain erased from the runtime ABI by contents, and all comptime parameters remain erased from the runtime ABI
- `io.print(writer, format, args)` validates and expands `{s}` / `{d}` formatting at comptime, - `io.print(writer, format, args)` validates and expands `{s}` / `{d}` formatting at comptime,
@@ -885,6 +885,16 @@
- integer output uses one base-aware 65-byte stack buffer; float output uses fixed-buffer - integer output uses one base-aware 65-byte stack buffer; float output uses fixed-buffer
libc `snprintf` with 32-bit and 64-bit general/scientific precision and propagates failure libc `snprintf` with 32-bit and 64-bit general/scientific precision and propagates failure
40. compile-time `expand` (implemented)
- expansion-oriented `inline for` is strictly renamed to `expand for`; `inline` remains available
for future function-inlining syntax
- final expanded enum and tagged-union match arms generate checker-local specialized arms only
for variants not covered by preceding explicit arms
- generated enum values and union tags are static bindings, heterogeneous payloads retain their
concrete types, and `void` payloads support value and pointer captures without runtime storage
- `tag!` reads or folds a tagged union discriminant, while `tagname!` turns a comptime-known enum
value into its immutable field-name string
## A word on unchecked casts ## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions: For casts that bypass safety checks, Honey provides builtin functions:
+3 -2
View File
@@ -180,7 +180,7 @@ Stmt :: struct {
immutable: bool, immutable: bool,
value_control_flow: bool, value_control_flow: bool,
pointer_capture: bool, pointer_capture: bool,
inline: bool, expand: bool,
error_only: bool, error_only: bool,
// Assignments store the lvalue in `target`, the right-hand side in `expr`, // Assignments store the lvalue in `target`, the right-hand side in `expr`,
// and the source operator in `assignment_op`. `Set` is ordinary `=`; // and the source operator in `assignment_op`. `Set` is ordinary `=`;
@@ -207,7 +207,8 @@ Stmt :: struct {
// list (empty marks the `else` arm; more than one is a multi-pattern arm), // list (empty marks the `else` arm; more than one is a multi-pattern arm),
// `captures` for the optional payload capture (0 or 1 name, tagged-union variants // `captures` for the optional payload capture (0 or 1 name, tagged-union variants
// only) with `pointer_capture` distinguishing `|@cap|` from `|cap|`, and `body` // only) with `pointer_capture` distinguishing `|@cap|` from `|cap|`, and `body`
// as the arm body. // as the arm body. An expanded arm has `expand` set, no patterns, and one or two
// captures for its specialized value/payload and optional tagged-union tag.
captures: []symbol.Id, captures: []symbol.Id,
// `Match_Arm` pattern list; empty ⇒ the `else` arm. // `Match_Arm` pattern list; empty ⇒ the `else` arm.
patterns: []Expr_Id, patterns: []Expr_Id,
+403 -69
View File
@@ -59,7 +59,7 @@ Static_Binding :: struct {
value: Ct_Value_Id, value: Ct_Value_Id,
} }
Inline_Expansion :: struct { Expand_Expansion :: struct {
statement: ast.Stmt_Id, statement: ast.Stmt_Id,
index: u32, index: u32,
} }
@@ -175,7 +175,7 @@ Type_Factory_Origin :: struct {
Call_Resolution :: struct { Call_Resolution :: struct {
expr: ast.Expr_Id, expr: ast.Expr_Id,
ctx: []Comptime_Value, ctx: []Comptime_Value,
inline_ctx: []Inline_Expansion, expand_ctx: []Expand_Expansion,
mapping: []int, mapping: []int,
comptime_values: []Comptime_Value, comptime_values: []Comptime_Value,
runtime_types: []types.Type, runtime_types: []types.Type,
@@ -235,7 +235,7 @@ Checker :: struct {
static_bindings: [dynamic]Static_Binding, static_bindings: [dynamic]Static_Binding,
comptime_keys: [dynamic]string, comptime_keys: [dynamic]string,
comptime_static_values: [dynamic]Ct_Value_Id, comptime_static_values: [dynamic]Ct_Value_Id,
inline_context: [dynamic]Inline_Expansion, expand_context: [dynamic]Expand_Expansion,
type_factories: [dynamic]Type_Factory_Entry, type_factories: [dynamic]Type_Factory_Entry,
generated_types: [dynamic]Generated_Type_Entry, generated_types: [dynamic]Generated_Type_Entry,
type_factory_origins: [dynamic]Type_Factory_Origin, type_factory_origins: [dynamic]Type_Factory_Origin,
@@ -549,7 +549,21 @@ persistent_field_value :: proc(checker: ^Checker, root: Ct_Value_Id, name: symbo
} }
build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span, expected: types.Type) -> hir.Expr_Id { build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span, expected: types.Type) -> hir.Expr_Id {
if value.kind == .Void {
return add_hir_expr(checker, hir.Expr{
kind=.Void, span=span, type=types.VOID,
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
if value.kind == .Integer { if value.kind == .Integer {
if types.is_enum(value.type, &checker.module.types) {
return add_hir_expr(checker, hir.Expr{
kind=.Integer, span=span, type=value.type, integer=i64(value.integer),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
expr := ast.Expr{ expr := ast.Expr{
kind=.Integer, span=span, integer=u64(value.integer), kind=.Integer, span=span, integer=u64(value.integer),
left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC, left=ast.INVALID_EXPR, right=ast.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
@@ -628,6 +642,28 @@ build_static_value :: proc(checker: ^Checker, value: Ct_Value, span: source.Span
return invalid_hir_expr(checker, span, id, expected) return invalid_hir_expr(checker, span, id, expected)
} }
push_static_integer_binding :: proc(checker: ^Checker, name: symbol.Id, value_type: types.Type, value: i128) -> int {
start := len(checker.static_bindings)
if symbol.is_valid(name) && name != checker.sink_symbol {
id := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=value_type, integer=value})
append(&checker.static_bindings, Static_Binding{name=name, type=value_type, value=id})
}
return start
}
push_static_void_binding :: proc(checker: ^Checker, name: symbol.Id) -> int {
start := len(checker.static_bindings)
if symbol.is_valid(name) && name != checker.sink_symbol {
id := ct_add_value(&checker.static_state, Ct_Value{kind=.Void, type=types.VOID})
append(&checker.static_bindings, Static_Binding{name=name, type=types.VOID, value=id})
}
return start
}
pop_static_bindings :: proc(checker: ^Checker, start: int) {
resize(&checker.static_bindings, start)
}
comptime_string_argument :: proc( comptime_string_argument :: proc(
checker: ^Checker, checker: ^Checker,
id: ast.Expr_Id, id: ast.Expr_Id,
@@ -856,6 +892,104 @@ build_type_builtin :: proc(
) )
} }
tag_result_type :: proc(checker: ^Checker, value: types.Type) -> (types.Type, bool) {
if !types.is_tagged_union(value, &checker.module.types) {
return types.INVALID, false
}
return types.union_tag_enum(value, &checker.module.types), true
}
enum_member_name_from_value :: proc(checker: ^Checker, enum_type: types.Type, value: i128) -> (string, bool) {
if !types.is_enum(enum_type, &checker.module.types) {
return "", false
}
for member in types.enum_members_for(&checker.module.types, enum_type) {
if member.value == value {
return symbol_text(checker, symbol.Id(member.name)), true
}
}
return "", false
}
build_tag_intrinsic :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Build_Local,
global_reads: ^[dynamic]hir.Global_Id,
calls: ^[dynamic]hir.Function_Id,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "tag! expects 1 argument, got %d", len(expr.args))
return invalid_hir_expr(checker, expr.span, id)
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
value_id, flow, comptime_ok := ct_eval_expr(&state, expr.args[0], types.INVALID, 0)
if comptime_ok && flow.kind == .Normal && value_id != INVALID_CT_VALUE && int(value_id) < len(state.values) {
value := state.values[value_id]
if tag_type, tagged := tag_result_type(checker, value.type); tagged && value.kind == .Struct &&
value.active >= 0 {
fields := types.fields_for(&checker.module.types, value.type)
if int(value.active) < len(fields) {
if member, found := find_enum_member(checker, tag_type, symbol.Id(fields[value.active].name)); found {
ct_state_destroy(&state)
return add_hir_expr(checker, hir.Expr{
kind=.Integer, span=expr.span, type=tag_type, integer=i64(member.value),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
}
}
ct_state_destroy(&state)
value := build_nested_expr(checker, expr.args[0], locals, global_reads, calls, types.INVALID, pkg, file)
if checker.module.exprs[value].kind == .Invalid {
return value
}
tag_type, ok := tag_result_type(checker, checker.module.exprs[value].type)
if !ok {
id := source.add(checker.diagnostics, expr.span, "tag! requires a tagged-union value")
return invalid_hir_expr(checker, expr.span, id)
}
return add_hir_expr(checker, hir.Expr{
kind=.Union_Tag, span=expr.span, type=tag_type, left=value,
target=hir.INVALID_REF, right=hir.INVALID_EXPR, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
build_tagname_intrinsic :: proc(
checker: ^Checker,
expr: ast.Expr,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> hir.Expr_Id {
if len(expr.args) != 1 {
id := source.addf(checker.diagnostics, expr.span, "tagname! expects 1 argument, got %d", len(expr.args))
return invalid_hir_expr(checker, expr.span, id)
}
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=false)
defer ct_state_destroy(&state)
value_id, flow, ok := ct_eval_expr(&state, expr.args[0], types.INVALID, 0)
if !ok || flow.kind != .Normal || value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) {
id := source.add(checker.diagnostics, expr.span, "tagname! requires a comptime-known enum value")
return invalid_hir_expr(checker, expr.span, id)
}
value := state.values[value_id]
name, name_ok := enum_member_name_from_value(checker, value.type, value.integer)
if value.kind != .Integer || !name_ok {
id := source.add(checker.diagnostics, expr.span, "tagname! requires a comptime-known enum value")
return invalid_hir_expr(checker, expr.span, id)
}
string_id := intern_comptime_string(checker, name)
return add_hir_expr(checker, hir.Expr{
kind=.String, span=expr.span, type=string_literal_type(checker, string_id), integer=i64(string_id),
target=hir.INVALID_REF, left=hir.INVALID_EXPR, right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool { is_type_metatype_syntax :: proc(checker: ^Checker, value: ast.Type_Syntax) -> bool {
item, ok := types.node(&checker.module.types, value) item, ok := types.node(&checker.module.types, value)
return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0 return ok && item.name == u32(checker.type_symbol) && item.qualifier == 0
@@ -2554,7 +2688,7 @@ clone_comptime_values :: proc(values: []Comptime_Value, allocator: mem.Allocator
return result return result
} }
inline_expansions_equal :: proc(left, right: []Inline_Expansion) -> bool { expand_expansions_equal :: proc(left, right: []Expand_Expansion) -> bool {
if len(left) != len(right) { if len(left) != len(right) {
return false return false
} }
@@ -2574,7 +2708,7 @@ find_call_resolution :: proc(
entry := checker.call_resolutions[index] entry := checker.call_resolutions[index]
if entry.expr == expr && if entry.expr == expr &&
comptime_values_equal(entry.ctx, checker.current_comptime_values) && comptime_values_equal(entry.ctx, checker.current_comptime_values) &&
inline_expansions_equal(entry.inline_ctx, checker.inline_context[:]) { expand_expansions_equal(entry.expand_ctx, checker.expand_context[:]) {
return index, true return index, true
} }
} }
@@ -2591,7 +2725,7 @@ store_call_resolution :: proc(
entry := Call_Resolution{ entry := Call_Resolution{
expr=expr, expr=expr,
ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator), ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator),
inline_ctx=slice.clone(checker.inline_context[:], checker.allocator), expand_ctx=slice.clone(checker.expand_context[:], checker.allocator),
mapping=slice.clone(mapping, checker.allocator), mapping=slice.clone(mapping, checker.allocator),
comptime_values=clone_comptime_values(comptime_values, checker.allocator), comptime_values=clone_comptime_values(comptime_values, checker.allocator),
runtime_types=slice.clone(runtime_types, checker.allocator), runtime_types=slice.clone(runtime_types, checker.allocator),
@@ -2599,7 +2733,7 @@ store_call_resolution :: proc(
if index, ok := find_call_resolution(checker, expr); ok { if index, ok := find_call_resolution(checker, expr); ok {
previous := checker.call_resolutions[index] previous := checker.call_resolutions[index]
delete(previous.ctx, checker.allocator) delete(previous.ctx, checker.allocator)
delete(previous.inline_ctx, checker.allocator) delete(previous.expand_ctx, checker.allocator)
delete(previous.mapping, checker.allocator) delete(previous.mapping, checker.allocator)
delete(previous.comptime_values, checker.allocator) delete(previous.comptime_values, checker.allocator)
delete(previous.runtime_types, checker.allocator) delete(previous.runtime_types, checker.allocator)
@@ -4276,7 +4410,11 @@ infer_expr :: proc(
} }
expr := checker.ast_module.exprs[frame.expr] expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 { if frame.stage == 0 {
constant := eval_constant(checker, frame.expr) constant := Constant{}
_, static_name := current_static_binding(checker, expr.name)
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr)
}
if constant.kind == .Overflow || constant.kind == .Div_By_Zero || if constant.kind == .Overflow || constant.kind == .Div_By_Zero ||
(constant.kind == .Value && !fits_i64(constant.value)) { (constant.kind == .Value && !fits_i64(constant.value)) {
last = types.I64 last = types.I64
@@ -4502,6 +4640,21 @@ infer_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
if is_intrinsic_call(checker, expr, "tag") {
if len(expr.args) == 1 {
value_type := infer_nested_expr(checker, expr.args[0], locals, pkg, file, demanded, local_types)
last, _ = tag_result_type(checker, value_type)
} else {
last = types.INVALID
}
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tagname") {
last = types.slice(&checker.module.types, types.U8, false)
_ = pop(&stack)
continue
}
if is_ptrcast_call(checker, expr) { if is_ptrcast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
last = types.INVALID last = types.INVALID
@@ -5087,21 +5240,21 @@ infer_statements :: proc(
infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, update[:], locals, local_types, pkg, file, demanded, result, result_hint)
} }
case .For: case .For:
if statement.inline { if statement.expand {
bindings, inline_error := inline_field_bindings(checker, statement.expr, statement.name, pkg, file) bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, pkg, file)
if inline_error == .None { if expand_error == .None {
for binding, inline_index in bindings { for binding, expand_index in bindings {
binding_start := push_inline_binding(checker, binding, statement.index_name, inline_index, statement_id) binding_start := push_expand_binding(checker, binding, statement.index_name, expand_index, statement_id)
iteration: [dynamic]ast.Stmt_Id iteration: [dynamic]ast.Stmt_Id
iteration.allocator = checker.allocator iteration.allocator = checker.allocator
control := flatten_inline_iteration( control := flatten_expand_iteration(
checker, statement.body, pkg, file, &iteration, statement.label, nil, checker, statement.body, pkg, file, &iteration, statement.label, nil,
) )
if control != .Invalid { if control != .Invalid {
infer_statements(checker, iteration[:], locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, iteration[:], locals, local_types, pkg, file, demanded, result, result_hint)
} }
delete(iteration) delete(iteration)
pop_inline_binding(checker, binding_start) pop_expand_binding(checker, binding_start)
if control == .Break || control == .Invalid { if control == .Break || control == .Invalid {
break break
} }
@@ -5164,13 +5317,65 @@ infer_statements :: proc(
// (e.g. `match get()`). Mirror the `.For`/unwrap-`.If` capture handling. // (e.g. `match get()`). Mirror the `.For`/unwrap-`.If` capture handling.
subject_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types) subject_type := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
is_tagged := types.is_tagged_union(subject_type, &checker.module.types) is_tagged := types.is_tagged_union(subject_type, &checker.module.types)
is_enum_subject := types.is_enum(subject_type, &checker.module.types)
covered: [dynamic]symbol.Id
covered.allocator = checker.allocator
for arm_id in statement.body { for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id] arm := checker.ast_module.statements[arm_id]
if arm.kind != .Match_Arm { if arm.kind != .Match_Arm {
continue continue
} }
if arm.expand && (is_tagged || is_enum_subject) {
tag_type := types.union_tag_enum(subject_type, &checker.module.types) if is_tagged else subject_type
if is_tagged {
for field in types.fields_for(&checker.module.types, subject_type) {
name := symbol.Id(field.name)
if contains_name(covered[:], name) {
continue
}
member, found := find_enum_member(checker, tag_type, name)
if !found {
continue
}
capture_start := len(locals^)
static_start := len(checker.static_bindings)
if len(arm.captures) > 1 {
_ = push_static_integer_binding(checker, arm.captures[1], tag_type, member.value)
}
if len(arm.captures) > 0 && arm.captures[0] != checker.sink_symbol {
if types.is_void(field.type) && !arm.pointer_capture {
_ = push_static_void_binding(checker, arm.captures[0])
} else {
capture_type := field.type
if arm.pointer_capture {
capture_type = types.pointer(&checker.module.types, field.type, true, false)
}
append(locals, Infer_Local{name=arm.captures[0], type=capture_type, declared=capture_type, statement=ast.INVALID_STMT})
}
}
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
pop_static_bindings(checker, static_start)
}
} else {
for member in types.enum_members_for(&checker.module.types, subject_type) {
name := symbol.Id(member.name)
if contains_name(covered[:], name) {
continue
}
static_start := push_static_integer_binding(checker, arm.captures[0] if len(arm.captures) > 0 else symbol.INVALID, tag_type, member.value)
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
pop_static_bindings(checker, static_start)
}
}
continue
}
for pattern in arm.patterns { for pattern in arm.patterns {
_ = infer_expr(checker, pattern, locals^[:], pkg, file, demanded, local_types) _ = infer_expr(checker, pattern, locals^[:], pkg, file, demanded, local_types)
pattern_expr := checker.ast_module.exprs[pattern]
if pattern_expr.kind == .Enum_Literal {
append(&covered, pattern_expr.name)
}
} }
capture_start := len(locals^) capture_start := len(locals^)
if len(arm.captures) > 0 && is_tagged && len(arm.patterns) > 0 { if len(arm.captures) > 0 && is_tagged && len(arm.patterns) > 0 {
@@ -5194,6 +5399,7 @@ infer_statements :: proc(
infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint) infer_statements(checker, arm.body, locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start) resize(locals, capture_start)
} }
delete(covered)
} }
} }
resize(locals, scope_start) resize(locals, scope_start)
@@ -7592,7 +7798,11 @@ build_expr :: proc(
} }
expr := checker.ast_module.exprs[frame.expr] expr := checker.ast_module.exprs[frame.expr]
if frame.stage == 0 { if frame.stage == 0 {
constant := eval_constant(checker, frame.expr) constant := Constant{}
_, static_name := current_static_binding(checker, expr.name)
if expr.kind != .Name || symbol.is_valid(expr.qualifier) || !static_name {
constant = eval_constant(checker, frame.expr)
}
if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero || constant.kind == .Non_Exact { if constant.kind == .Value || constant.kind == .Overflow || constant.kind == .Div_By_Zero || constant.kind == .Non_Exact {
last = build_constant_expr(checker, expr, constant, frame.expected) last = build_constant_expr(checker, expr, constant, frame.expected)
_ = pop(&stack) _ = pop(&stack)
@@ -7846,6 +8056,16 @@ build_expr :: proc(
_ = pop(&stack) _ = pop(&stack)
continue continue
} }
if is_intrinsic_call(checker, expr, "tag") {
last = build_tag_intrinsic(checker, expr, locals, global_reads, calls, pkg, file)
_ = pop(&stack)
continue
}
if is_intrinsic_call(checker, expr, "tagname") {
last = build_tagname_intrinsic(checker, expr, pkg, file)
_ = pop(&stack)
continue
}
if is_ptrcast_call(checker, expr) { if is_ptrcast_call(checker, expr) {
if len(expr.args) != 2 { if len(expr.args) != 2 {
id := source.addf(checker.diagnostics, expr.span, "ptrcast! expects 2 arguments, got %d", len(expr.args)) id := source.addf(checker.diagnostics, expr.span, "ptrcast! expects 2 arguments, got %d", len(expr.args))
@@ -8642,6 +8862,9 @@ specialization_match_body :: proc(
return nil, {}, false, false return nil, {}, false, false
} }
arm := checker.ast_module.statements[selection.arm] arm := checker.ast_module.statements[selection.arm]
if arm.expand {
return nil, {}, false, false
}
if arm.pointer_capture { if arm.pointer_capture {
return nil, {}, false, false return nil, {}, false, false
} }
@@ -8656,21 +8879,21 @@ specialization_match_body :: proc(
return arm.body, {}, false, true return arm.body, {}, false, true
} }
Inline_Binding_Error :: enum u8 { Expand_Binding_Error :: enum u8 {
None, None,
Invalid, Invalid,
Quota, Quota,
Diagnosed, Diagnosed,
} }
inline_field_bindings :: proc( expand_field_bindings :: proc(
checker: ^Checker, checker: ^Checker,
expr: ast.Expr_Id, expr: ast.Expr_Id,
capture: symbol.Id, capture: symbol.Id,
pkg: ast.Package_Id, pkg: ast.Package_Id,
file: ast.File_Id, file: ast.File_Id,
diagnose := false, diagnose := false,
) -> ([]Static_Binding, Inline_Binding_Error) { ) -> ([]Static_Binding, Expand_Binding_Error) {
state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=diagnose) state := ct_state_make(checker, pkg, file, values=checker.current_comptime_values, diagnose=diagnose)
defer ct_state_destroy(&state) defer ct_state_destroy(&state)
value_id, flow, ok := ct_eval_expr(&state, expr, types.INVALID, 0) value_id, flow, ok := ct_eval_expr(&state, expr, types.INVALID, 0)
@@ -8680,7 +8903,7 @@ inline_field_bindings :: proc(
value := state.values[value_id] value := state.values[value_id]
if ct_value_contains_undefined(&state, value_id) { if ct_value_contains_undefined(&state, value_id) {
if diagnose { if diagnose {
_ = ct_fail(&state, .Not_Comptime, checker.ast_module.exprs[expr].span, "inline for cannot expand an undefined comptime value") _ = ct_fail(&state, .Not_Comptime, checker.ast_module.exprs[expr].span, "expand for cannot expand an undefined comptime value")
} }
return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid
} }
@@ -8748,7 +8971,7 @@ inline_field_bindings :: proc(
return bindings, .None return bindings, .None
} }
push_inline_binding :: proc( push_expand_binding :: proc(
checker: ^Checker, checker: ^Checker,
binding: Static_Binding, binding: Static_Binding,
index_name: symbol.Id, index_name: symbol.Id,
@@ -8757,7 +8980,7 @@ push_inline_binding :: proc(
) -> int { ) -> int {
start := len(checker.static_bindings) start := len(checker.static_bindings)
append(&checker.static_bindings, binding) append(&checker.static_bindings, binding)
append(&checker.inline_context, Inline_Expansion{statement=statement, index=u32(index)}) append(&checker.expand_context, Expand_Expansion{statement=statement, index=u32(index)})
if symbol.is_valid(index_name) { if symbol.is_valid(index_name) {
value := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)}) value := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)})
append(&checker.static_bindings, Static_Binding{name=index_name, type=types.USIZE, value=value}) append(&checker.static_bindings, Static_Binding{name=index_name, type=types.USIZE, value=value})
@@ -8765,19 +8988,19 @@ push_inline_binding :: proc(
return start return start
} }
pop_inline_binding :: proc(checker: ^Checker, start: int) { pop_expand_binding :: proc(checker: ^Checker, start: int) {
resize(&checker.static_bindings, start) resize(&checker.static_bindings, start)
_ = pop(&checker.inline_context) _ = pop(&checker.expand_context)
} }
Inline_Control :: enum u8 { Expand_Control :: enum u8 {
Normal, Normal,
Break, Break,
Continue, Continue,
Invalid, Invalid,
} }
inline_control_target :: proc(statement: ast.Stmt, target_label: symbol.Id, allow_unlabeled: bool) -> Inline_Control { expand_control_target :: proc(statement: ast.Stmt, target_label: symbol.Id, allow_unlabeled: bool) -> Expand_Control {
if statement.kind != .Break && statement.kind != .Continue { if statement.kind != .Break && statement.kind != .Continue {
return .Normal return .Normal
} }
@@ -8791,7 +9014,7 @@ inline_control_target :: proc(statement: ast.Stmt, target_label: symbol.Id, allo
return .Break if statement.kind == .Break else .Continue return .Break if statement.kind == .Break else .Continue
} }
contains_inline_control :: proc( contains_expand_control :: proc(
checker: ^Checker, checker: ^Checker,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
target_label: symbol.Id, target_label: symbol.Id,
@@ -8799,32 +9022,32 @@ contains_inline_control :: proc(
) -> bool { ) -> bool {
for statement_id in statements { for statement_id in statements {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
if inline_control_target(statement, target_label, allow_unlabeled) != .Normal { if expand_control_target(statement, target_label, allow_unlabeled) != .Normal {
return true return true
} }
#partial switch statement.kind { #partial switch statement.kind {
case .Block: case .Block:
if contains_inline_control(checker, statement.body, target_label, allow_unlabeled) { if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) {
return true return true
} }
case .If: case .If:
if contains_inline_control(checker, statement.body, target_label, allow_unlabeled) || if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) ||
contains_inline_control(checker, statement.else_body, target_label, allow_unlabeled) { contains_expand_control(checker, statement.else_body, target_label, allow_unlabeled) {
return true return true
} }
case .Match, .Match_Arm: case .Match, .Match_Arm:
if contains_inline_control(checker, statement.body, target_label, allow_unlabeled) { if contains_expand_control(checker, statement.body, target_label, allow_unlabeled) {
return true return true
} }
case .For, .While: case .For, .While:
// Unlabelled control belongs to the nested loop. A labelled jump can still // Unlabelled control belongs to the nested loop. A labelled jump can still
// name the surrounding inline loop and is therefore relevant here. // name the surrounding expand loop and is therefore relevant here.
if contains_inline_control(checker, statement.body, target_label, false) { if contains_expand_control(checker, statement.body, target_label, false) {
return true return true
} }
case .Defer: case .Defer:
if statement.update != ast.INVALID_STMT && if statement.update != ast.INVALID_STMT &&
contains_inline_control(checker, []ast.Stmt_Id{statement.update}, target_label, false) { contains_expand_control(checker, []ast.Stmt_Id{statement.update}, target_label, false) {
return true return true
} }
} }
@@ -8861,7 +9084,7 @@ clone_statement_body :: proc(checker: ^Checker, statement: ast.Stmt, body: []ast
return id return id
} }
append_inline_block :: proc(checker: ^Checker, span: source.Span, body: []ast.Stmt_Id, out: ^[dynamic]ast.Stmt_Id) { append_expand_block :: proc(checker: ^Checker, span: source.Span, body: []ast.Stmt_Id, out: ^[dynamic]ast.Stmt_Id) {
if len(body) == 0 { if len(body) == 0 {
return return
} }
@@ -8891,7 +9114,7 @@ same_stmt_ids :: proc(left, right: []ast.Stmt_Id) -> bool {
return true return true
} }
flatten_inline_iteration :: proc( flatten_expand_iteration :: proc(
checker: ^Checker, checker: ^Checker,
statements: []ast.Stmt_Id, statements: []ast.Stmt_Id,
pkg: ast.Package_Id, pkg: ast.Package_Id,
@@ -8899,10 +9122,10 @@ flatten_inline_iteration :: proc(
out: ^[dynamic]ast.Stmt_Id, out: ^[dynamic]ast.Stmt_Id,
target_label: symbol.Id, target_label: symbol.Id,
diagnostic: ^source.Diagnostic_Id, diagnostic: ^source.Diagnostic_Id,
) -> Inline_Control { ) -> Expand_Control {
for statement_id in statements { for statement_id in statements {
statement := checker.ast_module.statements[statement_id] statement := checker.ast_module.statements[statement_id]
if control := inline_control_target(statement, target_label, true); control != .Normal { if control := expand_control_target(statement, target_label, true); control != .Normal {
return control return control
} }
if statement.kind == .If && len(statement.captures) == 0 { if statement.kind == .If && len(statement.captures) == 0 {
@@ -8910,20 +9133,20 @@ flatten_inline_iteration :: proc(
selected_body := statement.body if selected else statement.else_body selected_body := statement.body if selected else statement.else_body
branch: [dynamic]ast.Stmt_Id branch: [dynamic]ast.Stmt_Id
branch.allocator = checker.allocator branch.allocator = checker.allocator
flow := flatten_inline_iteration(checker, selected_body, pkg, file, &branch, target_label, diagnostic) flow := flatten_expand_iteration(checker, selected_body, pkg, file, &branch, target_label, diagnostic)
append_inline_block(checker, statement.span, branch[:], out) append_expand_block(checker, statement.span, branch[:], out)
delete(branch) delete(branch)
if flow != .Normal { if flow != .Normal {
return flow return flow
} }
continue continue
} }
if contains_inline_control(checker, statement.body, target_label, true) || if contains_expand_control(checker, statement.body, target_label, true) ||
contains_inline_control(checker, statement.else_body, target_label, true) { contains_expand_control(checker, statement.else_body, target_label, true) {
if diagnostic != nil { if diagnostic != nil {
diagnostic^ = source.add( diagnostic^ = source.add(
checker.diagnostics, statement.span, checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable", "break or continue targeting an expand loop must be compile-time-resolvable",
) )
} }
return .Invalid return .Invalid
@@ -8933,7 +9156,7 @@ flatten_inline_iteration :: proc(
if selected_body, _, _, comptime_ok := specialization_match_body(checker, statement, pkg, file); comptime_ok { if selected_body, _, _, comptime_ok := specialization_match_body(checker, statement, pkg, file); comptime_ok {
selected: [dynamic]ast.Stmt_Id selected: [dynamic]ast.Stmt_Id
selected.allocator = checker.allocator selected.allocator = checker.allocator
flow := flatten_inline_iteration(checker, selected_body, pkg, file, &selected, target_label, diagnostic) flow := flatten_expand_iteration(checker, selected_body, pkg, file, &selected, target_label, diagnostic)
if flow != .Normal { if flow != .Normal {
arm_index := -1 arm_index := -1
for arm_id, index in statement.body { for arm_id, index in statement.body {
@@ -8958,11 +9181,11 @@ flatten_inline_iteration :: proc(
append(out, statement_id) append(out, statement_id)
continue continue
} }
if contains_inline_control(checker, statement.body, target_label, true) { if contains_expand_control(checker, statement.body, target_label, true) {
if diagnostic != nil { if diagnostic != nil {
diagnostic^ = source.add( diagnostic^ = source.add(
checker.diagnostics, statement.span, checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable", "break or continue targeting an expand loop must be compile-time-resolvable",
) )
} }
return .Invalid return .Invalid
@@ -8971,11 +9194,11 @@ flatten_inline_iteration :: proc(
if statement.kind == .Block { if statement.kind == .Block {
block: [dynamic]ast.Stmt_Id block: [dynamic]ast.Stmt_Id
block.allocator = checker.allocator block.allocator = checker.allocator
flow := flatten_inline_iteration(checker, statement.body, pkg, file, &block, target_label, diagnostic) flow := flatten_expand_iteration(checker, statement.body, pkg, file, &block, target_label, diagnostic)
if flow == .Normal { if flow == .Normal {
append(out, statement_id) append(out, statement_id)
} else { } else {
append_inline_block(checker, statement.span, block[:], out) append_expand_block(checker, statement.span, block[:], out)
} }
delete(block) delete(block)
if flow != .Normal { if flow != .Normal {
@@ -8984,11 +9207,11 @@ flatten_inline_iteration :: proc(
continue continue
} }
if (statement.kind == .For || statement.kind == .While || statement.kind == .Defer) && if (statement.kind == .For || statement.kind == .While || statement.kind == .Defer) &&
contains_inline_control(checker, []ast.Stmt_Id{statement_id}, target_label, false) { contains_expand_control(checker, []ast.Stmt_Id{statement_id}, target_label, false) {
if diagnostic != nil { if diagnostic != nil {
diagnostic^ = source.add( diagnostic^ = source.add(
checker.diagnostics, statement.span, checker.diagnostics, statement.span,
"break or continue targeting an inline loop must be compile-time-resolvable", "break or continue targeting an expand loop must be compile-time-resolvable",
) )
} }
return .Invalid return .Invalid
@@ -9821,19 +10044,19 @@ build_block :: proc(
}) })
ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid ctx.problematic^ = ctx.problematic^ || checker.module.exprs[condition].kind == .Invalid
case .For: case .For:
if statement.inline { if statement.expand {
if statement.pointer_capture { if statement.pointer_capture {
id := source.add(checker.diagnostics, statement.span, "inline for does not support pointer captures") id := source.add(checker.diagnostics, statement.span, "expand for does not support pointer captures")
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id}) append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id})
ctx.problematic^ = true ctx.problematic^ = true
continue continue
} }
bindings, inline_error := inline_field_bindings(checker, statement.expr, statement.name, ctx.pkg, ctx.file, true) bindings, expand_error := expand_field_bindings(checker, statement.expr, statement.name, ctx.pkg, ctx.file, true)
if inline_error != .None && inline_error != .Diagnosed { if expand_error != .None && expand_error != .Diagnosed {
message := "inline for requires a comptime tuple, fixed array, range, slice, or reflection value" message := "expand for requires a comptime tuple, fixed array, range, slice, or reflection value"
if inline_error == .Quota { if expand_error == .Quota {
message = "inline for expansion exceeds the compile-time evaluation quota" message = "expand for expansion exceeds the compile-time evaluation quota"
} }
id := source.add( id := source.add(
checker.diagnostics, statement.span, checker.diagnostics, statement.span,
@@ -9842,13 +10065,13 @@ build_block :: proc(
append(&body, hir.stmt_id(len(checker.module.statements))) append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id}) append(&checker.module.statements, hir.Stmt{kind=.Trap, span=statement.span, diagnostic=id})
ctx.problematic^ = true ctx.problematic^ = true
} else if inline_error == .None { } else if expand_error == .None {
for binding, inline_index in bindings { for binding, expand_index in bindings {
binding_start := push_inline_binding(checker, binding, statement.index_name, inline_index, statement_id) binding_start := push_expand_binding(checker, binding, statement.index_name, expand_index, statement_id)
iteration: [dynamic]ast.Stmt_Id iteration: [dynamic]ast.Stmt_Id
iteration.allocator = checker.allocator iteration.allocator = checker.allocator
diagnostic := source.INVALID_DIAGNOSTIC diagnostic := source.INVALID_DIAGNOSTIC
control := flatten_inline_iteration( control := flatten_expand_iteration(
checker, statement.body, ctx.pkg, ctx.file, &iteration, statement.label, &diagnostic, checker, statement.body, ctx.pkg, ctx.file, &iteration, statement.label, &diagnostic,
) )
if control == .Invalid { if control == .Invalid {
@@ -9863,7 +10086,7 @@ build_block :: proc(
delete(expanded, checker.allocator) delete(expanded, checker.allocator)
} }
delete(iteration) delete(iteration)
pop_inline_binding(checker, binding_start) pop_expand_binding(checker, binding_start)
if control == .Break || control == .Invalid { if control == .Break || control == .Invalid {
break break
} }
@@ -10846,6 +11069,7 @@ emit_match :: proc(
covered.allocator = checker.allocator covered.allocator = checker.allocator
defer delete(covered) defer delete(covered)
has_else := false has_else := false
has_expand := false
for arm_id in statement.body { for arm_id in statement.body {
arm := checker.ast_module.statements[arm_id] arm := checker.ast_module.statements[arm_id]
@@ -10853,10 +11077,107 @@ emit_match :: proc(
ok = false ok = false
continue continue
} }
if has_else { if has_else || has_expand {
source.add(checker.diagnostics, arm.span, "arms after 'else' are unreachable") message := "arms after 'else' are unreachable" if has_else else "arms after 'expand' are unreachable"
source.add(checker.diagnostics, arm.span, message)
ok = false ok = false
} }
if arm.expand {
if !is_tagged && !is_enum_subject {
source.add(checker.diagnostics, arm.span, "'expand' requires an enum or tagged-union match subject")
ok = false
continue
}
expected_captures := 1 if is_enum_subject else 2
if len(arm.captures) == 0 || len(arm.captures) > expected_captures {
description := "exactly one capture" if is_enum_subject else "one or two captures"
source.addf(checker.diagnostics, arm.span, "expanded match on '%s' requires %s", type_label(checker, subject_type), description)
ok = false
}
if is_enum_subject && arm.pointer_capture {
source.add(checker.diagnostics, arm.span, "enum expansion does not support pointer captures")
ok = false
}
if len(arm.captures) > 1 && arm.captures[0] != checker.sink_symbol && arm.captures[0] == arm.captures[1] {
source.add(checker.diagnostics, arm.span, "expand captures must have distinct names")
ok = false
}
remaining := 0
member_enum := tag_enum if is_tagged else subject_type
if is_tagged {
for field, field_index in types.fields_for(store, subject_type) {
name := symbol.Id(field.name)
if contains_name(covered[:], name) {
continue
}
member, found := find_enum_member(checker, member_enum, name)
if !found {
ok = false
continue
}
remaining += 1
append(&covered, name)
member_expr := enum_member_hir(checker, member_enum, name, arm.span)
condition := add_hir_expr(checker, hir.Expr{
kind=.Eq, span=arm.span, type=types.BOOL,
left=slot_read(checker, key_local, key_type, arm.span), right=member_expr,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
static_start := len(checker.static_bindings)
if len(arm.captures) > 1 {
_ = push_static_integer_binding(checker, arm.captures[1], member_enum, member.value)
}
body_arm := arm
if types.is_void(field.type) && !arm.pointer_capture {
if len(arm.captures) > 0 {
_ = push_static_void_binding(checker, arm.captures[0])
}
body_arm.captures = nil
}
arm_body, body_ok := build_match_arm_body(
ctx, body_arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable,
field_index, field.type, as_value, slot, slot_type, span,
)
pop_static_bindings(checker, static_start)
ok = body_ok && ok
append(&built, Match_Built_Arm{condition=condition, body=arm_body})
}
} else {
for member in types.enum_members_for(store, subject_type) {
name := symbol.Id(member.name)
if contains_name(covered[:], name) {
continue
}
remaining += 1
append(&covered, name)
member_expr := enum_member_hir(checker, subject_type, name, arm.span)
condition := add_hir_expr(checker, hir.Expr{
kind=.Eq, span=arm.span, type=types.BOOL,
left=slot_read(checker, key_local, key_type, arm.span), right=member_expr,
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
static_start := push_static_integer_binding(
checker, arm.captures[0] if len(arm.captures) > 0 else symbol.INVALID,
subject_type, member.value,
)
body_arm := arm
body_arm.captures = nil
arm_body, body_ok := build_match_arm_body(
ctx, body_arm, subject_type, subj_local, subj_is_pointer, ptr_type, subj_writable,
-1, types.INVALID, as_value, slot, slot_type, span,
)
pop_static_bindings(checker, static_start)
ok = body_ok && ok
append(&built, Match_Built_Arm{condition=condition, body=arm_body})
}
}
if remaining == 0 {
source.add(checker.diagnostics, arm.span, "redundant 'expand': the 'match' already covers every variant")
ok = false
}
has_expand = true
continue
}
is_else := len(arm.patterns) == 0 is_else := len(arm.patterns) == 0
condition := hir.INVALID_EXPR condition := hir.INVALID_EXPR
field_index := -1 field_index := -1
@@ -11214,7 +11535,20 @@ build_value_match :: proc(
} }
subtree: [dynamic]hir.Stmt_Id subtree: [dynamic]hir.Stmt_Id
subtree.allocator = checker.allocator subtree.allocator = checker.allocator
ok := emit_match(ctx, &subtree, statement, true, &slot, &slot_type) ok := false
if selected_body, capture, has_capture, comptime_ok := specialization_match_body(
checker, statement, ctx.pkg, ctx.file,
); comptime_ok {
if has_capture {
append(&checker.static_bindings, capture)
}
ok = build_value_arm(ctx, &subtree, selected_body, &slot, &slot_type, span)
if has_capture {
_ = pop(&checker.static_bindings)
}
} else {
ok = emit_match(ctx, &subtree, statement, true, &slot, &slot_type)
}
if !ok || slot == hir.INVALID_LOCAL { if !ok || slot == hir.INVALID_LOCAL {
for s in subtree { for s in subtree {
append(body, s) append(body, s)
@@ -12391,7 +12725,7 @@ check :: proc(
checker.static_bindings.allocator = allocator checker.static_bindings.allocator = allocator
checker.comptime_keys.allocator = allocator checker.comptime_keys.allocator = allocator
checker.comptime_static_values.allocator = allocator checker.comptime_static_values.allocator = allocator
checker.inline_context.allocator = allocator checker.expand_context.allocator = allocator
checker.static_state = ct_state_make(&checker, 0, ast.INVALID_FILE) checker.static_state = ct_state_make(&checker, 0, ast.INVALID_FILE)
build_symbol_indexes(&checker) build_symbol_indexes(&checker)
checker.global_types = make([]types.Type, len(ast_module.globals), allocator) checker.global_types = make([]types.Type, len(ast_module.globals), allocator)
@@ -12467,7 +12801,7 @@ check :: proc(
} }
for resolution in checker.call_resolutions { for resolution in checker.call_resolutions {
delete(resolution.ctx, allocator) delete(resolution.ctx, allocator)
delete(resolution.inline_ctx, allocator) delete(resolution.expand_ctx, allocator)
delete(resolution.mapping, allocator) delete(resolution.mapping, allocator)
delete(resolution.comptime_values, allocator) delete(resolution.comptime_values, allocator)
delete(resolution.runtime_types, allocator) delete(resolution.runtime_types, allocator)
@@ -12483,7 +12817,7 @@ check :: proc(
} }
delete(checker.comptime_keys) delete(checker.comptime_keys)
delete(checker.comptime_static_values) delete(checker.comptime_static_values)
delete(checker.inline_context) delete(checker.expand_context)
} }
for function, index in ast_module.functions { for function, index in ast_module.functions {
+85 -1
View File
@@ -2515,6 +2515,50 @@ ct_eval_call_expr :: proc(state: ^Ct_State, expr: ast.Expr, expected: types.Type
} }
return ct_typeinfo_value(state, target, expr.span) return ct_typeinfo_value(state, target, expr.span)
} }
if is_intrinsic_call(checker, expr, "tag") {
if len(expr.args) != 1 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "tag! expects 1 argument, got %d", len(expr.args))
}
value_id, flow, ok := ct_eval_expr(state, expr.args[0], types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok
}
if value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
value := state.values[value_id]
tag_type, tag_ok := tag_result_type(checker, value.type)
if !tag_ok || value.kind != .Struct || value.active < 0 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "tag! requires a tagged-union value")
}
fields := types.fields_for(&checker.module.types, value.type)
if int(value.active) >= len(fields) {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
member, found := find_enum_member(checker, tag_type, symbol.Id(fields[value.active].name))
if !found {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
return ct_add_value(state, Ct_Value{kind=.Integer, type=tag_type, integer=member.value}), ct_flow(.Normal), true
}
if is_intrinsic_call(checker, expr, "tagname") {
if len(expr.args) != 1 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "tagname! expects 1 argument, got %d", len(expr.args))
}
value_id, flow, ok := ct_eval_expr(state, expr.args[0], types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
return INVALID_CT_VALUE, flow, ok
}
if value_id == INVALID_CT_VALUE || int(value_id) >= len(state.values) {
return INVALID_CT_VALUE, ct_flow(.Normal), false
}
value := state.values[value_id]
name, found := enum_member_name_from_value(checker, value.type, value.integer)
if value.kind != .Integer || !found {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "tagname! requires a comptime-known enum value")
}
return ct_reflection_string(state, name), ct_flow(.Normal), true
}
if builtin := type_builtin_call(checker, expr); builtin != .None { if builtin := type_builtin_call(checker, expr); builtin != .None {
if len(expr.args) != 1 { if len(expr.args) != 1 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "%s! expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args)) return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, expr.span, "%s! expects 1 argument, got %d", symbol_text(checker, expr.name), len(expr.args))
@@ -3660,6 +3704,7 @@ ct_exec_for :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool,
Ct_Match_Selection :: struct { Ct_Match_Selection :: struct {
arm: ast.Stmt_Id, arm: ast.Stmt_Id,
payload: Ct_Value_Id, payload: Ct_Value_Id,
tag: Ct_Value_Id,
payload_field: int, payload_field: int,
payload_type: types.Type, payload_type: types.Type,
} }
@@ -3684,9 +3729,40 @@ ct_select_match_arm :: proc(
selection := Ct_Match_Selection{ selection := Ct_Match_Selection{
arm=arm_id, arm=arm_id,
payload=INVALID_CT_VALUE, payload=INVALID_CT_VALUE,
tag=INVALID_CT_VALUE,
payload_field=-1, payload_field=-1,
payload_type=types.INVALID, payload_type=types.INVALID,
} }
if arm.expand {
if subject_value.kind == .Struct && types.is_tagged_union(subject_value.type, &checker.module.types) {
fields := types.fields_for(&checker.module.types, subject_value.type)
if subject_value.active < 0 || int(subject_value.active) >= len(fields) {
return {}, false
}
field_index := int(subject_value.active)
field := fields[field_index]
selection.payload_field = field_index
selection.payload_type = field.type
children := ct_child_slice(state, subject_value)
if len(children) > 0 {
selection.payload = children[0]
} else if types.is_void(field.type) {
selection.payload = ct_add_value(state, Ct_Value{kind=.Void, type=types.VOID})
}
tag_type := types.union_tag_enum(subject_value.type, &checker.module.types)
member, found := find_enum_member(checker, tag_type, symbol.Id(field.name))
if !found {
return {}, false
}
selection.tag = ct_add_value(state, Ct_Value{kind=.Integer, type=tag_type, integer=member.value})
return selection, true
}
if subject_value.kind == .Integer && types.is_enum(subject_value.type, &checker.module.types) {
selection.tag = subject
return selection, true
}
return {}, false
}
if !matched { if !matched {
for pattern_id in arm.patterns { for pattern_id in arm.patterns {
pattern := checker.ast_module.exprs[pattern_id] pattern := checker.ast_module.exprs[pattern_id]
@@ -3765,7 +3841,11 @@ ct_exec_match :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool
if selected { if selected {
arm := checker.ast_module.statements[selection.arm] arm := checker.ast_module.statements[selection.arm]
scope_start := len(state.bindings) scope_start := len(state.bindings)
if len(arm.captures) > 0 && selection.payload != INVALID_CT_VALUE { if arm.expand && types.is_enum(state.values[subject].type, &checker.module.types) {
if len(arm.captures) > 0 && arm.captures[0] != checker.sink_symbol {
ct_bind_value(state, arm.captures[0], state.values[subject].type, subject, false)
}
} else if len(arm.captures) > 0 && selection.payload != INVALID_CT_VALUE {
capture := arm.captures[0] capture := arm.captures[0]
if arm.pointer_capture { if arm.pointer_capture {
if subject_place == INVALID_CT_PLACE || selection.payload_field < 0 || !types.is_valid(selection.payload_type) { if subject_place == INVALID_CT_PLACE || selection.payload_field < 0 || !types.is_valid(selection.payload_type) {
@@ -3785,6 +3865,10 @@ ct_exec_match :: proc(state: ^Ct_State, statement: ast.Stmt, yield_returns: bool
ct_bind_value(state, capture, state.values[selection.payload].type, selection.payload, false) ct_bind_value(state, capture, state.values[selection.payload].type, selection.payload, false)
} }
} }
if arm.expand && len(arm.captures) > 1 && arm.captures[1] != checker.sink_symbol &&
selection.tag != INVALID_CT_VALUE {
ct_bind_value(state, arm.captures[1], state.values[selection.tag].type, selection.tag, false)
}
if yield_returns && len(arm.body) == 1 && checker.ast_module.statements[arm.body[0]].kind == .Expression { if yield_returns && len(arm.body) == 1 && checker.ast_module.statements[arm.body[0]].kind == .Expression {
expr_stmt := checker.ast_module.statements[arm.body[0]] expr_stmt := checker.ast_module.statements[arm.body[0]]
value, expr_flow, expr_ok := ct_eval_expr(state, expr_stmt.expr, types.INVALID, depth+1) value, expr_flow, expr_ok := ct_eval_expr(state, expr_stmt.expr, types.INVALID, depth+1)
+1
View File
@@ -74,6 +74,7 @@ Linkage :: enum u8 {
Expr_Kind :: enum u8 { Expr_Kind :: enum u8 {
Invalid, Invalid,
Void,
Integer, Integer,
Float, Float,
String, String,
+1
View File
@@ -37,6 +37,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "if": return .Keyword_If case "if": return .Keyword_If
case "while": return .Keyword_While case "while": return .Keyword_While
case "for": return .Keyword_For case "for": return .Keyword_For
case "expand": return .Keyword_Expand
case "break": return .Keyword_Break case "break": return .Keyword_Break
case "continue": return .Keyword_Continue case "continue": return .Keyword_Continue
case "defer": return .Keyword_Defer case "defer": return .Keyword_Defer
+3 -1
View File
@@ -1103,7 +1103,9 @@ emit_instruction_stream :: proc(
fmt.sbprintf( fmt.sbprintf(
&emitter.builder, &emitter.builder,
" %%v%d = getelementptr %s, ptr %%v%d, i64 0\n", " %%v%d = getelementptr %s, ptr %%v%d, i64 0\n",
instruction_index, llvm_type(child, &emitter.module.types), instruction.a, instruction_index,
"i8" if types.is_void(child) else llvm_type(child, &emitter.module.types),
instruction.a,
) )
case .Alloca: case .Alloca:
if !types.is_runtime_value(instruction.type, &emitter.module.types) { if !types.is_runtime_value(instruction.type, &emitter.module.types) {
+7
View File
@@ -659,6 +659,13 @@ lower_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instruction_Id {
case .Invalid: case .Invalid:
last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic) last = append_recovery_value(state, expr.span, expr.type, expr.diagnostic)
_ = pop(&stack) _ = pop(&stack)
case .Void:
last = append_instruction(state, ir.Instruction{
op=.Const, span=expr.span, type=types.VOID,
target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
_ = pop(&stack)
case .Integer, .Float, .Bool: case .Integer, .Float, .Bool:
last = append_instruction(state, ir.Instruction{ last = append_instruction(state, ir.Instruction{
op=.Const, span=expr.span, type=expr.type, integer=expr.integer, op=.Const, span=expr.span, type=expr.type, integer=expr.integer,
+49 -6
View File
@@ -1680,7 +1680,15 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
peek(parser).kind == .Keyword_For { peek(parser).kind == .Keyword_For {
start := advance(parser) start := advance(parser)
id := parse_for(parser) id := parse_for(parser)
parser.module.statements[id].inline = true parser.module.statements[id].expand = true
parser.module.statements[id].span = span_from(start.span, parser.module.statements[id].span)
source.add(parser.diagnostics, start.span, "'inline for' was renamed to 'expand for'")
return id
}
if current(parser).kind == .Keyword_Expand && peek(parser).kind == .Keyword_For {
start := advance(parser)
id := parse_for(parser)
parser.module.statements[id].expand = true
parser.module.statements[id].span = span_from(start.span, parser.module.statements[id].span) parser.module.statements[id].span = span_from(start.span, parser.module.statements[id].span)
return id return id
} }
@@ -2104,10 +2112,8 @@ parse_arm_body :: proc(parser: ^Parser) -> []ast.Stmt_Id {
return single return single
} }
// parse_match_arm parses one `<pattern,...> [|[@]capture|]: <body>` arm (or // parse_match_arm parses one `<pattern,...> [|[@]capture|]: <body>`, `else: <body>`,
// `else: <body>`). `patterns` is empty for `else`, one expr for a single pattern, or // or `expand |[@]value[, tag]|: <body>` arm.
// several for a multi-pattern arm; `captures` holds the optional 0-or-1 payload capture
// name with `pointer_capture` set for the `|@cap|` form (validated in the checker).
parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id { parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
start := current(parser).span start := current(parser).span
patterns: [dynamic]ast.Expr_Id patterns: [dynamic]ast.Expr_Id
@@ -2115,7 +2121,43 @@ parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
captures: [dynamic]symbol.Id captures: [dynamic]symbol.Id
captures.allocator = parser.module.allocator captures.allocator = parser.module.allocator
pointer_capture := false pointer_capture := false
if _, is_else := allow(parser, .Keyword_Else); !is_else { expand := false
if _, is_expand := allow(parser, .Keyword_Expand); is_expand {
expand = true
if _, ok := allow(parser, .Pipe); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' before expand captures")
} else {
if _, at_ok := allow(parser, .At); at_ok {
pointer_capture = true
}
name_tok := current(parser)
if name_tok.kind == .Identifier || name_tok.kind == .Underscore {
advance(parser)
append(&captures, name_tok.symbol)
} else {
source.add(parser.diagnostics, current(parser).span, "expected an expand value capture")
}
if _, comma_ok := allow(parser, .Comma); comma_ok {
tag_tok := current(parser)
if tag_tok.kind == .Identifier || tag_tok.kind == .Underscore {
advance(parser)
append(&captures, tag_tok.symbol)
} else {
source.add(parser.diagnostics, current(parser).span, "expected an expand tag capture")
}
if _, extra := allow(parser, .Comma); extra {
source.add(parser.diagnostics, current(parser).span, "'expand' accepts at most two captures")
for current(parser).kind != .Pipe && current(parser).kind != .Colon &&
current(parser).kind != .Newline && current(parser).kind != .Eof {
advance(parser)
}
}
}
if _, close_ok := allow(parser, .Pipe); !close_ok {
source.add(parser.diagnostics, current(parser).span, "expected '|' to close expand captures")
}
}
} else if _, is_else := allow(parser, .Keyword_Else); !is_else {
saved := parser.no_struct_literal saved := parser.no_struct_literal
parser.no_struct_literal = true parser.no_struct_literal = true
append(&patterns, parse_expression(parser)) append(&patterns, parse_expression(parser))
@@ -2155,6 +2197,7 @@ parse_match_arm :: proc(parser: ^Parser) -> ast.Stmt_Id {
patterns=patterns[:], patterns=patterns[:],
captures=captures[:], captures=captures[:],
pointer_capture=pointer_capture, pointer_capture=pointer_capture,
expand=expand,
body=body, body=body,
target=ast.INVALID_EXPR, target=ast.INVALID_EXPR,
update=ast.INVALID_STMT, update=ast.INVALID_STMT,
+1
View File
@@ -73,6 +73,7 @@ Kind :: enum u8 {
Keyword_If, Keyword_If,
Keyword_While, Keyword_While,
Keyword_For, Keyword_For,
Keyword_Expand,
Keyword_Break, Keyword_Break,
Keyword_Continue, Keyword_Continue,
Keyword_Defer, Keyword_Defer,
+111 -15
View File
@@ -2404,7 +2404,7 @@ main func(init process.Init) void { _ = init }
} }
@(test) @(test)
milestone_37_tuples_reflection_inline_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) { milestone_37_tuples_reflection_expand_for_and_debug_print_compile_and_run :: proc(t: ^testing.T) {
sources := source.init_store() sources := source.init_store()
defer source.destroy_store(&sources) defer source.destroy_store(&sources)
diagnostics := source.init_store_diagnostics(&sources) diagnostics := source.init_store_diagnostics(&sources)
@@ -2445,6 +2445,102 @@ milestone_37_tuples_reflection_inline_for_and_debug_print_compile_and_run :: pro
testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615") testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615")
} }
@(test)
expanded_matches_and_tag_intrinsics_compile_and_run :: proc(t: ^testing.T) {
output := "/tmp/brolang-test-expand"
defer _ = os.remove(output)
status := compiler_core.compile_package(
"examples/programs/expand", output, nil, target.DEFAULT, cimport.Options{}, ".",
)
testing.expect_value(t, status, 0)
state, stdout, stderr, _ := os2.process_exec(
os2.Process_Desc{command=[]string{output}}, context.allocator,
)
defer delete(stdout)
defer delete(stderr)
testing.expect_value(t, state.exit_code, 0)
testing.expect_value(t, string(stdout), "")
testing.expect_value(t, string(stderr), "")
}
@(test)
expanded_match_and_tag_diagnostics :: proc(t: ^testing.T) {
text := `E :: enum { a, b }
Plain :: union { value i32 }
main func() void {
inline for {1} |value| { _ = value }
n i32 = 1
match n { expand |value|: _ = value }
e E = .a
match e { expand |value, tag|: _ = value }
match e {
.a, .b: {}
expand |value|: _ = value
}
match e {
expand |value|: _ = value
.b: {}
}
match e {
else: {}
expand |value|: _ = value
}
u Plain = Plain{value = 1}
_ = tag!(u)
_ = tagname!(e)
match e { expand ||: {} }
match e { expand |a, b, c|: {} }
}
`
source_file := source.Source{path="expand_diagnostics.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table()
defer symbol.destroy_table(&symbols)
stream := lexer.lex(&source_file, &diagnostics, &symbols)
defer delete(stream.items)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&ast_module)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
defer hir.destroy_module(&hir_module)
found_old := false
found_subject := false
found_captures := false
found_redundant := false
found_after := false
found_missing := false
found_many := false
found_tag := false
found_tagname := false
for diagnostic in diagnostics.items {
message := diagnostic.message
found_old = found_old || strings.contains(message, "'inline for' was renamed to 'expand for'")
found_subject = found_subject || strings.contains(message, "'expand' requires an enum or tagged-union")
found_captures = found_captures || strings.contains(message, "requires exactly one capture")
found_redundant = found_redundant || strings.contains(message, "redundant 'expand'")
found_after = found_after || strings.contains(message, "arms after 'expand' are unreachable") || strings.contains(message, "arms after 'else' are unreachable")
found_missing = found_missing || strings.contains(message, "expected an expand value capture")
found_many = found_many || strings.contains(message, "at most two captures")
found_tag = found_tag || strings.contains(message, "tag! requires a tagged-union value")
found_tagname = found_tagname || strings.contains(message, "tagname! requires a comptime-known enum value")
}
testing.expect(t, found_old)
testing.expect(t, found_subject)
testing.expect(t, found_captures)
testing.expect(t, found_redundant)
testing.expect(t, found_after)
testing.expect(t, found_missing)
testing.expect(t, found_many)
testing.expect(t, found_tag)
testing.expect(t, found_tagname)
}
@(test) @(test)
milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) { milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-format-errors" directory := "/tmp/brolang-test-format-errors"
@@ -2620,10 +2716,10 @@ main func() void {
} }
@(test) @(test)
milestone_37_inline_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) { milestone_37_expand_loop_control_must_be_statically_resolvable :: proc(t: ^testing.T) {
text := `main func() void { text := `main func() void {
total i32 = 0 total i32 = 0
inline for {1, 2} |value| { expand for {1, 2} |value| {
if total == 0 { if total == 0 {
break break
} }
@@ -2647,7 +2743,7 @@ milestone_37_inline_loop_control_must_be_statically_resolvable :: proc(t: ^testi
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
found = found || strings.contains( found = found || strings.contains(
diagnostic.message, diagnostic.message,
"break or continue targeting an inline loop must be compile-time-resolvable", "break or continue targeting an expand loop must be compile-time-resolvable",
) )
} }
testing.expect(t, found) testing.expect(t, found)
@@ -2675,7 +2771,7 @@ read_initialized_sibling func() i32 {
answer :: $read_initialized_sibling() answer :: $read_initialized_sibling()
main func() i32 { main func() i32 {
total usize = 0 total usize = 0
inline for make_tokens() |token| { expand for make_tokens() |token| {
total += token.text.len + token.count total += token.text.len + token.count
} }
if answer != 42 or total != 8 { if answer != 42 or total != 8 {
@@ -2750,14 +2846,14 @@ main func() void {}
} }
@(test) @(test)
milestone_37_inline_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) { milestone_37_expand_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) {
text := `identity func($T type, value T) T { text := `identity func($T type, value T) T {
return value return value
} }
main func() i32 { main func() i32 {
total i64 = 0 total i64 = 0
inline for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| { expand for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| {
inline for row |value| { expand for row |value| {
total += i64(identity(value)) total += i64(identity(value))
} }
} }
@@ -2786,16 +2882,16 @@ main func() i32 {
} }
@(test) @(test)
milestone_37_inline_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) { milestone_37_expand_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) {
text := `take_i8 func(value i8) void { _ = value } text := `take_i8 func(value i8) void { _ = value }
main func() void { main func() void {
inline for {i8(1), "skip"} |value, index| { expand for {i8(1), "skip"} |value, index| {
if index == 1 { if index == 1 {
continue continue
} }
take_i8(value) take_i8(value)
} }
inline for {i8(1), "stop"} |value, index| { expand for {i8(1), "stop"} |value, index| {
if index == 1 { if index == 1 {
break break
} }
@@ -2819,14 +2915,14 @@ main func() void {
} }
@(test) @(test)
milestone_37_inline_match_specialization_prunes_unselected_arms :: proc(t: ^testing.T) { milestone_37_expand_match_specialization_prunes_unselected_arms :: proc(t: ^testing.T) {
text := `Kind :: enum { integer, string, stop } text := `Kind :: enum { integer, string, stop }
IntToken :: struct { kind Kind, value i8 } IntToken :: struct { kind Kind, value i8 }
StringToken :: struct { kind Kind, value []u8 } StringToken :: struct { kind Kind, value []u8 }
StopToken :: struct { kind Kind, value bool } StopToken :: struct { kind Kind, value bool }
take_i8 func(value i8) void { _ = value } take_i8 func(value i8) void { _ = value }
main func() void { main func() void {
inline for { expand for {
IntToken {kind = .integer, value = 1}, IntToken {kind = .integer, value = 1},
StringToken {kind = .string, value = "ok"}, StringToken {kind = .string, value = "ok"},
StopToken {kind = .stop, value = false}, StopToken {kind = .stop, value = false},
@@ -2840,7 +2936,7 @@ main func() void {
.stop: break .stop: break
} }
} }
inline for {i8(2), "skip", "stop"} |value, index| { expand for {i8(2), "skip", "stop"} |value, index| {
match index { match index {
0: {} 0: {}
1..=1, 7: continue 1..=1, 7: continue
@@ -5416,7 +5512,7 @@ main func() i32 {
return a + b - 16 return a + b - 16
} }
` `
source_file := source.Source{path="inline_errors.bro", text=text} source_file := source.Source{path="expand_errors.bro", text=text}
diagnostics := source.init_diagnostics(&source_file) diagnostics := source.init_diagnostics(&source_file)
defer source.destroy_diagnostics(&diagnostics) defer source.destroy_diagnostics(&diagnostics)
symbols := symbol.init_table() symbols := symbol.init_table()
+94
View File
@@ -0,0 +1,94 @@
Kind :: enum {
first
second
third
}
Pair :: struct {
left i32
right i32
}
Value :: union(enum) {
number i32
pair Pair
empty void
}
enum_score func(kind Kind) i32 {
result :: match kind {
.first: 1
expand |value|: {
yield match value {
.second: 2
.third: 3
}
}
}
return result
}
equal_value func(a, b Value) bool {
if (tag!(a) != tag!(b)) return false
result :: match a {
expand |value, tag|: {
yield match tag {
.number: value == field!(b, tagname!(tag))
.pair: {
other :: field!(b, tagname!(tag))
yield value.left == other.left and value.right == other.right
}
.empty: {
yield true
}
}
}
}
return result
}
increment func(value @mut Value) void {
match value^ {
expand |@payload, tag|: match tag {
.number: payload^ += 1
.pair: payload.left += 1
.empty: _ = payload
}
}
}
main func() i32 {
if enum_score(.first) != 1 or enum_score(.second) != 2 or enum_score(.third) != 3 {
return 1
}
known :: $tag!(Value{number = 1})
known_direct :: tag!(Value{pair = Pair{left = 0, right = 0}})
if (known != .number) or (known_direct != .pair) {
return 2
}
a Value = .number{41}
b Value = .number{41}
if !equal_value(a, b) or equal_value(a, .pair{left = 41, right = 0}) {
return 3
}
increment(&a)
if a.number != 42 {
return 4
}
pair_a Value = .pair{left = 2, right = 3}
pair_b Value = .pair{left = 2, right = 3}
if !equal_value(pair_a, pair_b) or !equal_value(.empty, .empty) {
return 5
}
increment(&pair_a)
if pair_a.pair.left != 3 {
return 6
}
empty Value = .empty
increment(&empty)
return 0
}
+3 -3
View File
@@ -11,7 +11,7 @@ format func() []u8 {
sum func($T type, value T) i32 { sum func($T type, value T) i32 {
total i32 = 0 total i32 = 0
match typeinfo!(T) { match typeinfo!(T) {
.record |record|: inline for record.fields |field| { .record |record|: expand for record.fields |field| {
total += i32(field!(value, field.name)) total += i32(field!(value, field.name))
} }
else: compile_error!("sum requires a record") else: compile_error!("sum requires a record")
@@ -22,7 +22,7 @@ sum func($T type, value T) i32 {
static_control func($T type, value T) i32 { static_control func($T type, value T) i32 {
total i32 = 0 total i32 = 0
match typeinfo!(T) { match typeinfo!(T) {
.record |record|: inline for record.fields |field| { .record |record|: expand for record.fields |field| {
{ {
if field.index == 1 { if field.index == 1 {
continue continue
@@ -50,7 +50,7 @@ row_value func(row Row) i32 {
static_aggregates func() i32 { static_aggregates func() i32 {
total i32 = 0 total i32 = 0
inline for {Row {value = 2}, Row {value = 40}} |row| { expand for {Row {value = 2}, Row {value = 40}} |row| {
total += row_value(row) total += row_value(row)
} }
return total return total
+1 -1
View File
@@ -72,7 +72,7 @@
"if" "if"
"while" "while"
"for" "for"
"inline" "expand"
"break" "break"
"continue" "continue"
"defer" "defer"
+2 -2
View File
@@ -339,7 +339,7 @@ hide write_default func(writer Writer, $T type, value T) void ! WriteError {
.pointer: try write_all(writer, value) .pointer: try write_all(writer, value)
.slice: try write_all(writer, value) .slice: try write_all(writer, value)
.enum |enum_info|: { .enum |enum_info|: {
inline for enum_info.fields |field| { expand for enum_info.fields |field| {
if value == field!(T, field.name) { if value == field!(T, field.name) {
try write_all(writer, ".") try write_all(writer, ".")
try write_all(writer, field.name) try write_all(writer, field.name)
@@ -354,7 +354,7 @@ hide write_default func(writer Writer, $T type, value T) void ! WriteError {
} }
print func(writer Writer, $format []u8, $Args type, args Args) void ! WriteError { print func(writer Writer, $format []u8, $Args type, args Args) void ! WriteError {
inline for parse_format(format.len, format, Args) |token| { expand for parse_format(format.len, format, Args) |token| {
match token.kind { match token.kind {
.unused: break .unused: break
.literal: try write_all(writer, format[token.start..token.end]) .literal: try write_all(writer, format[token.start..token.end])
+17 -3
View File
@@ -339,7 +339,7 @@ module.exports = grammar({
), ),
for_statement: $ => seq( for_statement: $ => seq(
optional('inline'), optional('expand'),
'for', 'for',
repeat($._newline), repeat($._newline),
field('iterable', $.expression), field('iterable', $.expression),
@@ -364,16 +364,30 @@ module.exports = grammar({
'}', '}',
), ),
match_arm: $ => seq( match_arm: $ => choice(seq(
field('pattern', choice('else', commaSep1($, $.expression))), field('pattern', choice('else', commaSep1($, $.expression))),
optional($.match_capture), optional($.match_capture),
':', ':',
repeat($._newline), repeat($._newline),
field('body', $._branch_body), field('body', $._branch_body),
), ), seq(
'expand',
$.expand_match_capture,
':',
repeat($._newline),
field('body', $._branch_body),
)),
match_capture: $ => seq('|', optional('@'), field('name', choice($.identifier, $.sink)), '|'), match_capture: $ => seq('|', optional('@'), field('name', choice($.identifier, $.sink)), '|'),
expand_match_capture: $ => seq(
'|',
optional('@'),
field('value', choice($.identifier, $.sink)),
optional(seq(',', field('tag', choice($.identifier, $.sink)))),
'|',
),
_branch_body: $ => $.statement, _branch_body: $ => $.statement,
_value: $ => choice( _value: $ => choice(
+1 -1
View File
@@ -72,7 +72,7 @@
"if" "if"
"while" "while"
"for" "for"
"inline" "expand"
"break" "break"
"continue" "continue"
"defer" "defer"
+182 -69
View File
@@ -2402,7 +2402,7 @@
"members": [ "members": [
{ {
"type": "STRING", "type": "STRING",
"value": "inline" "value": "expand"
}, },
{ {
"type": "BLANK" "type": "BLANK"
@@ -2593,90 +2593,127 @@
] ]
}, },
"match_arm": { "match_arm": {
"type": "SEQ", "type": "CHOICE",
"members": [ "members": [
{ {
"type": "FIELD", "type": "SEQ",
"name": "pattern", "members": [
"content": { {
"type": "CHOICE", "type": "FIELD",
"members": [ "name": "pattern",
{ "content": {
"type": "STRING", "type": "CHOICE",
"value": "else"
},
{
"type": "SEQ",
"members": [ "members": [
{ {
"type": "SYMBOL", "type": "STRING",
"name": "expression" "value": "else"
}, },
{ {
"type": "REPEAT", "type": "SEQ",
"content": { "members": [
"type": "SEQ", {
"members": [ "type": "SYMBOL",
{ "name": "expression"
"type": "REPEAT", },
"content": { {
"type": "SYMBOL", "type": "REPEAT",
"name": "_newline" "content": {
} "type": "SEQ",
}, "members": [
{ {
"type": "STRING", "type": "REPEAT",
"value": "," "content": {
}, "type": "SYMBOL",
{ "name": "_newline"
"type": "REPEAT", }
"content": { },
"type": "SYMBOL", {
"name": "_newline" "type": "STRING",
} "value": ","
}, },
{ {
"type": "SYMBOL", "type": "REPEAT",
"name": "expression" "content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "SYMBOL",
"name": "expression"
}
]
} }
] }
} ]
} }
] ]
} }
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "match_capture"
}, },
{ {
"type": "BLANK" "type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "match_capture"
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": ":"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "_branch_body"
}
} }
] ]
}, },
{ {
"type": "STRING", "type": "SEQ",
"value": ":" "members": [
}, {
{ "type": "STRING",
"type": "REPEAT", "value": "expand"
"content": { },
"type": "SYMBOL", {
"name": "_newline" "type": "SYMBOL",
} "name": "expand_match_capture"
}, },
{ {
"type": "FIELD", "type": "STRING",
"name": "body", "value": ":"
"content": { },
"type": "SYMBOL", {
"name": "_branch_body" "type": "REPEAT",
} "content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "_branch_body"
}
}
]
} }
] ]
}, },
@@ -2722,6 +2759,82 @@
} }
] ]
}, },
"expand_match_capture": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "|"
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "@"
},
{
"type": "BLANK"
}
]
},
{
"type": "FIELD",
"name": "value",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "sink"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "FIELD",
"name": "tag",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "sink"
}
]
}
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "|"
}
]
},
"_branch_body": { "_branch_body": {
"type": "SYMBOL", "type": "SYMBOL",
"name": "statement" "name": "statement"
+43 -5
View File
@@ -658,6 +658,40 @@
] ]
} }
}, },
{
"type": "expand_match_capture",
"named": true,
"fields": {
"tag": {
"multiple": false,
"required": false,
"types": [
{
"type": "identifier",
"named": true
},
{
"type": "sink",
"named": true
}
]
},
"value": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
},
{
"type": "sink",
"named": true
}
]
}
}
},
{ {
"type": "expression", "type": "expression",
"named": true, "named": true,
@@ -1429,7 +1463,7 @@
}, },
"pattern": { "pattern": {
"multiple": true, "multiple": true,
"required": true, "required": false,
"types": [ "types": [
{ {
"type": ",", "type": ",",
@@ -1450,6 +1484,10 @@
"multiple": false, "multiple": false,
"required": false, "required": false,
"types": [ "types": [
{
"type": "expand_match_capture",
"named": true
},
{ {
"type": "match_capture", "type": "match_capture",
"named": true "named": true
@@ -2575,6 +2613,10 @@
"type": "escape_sequence", "type": "escape_sequence",
"named": true "named": true
}, },
{
"type": "expand",
"named": false
},
{ {
"type": "f32", "type": "f32",
"named": false "named": false
@@ -2635,10 +2677,6 @@
"type": "import", "type": "import",
"named": false "named": false
}, },
{
"type": "inline",
"named": false
},
{ {
"type": "int", "type": "int",
"named": false "named": false
+119691 -119134
View File
File diff suppressed because it is too large Load Diff
+107 -2
View File
@@ -279,7 +279,7 @@ choose func() i32 {
(identifier))))))) (identifier)))))))
================== ==================
Tuples and inline for Tuples and expand for
================== ==================
Pair :: struct { i32, []u8 } Pair :: struct { i32, []u8 }
@@ -289,7 +289,7 @@ main func() void {
singleton :: {1,} singleton :: {1,}
empty :: {} empty :: {}
_ = pair.0 _ = pair.0
inline for singleton |value| { expand for singleton |value| {
_ = value _ = value
} }
} }
@@ -365,3 +365,108 @@ main func() void {
(sink)) (sink))
(expression (expression
(identifier)))))))))) (identifier))))))))))
==================
Expanded match arms
==================
Kind :: enum { one, two }
Value :: union(enum) { number i32, empty void }
test func(kind Kind, value Value) void {
match kind {
.one: {}
expand |tag|: _ = tag
}
match value {
expand |@payload, tag|: {
_ = payload
_ = tag
}
}
}
---
(source_file
(type_declaration
(identifier)
(enum_type
(enum_body
(enum_member
(identifier))
(enum_member
(identifier)))))
(type_declaration
(identifier)
(union_type
(record_body
(record_field
(identifier)
(type
(builtin_type)))
(record_field
(identifier)
(type
(builtin_type))))))
(function_declaration
(identifier)
(parameter_list
(parameter
(identifier)
(type
(named_type
(qualified_identifier
(identifier)))))
(parameter
(identifier)
(type
(named_type
(qualified_identifier
(identifier))))))
(type
(builtin_type))
(block
(statement
(match_statement
(expression
(identifier))
(match_arm
(expression
(enum_literal
(identifier)))
(statement
(expression_statement
(expression
(tuple_literal)))))
(match_arm
(expand_match_capture
(identifier))
(statement
(assignment_statement
(expression
(sink))
(expression
(identifier)))))))
(statement
(match_statement
(expression
(identifier))
(match_arm
(expand_match_capture
(identifier)
(identifier))
(statement
(block
(statement
(assignment_statement
(expression
(sink))
(expression
(identifier))))
(statement
(assignment_statement
(expression
(sink))
(expression
(identifier))))))))))))