Compare commits
2 Commits
9e75549d02
...
0eeacc2e37
| Author | SHA1 | Date | |
|---|---|---|---|
| 0eeacc2e37 | |||
| 6de4d9f9f3 |
+15
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -75,6 +75,7 @@ Kind :: enum u8 {
|
||||
Keyword_Break,
|
||||
Keyword_Continue,
|
||||
Keyword_Defer,
|
||||
Keyword_Errdefer,
|
||||
Keyword_Yield,
|
||||
Keyword_Match,
|
||||
Keyword_Else,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
@@ -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 @@
|
||||
"/"
|
||||
"!"
|
||||
"&"
|
||||
"@"
|
||||
"?"
|
||||
"^"
|
||||
".."
|
||||
|
||||
@@ -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 @@
|
||||
"/"
|
||||
"!"
|
||||
"&"
|
||||
"@"
|
||||
"?"
|
||||
"^"
|
||||
".."
|
||||
|
||||
@@ -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": "|"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
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)))))))
|
||||
|
||||
Reference in New Issue
Block a user