Compare commits

...

2 Commits

Author SHA1 Message Date
hl-valdemar 0eeacc2e37 scalar family constraints for struct fields 2026-07-13 23:15:22 +02:00
hl-valdemar 6de4d9f9f3 errdefer and try/defer fix 2026-07-13 19:57:23 +02:00
23 changed files with 113775 additions and 105494 deletions
+15 -1
View File
@@ -39,6 +39,20 @@ roadmap and milestone history.
- native sum composition with `A | B` for unbacked enums and tagged unions, using program-global `u16` variant ids
- fallible channel types `T ! E`, where `E` is a native enum/tagged union or supported sum composition
#### native record constraint fields
A direct `int`, `float`, or `range` field in a named native struct or union is a
program-wide constraint, not per-value polymorphism. Before record layout, all reachable keyed
constructors, field assignments, and concrete uses of field reads contribute demands and the field
resolves once to one concrete runtime type. Compatible scalar demands widen normally. Integer
literals remain provisional until inference settles, so a later `usize` use can resolve an `int`
field to `usize`; otherwise literal-only `int` fields use the widest smallest-signed type required,
and literal-only `float` fields use `f64`.
An undemanded field or incompatible demands are errors. This inference applies only to direct
fields of named native records. `c_struct` fields, nested constraints such as `[]int`, and fields in
anonymous generated records still require concrete runtime types.
#### keyword member names
Reserved keywords are valid native enum members and tagged-union variants when used in an
@@ -76,7 +90,7 @@ fields. `_` is not a keyword member name.
- `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
- `break`, `continue`, labeled `break :label`, labeled `continue :label`, and labeled plain blocks
- bare block scopes and `defer`, including LIFO flushing on fall-through, `return`, `break`, and `continue`
- bare block scopes, `defer`, and fallible-function `errdefer` with optional error capture; cleanup is block-scoped and LIFO
- value blocks, value `if`, value loops, value `match`, `yield`, and labeled `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
- fallible `try`, fallback `catch`, and `catch |e| { ... }` handler blocks
+1 -1
View File
@@ -196,7 +196,7 @@ Current prototype features:
- Integer and type comptime parameters (`func($N usize) [N]u8`, `func($T type, value T) T`) specialized by explicit or uniquely inferred leading comptime arguments
- Forced typed comptime expressions (`$sum(1, 2)`, `$Point { x = 1, y = 2 }`) and comptime value blocks (`${ yield 4 }`)
- Zig-style comptime type factories returning anonymous native structs (`Box func($T type) type`, used as `Box(i32)`)
- 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
- Comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`/`errdefer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values
- Native function pointer values and types (`*func(...) R`, `*func(...) R ! E`, `?*func(...) R`)
- Typed allocation/reallocation through `std/mem` and generic dynamic arrays through `std/arraylist`
- Bodyless concrete C function declarations with exact external symbol names
+9 -4
View File
@@ -300,6 +300,9 @@
- `defer <stmt>` runs the statement when the enclosing block scope exits, in reverse
(LIFO) order, on every exit path: fall-through, `return`, `break`, `continue`. The
deferred statement may be a block (`defer { ... }`)
- `errdefer [|error|] <stmt>` is the fallible-function counterpart: it runs only when
an explicit or `try`-propagated error exits its active block scope, can capture the
widened enclosing error, and stays interleaved with ordinary defers in LIFO order
- bare block statements `{ ... }` were added as the enabling feature: a `{ ... }`
introduces a nested scope (locals are name-scoped to it; defers inside it fire at the
closing brace). A leading `{` is unambiguous since struct literals are postfix only
@@ -310,10 +313,8 @@
the innermost loop body; fall-through flushes the current block's own defers. Deferring
a `return`/`break`/`continue`/`defer`, a `return` inside a `defer`, or a `break`/
`continue` that would escape a `defer` are all rejected
- implemented entirely in lexer/parser/checker (new `Keyword_Defer`; `Block`/`Defer` AST
kinds reusing `body`/`update`; `parse_block_statement`/`parse_defer`). No HIR opcode:
a bare block is built and spliced inline, and a deferred statement is built once at the
`defer` site and its hir replayed at each exit, so lowering/codegen are unchanged
- cleanup is statically expanded with no runtime registration stack; `try` carries its
active error-exit cleanup into lowering so propagation cannot bypass either defer form
20. add `yield` statement (implemented; first pass — value blocks only; see below)
- a `{ ... }` on the right of a declaration or assignment is a *value block*: its final
@@ -823,6 +824,10 @@
conflicting targets
- root `std` re-exports only `ArrayList(T)` for now; operations remain under `std/arraylist`
35. syntax highlighting (tree-sitter) updates (implemented)
- pointer sigils are highlighted as operators
- type-factory calls in type positions and struct literals are highlighted as functions
## A word on unchecked casts
For casts that bypass safety checks, Honey provides builtin functions:
+3 -1
View File
@@ -169,6 +169,7 @@ Stmt :: struct {
immutable: bool,
value_control_flow: bool,
pointer_capture: bool,
error_only: bool,
// Assignments store the lvalue in `target`, the right-hand side in `expr`,
// and the source operator in `assignment_op`. `Set` is ordinary `=`;
// the arithmetic variants are `+=`, `-=`, `*=`, and `/=`.
@@ -187,7 +188,8 @@ Stmt :: struct {
// distinguish `|@item|` from copy capture.
// `Block` statements (a bare `{ ... }` scope) use `body` as their statements.
// `Defer` statements use `update` as the deferred statement (which may itself
// be a `Block`).
// be a `Block`), `error_only` for `errdefer`, and `captures` for its optional
// error capture.
// `Match` statements use `expr` as the subject and `body` as the list of arm
// statements (each a `Match_Arm`). A `Match_Arm` uses `patterns` as its pattern
// list (empty marks the `else` arm; more than one is a multi-pattern arm),
+475 -33
View File
@@ -72,6 +72,12 @@ Yield_Target :: struct {
defer_floor: int,
}
Defer_Entry :: struct {
body: []hir.Stmt_Id,
error_only: bool,
capture: hir.Local_Id,
}
Build_Ctx :: struct {
checker: ^Checker,
pkg: ast.Package_Id,
@@ -96,7 +102,7 @@ Build_Ctx :: struct {
// to be valid). `defer_depth`/`loop_floor` guard control flow inside a deferred
// statement: `return` is rejected while `defer_depth > 0`, and `break`/`continue`
// only see loops opened within the defer (those past `loop_floor`).
defers: ^[dynamic][]hir.Stmt_Id,
defers: ^[dynamic]Defer_Entry,
loop_defer_starts: ^[dynamic]int,
// Parallel to `loop_defer_starts`: the label of each enclosing break target (INVALID
// when unlabeled), so a `break :L` / `continue :L` can target an outer one. A labeled
@@ -168,6 +174,14 @@ Checker :: struct {
global_open_float: []bool,
global_const_value: []i128,
global_demands_dirty: bool,
// Program-wide inference for direct constraint fields in named native records.
// The arrays use Store.fields' existing dense indices; resolved concrete types
// live directly in module.types.fields so layout and lowering need no side table.
record_field_constraints: []types.Type,
record_field_defaults: []types.Type,
record_field_conflicts: []types.Type,
record_field_conflict_spans: []source.Span,
record_field_demands_dirty: bool,
external_global_canonical: []ast.Global_Id,
external_global_diagnostics: []source.Diagnostic_Id,
constants: []Constant,
@@ -2393,6 +2407,211 @@ validate_declarations :: proc(checker: ^Checker) {
}
}
init_record_field_inference :: proc(checker: ^Checker) {
store := &checker.module.types
for item in store.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
end := start+int(item.field_count)
if start < 0 || end > len(store.fields) {
continue
}
for slot in start..<end {
if types.is_constraint(store.fields[slot].type) {
checker.record_field_constraints[slot] = store.fields[slot].type
}
}
}
}
is_inferred_record_field :: proc(checker: ^Checker, slot: int) -> bool {
return slot >= 0 && slot < len(checker.record_field_constraints) &&
types.is_constraint(checker.record_field_constraints[slot])
}
record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, bool) {
for item in checker.module.types.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) {
continue
}
start := int(item.field_start)
if slot >= start && slot < start+int(item.field_count) &&
slot >= 0 && slot < len(checker.module.types.fields) {
return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), true
}
}
return symbol.INVALID, symbol.INVALID, false
}
record_field_conflict :: proc(checker: ^Checker, slot: int, actual: types.Type, span: source.Span) {
if !is_inferred_record_field(checker, slot) ||
types.is_valid(checker.record_field_conflicts[slot]) {
return
}
checker.record_field_conflicts[slot] = actual
checker.record_field_conflict_spans[slot] = span
}
merge_record_field_default :: proc(checker: ^Checker, slot: int, candidate: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, candidate) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, candidate, &checker.module.types) {
record_field_conflict(checker, slot, candidate, span)
return false
}
current := checker.record_field_defaults[slot]
if !is_runtime_type(checker, current) {
checker.record_field_defaults[slot] = candidate
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, candidate) {
return false
}
merged := types.widest(current, candidate)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.record_field_defaults[slot] = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, candidate, span)
return false
}
merge_record_field_demand :: proc(checker: ^Checker, slot: int, demand: types.Type, span: source.Span) -> bool {
if !is_inferred_record_field(checker, slot) || !is_runtime_type(checker, demand) {
return false
}
constraint := checker.record_field_constraints[slot]
if !types.constraint_accepts(constraint, demand, &checker.module.types) {
record_field_conflict(checker, slot, demand, span)
return false
}
current := checker.module.types.fields[slot].type
if types.is_constraint(current) {
checker.module.types.fields[slot].type = demand
checker.record_field_demands_dirty = true
return true
}
if types.equal(current, demand) {
return false
}
merged := types.widest(current, demand)
if types.is_concrete_scalar(merged) {
if !types.equal(current, merged) {
checker.module.types.fields[slot].type = merged
checker.record_field_demands_dirty = true
return true
}
return false
}
record_field_conflict(checker, slot, demand, span)
return false
}
record_field_expr_candidate :: proc(
checker: ^Checker,
slot: int,
expr_id: ast.Expr_Id,
inferred: types.Type,
locals: []Infer_Local,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if !is_inferred_record_field(checker, slot) || expr_id == ast.INVALID_EXPR {
return false
}
expr := checker.ast_module.exprs[expr_id]
constraint := checker.record_field_constraints[slot]
if constant := eval_integer_constant_in_context(checker, expr_id, pkg, file);
constant.kind == .Value && fits_i64(constant.value) {
candidate := types.smallest_signed_for_literal(i64(constant.value))
if constraint == types.FLOAT {
candidate = types.F64
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
if is_float_constant_expr(checker, expr_id) {
return merge_record_field_default(checker, slot, types.F64, expr.span)
}
if numeric_operand_is_open(checker, expr_id, locals, pkg, file) {
candidate := inferred
if constraint == types.FLOAT && types.is_concrete_integer(candidate) {
candidate = types.F64
}
return merge_record_field_default(checker, slot, candidate, expr.span)
}
concrete := types.constraint_target(constraint, inferred, &checker.module.types)
if !is_runtime_type(checker, concrete) {
if is_runtime_type(checker, inferred) {
record_field_conflict(checker, slot, inferred, expr.span)
}
return false
}
return merge_record_field_demand(checker, slot, concrete, expr.span)
}
finalize_record_field_inference :: proc(checker: ^Checker) {
for constraint, slot in checker.record_field_constraints {
if !types.is_constraint(constraint) {
continue
}
record_name, field_name, ok := record_field_owner(checker, slot)
if !ok {
continue
}
current := checker.module.types.fields[slot].type
conflict := checker.record_field_conflicts[slot]
if types.is_valid(conflict) {
if is_runtime_type(checker, current) {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"conflicting types %s and %s for field '%s.%s' declared as '%s'",
types.name(current), types.name(conflict),
symbol_text(checker, record_name), symbol_text(checker, field_name),
types.name(constraint),
)
} else {
source.addf(
checker.diagnostics,
checker.record_field_conflict_spans[slot],
"type %s does not satisfy the '%s' constraint for field '%s.%s'",
types.name(conflict), types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
} else if types.is_constraint(current) {
source.addf(
checker.diagnostics,
source.Span{},
"could not resolve the '%s' constraint for field '%s.%s'",
types.name(constraint),
symbol_text(checker, record_name), symbol_text(checker, field_name),
)
}
if types.is_constraint(current) {
switch constraint {
case types.INT:
checker.module.types.fields[slot].type = types.I64
case types.FLOAT:
checker.module.types.fields[slot].type = types.F64
case types.RANGE:
checker.module.types.fields[slot].type = types.range(&checker.module.types, types.I64)
}
}
}
}
validate_type_nodes :: proc(checker: ^Checker) {
for item, index in checker.module.types.nodes {
id := types.DYNAMIC_START+types.Type(index)
@@ -2453,9 +2672,14 @@ validate_type_nodes :: proc(checker: ^Checker) {
// variant has no runtime value, only a tag. Allowed only here, not for
// structs, untagged unions, or c_structs.
tagged_union := item.kind == .Union && types.is_enum(item.child, &checker.module.types)
for field in types.fields_for(&checker.module.types, id) {
for field, field_index in types.fields_for(&checker.module.types, id) {
field_slot := int(item.field_start)+field_index
if tagged_union && types.is_void(field.type) {
// void variant: no payload to validate.
} else if is_inferred_record_field(checker, field_slot) {
// Direct constraints in named native records are validated after the
// program-wide inference fixpoint. C, nested, and anonymous fields do
// not enter this state and retain the existing validation below.
} else if !types.is_runtime_value(field.type, &checker.module.types) {
source.add(
checker.diagnostics,
@@ -2844,7 +3068,11 @@ infer_compound_expr :: proc(
if types.is_pointer(value, store) {
value = types.child_type(value, store)
}
_, field, ok := find_struct_field(checker, value, expr.name)
slot, field, ok := find_struct_field_slot(checker, value, expr.name)
if ok && is_inferred_record_field(checker, slot) && is_runtime_type(checker, expected) {
_ = merge_record_field_demand(checker, slot, expected, expr.span)
field = checker.module.types.fields[slot]
}
return field.type if ok else types.INVALID
case .Unwrap:
value := infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
@@ -2881,15 +3109,37 @@ infer_compound_expr :: proc(
resize(&block_locals, capture_start)
return success
case .Struct_Literal:
value := types.INVALID
if expr.left != ast.INVALID_EXPR {
value, _ = resolve_type_argument(checker, expr.left, pkg, file)
} else if symbol.is_valid(expr.name) {
target_pkg, available := expr_package(checker, expr, pkg, file)
value = types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
} else {
value = expected
}
value = types.resolve_alias(value, store)
for keyed in expr.args {
_ = infer_nested_expr(checker, checker.ast_module.exprs[keyed].left, locals, pkg, file, demanded, local_types)
keyed_expr := checker.ast_module.exprs[keyed]
if keyed_expr.left == ast.INVALID_EXPR {
continue
}
slot, field, ok := find_struct_field_slot(checker, value, keyed_expr.name)
if !ok || !is_inferred_record_field(checker, slot) {
_ = infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types)
continue
}
field_expected := field.type if is_runtime_type(checker, field.type) else types.INVALID
actual := infer_nested_expr(checker, keyed_expr.left, locals, pkg, file, demanded, local_types, field_expected)
_ = record_field_expr_candidate(checker, slot, keyed_expr.left, actual, locals, pkg, file)
if is_runtime_type(checker, checker.module.types.fields[slot].type) {
_ = record_demand(
checker, keyed_expr.left, checker.module.types.fields[slot].type,
locals, local_types, pkg, file,
)
}
}
if !symbol.is_valid(expr.name) {
return types.INVALID
}
target_pkg, available := expr_package(checker, expr, pkg, file)
value := types.find_named(store, u32(target_pkg), u32(expr.name), file=u32(expr_lookup_file(expr, file))) if available else types.INVALID
return types.resolve_alias(value, store)
return value
case .Keyed:
return infer_nested_expr(checker, expr.left, locals, pkg, file, demanded, local_types)
case:
@@ -2990,8 +3240,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
_, field, ok := find_struct_field(checker, base_type, expr.name)
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
@@ -3014,8 +3268,12 @@ infer_expr :: proc(
base_type = types.child_type(base_type, &checker.module.types)
}
if !types.is_valid(last) {
_, field, ok := find_struct_field(checker, base_type, expr.name)
slot, field, ok := find_struct_field_slot(checker, base_type, expr.name)
if ok {
if is_inferred_record_field(checker, slot) && is_runtime_type(checker, frame.expected) {
_ = merge_record_field_demand(checker, slot, frame.expected, expr.span)
field = checker.module.types.fields[slot]
}
last = field.type
}
}
@@ -3569,6 +3827,16 @@ infer_statements :: proc(
locals^[local_index].mutable {
_ = merge_infer_local_type(checker, &locals^[local_index], value_type, local_types)
}
} else if target_expr.kind == .Field ||
target_expr.kind == .Name && symbol.is_valid(target_expr.qualifier) {
if slot, ok := inferred_record_field_slot_from_expr(
checker, target_expr, locals^[:], local_types, pkg, file,
); ok {
_ = record_field_expr_candidate(
checker, slot, statement.expr, value_type, locals^[:], pkg, file,
)
target_type = checker.module.types.fields[slot].type
}
}
if !rhs_is_arith {
_ = record_demand(checker, statement.expr, target_type, locals^[:], local_types, pkg, file)
@@ -3686,8 +3954,19 @@ infer_statements :: proc(
case .Block:
infer_statements(checker, statement.body, locals, local_types, pkg, file, demanded, result, result_hint)
case .Defer:
capture_start := len(locals^)
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
append(locals, Infer_Local{
name=statement.captures[0],
type=types.fallible_error(result^, &checker.module.types),
declared=types.fallible_error(result^, &checker.module.types),
statement=ast.INVALID_STMT,
})
}
deferred := [1]ast.Stmt_Id{statement.update}
infer_statements(checker, deferred[:], locals, local_types, pkg, file, demanded, result, result_hint)
resize(locals, capture_start)
case .Match:
// The build pass desugars `match` to an if/else chain, but inference runs first
// and must still visit the subject and arm bodies so calls there get specialized
@@ -4080,8 +4359,39 @@ record_demand_shallow :: proc(
return record_demand(checker, expr_id, demand, locals, local_types, pkg, file)
}
inferred_record_field_slot_from_expr :: proc(
checker: ^Checker,
expr: ast.Expr,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> (int, bool) {
base := types.INVALID
field_name := expr.name
if expr.kind == .Field {
base = infer_nested_expr(checker, expr.left, locals, pkg, file, nil, local_types)
} else if expr.kind == .Name && symbol.is_valid(expr.qualifier) &&
find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT {
base = find_infer_local(locals, expr.qualifier)
if !types.is_valid(base) {
if global := find_global(checker, expr.qualifier, pkg, file); global != ast.INVALID_GLOBAL {
base = checker.global_types[global]
}
}
} else {
return -1, false
}
if types.is_pointer(base, &checker.module.types) {
base = types.child_type(base, &checker.module.types)
}
slot, _, ok := find_struct_field_slot(checker, base, field_name)
return slot, ok && is_inferred_record_field(checker, slot)
}
// record_demand pushes a concrete type demand onto open numeric slots reachable
// through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5).
// through bare names, inferred record fields, and numeric arithmetic. Calls remain
// a boundary (milestone 14.5).
record_demand :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
@@ -4098,6 +4408,13 @@ record_demand :: proc(
expr := checker.ast_module.exprs[expr_id]
#partial switch expr.kind {
case .Name:
if symbol.is_valid(expr.qualifier) {
if slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
}
if !symbol.is_valid(expr.qualifier) {
if index, ok := find_infer_local_index(locals, expr.name); ok {
return merge_local_demand(checker, &locals[index], demand, local_types)
@@ -4111,6 +4428,12 @@ record_demand :: proc(
if global != ast.INVALID_GLOBAL {
return merge_global_demand(checker, global, demand)
}
case .Field:
if slot, ok := inferred_record_field_slot_from_expr(
checker, expr, locals, local_types, pkg, file,
); ok {
return merge_record_field_demand(checker, slot, demand, expr.span)
}
case .Negate:
if types.is_signed(demand, checker.target) || types.is_float(demand, checker.target) {
return record_demand(checker, expr.left, demand, locals, local_types, pkg, file)
@@ -4184,6 +4507,7 @@ infer_all :: proc(checker: ^Checker) {
for {
changed := false
checker.global_demands_dirty = false
checker.record_field_demands_dirty = false
spec_count := len(checker.specs)
// Backward demands: a global pushes its own (declared or already-resolved) type
@@ -4269,6 +4593,9 @@ infer_all :: proc(checker: ^Checker) {
if checker.global_demands_dirty {
changed = true
}
if checker.record_field_demands_dirty {
changed = true
}
if !changed {
if !defaults_applied {
defaults_applied = true
@@ -4287,6 +4614,15 @@ infer_all :: proc(checker: ^Checker) {
defaulted = true
}
}
for fallback, slot in checker.record_field_defaults {
if !is_inferred_record_field(checker, slot) ||
!types.is_constraint(checker.module.types.fields[slot].type) ||
!is_runtime_type(checker, fallback) {
continue
}
checker.module.types.fields[slot].type = fallback
defaulted = true
}
if defaulted {
continue
}
@@ -4785,6 +5121,19 @@ find_struct_field :: proc(checker: ^Checker, struct_type: types.Type, name: symb
return 0, {}, false
}
find_struct_field_slot :: proc(checker: ^Checker, struct_type: types.Type, name: symbol.Id) -> (int, types.Field, bool) {
item, ok := types.node(&checker.module.types, struct_type)
if !ok || (item.kind != .Struct && item.kind != .Union) {
return -1, {}, false
}
for field, index in types.fields_for(&checker.module.types, struct_type) {
if field.name == u32(name) {
return int(item.field_start)+index, field, true
}
}
return -1, {}, false
}
field_type_from_value :: proc(checker: ^Checker, expr: ast.Expr, base_type: types.Type) -> types.Type {
store := &checker.module.types
field_name := symbol_text(checker, expr.name)
@@ -5508,6 +5857,10 @@ build_compound_expr :: proc(
target=hir.INVALID_REF, diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Try:
if checker.current_build_ctx != nil && checker.current_build_ctx.defer_depth > 0 {
id := source.add(checker.diagnostics, expr.span, "cannot 'try' inside a 'defer'")
return invalid_hir_expr(checker, expr.span, id)
}
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID
channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file)
channel_type := checker.module.exprs[channel].type
@@ -5532,11 +5885,18 @@ build_compound_expr :: proc(
id := source.add(checker.diagnostics, expr.span, "'try' error channel cannot be widened to the enclosing error channel")
return invalid_hir_expr(checker, expr.span, id, success)
}
cleanup: []hir.Stmt_Id
captures: []hir.Expr_Id
if checker.current_build_ctx != nil {
cleanup, captures = try_cleanup(checker.current_build_ctx)
}
return add_hir_expr(checker, hir.Expr{
kind=.Try,
span=expr.span,
type=success,
left=channel,
body=cleanup,
args=captures,
target=hir.INVALID_REF,
right=hir.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
@@ -6701,17 +7061,51 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
return fmt.aprintf("%s", strings.to_string(builder), allocator = checker.allocator)
}
// Replay the deferred statements in frames `[lo, len(defers))` into `body`,
// innermost-most-recent first (LIFO across frames); each frame's own statements
// keep their forward order. Used at every scope-exit path in `build_block`.
flush_defers :: proc(ctx: ^Build_Ctx, body: ^[dynamic]hir.Stmt_Id, lo: int) {
// Replay eligible cleanup in `[lo, len(defers))` in LIFO order. Error exits run
// both defer forms; other exits skip errdefer. A direct error return supplies its
// preserved payload so captured errors can be initialized before each cleanup.
flush_defers :: proc(
ctx: ^Build_Ctx,
body: ^[dynamic]hir.Stmt_Id,
lo: int,
error_exit := false,
error_value := hir.INVALID_EXPR,
) {
for i := len(ctx.defers^) - 1; i >= lo; i -= 1 {
for stmt_id in ctx.defers^[i] {
entry := ctx.defers^[i]
if entry.error_only && !error_exit {
continue
}
if error_exit && entry.capture != hir.INVALID_LOCAL && error_value != hir.INVALID_EXPR {
append(body, hir.stmt_id(len(ctx.checker.module.statements)))
append(&ctx.checker.module.statements, hir.Stmt{
kind=.Declaration, local=entry.capture, expr=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
for stmt_id in entry.body {
append(body, stmt_id)
}
}
}
// Copy the active error-exit cleanup onto a Try expression. Capture locals are
// initialized by lowering once the propagated error has been extracted/widened.
try_cleanup :: proc(ctx: ^Build_Ctx) -> ([]hir.Stmt_Id, []hir.Expr_Id) {
body: [dynamic]hir.Stmt_Id
body.allocator = ctx.checker.allocator
captures: [dynamic]hir.Expr_Id
captures.allocator = ctx.checker.allocator
for i := len(ctx.defers^) - 1; i >= 0; i -= 1 {
entry := ctx.defers^[i]
if entry.error_only && entry.capture != hir.INVALID_LOCAL {
append(&captures, hir.Expr_Id(entry.capture))
}
append(&body, ..entry.body)
}
return body[:], captures[:]
}
build_block :: proc(
ctx: ^Build_Ctx,
statements: []ast.Stmt_Id,
@@ -7144,6 +7538,8 @@ build_block :: proc(
continue
}
value := hir.INVALID_EXPR
error_exit := false
error_value := hir.INVALID_EXPR
if statement.value_control_flow {
if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
@@ -7157,13 +7553,12 @@ build_block :: proc(
} else if types.kind(ctx.result, store) == .Fallible {
success := types.fallible_success(ctx.result, store)
error_type := types.fallible_error(ctx.result, store)
error_path := false
expr_ast := checker.ast_module.exprs[statement.expr]
if expr_ast.kind == .Enum_Literal {
success_has := types.sum_has_name(store, success, u32(expr_ast.name))
error_has := types.sum_has_name(store, error_type, u32(expr_ast.name))
if error_has && !success_has {
error_path = true
error_exit = true
} else if error_has && success_has {
id := source.add(checker.diagnostics, expr_ast.span, "ambiguous fallible return member")
value = invalid_hir_expr(checker, expr_ast.span, id, ctx.result)
@@ -7172,7 +7567,7 @@ build_block :: proc(
target_pkg, available := expr_package(checker, expr_ast, ctx.pkg, ctx.file, true)
named := types.find_named(store, u32(target_pkg), u32(expr_ast.name), file=u32(expr_lookup_file(expr_ast, ctx.file))) if available else types.INVALID
named = types.resolve_alias(named, store)
error_path = can_implicitly_convert_type(checker, named, error_type)
error_exit = can_implicitly_convert_type(checker, named, error_type)
} else {
probe := build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -7181,22 +7576,38 @@ build_block :: proc(
probe_type := checker.module.exprs[probe].type
if can_implicitly_convert_type(checker, probe_type, error_type) &&
!can_implicitly_convert_type(checker, probe_type, success) {
error_path = true
error_exit = true
value = probe
} else if can_implicitly_convert_type(checker, probe_type, success) {
value = probe
}
}
if value == hir.INVALID_EXPR {
expected := error_type if error_path else success
expected := error_type if error_exit else success
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
expected, ctx.pkg, ctx.file,
)
}
expected := error_type if error_path else success
expected := error_type if error_exit else success
value = coerce_expr(checker, value, expected, statement.span)
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_path)
if error_exit && len(ctx.defers^) > 0 && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals, ctx.local_spans, ctx.local_used, ctx.local_warnable,
hir.Local{name=checker.sink_symbol, type=error_type, mutable=false}, source.Span{},
)
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Declaration, span=statement.span, local=tmp, expr=value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
error_value = hir.expr_id(len(checker.module.exprs))
append(&checker.module.exprs, hir.Expr{
kind=.Local, span=statement.span, type=error_type, target=hir.local_ref(tmp),
})
value = error_value
}
value = fallible_aggregate(checker, statement.span, ctx.result, value, error_exit)
} else {
value = build_expr(
checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
@@ -7210,7 +7621,7 @@ build_block :: proc(
// can't change what is returned Zig evaluates the return value, then
// runs defers.
if len(ctx.defers^) > 0 {
if checker.module.exprs[value].kind != .Invalid {
if !error_exit && checker.module.exprs[value].kind != .Invalid {
tmp := append_tracked_local(
ctx.hir_locals,
ctx.local_spans,
@@ -7229,7 +7640,7 @@ build_block :: proc(
kind = .Local, span = statement.span, type = ctx.result, target = hir.local_ref(tmp),
})
}
flush_defers(ctx, &body, 0)
flush_defers(ctx, &body, 0, error_exit, error_value)
}
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
@@ -7753,16 +8164,37 @@ build_block :: proc(
ctx.problematic^ = true
continue
}
if statement.error_only && types.kind(ctx.result, store) != .Fallible {
id := source.add(checker.diagnostics, statement.span, "'errdefer' requires an enclosing fallible function")
append(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{
kind=.Trap, span=statement.span, expr=hir.INVALID_EXPR,
local=hir.INVALID_LOCAL, diagnostic=id,
})
ctx.problematic^ = true
continue
}
// Build the deferred statement once, guarded so a `return` inside it is
// rejected and `break`/`continue` only target loops opened within the
// defer; its hir is replayed at each scope exit, not emitted here.
capture_start := len(ctx.locals^)
capture := hir.INVALID_LOCAL
if statement.error_only && len(statement.captures) > 0 &&
statement.captures[0] != checker.sink_symbol {
capture = append_build_local(
ctx, statement.captures[0], types.fallible_error(ctx.result, store), false, statement.span,
)
}
saved_floor := ctx.loop_floor
ctx.defer_depth += 1
ctx.loop_floor = len(ctx.loop_defer_starts^)
entry := build_block(ctx, []ast.Stmt_Id{statement.update})
ctx.loop_floor = saved_floor
ctx.defer_depth -= 1
append(ctx.defers, entry)
resize(ctx.locals, capture_start)
append(ctx.defers, Defer_Entry{
body=entry, error_only=statement.error_only, capture=capture,
})
case .Match:
build_match(ctx, &body, statement)
case .Match_Arm:
@@ -7795,7 +8227,7 @@ build_block :: proc(
// Free this block's deferred-statement entry slices (their stmt ids were
// already replayed at every path that can leave this block) and pop the frame.
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -7887,7 +8319,7 @@ build_value_block :: proc(
}
// Close the scope (build_block left it open for us).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -8924,7 +9356,7 @@ block_element_type :: proc(ctx: ^Build_Ctx, block_stmts: []ast.Stmt_Id) -> types
result := checker.module.exprs[probe].type if checker.module.exprs[probe].kind != .Invalid else types.INVALID
// Discard the throwaway leading build's scope (its hir stmts/locals are dead but stable).
for i := defer_start; i < len(ctx.defers^); i += 1 {
delete(ctx.defers^[i], checker.allocator)
delete(ctx.defers^[i].body, checker.allocator)
}
resize(ctx.defers, defer_start)
resize(ctx.locals, scope_start)
@@ -9310,7 +9742,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
},
)
}
defers: [dynamic][]hir.Stmt_Id
defers: [dynamic]Defer_Entry
defers.allocator = checker.allocator
loop_defer_starts: [dynamic]int
loop_defer_starts.allocator = checker.allocator
@@ -9392,7 +9824,7 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
},
)
for entry in defers {
delete(entry, checker.allocator)
delete(entry.body, checker.allocator)
}
delete(defers)
delete(loop_defer_starts)
@@ -9856,6 +10288,11 @@ check :: proc(
checker.global_open_const = make([]bool, len(ast_module.globals), allocator)
checker.global_open_float = make([]bool, len(ast_module.globals), allocator)
checker.global_const_value = make([]i128, len(ast_module.globals), allocator)
checker.record_field_constraints = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflicts = make([]types.Type, len(checker.module.types.fields), allocator)
checker.record_field_conflict_spans = make([]source.Span, len(checker.module.types.fields), allocator)
init_record_field_inference(&checker)
checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator)
checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator)
for &canonical in checker.external_global_canonical {
@@ -9883,6 +10320,10 @@ check :: proc(
delete(checker.global_open_const, allocator)
delete(checker.global_open_float, allocator)
delete(checker.global_const_value, allocator)
delete(checker.record_field_constraints, allocator)
delete(checker.record_field_defaults, allocator)
delete(checker.record_field_conflicts, allocator)
delete(checker.record_field_conflict_spans, allocator)
delete(checker.external_global_canonical, allocator)
delete(checker.external_global_diagnostics, allocator)
delete(checker.constants, allocator)
@@ -9943,6 +10384,7 @@ check :: proc(
validate_declarations(&checker)
configure_io_main(&checker)
infer_all(&checker)
finalize_record_field_inference(&checker)
validate_external_globals(&checker)
prune_specs(&checker)
build_globals(&checker)
+54 -6
View File
@@ -290,7 +290,8 @@ Ct_State :: struct {
places: [dynamic]Ct_Place,
paths: [dynamic]Ct_Path_Elem,
bindings: [dynamic]Ct_Binding,
defers: [dynamic]ast.Stmt_Id,
defers: [dynamic]Ct_Defer,
defer_depth: int,
steps: int,
error: Ct_Error_Kind,
diagnostic: source.Diagnostic_Id,
@@ -298,6 +299,12 @@ Ct_State :: struct {
demanded: ^[dynamic]Spec_Id,
}
Ct_Defer :: struct {
statement: ast.Stmt_Id,
error_only: bool,
capture: symbol.Id,
}
ct_state_make :: proc(
checker: ^Checker,
pkg: ast.Package_Id,
@@ -2217,6 +2224,9 @@ ct_clone_value :: proc(dst, src: ^Ct_State, id: Ct_Value_Id) -> Ct_Value_Id {
}
ct_eval_try_expr :: proc(state: ^Ct_State, expr: ast.Expr, depth: int) -> (Ct_Value_Id, Ct_Flow, bool) {
if state.defer_depth > 0 {
return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "cannot 'try' inside a 'defer'")
}
checker := state.checker
channel, flow, ok := ct_eval_expr(state, expr.left, types.INVALID, depth+1)
if !ok || flow.kind != .Normal {
@@ -2465,7 +2475,17 @@ ct_exec_statements :: proc(
case .Block:
flow, ok = ct_exec_statements(state, statement.body, yield_returns, depth+1)
case .Defer:
append(&state.defers, statement.update)
if statement.error_only && types.kind(state.result, &checker.module.types) != .Fallible {
ok = ct_fail(state, .Not_Comptime, statement.span, "'errdefer' requires an enclosing fallible function")
} else {
capture := symbol.INVALID
if len(statement.captures) > 0 {
capture = statement.captures[0]
}
append(&state.defers, Ct_Defer{
statement=statement.update, error_only=statement.error_only, capture=capture,
})
}
case .Match:
flow, ok = ct_exec_match(state, statement, yield_returns, depth+1)
case .Match_Arm:
@@ -2477,22 +2497,50 @@ ct_exec_statements :: proc(
return flow, false
}
if flow.kind != .Normal {
if !ct_flush_defers(state, defer_start, depth+1) {
if !ct_flush_defers(state, defer_start, flow, depth+1) {
return flow, false
}
return flow, true
}
}
if !ct_flush_defers(state, defer_start, depth+1) {
if !ct_flush_defers(state, defer_start, ct_flow(.Normal), depth+1) {
return ct_flow(.Normal), false
}
return ct_flow(.Normal), true
}
ct_flush_defers :: proc(state: ^Ct_State, start: int, depth: int) -> bool {
ct_flush_defers :: proc(state: ^Ct_State, start: int, exit: Ct_Flow, depth: int) -> bool {
error_exit := false
error_value := INVALID_CT_VALUE
if exit.kind == .Return && exit.value != INVALID_CT_VALUE && int(exit.value) < len(state.values) {
returned := state.values[exit.value]
if returned.kind == .Fallible && returned.active != 0 {
error_exit = true
children := ct_child_slice(state, returned)
if len(children) > 0 {
error_value = children[0]
}
}
}
for index := len(state.defers) - 1; index >= start; index -= 1 {
stmt := [1]ast.Stmt_Id{state.defers[index]}
entry := state.defers[index]
if entry.error_only && !error_exit {
continue
}
binding_start := len(state.bindings)
bound_capture := false
if entry.error_only && symbol.is_valid(entry.capture) &&
entry.capture != state.checker.sink_symbol && error_value != INVALID_CT_VALUE {
ct_bind_value(state, entry.capture, state.values[error_value].type, error_value, false)
bound_capture = true
}
stmt := [1]ast.Stmt_Id{entry.statement}
state.defer_depth += 1
flow, ok := ct_exec_statements(state, stmt[:], false, depth+1)
state.defer_depth -= 1
if bound_capture {
ct_pop_bindings(state, binding_start)
}
if !ok || flow.kind != .Normal {
return false
}
+4 -1
View File
@@ -136,10 +136,13 @@ Expr :: struct {
span: source.Span,
type: types.Type,
integer: i64,
// Aggregate/call children normally; Try stores errdefer capture local IDs
// encoded as Expr_Id because it otherwise has no args.
args: []Expr_Id,
// `Catch` block handlers use `body` for the handler statements and `target`
// for the optional captured error local. A missing `right` means the handler
// exits on every path and therefore has no fallback value.
// exits on every path and therefore has no fallback value. `Try` uses `body`
// for active error-exit cleanup.
body: []Stmt_Id,
target: Ref,
left: Expr_Id,
+1
View File
@@ -39,6 +39,7 @@ keyword_kind :: proc(text: string) -> token.Kind {
case "break": return .Keyword_Break
case "continue": return .Keyword_Continue
case "defer": return .Keyword_Defer
case "errdefer": return .Keyword_Errdefer
case "yield": return .Keyword_Yield
case "match": return .Keyword_Match
case "else": return .Keyword_Else
+31 -8
View File
@@ -416,10 +416,11 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
})
if expr.kind == .Try {
result := channel
if !types.equal(channel_type, state.func_result) {
error_value := ir.INVALID_INSTRUCTION
if !types.equal(channel_type, state.func_result) || len(expr.args) > 0 {
error_type := types.fallible_error(channel_type, &state.hir_module.types)
enclosing_error := types.fallible_error(state.func_result, &state.hir_module.types)
error_value := append_instruction(state, ir.Instruction{
error_value = append_instruction(state, ir.Instruction{
op=.Fallible_Error, span=expr.span, type=error_type,
target=ir.INVALID_REF, a=channel_slot, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
@@ -431,14 +432,36 @@ lower_compound_expr :: proc(state: ^State, expr_id: hir.Expr_Id) -> ir.Instructi
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
args := make([]ir.Instruction_Id, 1, state.allocator)
args[0] = error_value
result = append_instruction(state, ir.Instruction{
op=.Aggregate, span=expr.span, type=state.func_result, integer=1,
args=args, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
if !types.equal(channel_type, state.func_result) {
args := make([]ir.Instruction_Id, 1, state.allocator)
args[0] = error_value
result = append_instruction(state, ir.Instruction{
op=.Aggregate, span=expr.span, type=state.func_result, integer=1,
args=args, target=ir.INVALID_REF, a=ir.INVALID_INSTRUCTION,
b=ir.INVALID_INSTRUCTION, diagnostic=source.INVALID_DIAGNOSTIC,
})
}
}
for encoded_capture in expr.args {
capture := hir.Local_Id(encoded_capture)
if capture == hir.INVALID_LOCAL || int(capture) >= len(state.func_locals) {
continue
}
error_type := state.func_locals[capture].type
capture_slot := append_instruction(state, ir.Instruction{
op=.Alloca, span=expr.span, type=error_type,
target=ir.local_ref(ir.Local_Id(capture)),
a=ir.INVALID_INSTRUCTION, b=ir.INVALID_INSTRUCTION,
diagnostic=source.INVALID_DIAGNOSTIC,
})
state.local_slots[capture] = capture_slot
append_instruction(state, ir.Instruction{
op=.Store, span=expr.span, type=error_type,
target=ir.INVALID_REF, a=capture_slot, b=error_value,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
lower_statements(state, expr.body)
append_instruction(state, ir.Instruction{
op=.Return, span=expr.span, type=state.func_result,
target=ir.INVALID_REF, a=result, b=ir.INVALID_INSTRUCTION,
+31 -5
View File
@@ -1410,18 +1410,44 @@ parse_block_statement :: proc(parser: ^Parser, label := symbol.INVALID) -> ast.S
return id
}
// `defer <statement>` runs the statement when the enclosing scope exits. The
// statement may be a block (`defer { ... }`). The checker rejects deferring a
// `return`/`break`/`continue`/`defer`.
// `defer <statement>` runs whenever the enclosing scope exits; `errdefer`
// runs only when it exits through a function error and may capture that error.
parse_defer :: proc(parser: ^Parser) -> ast.Stmt_Id {
marker := advance(parser) // consume 'defer'
marker := advance(parser)
error_only := marker.kind == .Keyword_Errdefer
skip_newlines(parser)
captures: []symbol.Id
if error_only {
if _, ok := allow(parser, .Pipe); ok {
capture := current(parser)
if capture.kind != .Identifier && capture.kind != .Underscore {
source.add(parser.diagnostics, capture.span, "expected an errdefer capture name")
} else {
advance(parser)
captures = make([]symbol.Id, 1, parser.module.allocator)
captures[0] = capture.symbol
}
if current(parser).kind == .Comma {
source.add(parser.diagnostics, current(parser).span, "'errdefer' accepts exactly one capture")
for current(parser).kind != .Pipe && 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 '|' after errdefer capture")
}
skip_newlines(parser)
}
}
inner := parse_statement(parser)
id := ast.stmt_id(len(parser.module.statements))
append(&parser.module.statements, ast.Stmt{
kind=.Defer,
span=marker.span,
update=inner,
error_only=error_only,
captures=captures,
expr=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
@@ -1486,7 +1512,7 @@ parse_statement :: proc(parser: ^Parser) -> ast.Stmt_Id {
if current(parser).kind == .Keyword_Continue {
return parse_loop_control(parser, .Continue)
}
if current(parser).kind == .Keyword_Defer {
if current(parser).kind == .Keyword_Defer || current(parser).kind == .Keyword_Errdefer {
return parse_defer(parser)
}
if current(parser).kind == .Keyword_Yield {
+1
View File
@@ -75,6 +75,7 @@ Kind :: enum u8 {
Keyword_Break,
Keyword_Continue,
Keyword_Defer,
Keyword_Errdefer,
Keyword_Yield,
Keyword_Match,
Keyword_Else,
+269
View File
@@ -3747,8 +3747,13 @@ defer_misuse_is_diagnosed :: proc(t: ^testing.T) {
bad_defer_return()
bad_defer_break()
bad_return_in_defer()
bad_errdefer_nonfallible()
_ = bad_try_in_defer() catch 0
_ = bad_try_in_errdefer() catch 0
return 0
}
Failure :: enum { bad }
fail func() i32 ! Failure { return .bad }
bad_defer_return func() void {
defer return
}
@@ -3763,6 +3768,17 @@ bad_return_in_defer func() void {
return
}
}
bad_errdefer_nonfallible func() void {
errdefer {}
}
bad_try_in_defer func() i32 ! Failure {
defer _ = try fail()
return 1
}
bad_try_in_errdefer func() i32 ! Failure {
errdefer _ = try fail()
return 1
}
`
source_file := source.Source{path="test.bro", text=text}
diagnostics := source.init_diagnostics(&source_file)
@@ -3779,14 +3795,50 @@ bad_return_in_defer func() void {
defer_return := false
defer_break := false
return_in_defer := false
errdefer_nonfallible := false
try_in_defer := 0
for diagnostic in diagnostics.items {
defer_return = defer_return || strings.contains(diagnostic.message, "cannot defer a 'return' statement")
defer_break = defer_break || strings.contains(diagnostic.message, "cannot defer a 'break' statement")
return_in_defer = return_in_defer || strings.contains(diagnostic.message, "cannot 'return' inside a 'defer'")
errdefer_nonfallible = errdefer_nonfallible || strings.contains(diagnostic.message, "'errdefer' requires an enclosing fallible function")
try_in_defer += 1 if strings.contains(diagnostic.message, "cannot 'try' inside a 'defer'") else 0
}
testing.expect(t, defer_return)
testing.expect(t, defer_break)
testing.expect(t, return_in_defer)
testing.expect(t, errdefer_nonfallible)
testing.expect_value(t, try_in_defer, 2)
}
@(test)
errdefer_capture_syntax_is_diagnosed :: proc(t: ^testing.T) {
text := `Failure :: enum { bad }
bad func() i32 ! Failure {
errdefer || {}
errdefer |first, second| {}
return .bad
}
main func() i32 { return bad() catch 0 }
`
source_file := source.Source{path="test.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)
module := parser.parse(&stream, &source_file, &diagnostics)
defer ast.destroy_module(&module)
missing := false
multiple := false
for diagnostic in diagnostics.items {
missing = missing || strings.contains(diagnostic.message, "expected an errdefer capture name")
multiple = multiple || strings.contains(diagnostic.message, "'errdefer' accepts exactly one capture")
}
testing.expect(t, missing)
testing.expect(t, multiple)
}
@(test)
@@ -7411,6 +7463,7 @@ main func() void { value Bad(i32) = undefined; _ = &value }
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
@@ -7880,6 +7933,7 @@ parser_diagnoses_malformed_conditional_unwrap_captures_and_guards :: proc(t: ^te
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
}
@(test)
@@ -10946,3 +11000,218 @@ main func() void {
}
testing.expect(t, found)
}
named_record_field_type :: proc(
module: ^hir.Module,
symbols: ^symbol.Table,
record_name, field_name: string,
) -> (types.Type, bool) {
record := types.find_named(&module.types, 0, u32(symbol.intern(symbols, record_name)))
if !types.is_record(record, &module.types) {
return types.INVALID, false
}
field_symbol := symbol.intern(symbols, field_name)
for field in types.fields_for(&module.types, record) {
if field.name == u32(field_symbol) {
return field.type, true
}
}
return types.INVALID, false
}
@(test)
native_record_constraint_fields_resolve_program_wide :: proc(t: ^testing.T) {
text := `Token :: struct { start int }
Backward :: struct { start int }
Wide :: struct { value int }
Literal :: struct { value int }
Measurement :: struct {
ratio float
span range
}
Payload :: union { count int }
take_usize func(value usize) usize { return value }
exercise func(cursor usize, narrow i8, wider i16, count i32) i32 {
token Token = Token{start = cursor}
wide Wide = Wide{value = narrow}
wide.value = wider
small Literal = Literal{value = 1}
large Literal = Literal{value = 1000}
backward Backward = Backward{start = 1}
measurement Measurement = Measurement{ratio = 1.5, span = 0..3}
payload Payload = Payload{count = count}
_ = token.start
_ = wide.value
_ = small.value
_ = large.value
_ = take_usize(backward.start)
_ = measurement.ratio
_ = measurement.span
_ = payload.count
return 0
}
main func() i32 { return exercise(7, 1, 1000, 3) }
`
source_file := source.Source{path="record_constraints.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)
token_start, token_ok := named_record_field_type(&hir_module, &symbols, "Token", "start")
backward_start, backward_ok := named_record_field_type(&hir_module, &symbols, "Backward", "start")
wide_value, wide_ok := named_record_field_type(&hir_module, &symbols, "Wide", "value")
literal_value, literal_ok := named_record_field_type(&hir_module, &symbols, "Literal", "value")
ratio, ratio_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "ratio")
span, span_ok := named_record_field_type(&hir_module, &symbols, "Measurement", "span")
count, count_ok := named_record_field_type(&hir_module, &symbols, "Payload", "count")
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect(t, token_ok && backward_ok && wide_ok && literal_ok && ratio_ok && span_ok && count_ok)
testing.expect_value(t, token_start, types.USIZE)
testing.expect_value(t, backward_start, types.USIZE)
testing.expect_value(t, wide_value, types.I16)
testing.expect_value(t, literal_value, types.I16)
testing.expect_value(t, ratio, types.F64)
testing.expect(t, types.is_range(span, &hir_module.types))
testing.expect_value(t, types.child_type(span, &hir_module.types), types.I8)
testing.expect_value(t, count, types.I32)
}
@(test)
native_record_int_field_compiles_and_runs_as_usize :: proc(t: ^testing.T) {
directory := "/tmp/brolang-test-record-field-usize"
main_path := "/tmp/brolang-test-record-field-usize/main.bro"
output := "/tmp/brolang-test-record-field-usize-output"
text := `Token :: struct { start int }
main func() i32 {
cursor usize = 7
token Token = Token{start = cursor}
if (token.start != cursor) return 1
return 0
}
`
_ = 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)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)
native_record_constraint_fields_report_unresolved_and_conflicting_demands :: proc(t: ^testing.T) {
cases := [2]struct {
text: string,
needle: string,
}{
{
text = `Token :: struct { start int }
main func() void {}
`,
needle = "could not resolve the 'int' constraint for field 'Token.start'",
},
{
text = `Token :: struct { start int }
use func(signed i32, unsigned usize) void {
a Token = Token{start = signed}
b Token = Token{start = unsigned}
_ = a
_ = b
}
main func() void { use(1, 2) }
`,
needle = "conflicting types i32 and usize for field 'Token.start' declared as 'int'",
},
}
for test_case in cases {
source_file := source.Source{path="bad_record_constraint.bro", text=test_case.text}
diagnostics := source.init_diagnostics(&source_file)
symbols := symbol.init_table()
stream := lexer.lex(&source_file, &diagnostics, &symbols)
ast_module := parser.parse(&stream, &source_file, &diagnostics)
hir_module := checker.check(&ast_module, &diagnostics, &symbols)
ir_module := lower.lower(&hir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
matching := 0
for diagnostic in diagnostics.items {
matching += 1 if strings.contains(diagnostic.message, test_case.needle) else 0
}
testing.expect_value(t, matching, 1)
testing.expect(t, len(llvm_text) > 0)
delete(llvm_text)
ir.destroy_module(&ir_module)
hir.destroy_module(&hir_module)
ast.destroy_module(&ast_module)
delete(stream.items)
symbol.destroy_table(&symbols)
source.destroy_diagnostics(&diagnostics)
}
directory := "/tmp/brolang-test-record-field-conflict"
main_path := "/tmp/brolang-test-record-field-conflict/main.bro"
output := "/tmp/brolang-test-record-field-conflict-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)cases[1].text))
testing.expect_value(t, compiler_core.compile_package(directory, output), 1)
state := run_executable(output)
testing.expect(t, !state.success)
}
@(test)
constraint_fields_remain_rejected_outside_named_native_records :: proc(t: ^testing.T) {
text := `BadC :: c_struct { value int }
BadNested :: struct { values []int }
make_type func() type {
return struct { value int }
}
main func() void {
Generated :: @make_type()
_ = Generated
}
`
source_file := source.Source{path="excluded_record_constraints.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)
record_errors := 0
anonymous_error := false
for diagnostic in diagnostics.items {
record_errors += 1 if strings.contains(diagnostic.message, "record fields must have runtime value types") else 0
anonymous_error = anonymous_error || strings.contains(diagnostic.message, "anonymous struct field 'value' requires a concrete runtime type")
}
c_field, c_ok := named_record_field_type(&hir_module, &symbols, "BadC", "value")
nested_field, nested_ok := named_record_field_type(&hir_module, &symbols, "BadNested", "values")
testing.expect_value(t, record_errors, 1)
testing.expect(t, anonymous_error)
testing.expect(t, c_ok && nested_ok)
testing.expect_value(t, c_field, types.INT)
testing.expect(t, types.is_slice(nested_field, &hir_module.types))
testing.expect_value(t, types.child_type(nested_field, &hir_module.types), types.INT)
}
+34
View File
@@ -91,6 +91,36 @@ use_try func() i32 ! Error {
return value + 1
}
ct_errdefer func(fail bool) i32 ! Error {
trace i32 = 0
defer trace = trace * 10 + 1
errdefer |err| {
if (err == .bad) trace = trace * 10 + 2
}
defer trace = trace * 10 + 3
if (fail) return .bad
return 7
}
ct_try_errdefer func() i32 ! Error {
trace i32 = 0
defer trace = trace * 10 + 4
errdefer |err| {
if (err == .bad) trace = trace * 10 + 5
}
return try may_fail(true)
}
ct_errdefer_check func() i32 {
ok i32 :: ct_errdefer(false) catch 0
if (ok != 7) return 1
explicit_error i32 :: ct_errdefer(true) catch 9
if (explicit_error != 9) return 2
try_error i32 :: ct_try_errdefer() catch 9
if (try_error != 9) return 3
return 42
}
recover func() i32 {
return may_fail(true) catch |e| {
match e {
@@ -135,6 +165,7 @@ storage_mutation func() i32 {
}
GLOBAL :: $sum_loop(4)
ERRDEFER :: $ct_errdefer_check()
main func() i32 {
point Point :: $make_point()
@@ -205,5 +236,8 @@ main func() i32 {
if fallible_err != 13 {
return 16
}
if ERRDEFER != 42 {
return 17
}
return 0
}
+87 -1
View File
@@ -1,4 +1,4 @@
# Milestone 19: `defer` and bare block statements.
# Milestone 19: `defer`, `errdefer`, and bare block statements.
#
# `defer <stmt>` runs the statement when the enclosing scope exits, in reverse
# (LIFO) order, on every exit path. A bare `{ ... }` introduces a scope. Each
@@ -24,6 +24,66 @@ enclosing_defer_check func() i32 {
return v # 0->1 (i=0 fall-through), ->2 (i=1 break); the +100 runs after capture
}
CleanupError :: enum {
bad
}
ExtraError :: enum {
extra
}
CleanupErrors :: alias CleanupError | ExtraError
explicit_cleanup func(fail bool, trace *mut i32) i32 ! CleanupError {
defer trace^ = trace^ * 10 + 1
errdefer |err| {
if (err == .bad) {
trace^ = trace^ * 10 + 2
} else {
trace^ = 99
}
}
defer trace^ = trace^ * 10 + 3
if (fail) return .bad
return 7
}
fail_cleanup func() i32 ! CleanupError {
return .bad
}
try_cleanup func(trace *mut i32) i32 ! CleanupError {
defer trace^ = trace^ * 10 + 4
errdefer trace^ = trace^ * 10 + 5
return try fail_cleanup()
}
widen_cleanup func(trace *mut i32) i32 ! CleanupErrors {
errdefer |err| {
match err {
.bad: trace^ = trace^ * 10 + 6
.extra: trace^ = 99
}
}
return try fail_cleanup()
}
scoped_cleanup func(trace *mut i32) i32 ! CleanupError {
errdefer trace^ = trace^ * 10 + 7
{
errdefer trace^ = 99
}
return .bad
}
multi_exit_cleanup func(direct bool, trace *mut i32) i32 ! CleanupError {
errdefer |err| {
if (err == .bad) trace^ = trace^ + 8
}
if (direct) return .bad
return try fail_cleanup()
}
main func() i32 {
# 1. return value captured before defers run.
if (spill_check() != 5) return 101
@@ -78,5 +138,31 @@ main func() i32 {
# 7. break does not run an enclosing function-scope defer.
if (enclosing_defer_check() != 2) return 107
# 8. errdefer is skipped on success; ordinary defers stay interleaved.
trace i32 = 0
if ((explicit_cleanup(false, &trace) catch 0) != 7 or trace != 31) return 108
# 9. Explicit errors run errdefer and expose the captured error.
trace = 0
if ((explicit_cleanup(true, &trace) catch 9) != 9 or trace != 321) return 109
# 10. Propagated errors run both errdefer and ordinary defer.
trace = 0
if ((try_cleanup(&trace) catch 9) != 9 or trace != 54) return 110
# 11. Captures observe the widened enclosing error type.
trace = 0
if ((widen_cleanup(&trace) catch 9) != 9 or trace != 6) return 111
# 12. An errdefer expires when its block exits normally.
trace = 0
if ((scoped_cleanup(&trace) catch 9) != 9 or trace != 7) return 112
# 13. One captured errdefer can be replayed at several distinct error exits.
trace = 0
_ = multi_exit_cleanup(true, &trace) catch 0
_ = multi_exit_cleanup(false, &trace) catch 0
if (trace != 16) return 113
return 42
}
+2 -2
View File
@@ -1,6 +1,6 @@
id = "brolang"
name = "Brolang"
version = "0.1.0"
version = "0.1.1"
schema_version = 1
authors = ["Brolang contributors"]
description = "Brolang language support"
@@ -9,5 +9,5 @@ languages = ["languages/brolang"]
[grammars.brolang]
repository = "file:///Users/valdemar/Developer/Personal/Languages/brolang"
rev = "zed-dev"
rev = "ba171a2d5e248fa24f5f4ee960e21056893f72e7"
path = "tree-sitter-brolang"
Binary file not shown.
+12
View File
@@ -23,6 +23,16 @@
(builtin_type) @type.builtin
(named_type) @type
(named_type
(qualified_identifier
(identifier) @function .)
(argument_list))
(struct_literal
type: (qualified_identifier
(identifier) @function .)
(argument_list))
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
@@ -62,6 +72,7 @@
"break"
"continue"
"defer"
"errdefer"
"yield"
"match"
"else"
@@ -86,6 +97,7 @@
"/"
"!"
"&"
"@"
"?"
"^"
".."
+11 -1
View File
@@ -278,7 +278,17 @@ module.exports = grammar({
break_statement: $ => seq('break', optional(seq(':', field('label', $.identifier)))),
continue_statement: $ => seq('continue', optional(seq(':', field('label', $.identifier)))),
defer_statement: $ => seq('defer', repeat($._newline), field('body', $.statement)),
defer_statement: $ => choice(
seq('defer', repeat($._newline), field('body', $.statement)),
seq(
'errdefer',
repeat($._newline),
optional(seq(field('capture', $.error_capture), repeat($._newline))),
field('body', $.statement),
),
),
error_capture: $ => seq('|', choice($.identifier, $.sink), '|'),
labeled_block: $ => seq(
field('label', $.identifier),
@@ -23,6 +23,16 @@
(builtin_type) @type.builtin
(named_type) @type
(named_type
(qualified_identifier
(identifier) @function .)
(argument_list))
(struct_literal
type: (qualified_identifier
(identifier) @function .)
(argument_list))
(type_declaration name: (identifier) @type)
(function_declaration name: (identifier) @function)
(parameter name: (identifier) @variable.parameter)
@@ -62,6 +72,7 @@
"break"
"continue"
"defer"
"errdefer"
"yield"
"match"
"else"
@@ -86,6 +97,7 @@
"/"
"!"
"&"
"@"
"?"
"^"
".."
+95 -12
View File
@@ -1855,26 +1855,109 @@
]
},
"defer_statement": {
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "defer"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "statement"
}
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "errdefer"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "capture",
"content": {
"type": "SYMBOL",
"name": "error_capture"
}
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "statement"
}
}
]
}
]
},
"error_capture": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "defer"
"value": "|"
},
{
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_newline"
}
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "sink"
}
]
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "statement"
}
"type": "STRING",
"value": "|"
}
]
},
+33
View File
@@ -517,6 +517,16 @@
"named": true
}
]
},
"capture": {
"multiple": false,
"required": false,
"types": [
{
"type": "error_capture",
"named": true
}
]
}
}
},
@@ -629,6 +639,25 @@
]
}
},
{
"type": "error_capture",
"named": true,
"fields": {},
"children": {
"multiple": false,
"required": true,
"types": [
{
"type": "identifier",
"named": true
},
{
"type": "sink",
"named": true
}
]
}
},
{
"type": "expression",
"named": true,
@@ -2471,6 +2500,10 @@
"type": "enum",
"named": false
},
{
"type": "errdefer",
"named": false
},
{
"type": "escape_sequence",
"named": true
+112536 -105418
View File
File diff suppressed because it is too large Load Diff
@@ -95,3 +95,62 @@ sum func(a, b i32) i32 ! Status {
(expression
(enum_literal
(identifier))))))))))))
==================
Errdefer
==================
Failure :: enum { bad }
work func() i32 ! Failure {
errdefer cleanup()
errdefer |err| {
_ = err
}
return 1
}
---
(source_file
(type_declaration
(identifier)
(enum_type
(enum_body
(enum_member
(identifier)))))
(function_declaration
(identifier)
(parameter_list)
(type
(builtin_type))
(type
(named_type
(qualified_identifier
(identifier))))
(block
(statement
(defer_statement
(statement
(expression_statement
(expression
(call_expression
(expression
(identifier))
(argument_list)))))))
(statement
(defer_statement
(error_capture
(identifier))
(statement
(block
(statement
(assignment_statement
(expression
(sink))
(expression
(identifier))))))))
(statement
(return_statement
(expression
(integer)))))))