diff --git a/AGENTS.md b/AGENTS.md index e1d51b2..9d76f4d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,10 +57,12 @@ This repository contains the Ousia compiler workspace (`crates/*`) plus editor t - External declarations use `extern fun name(args...) -> Type` (no body). In v1 they may appear at top level and inside `namespace` blocks (no bodies, no `comptime`). - In v2 ABI, `extern fun` signatures cannot use struct parameter or return types; C interop boundaries that move struct-like payloads must use manual `PtrInt` wrapper signatures. - Struct field lists allow optional trailing commas in both type declarations and struct literals. -- Struct values use byte-value semantics at assignment/call/return boundaries: codegen inserts copy barriers (`calloc` + `memcpy`) so pointer identity is not language-visible at those boundaries. +- Ownership is move-only by default: reading a non-`Copy` value moves it, and subsequent reads fail (`use of moved value ...` / `cannot move from uninitialized value ...` diagnostics). +- `Copy` controls implicit cloning: when a concrete type implements `Copy`, read sites lower through static dispatch to the concrete `Copy.copy(...)` impl; the canonical trait shape is `copy(v: Ref[Self]) -> Self`, and the backend synthesizes a temporary `Ref` wrapper at read sites for ref-style impls (legacy by-value impl params are still accepted for now). +- `Drop` controls deterministic destruction: the resolver inserts `Drop.drop(value)` calls at reassignment overwrite points and lexical scope exits (reverse declaration order), skipping moved/uninitialized bindings. - Struct equality is universal bytewise comparison: `==` / `!=` lower to `memcmp` over full struct size (including pointer-containing structs). - The stdlib entrypoint `crates/oac/src/std/std.oa` is now an import aggregator over split files in `crates/oac/src/std/` (`std_ascii.oa`, `std_char.oa`, `std_null.oa`, `std_option_result.oa`, `std_string.oa`, `std_ref.oa`, `std_collections.oa`, `std_set.oa`, `std_vec.oa`, `std_traits.oa`, `std_json.oa`, `std_clib.oa`, `std_io.oa`). -- `crates/oac/src/std/std_traits.oa` defines core traits (`Hash`, `Eq`) with concrete impls for practical key/value types used by stdlib and user generics. +- `crates/oac/src/std/std_traits.oa` defines core traits (`Hash`, `Eq`, `Copy`, `Drop`) and concrete scalar `Copy` impls (`Bool`, `U8`, `I32`, `I64`, `FP32`, `FP64`, `Char`, `AsciiChar`). - The split stdlib now exposes namespaced helper APIs where applicable: JSON parsing helpers are called via `Json.*` (for example `Json.json_kind`, `Json.parse_json_document_result`, `Json.parse_json_document_value_result`). - `crates/oac/src/std/std_json.oa` now defines a structured JSON value surface (`JsonValue`, `JsonMembers`, `JsonValues`) plus typed lookup helpers (`Json.object_get`, `Json.array_get`, `Json.value_string`, `Json.value_number`) so callers can parse into and inspect object/array trees. - JSON booleans in the structured value surface now use payload form `JsonValue.Bool(Bool)` (replacing separate `True`/`False` variants), and `JsonKind` now reports booleans as `JsonKind.Bool`. @@ -81,7 +83,7 @@ This repository contains the Ousia compiler workspace (`crates/*`) plus editor t - The stdlib now also defines generic `Vec[T]` in `crates/oac/src/std/std_vec.oa` as a persistent vector-style container with APIs `new`, `with_capacity`, `push`, `pop` (`PopResult`), `get` (`Lookup`), `set` (`SetResult`), `len`, `capacity`, `reserve`, and `clear`. - The stdlib now also defines `Io` wrappers in `crates/oac/src/std/std_io.oa` over `Clib` file-descriptor calls with explicit result enums (`IoError`, `IoReadResult`, `IoWriteResult`) and namespaced helpers `Io.read_all`, `Io.write_all`, `Io.read_file`, and `Io.write_file`. - C interop in std is exposed through namespaced calls (`Clib.*`) and declared in `crates/oac/src/std/std_clib.oa` as `namespace Clib { extern fun ... }`; resolver keeps namespaced internal keys (`Clib__name`) while codegen emits declared extern symbol names for linking (for example `malloc`). -- Built-in `Void` is available for C-style procedure signatures; in v1 only `extern fun` may return `Void`, and `Void` is rejected as a parameter type. +- Built-in `Void` is available for procedure-style signatures; `Void` is rejected as a parameter type, and both extern and non-extern functions may return `Void`. - Built-in `U8` is available as an unsigned byte-like numeric type (`U8/U8` arithmetic and comparisons are allowed with no implicit coercions). - The resolver also exposes `PtrInt` as a standard numeric alias hardcoded to `I64` (for pointer-sized integer use sites). - Runtime pointer memory helpers are compiler builtins: `load_u8(addr: PtrInt) -> U8`, `load_i32(addr: PtrInt) -> I32`, `load_i64(addr: PtrInt) -> I64`, `load_bool(addr: PtrInt) -> Bool`, `store_u8(addr: PtrInt, value: U8) -> Void`, `store_i32(addr: PtrInt, value: I32) -> Void`, `store_i64(addr: PtrInt, value: I64) -> Void`, and `store_bool(addr: PtrInt, value: Bool) -> Void`. diff --git a/agents/02-compiler-pipeline.md b/agents/02-compiler-pipeline.md index f1ed495..a8cc4a8 100644 --- a/agents/02-compiler-pipeline.md +++ b/agents/02-compiler-pipeline.md @@ -121,6 +121,11 @@ Important enforced invariants include: - namespace call lowering is also used for generic-specialized helpers (`Alias.fn(args)` resolving to generated `Alias__fn` symbols) - trait calls are v1 namespaced calls (`Trait.method(value, ...)`) that type-check against trait signatures and resolve to concrete impl function names (`Trait__Type__method`) - trait coherence is enforced globally: duplicate `impl Trait for Type` is rejected, and missing impls for generic bounds are rejected at specialization time +- special lifecycle-trait signatures are enforced: `Copy.copy(Ref[Self]) -> Self` and `Drop.drop(Self) -> Void` +- ownership analysis/rewriting runs during resolve for user runtime functions: + - move-only default for non-`Copy` locals (use-after-move diagnostics) + - implicit-copy eligibility for types with concrete `Copy` impls + - inserted `Drop.drop(...)` statements at reassignment overwrite points and lexical scope exits - built-in `U8`/`FP32`/`FP64` exist alongside integer primitives; unsuffixed decimal literals type-check as `FP32`, and `f64`-suffixed decimal literals type-check as `FP64` - arithmetic/comparison on numerics requires matching widths/types (`U8/U8`, `I32/I32`, `I64/I64`, `FP32/FP32`, `FP64/FP64`), with no implicit int/float coercions - `U8` comparisons/codegen are unsigned (`ult/ule/ugt/uge`), and `U8` division lowers to unsigned division (`udiv`) @@ -142,7 +147,7 @@ Important enforced invariants include: - resolver builtins also include pointer-memory helpers `load_u8(addr: PtrInt) -> U8`, `load_i32(addr: PtrInt) -> I32`, `load_i64(addr: PtrInt) -> I64`, `load_bool(addr: PtrInt) -> Bool`, `store_u8(addr: PtrInt, value: U8) -> Void`, `store_i32(addr: PtrInt, value: I32) -> Void`, `store_i64(addr: PtrInt, value: I64) -> Void`, and `store_bool(addr: PtrInt, value: Bool) -> Void` - `extern fun` declarations are signature-only (`extern` cannot be `comptime` and extern functions must not have bodies) - v2 ABI restriction: `extern fun` signatures cannot use struct parameter or return types; use manual `PtrInt` wrappers at C ABI boundaries when struct-like payloads are needed -- `Void` is restricted in v1: function parameters cannot be `Void`, and only `extern fun` may return `Void` +- `Void` is restricted in v1: function parameters cannot be `Void`, but both extern and non-extern functions may return `Void` - declaration-based stdlib invariants (for example `AsciiChar` range checks over wrapped `Char.code`) are synthesized and registered during resolve like user-declared invariants - consistent return types inside a function - `main` must be either `fun main() -> I32`, `fun main(argc: I32, argv: I64) -> I32`, or `fun main(argc: I32, argv: PtrInt) -> I32` @@ -162,7 +167,9 @@ Important enforced invariants include: - Includes builtins and interop helpers (for example integer ops, print, string utilities) plus user/std-declared extern call targets. - Extern calls emit symbol names from signature metadata; namespace externs (for example `Clib.malloc`) therefore call raw declared extern symbols (for example `malloc`) while keeping namespaced lookup keys internal. - Struct literals allocate zero-initialized storage via `calloc` before field stores, so padding bytes are deterministic for bytewise equality. -- Struct assignment, struct call arguments, and struct returns insert copy barriers (`calloc` + `memcpy`) to enforce by-value byte-copy semantics at language boundaries. +- Struct assignment/call/return no longer inject unconditional clone barriers. +- Value reads lower through static `Copy.copy(...)` calls only when the resolved concrete type implements `Copy`; codegen passes either the value directly (legacy by-value impl signatures) or a synthesized temporary `Ref` wrapper (ref-style impl signatures). +- Resolver-inserted `Drop.drop(...)` statements lower as ordinary `Void` call statements in codegen. - Struct `==` / `!=` lower through `memcmp(lhs, rhs, size)` and compare the result with zero. - Handles expression lowering and control-flow generation. - Trait calls are lowered with static dispatch only (resolved concrete impl symbols), with no runtime dictionaries or vtables. diff --git a/agents/03-language-semantics.md b/agents/03-language-semantics.md index 092d601..a896129 100644 --- a/agents/03-language-semantics.md +++ b/agents/03-language-semantics.md @@ -61,7 +61,9 @@ Observed in parser/IR implementation: - `main` must use one of these signatures: `fun main() -> I32`, `fun main(argc: I32, argv: I64) -> I32`, or `fun main(argc: I32, argv: PtrInt) -> I32`. - Assignments bind variable type to expression type. - Struct literals are zero-initialized before field stores (`calloc`) so bytewise struct equality has deterministic padding bytes. -- Struct values use by-value byte-copy semantics at assignment/call/return boundaries (implemented with clone barriers in codegen, not pointer-identity semantics). +- Ownership is move-only by default: reading a non-`Copy` binding moves it and invalidates that binding until reassigned. +- For types with a concrete `Copy` impl, read sites implicitly clone by lowering to static `Copy.copy(...)` calls; codegen passes either the value directly (legacy by-value impl signatures) or a synthesized temporary `Ref` wrapper (ref-style impl signatures). +- Deterministic destruction uses `Drop`: resolver inserts `Drop.drop(...)` calls before overwriting initialized bindings and at scope exits (reverse declaration order); moved/uninitialized bindings are not dropped. - Struct `==` / `!=` are universal bytewise comparisons (`memcmp` over struct size), including pointer-containing structs. - Numeric binary ops are strict and same-type only: `U8/U8`, `I32/I32`, `I64/I64`, `FP32/FP32`, `FP64/FP64` (no implicit int/float coercions). - `U8` relational operators (`<`, `>`, `<=`, `>=`) use unsigned comparisons in codegen. @@ -71,13 +73,16 @@ Observed in parser/IR implementation: - `extern fun` declarations cannot be marked `comptime` and must not define a body. - In v2 ABI, `extern fun` declarations cannot use struct parameter types or struct return types; use `PtrInt` wrappers for manual ABI bridging. - `Void` cannot be used as a function parameter type. -- In v1, only `extern fun` may return `Void`. +- Non-extern functions may return `Void`. - Assignment statements cannot bind variables to `Void`-typed expressions. - `Void`-return calls are statement-only (cannot be used as expression values). - Namespace calls are syntactic sugar for internal function names using `Namespace__function` lowering, while preserving existing enum constructor call syntax `Enum.Variant(...)`. - Namespace call lowering also applies to generic-specialized helpers when matching mangled symbols exist (`Alias.helper(...)` -> `Alias__helper`). - Traits are static-only in v1: method calls use `Trait.method(value, ...)` and are resolved to concrete impl symbols (`Trait__Type__method`) before backend lowering. - Trait method signatures must take `Self` as the first parameter type in v1. +- Special lifecycle-trait shapes are enforced exactly: + - `trait Copy { fun copy(v: Ref[Self]) -> Self }` + - `trait Drop { fun drop(v: Self) -> Void }` - Impl coherence is global: exactly one `impl Trait for Type` is allowed in a program. - Impl method sets/signatures must match trait declarations exactly (arity, parameter types after `Self` substitution, return type); missing/extra methods are compile errors. - `impl` methods cannot be `extern` or `comptime` in v1. @@ -86,6 +91,7 @@ Observed in parser/IR implementation: - Generic expansion is ahead-of-type-checking and ahead-of-codegen: backend/proving/invariant passes still operate on concrete non-generic IR/function symbols. - Imports are file-local-only and flat: import paths must be string literals naming `.oa` files in the same directory. - The built-in stdlib is composed through flat imports from `crates/oac/src/std/std.oa` into split sibling files under `crates/oac/src/std/` (including `std_clib.oa` extern bindings and `std_traits.oa` trait/impl declarations), then merged into one global scope before user type-checking (including stdlib invariant declarations). +- `std_traits.oa` defines `Copy` and `Drop` alongside `Hash`/`Eq`; scalar types (`Bool`, `U8`, `I32`, `I64`, `FP32`, `FP64`, `Char`, `AsciiChar`) ship concrete `Copy` impls in std. - The split stdlib includes generic `Option[T]` / `Result[T,E]` in `crates/oac/src/std/std_option_result.oa` with namespaced constructors/predicates/unwrapping helpers. - The split stdlib includes generic `Ref[T]` and `Mut[T]` (in `crates/oac/src/std/std_ref.oa`) for pointer-wrapper value semantics (`from_ptr`, `ptr`, `is_null`, `add_bytes`), with typed read-only dereference helpers (`U8Ref.read`, `I32Ref.read`, `I64Ref.read`, `PtrIntRef.read`, `BoolRef.read`) and typed mutable helpers (`U8Mut.*`, `I32Mut.*`, `I64Mut.*`, `PtrIntMut.*`, `BoolMut.*`) that use builtin stores. - Stdlib `HashTable` is now a bounded generic (`HashTable[K: Hash + Eq, V]`) with separate-chaining semantics while routing key hashing/equality through `Hash.hash(k)` and `Eq.equals(a, b)`. diff --git a/agents/04-testing-ci.md b/agents/04-testing-ci.md index 4c155b7..58979bf 100644 --- a/agents/04-testing-ci.md +++ b/agents/04-testing-ci.md @@ -124,13 +124,13 @@ Key tests: - `crates/oac/src/flat_imports.rs` tests assert flat import resolution: merge behavior, same-directory path constraints, and cycle detection. - `crates/oac/src/flat_imports.rs` merge coverage also includes imported test declaration propagation. - `crates/oac/src/ir.rs` includes a regression test that stdlib split files are loaded through `crates/oac/src/std/std.oa` imports. -- That regression currently asserts representative split-stdlib symbols including JSON (`Json__parse_json_document`), trait symbols (`Hash::hash`, `Eq::equals`, and synthesized impl functions like `Hash__I32__hash`), ASCII helpers (`AsciiChar`, `AsciiChar__from_code`), char/null/string helpers (`Char__from_code`, `Null__value`, `String__from_literal_parts`, `String__from_heap_parts`, `String__equals`, `String__starts_with`, `String__ends_with`, `String__slice_clamped`), option/result generics (`Option`, `Result` generic declarations), set/vector generics (`HashSet`, `Vec` generic declarations), IO/result types (`IoError`, `IoReadResult`, `IoWriteResult`), `Io` namespace functions (`Io__read_all`, `Io__write_all`, `Io__read_file`, `Io__write_file`), ref/mut helpers (`U8Ref`, `I32Ref`, `I64Ref`, `PtrIntRef`, `BoolRef`, `U8Mut`, `I32Mut`, `I64Mut`, `PtrIntMut`, `BoolMut`, plus `*Ref__read` and `*Mut__write` functions), C externs (`Clib__malloc`, `Clib__free`, `Clib__memcmp`), and standard aliases/types (`PtrInt`, `U8`, `Void`, `Bytes`, std-defined `String` enum). +- That regression currently asserts representative split-stdlib symbols including JSON (`Json__parse_json_document`), trait symbols (`Hash::hash`, `Eq::equals`, `Copy::copy`, `Drop::drop`, and synthesized impl functions like `Hash__I32__hash`), ASCII helpers (`AsciiChar`, `AsciiChar__from_code`), char/null/string helpers (`Char__from_code`, `Null__value`, `String__from_literal_parts`, `String__from_heap_parts`, `String__equals`, `String__starts_with`, `String__ends_with`, `String__slice_clamped`), option/result generics (`Option`, `Result` generic declarations), set/vector generics (`HashSet`, `Vec` generic declarations), IO/result types (`IoError`, `IoReadResult`, `IoWriteResult`), `Io` namespace functions (`Io__read_all`, `Io__write_all`, `Io__read_file`, `Io__write_file`), ref/mut helpers (`U8Ref`, `I32Ref`, `I64Ref`, `PtrIntRef`, `BoolRef`, `U8Mut`, `I32Mut`, `I64Mut`, `PtrIntMut`, `BoolMut`, plus `*Ref__read` and `*Mut__write` functions), C externs (`Clib__malloc`, `Clib__free`, `Clib__memcmp`), and standard aliases/types (`PtrInt`, `U8`, `Void`, `Bytes`, std-defined `String` enum). - The same regression also asserts stdlib invariant registration/synthesis for `AsciiChar` and `Bytes` (`struct_invariants[...]` metadata plus synthesized `__struct__*__invariant__` functions). - `crates/oac/src/ir.rs` also validates accepted `main` signatures (`main()`, `main(argc: I32, argv: I64)`, and `main(argc: I32, argv: PtrInt)`). - `crates/oac/src/ir.rs` includes alias coverage for `PtrInt` behaving as `I64` in function calls/equality and type-definition mapping. - `crates/oac/src/ir.rs` also validates namespace call resolution/type-checking by lowering to mangled function names (`TypeName__helper`). - `crates/oac/src/ir.rs` also validates trait-system behavior: duplicate impl rejection, impl signature mismatch rejection, missing bound impl failures at specialization, and trait-call dispatch/type-check through concrete impl symbols. -- `crates/oac/src/ir.rs` also validates `Void`/extern constraints: accepted statement calls to `Void` externs, rejection of `Void` parameters, and rejection of non-extern `Void` returns. +- `crates/oac/src/ir.rs` also validates ownership/lifecycle constraints: `Copy`/`Drop` trait-shape enforcement, move diagnostics (`use of moved value ...`, `cannot move from uninitialized value ...`), acceptance of repeated reads for `Copy` types, accepted statement calls to `Void` externs, and rejection of `Void` parameters. - `crates/oac/src/ir.rs` also validates v2 extern ABI restrictions by rejecting struct parameters/returns in `extern fun` signatures with diagnostics that direct users to `PtrInt` wrappers. - `crates/oac/src/ir.rs` also includes FP32 resolve/type-check regression coverage (FP32 arithmetic + comparison in `main`). - `crates/oac/src/ir.rs` also includes FP64 resolve/type-check regression coverage (FP64 arithmetic + comparison in `main`). @@ -170,7 +170,7 @@ Key tests: - `crates/oac/src/qbe_backend.rs` also has a unit test that asserts `U8` lowers with unsigned comparison/division ops (`cultw`, `udiv`). - `crates/oac/src/qbe_backend.rs` also has a unit test that asserts pointer-memory builtins lower to expected QBE ops (`loadub`, `loadw`, `loadl`, compare-to-zero for bool, `storeb`, `storew`, `storel`). - `crates/oac/src/qbe_backend.rs` also has a unit test that asserts char literals lower through `call $Char__from_code`. -- `crates/oac/src/qbe_backend.rs` also has a unit regression that asserts struct by-value barriers and equality lowering emit `calloc`/`memcpy` clones plus `call $memcmp` (`qbe_codegen_structs_use_copy_barriers_and_memcmp_equality`). +- `crates/oac/src/qbe_backend.rs` also has a unit regression that asserts move-only struct behavior with preserved memcmp equality lowering (`qbe_codegen_structs_are_move_only_and_keep_memcmp_equality`). - `crates/qbe-rs/src/tests.rs` includes coverage for FP32/FP64 constant formatting (`s_`, `d_`) and ordered float compare formatting (`clts`, `cgtd`). - Execution fixtures now include dedicated prove/assert coverage: - `prove_pass.oa` diff --git a/crates/oac/execution_tests/basic_struct_and_field_access.oa b/crates/oac/execution_tests/basic_struct_and_field_access.oa index 785b736..03777a0 100644 --- a/crates/oac/execution_tests/basic_struct_and_field_access.oa +++ b/crates/oac/execution_tests/basic_struct_and_field_access.oa @@ -3,6 +3,12 @@ struct Foo { b: I32, } +impl Copy for Foo { + fun copy(v: Foo) -> Foo { + return v + } +} + fun main() -> I32 { s = Foo struct { a: 10, b: 1000, } print(s.a) diff --git a/crates/oac/execution_tests/enum_basic.oa b/crates/oac/execution_tests/enum_basic.oa index 2dbdbe1..d652ffc 100644 --- a/crates/oac/execution_tests/enum_basic.oa +++ b/crates/oac/execution_tests/enum_basic.oa @@ -3,6 +3,12 @@ enum Color { Green, } +impl Copy for Color { + fun copy(v: Color) -> Color { + return v + } +} + fun main() -> I32 { c = Color.Red d = Color.Green diff --git a/crates/oac/execution_tests/generic_hash_table_custom_key.oa b/crates/oac/execution_tests/generic_hash_table_custom_key.oa index 7fc02d8..cc9120c 100644 --- a/crates/oac/execution_tests/generic_hash_table_custom_key.oa +++ b/crates/oac/execution_tests/generic_hash_table_custom_key.oa @@ -17,6 +17,30 @@ impl Eq for UserKey { specialize UserTable = HashTable[UserKey, I32] +impl Copy for UserKey { + fun copy(v: UserKey) -> UserKey { + return v + } +} + +impl Copy for UserTable { + fun copy(v: UserTable) -> UserTable { + return v + } +} + +impl Copy for UserTable__SetResult { + fun copy(v: UserTable__SetResult) -> UserTable__SetResult { + return v + } +} + +impl Copy for UserTable__RemoveResult { + fun copy(v: UserTable__RemoveResult) -> UserTable__RemoveResult { + return v + } +} + fun print_lookup(v: UserTable__Lookup) -> I32 { match v { UserTable__Lookup.Missing => { diff --git a/crates/oac/execution_tests/json_value_parser.oa b/crates/oac/execution_tests/json_value_parser.oa index 2fcfdcd..8f876bb 100644 --- a/crates/oac/execution_tests/json_value_parser.oa +++ b/crates/oac/execution_tests/json_value_parser.oa @@ -1,3 +1,21 @@ +impl Copy for JsonValue { + fun copy(v: JsonValue) -> JsonValue { + return v + } +} + +impl Copy for JsonValueLookup { + fun copy(v: JsonValueLookup) -> JsonValueLookup { + return v + } +} + +impl Copy for JsonStringLookup { + fun copy(v: JsonStringLookup) -> JsonStringLookup { + return v + } +} + fun kind_code(kind: JsonKind) -> I32 { match kind { JsonKind.Bool => { diff --git a/crates/oac/execution_tests/std_hash_set.oa b/crates/oac/execution_tests/std_hash_set.oa index 79c9f80..528b1f5 100644 --- a/crates/oac/execution_tests/std_hash_set.oa +++ b/crates/oac/execution_tests/std_hash_set.oa @@ -1,5 +1,23 @@ specialize IntSet = HashSet[I32] +impl Copy for IntSet { + fun copy(v: IntSet) -> IntSet { + return v + } +} + +impl Copy for IntSet__InsertResult { + fun copy(v: IntSet__InsertResult) -> IntSet__InsertResult { + return v + } +} + +impl Copy for IntSet__RemoveResult { + fun copy(v: IntSet__RemoveResult) -> IntSet__RemoveResult { + return v + } +} + fun print_bool(v: Bool) -> I32 { if v { print(1) diff --git a/crates/oac/execution_tests/std_io.oa b/crates/oac/execution_tests/std_io.oa index 23a5636..cacbb7d 100644 --- a/crates/oac/execution_tests/std_io.oa +++ b/crates/oac/execution_tests/std_io.oa @@ -1,3 +1,9 @@ +impl Copy for Bytes { + fun copy(v: Bytes) -> Bytes { + return v + } +} + fun print_read_result(v: IoReadResult) -> I32 { match v { IoReadResult.Ok(bytes) => { diff --git a/crates/oac/execution_tests/std_mut_read_write.oa b/crates/oac/execution_tests/std_mut_read_write.oa index 65930d9..dc7e56c 100644 --- a/crates/oac/execution_tests/std_mut_read_write.oa +++ b/crates/oac/execution_tests/std_mut_read_write.oa @@ -1,3 +1,33 @@ +impl Copy for U8Mut { + fun copy(v: U8Mut) -> U8Mut { + return v + } +} + +impl Copy for I32Mut { + fun copy(v: I32Mut) -> I32Mut { + return v + } +} + +impl Copy for I64Mut { + fun copy(v: I64Mut) -> I64Mut { + return v + } +} + +impl Copy for BoolMut { + fun copy(v: BoolMut) -> BoolMut { + return v + } +} + +impl Copy for PtrIntMut { + fun copy(v: PtrIntMut) -> PtrIntMut { + return v + } +} + fun print_bool(v: Bool) -> I32 { if v { print(1) diff --git a/crates/oac/execution_tests/std_option_result.oa b/crates/oac/execution_tests/std_option_result.oa index ce2e44a..3bbab6b 100644 --- a/crates/oac/execution_tests/std_option_result.oa +++ b/crates/oac/execution_tests/std_option_result.oa @@ -1,6 +1,18 @@ specialize OptionI32 = Option[I32] specialize ResultI32Bool = Result[I32, Bool] +impl Copy for OptionI32 { + fun copy(v: OptionI32) -> OptionI32 { + return v + } +} + +impl Copy for ResultI32Bool { + fun copy(v: ResultI32Bool) -> ResultI32Bool { + return v + } +} + fun print_bool(v: Bool) -> I32 { if v { print(1) diff --git a/crates/oac/execution_tests/std_string_helpers.oa b/crates/oac/execution_tests/std_string_helpers.oa index b1a671d..3eb90f3 100644 --- a/crates/oac/execution_tests/std_string_helpers.oa +++ b/crates/oac/execution_tests/std_string_helpers.oa @@ -1,3 +1,9 @@ +impl Copy for String { + fun copy(v: String) -> String { + return v + } +} + fun print_bool(v: Bool) -> I32 { if v { print(1) diff --git a/crates/oac/execution_tests/std_vec.oa b/crates/oac/execution_tests/std_vec.oa index 70467a3..00607aa 100644 --- a/crates/oac/execution_tests/std_vec.oa +++ b/crates/oac/execution_tests/std_vec.oa @@ -1,5 +1,23 @@ specialize IntVec = Vec[I32] +impl Copy for IntVec { + fun copy(v: IntVec) -> IntVec { + return v + } +} + +impl Copy for IntVec__SetResult { + fun copy(v: IntVec__SetResult) -> IntVec__SetResult { + return v + } +} + +impl Copy for IntVec__PopValue { + fun copy(v: IntVec__PopValue) -> IntVec__PopValue { + return v + } +} + fun print_bool(v: Bool) -> I32 { if v { print(1) diff --git a/crates/oac/execution_tests/string_escape_char_at.oa b/crates/oac/execution_tests/string_escape_char_at.oa index 5c8999c..1ff205f 100644 --- a/crates/oac/execution_tests/string_escape_char_at.oa +++ b/crates/oac/execution_tests/string_escape_char_at.oa @@ -1,3 +1,9 @@ +impl Copy for String { + fun copy(v: String) -> String { + return v + } +} + fun main() -> I32 { // "{\n\t\\\"x\\\":1}" decoded should start with '{', newline, tab, '\\', '"', 'x' s = "{\n\t\\\"x\\\":1}" diff --git a/crates/oac/execution_tests/string_slice.oa b/crates/oac/execution_tests/string_slice.oa index f9d4e07..714ac72 100644 --- a/crates/oac/execution_tests/string_slice.oa +++ b/crates/oac/execution_tests/string_slice.oa @@ -1,3 +1,9 @@ +impl Copy for String { + fun copy(v: String) -> String { + return v + } +} + fun main() -> I32 { s = "abcdef" t = slice(s, 1, 3) diff --git a/crates/oac/execution_tests/struct_equality_v2_memcmp.oa b/crates/oac/execution_tests/struct_equality_v2_memcmp.oa index 9135cbd..f5a72b9 100644 --- a/crates/oac/execution_tests/struct_equality_v2_memcmp.oa +++ b/crates/oac/execution_tests/struct_equality_v2_memcmp.oa @@ -8,6 +8,18 @@ struct PtrBox { tag: I32, } +impl Copy for Pair { + fun copy(v: Pair) -> Pair { + return v + } +} + +impl Copy for PtrBox { + fun copy(v: PtrBox) -> PtrBox { + return v + } +} + fun main() -> I32 { x = Pair struct { a: 7, b: 9 } y = Pair struct { a: 7, b: 9 } diff --git a/crates/oac/execution_tests/template_hash_table_i32.oa b/crates/oac/execution_tests/template_hash_table_i32.oa index 3171d07..7c2bb66 100644 --- a/crates/oac/execution_tests/template_hash_table_i32.oa +++ b/crates/oac/execution_tests/template_hash_table_i32.oa @@ -1,5 +1,23 @@ specialize IntTable = HashTable[I32, I32] +impl Copy for IntTable { + fun copy(v: IntTable) -> IntTable { + return v + } +} + +impl Copy for IntTable__SetResult { + fun copy(v: IntTable__SetResult) -> IntTable__SetResult { + return v + } +} + +impl Copy for IntTable__RemoveResult { + fun copy(v: IntTable__RemoveResult) -> IntTable__RemoveResult { + return v + } +} + fun print_lookup(v: IntTable__Lookup) -> I32 { match v { IntTable__Lookup.Missing => { diff --git a/crates/oac/execution_tests/template_linked_list_i32.oa b/crates/oac/execution_tests/template_linked_list_i32.oa index 930cefe..6e5f906 100644 --- a/crates/oac/execution_tests/template_linked_list_i32.oa +++ b/crates/oac/execution_tests/template_linked_list_i32.oa @@ -1,5 +1,11 @@ specialize IntList = LinkedList[I32] +impl Copy for IntList { + fun copy(v: IntList) -> IntList { + return v + } +} + fun main() -> I32 { xs = IntList.empty() xs = IntList.push_front(3, xs) diff --git a/crates/oac/execution_tests/template_linked_list_v2_i32.oa b/crates/oac/execution_tests/template_linked_list_v2_i32.oa index 6f336dd..e975079 100644 --- a/crates/oac/execution_tests/template_linked_list_v2_i32.oa +++ b/crates/oac/execution_tests/template_linked_list_v2_i32.oa @@ -1,5 +1,17 @@ specialize IntList = LinkedList[I32] +impl Copy for IntList { + fun copy(v: IntList) -> IntList { + return v + } +} + +impl Copy for IntList__PopFrontValue { + fun copy(v: IntList__PopFrontValue) -> IntList__PopFrontValue { + return v + } +} + fun print_front(v: IntList__FrontOption) -> I32 { match v { IntList__FrontOption.None => { diff --git a/crates/oac/execution_tests/template_option_i32.oa b/crates/oac/execution_tests/template_option_i32.oa index 7699624..fa898fc 100644 --- a/crates/oac/execution_tests/template_option_i32.oa +++ b/crates/oac/execution_tests/template_option_i32.oa @@ -1,5 +1,11 @@ specialize OptionI32 = Option[I32] +impl Copy for OptionI32 { + fun copy(v: OptionI32) -> OptionI32 { + return v + } +} + fun main() -> I32 { a = OptionI32.Some(7) b = OptionI32.None diff --git a/crates/oac/src/bench_prove.rs b/crates/oac/src/bench_prove.rs index 370035f..d0603d3 100644 --- a/crates/oac/src/bench_prove.rs +++ b/crates/oac/src/bench_prove.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{Instant, SystemTime, UNIX_EPOCH}; @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::diagnostics::{CompilerDiagnostic, CompilerDiagnosticBundle, DiagnosticStage}; use crate::verification_outcomes::{ begin_outcome_collection, end_outcome_collection, forbidden_transition_deltas, + with_fixture_context, }; use crate::verification_profile::VerificationProfile; use crate::{ @@ -471,8 +472,6 @@ where }) } -const VERIFICATION_OUTCOME_FIXTURE_ENV: &str = "OAC_VERIFICATION_OUTCOME_FIXTURE"; - fn run_verification_outcome_gate( current_dir: &Path, fixtures: &[FixtureSpec], @@ -506,6 +505,12 @@ fn run_verification_outcome_gate( &runs_root, &candidate_path, )?; + let expected_fixtures = fixtures + .iter() + .map(|fixture| fixture.id) + .collect::>(); + let baseline = filter_outcomes_to_expected_fixtures(baseline, &expected_fixtures); + let candidate = filter_outcomes_to_expected_fixtures(candidate, &expected_fixtures); let deltas = forbidden_transition_deltas(&baseline, &candidate)?; if deltas.is_empty() { @@ -525,6 +530,19 @@ fn run_verification_outcome_gate( ); } +fn filter_outcomes_to_expected_fixtures( + mut file: crate::verification_outcomes::VerificationOutcomeFile, + expected_fixtures: &HashSet<&'static str>, +) -> crate::verification_outcomes::VerificationOutcomeFile { + file.records.retain(|record| { + let Some(fixture) = record.fixture.as_deref() else { + return false; + }; + expected_fixtures.contains(fixture) + }); + file +} + fn capture_verification_outcomes_for_profile( current_dir: &Path, fixtures: &[FixtureSpec], @@ -580,15 +598,7 @@ fn capture_verification_outcomes_for_profile( } fn with_fixture_outcome_env(fixture_id: &str, run: impl FnOnce() -> T) -> T { - let previous = std::env::var(VERIFICATION_OUTCOME_FIXTURE_ENV).ok(); - std::env::set_var(VERIFICATION_OUTCOME_FIXTURE_ENV, fixture_id); - let output = run(); - if let Some(value) = previous { - std::env::set_var(VERIFICATION_OUTCOME_FIXTURE_ENV, value); - } else { - std::env::remove_var(VERIFICATION_OUTCOME_FIXTURE_ENV); - } - output + with_fixture_context(fixture_id, run) } fn run_fixture_iteration( diff --git a/crates/oac/src/invariant_metadata.rs b/crates/oac/src/invariant_metadata.rs index 544c42c..a8eb726 100644 --- a/crates/oac/src/invariant_metadata.rs +++ b/crates/oac/src/invariant_metadata.rs @@ -255,6 +255,12 @@ struct Counter { max: I32, } +impl Copy for Counter { + fun copy(v: Counter) -> Counter { + return v + } +} + invariant value_non_negative "counter value must be non-negative" for (v: Counter) { return v.value >= 0 } diff --git a/crates/oac/src/ir.rs b/crates/oac/src/ir.rs index 1f60254..5351e43 100644 --- a/crates/oac/src/ir.rs +++ b/crates/oac/src/ir.rs @@ -388,8 +388,640 @@ pub struct FunctionDefinition { pub sig: FunctionSignature, } +#[derive(Clone, Debug)] +struct OwnershipLocal { + ty: TypeRef, + state: OwnershipBindingState, + order: usize, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum OwnershipBindingState { + Initialized, + Moved, + Uninitialized, +} + +#[derive(Clone, Debug, Default)] +struct OwnershipState { + locals: HashMap, + next_order: usize, +} + +impl OwnershipState { + fn with_params(parameters: &[FunctionParameter]) -> Self { + let mut out = OwnershipState::default(); + for parameter in parameters { + out.locals.insert( + parameter.name.clone(), + OwnershipLocal { + ty: parameter.ty.clone(), + state: OwnershipBindingState::Initialized, + order: out.next_order, + }, + ); + out.next_order += 1; + } + out + } + + fn as_var_types(&self) -> HashMap { + self.locals + .iter() + .map(|(name, local)| (name.clone(), local.ty.clone())) + .collect() + } +} + +impl ResolvedProgram { + fn has_copy_impl_for_type(&self, ty: &str) -> bool { + let impl_key = trait_impl_method_key("Copy", normalize_numeric_alias(ty), "copy"); + self.trait_impl_methods.contains_key(&impl_key) + } + + fn drop_impl_for_type(&self, ty: &str) -> Option { + let impl_key = trait_impl_method_key("Drop", normalize_numeric_alias(ty), "drop"); + self.trait_impl_methods.get(&impl_key).cloned() + } + + fn ownership_consume_local( + &self, + name: &str, + state: &mut OwnershipState, + ) -> anyhow::Result<()> { + let local = state + .locals + .get_mut(name) + .ok_or_else(|| anyhow::anyhow!("unknown variable {}", name))?; + match local.state { + OwnershipBindingState::Initialized => {} + OwnershipBindingState::Moved => { + return Err(anyhow::anyhow!("use of moved value {}", name)) + } + OwnershipBindingState::Uninitialized => { + return Err(anyhow::anyhow!( + "cannot move from uninitialized value {}", + name + )) + } + } + if !self.has_copy_impl_for_type(&local.ty) { + local.state = OwnershipBindingState::Moved; + } + Ok(()) + } + + fn ownership_insert_or_assign_local(&self, state: &mut OwnershipState, name: &str, ty: &str) { + if let Some(existing) = state.locals.get_mut(name) { + existing.ty = ty.to_string(); + existing.state = OwnershipBindingState::Initialized; + return; + } + state.locals.insert( + name.to_string(), + OwnershipLocal { + ty: ty.to_string(), + state: OwnershipBindingState::Initialized, + order: state.next_order, + }, + ); + state.next_order += 1; + } + + fn ownership_analyze_expression( + &self, + expr: &Expression, + state: &mut OwnershipState, + ) -> anyhow::Result { + let var_types = state.as_var_types(); + let expr_ty = self.expression_type(expr, &var_types)?; + match expr { + Expression::Literal(_) => {} + Expression::Variable(name) => { + self.ownership_consume_local(name, state)?; + } + Expression::UnaryOp(_, inner) => { + let _ = self.ownership_analyze_expression(inner, state)?; + } + Expression::BinOp(_, left, right) => { + let _ = self.ownership_analyze_expression(left, state)?; + let _ = self.ownership_analyze_expression(right, state)?; + } + Expression::Call(_, arguments) => { + for argument in arguments { + let _ = self.ownership_analyze_expression(argument, state)?; + } + } + Expression::PostfixCall { callee, args } => { + if !matches!(callee.as_ref(), Expression::FieldAccess { .. }) { + let _ = self.ownership_analyze_expression(callee, state)?; + } + for argument in args { + let _ = self.ownership_analyze_expression(argument, state)?; + } + } + Expression::FieldAccess { + struct_variable, .. + } => { + if state.locals.contains_key(struct_variable) { + self.ownership_consume_local(struct_variable, state)?; + } + } + Expression::StructValue { field_values, .. } => { + for (_, value) in field_values { + let _ = self.ownership_analyze_expression(value, state)?; + } + } + Expression::Match { subject, arms } => { + let subject_ty = { + let var_types = state.as_var_types(); + self.expression_type(subject, &var_types)? + }; + let enum_def = match self.type_definitions.get(&subject_ty) { + Some(TypeDef::Enum(enum_def)) => enum_def, + _ => { + return Err(anyhow::anyhow!( + "match subject must be an enum, got {:?}", + subject_ty + )); + } + }; + + let _ = self.ownership_analyze_expression(subject, state)?; + let base_after_subject = state.clone(); + + let mut arm_states = vec![]; + for arm in arms { + let mut arm_state = base_after_subject.clone(); + let (variant_name, binder) = match &arm.pattern { + parser::MatchPattern::Variant { + type_name, + variant_name, + binder, + } => { + if type_name != &subject_ty { + return Err(anyhow::anyhow!( + "match arm type {:?} does not match subject type {:?}", + type_name, + subject_ty + )); + } + (variant_name, binder) + } + }; + let variant = enum_def + .variants + .iter() + .find(|v| &v.name == variant_name) + .ok_or_else(|| { + anyhow::anyhow!( + "unknown variant {} for enum {}", + variant_name, + enum_def.name + ) + })?; + if let (Some(payload_ty), Some(binder_name)) = (&variant.payload_ty, binder) { + self.ownership_insert_or_assign_local( + &mut arm_state, + binder_name, + payload_ty, + ); + } + let _ = self.ownership_analyze_expression(&arm.value, &mut arm_state)?; + arm_states.push(arm_state); + } + *state = self.ownership_merge_states(&base_after_subject, &arm_states)?; + } + } + Ok(expr_ty) + } + + fn ownership_merge_states( + &self, + base: &OwnershipState, + branch_states: &[OwnershipState], + ) -> anyhow::Result { + if branch_states.is_empty() { + return Ok(base.clone()); + } + + let mut merged = OwnershipState::default(); + merged.next_order = branch_states + .iter() + .fold(base.next_order, |max_seen, state| { + std::cmp::max(max_seen, state.next_order) + }); + + let mut all_names = HashSet::new(); + all_names.extend(base.locals.keys().cloned()); + for state in branch_states { + all_names.extend(state.locals.keys().cloned()); + } + + for name in all_names { + let mut selected: Option = base.locals.get(&name).cloned(); + for branch in branch_states { + if let Some(local) = branch.locals.get(&name) { + match &selected { + Some(existing) => { + if normalize_numeric_alias(&existing.ty) + != normalize_numeric_alias(&local.ty) + { + return Err(anyhow::anyhow!( + "incompatible variable type for {} across control-flow branches: {} vs {}", + name, + existing.ty, + local.ty + )); + } + } + None => { + selected = Some(local.clone()); + } + } + } + } + if let Some(mut local) = selected { + let branch_locals = branch_states + .iter() + .map(|branch| branch.locals.get(&name)) + .collect::>(); + + local.state = if branch_locals.iter().all(|candidate| { + matches!( + candidate, + Some(candidate) if candidate.state == OwnershipBindingState::Initialized + ) + }) { + OwnershipBindingState::Initialized + } else if branch_locals.iter().all(|candidate| { + matches!( + candidate, + Some(candidate) if candidate.state == OwnershipBindingState::Moved + ) + }) { + OwnershipBindingState::Moved + } else { + OwnershipBindingState::Uninitialized + }; + local.order = branch_states.iter().fold(local.order, |min_order, branch| { + branch + .locals + .get(&name) + .map(|candidate| std::cmp::min(min_order, candidate.order)) + .unwrap_or(min_order) + }); + merged.locals.insert(name, local); + } + } + Ok(merged) + } + + fn ownership_drop_statements_in_reverse( + &self, + state: &OwnershipState, + ) -> Vec { + let mut live = state + .locals + .iter() + .filter_map(|(name, local)| { + if local.state != OwnershipBindingState::Initialized { + return None; + } + self.drop_impl_for_type(&local.ty) + .map(|drop_impl| (name.clone(), local.order, drop_impl)) + }) + .collect::>(); + live.sort_by_key(|(_, order, _)| std::cmp::Reverse(*order)); + live.into_iter() + .map(|(name, _order, drop_impl)| parser::Statement::Expression { + expr: Expression::Call(drop_impl, vec![Expression::Variable(name)]), + }) + .collect() + } + + fn ownership_drop_statements_for_scope_exit( + &self, + from: &OwnershipState, + to: &OwnershipState, + ) -> Vec { + let mut live = from + .locals + .iter() + .filter_map(|(name, local)| { + if local.state != OwnershipBindingState::Initialized { + return None; + } + let destination_is_initialized = to + .locals + .get(name) + .map(|target| target.state == OwnershipBindingState::Initialized) + .unwrap_or(false); + if destination_is_initialized { + return None; + } + self.drop_impl_for_type(&local.ty) + .map(|drop_impl| (name.clone(), local.order, drop_impl)) + }) + .collect::>(); + live.sort_by_key(|(_, order, _)| std::cmp::Reverse(*order)); + live.into_iter() + .map(|(name, _order, drop_impl)| parser::Statement::Expression { + expr: Expression::Call(drop_impl, vec![Expression::Variable(name)]), + }) + .collect() + } + + fn ownership_rewrite_block( + &self, + body: &[parser::Statement], + state: &mut OwnershipState, + ) -> anyhow::Result> { + let mut out = Vec::new(); + for statement in body { + let mut rewritten = self.ownership_rewrite_statement(statement, state)?; + out.append(&mut rewritten); + } + Ok(out) + } + + fn ownership_rewrite_statement( + &self, + statement: &parser::Statement, + state: &mut OwnershipState, + ) -> anyhow::Result> { + match statement { + parser::Statement::StructDef { .. } => Ok(vec![statement.clone()]), + parser::Statement::Expression { expr } => { + let _ = self.ownership_analyze_expression(expr, state)?; + Ok(vec![statement.clone()]) + } + parser::Statement::Prove { condition } => { + let condition_ty = self.ownership_analyze_expression(condition, state)?; + if condition_ty != "Bool" { + return Err(anyhow::anyhow!( + "prove expects Bool condition, got {:?}", + condition_ty + )); + } + Ok(vec![statement.clone()]) + } + parser::Statement::Assert { condition } => { + let condition_ty = self.ownership_analyze_expression(condition, state)?; + if condition_ty != "Bool" { + return Err(anyhow::anyhow!( + "assert expects Bool condition, got {:?}", + condition_ty + )); + } + Ok(vec![statement.clone()]) + } + parser::Statement::Assign { variable, value } => { + let value_ty = self.ownership_analyze_expression(value, state)?; + if value_ty == "Void" { + return Err(anyhow::anyhow!( + "cannot assign expression of type Void to variable {}", + variable + )); + } + + let mut out = vec![]; + if let Some(existing) = state.locals.get_mut(variable) { + if existing.state == OwnershipBindingState::Initialized { + if let Some(drop_impl) = self.drop_impl_for_type(&existing.ty) { + out.push(parser::Statement::Expression { + expr: Expression::Call( + drop_impl, + vec![Expression::Variable(variable.clone())], + ), + }); + existing.state = OwnershipBindingState::Uninitialized; + } + } + } + + self.ownership_insert_or_assign_local(state, variable, &value_ty); + out.push(statement.clone()); + Ok(out) + } + parser::Statement::Return { expr } => { + let _ = self.ownership_analyze_expression(expr, state)?; + let mut out = self.ownership_drop_statements_in_reverse(state); + out.push(statement.clone()); + Ok(out) + } + parser::Statement::Conditional { + condition, + body, + else_body, + } => { + let condition_ty = self.ownership_analyze_expression(condition, state)?; + if condition_ty != "Bool" { + return Err(anyhow::anyhow!( + "expected condition to be of type Bool, but got {:?}", + condition_ty + )); + } + let state_after_condition = state.clone(); + + let mut then_state = state_after_condition.clone(); + let mut rewritten_then = self.ownership_rewrite_block(body, &mut then_state)?; + + let mut else_state = state_after_condition.clone(); + let mut rewritten_else = if let Some(else_body) = else_body { + Some(self.ownership_rewrite_block(else_body, &mut else_state)?) + } else { + None + }; + + let merged = if rewritten_else.is_some() { + self.ownership_merge_states( + &state_after_condition, + &[then_state.clone(), else_state.clone()], + )? + } else { + self.ownership_merge_states( + &state_after_condition, + &[then_state.clone(), state_after_condition.clone()], + )? + }; + rewritten_then + .extend(self.ownership_drop_statements_for_scope_exit(&then_state, &merged)); + if let Some(else_body) = rewritten_else.as_mut() { + else_body.extend( + self.ownership_drop_statements_for_scope_exit(&else_state, &merged), + ); + } + *state = merged; + + Ok(vec![parser::Statement::Conditional { + condition: condition.clone(), + body: rewritten_then, + else_body: rewritten_else, + }]) + } + parser::Statement::Match { subject, arms } => { + let subject_ty = { + let var_types = state.as_var_types(); + self.expression_type(subject, &var_types)? + }; + let enum_def = match self.type_definitions.get(&subject_ty) { + Some(TypeDef::Enum(enum_def)) => enum_def, + _ => { + return Err(anyhow::anyhow!( + "match subject must be an enum, got {:?}", + subject_ty + )); + } + }; + let _ = self.ownership_analyze_expression(subject, state)?; + let state_after_subject = state.clone(); + + let mut rewritten_arms = vec![]; + let mut arm_states = vec![]; + for arm in arms { + let mut arm_state = state_after_subject.clone(); + let (variant_name, binder) = match &arm.pattern { + parser::MatchPattern::Variant { + type_name, + variant_name, + binder, + } => { + if type_name != &subject_ty { + return Err(anyhow::anyhow!( + "match arm type {:?} does not match subject type {:?}", + type_name, + subject_ty + )); + } + (variant_name, binder) + } + }; + let variant = enum_def + .variants + .iter() + .find(|v| &v.name == variant_name) + .ok_or_else(|| { + anyhow::anyhow!( + "unknown variant {} for enum {}", + variant_name, + enum_def.name + ) + })?; + if let (Some(payload_ty), Some(binder_name)) = (&variant.payload_ty, binder) { + self.ownership_insert_or_assign_local( + &mut arm_state, + binder_name, + payload_ty, + ); + } + let rewritten_body = self.ownership_rewrite_block(&arm.body, &mut arm_state)?; + arm_states.push(arm_state); + rewritten_arms.push(parser::MatchArm { + pattern: arm.pattern.clone(), + body: rewritten_body, + }); + } + + let merged = self.ownership_merge_states(&state_after_subject, &arm_states)?; + for (rewritten_arm, arm_state) in rewritten_arms.iter_mut().zip(arm_states.iter()) { + rewritten_arm + .body + .extend(self.ownership_drop_statements_for_scope_exit(arm_state, &merged)); + } + *state = merged; + Ok(vec![parser::Statement::Match { + subject: subject.clone(), + arms: rewritten_arms, + }]) + } + parser::Statement::While { condition, body } => { + let condition_ty = self.ownership_analyze_expression(condition, state)?; + if condition_ty != "Bool" { + return Err(anyhow::anyhow!( + "expected condition to be of type Bool, but got {:?}", + condition_ty + )); + } + let state_after_condition = state.clone(); + + let mut body_state = state_after_condition.clone(); + let mut rewritten_body = self.ownership_rewrite_block(body, &mut body_state)?; + rewritten_body.extend( + self.ownership_drop_statements_for_scope_exit( + &body_state, + &state_after_condition, + ), + ); + + *state = self.ownership_merge_states( + &state_after_condition, + &[state_after_condition.clone(), body_state], + )?; + + Ok(vec![parser::Statement::While { + condition: condition.clone(), + body: rewritten_body, + }]) + } + } + } + + fn apply_ownership_model( + &mut self, + ownership_target_functions: &HashSet, + ) -> anyhow::Result<()> { + let function_names = self + .function_definitions + .keys() + .cloned() + .collect::>(); + for function_name in function_names { + if !ownership_target_functions.contains(&function_name) { + continue; + } + let (sig, body) = { + let definition = + self.function_definitions + .get(&function_name) + .ok_or_else(|| { + anyhow::anyhow!("missing function definition {}", function_name) + })?; + (definition.sig.clone(), definition.body.clone()) + }; + + let mut state = OwnershipState::with_params(&sig.parameters); + let mut rewritten = self.ownership_rewrite_block(&body, &mut state)?; + rewritten.extend(self.ownership_drop_statements_in_reverse(&state)); + + let definition = self + .function_definitions + .get_mut(&function_name) + .ok_or_else(|| anyhow::anyhow!("missing function definition {}", function_name))?; + definition.body = rewritten; + } + Ok(()) + } +} + #[tracing::instrument(level = "trace", skip_all)] pub fn resolve(mut ast: Ast) -> anyhow::Result { + let mut ownership_target_functions = ast + .top_level_functions + .iter() + .filter(|f| !f.is_comptime && !f.is_extern) + .map(|f| f.name.clone()) + .collect::>(); + for impl_decl in &ast.impl_declarations { + for method in &impl_decl.methods { + ownership_target_functions.insert(trait_impl_function_name( + &impl_decl.trait_name, + &impl_decl.for_type, + &method.name, + )); + } + } + { let stdlib_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("src") @@ -1056,6 +1688,10 @@ pub fn resolve(mut ast: Ast) -> anyhow::Result { for func_def in program.function_definitions.values() { program.type_check(func_def)?; } + program.apply_ownership_model(&ownership_target_functions)?; + for func_def in program.function_definitions.values() { + program.type_check(func_def)?; + } reject_runtime_semantic_builtin_usage(&program)?; program.semantic_expr_metadata = index_semantic_expression_metadata(&program); @@ -1124,13 +1760,6 @@ fn validate_function_signature_types( )); } - if sig.return_type == "Void" && !allow_void_return { - return Err(anyhow::anyhow!( - "function {} cannot return Void (only extern functions may return Void in v1)", - function_name - )); - } - Ok(()) } @@ -1460,11 +2089,47 @@ fn register_legacy_struct_invariants( } fn replace_self_type(ty: &str, concrete_self: &str) -> String { - if ty == "Self" { - concrete_self.to_string() - } else { - ty.to_string() + let mut substitutions = HashMap::new(); + substitutions.insert("Self".to_string(), concrete_self.to_string()); + rewrite_type_ref(ty, &substitutions, &HashMap::new()) +} + +fn validate_special_trait_shape(trait_decl: &parser::TraitDecl) -> anyhow::Result<()> { + if trait_decl.name == "Copy" { + if trait_decl.methods.len() != 1 { + return Err(anyhow::anyhow!( + "trait Copy must define copy(v: Ref[Self]) -> Self" + )); + } + let method = &trait_decl.methods[0]; + if method.name != "copy" + || method.parameters.len() != 1 + || method.parameters[0].ty != "Ref[Self]" + || method.return_type != "Self" + { + return Err(anyhow::anyhow!( + "trait Copy must define copy(v: Ref[Self]) -> Self" + )); + } } + if trait_decl.name == "Drop" { + if trait_decl.methods.len() != 1 { + return Err(anyhow::anyhow!( + "trait Drop must define drop(v: Self) -> Void" + )); + } + let method = &trait_decl.methods[0]; + if method.name != "drop" + || method.parameters.len() != 1 + || method.parameters[0].ty != "Self" + || method.return_type != "Void" + { + return Err(anyhow::anyhow!( + "trait Drop must define drop(v: Self) -> Void" + )); + } + } + Ok(()) } fn collect_trait_metadata( @@ -1479,6 +2144,7 @@ fn collect_trait_metadata( let mut trait_methods_by_trait: HashMap> = HashMap::new(); for trait_decl in &ast.trait_declarations { + validate_special_trait_shape(trait_decl)?; if trait_methods_by_trait.contains_key(&trait_decl.name) { return Err(anyhow::anyhow!( "duplicate trait declaration {}", @@ -1502,7 +2168,7 @@ fn collect_trait_metadata( method.name )); } - if method.parameters[0].ty != "Self" { + if trait_decl.name != "Copy" && method.parameters[0].ty != "Self" { return Err(anyhow::anyhow!( "trait method {}.{} must use Self as first parameter type in v1", trait_decl.name, @@ -1598,7 +2264,16 @@ fn collect_trait_metadata( .zip(trait_method.parameter_types.iter()) { let expected = replace_self_type(expected_ty, &impl_decl.for_type); - if normalize_numeric_alias(¶meter.ty) != normalize_numeric_alias(&expected) { + let matches_expected = + normalize_numeric_alias(¶meter.ty) == normalize_numeric_alias(&expected); + let matches_legacy_copy_by_value = impl_decl.trait_name == "Copy" + && trait_method.method_name == "copy" + && normalize_numeric_alias(¶meter.ty) + == normalize_numeric_alias(&impl_decl.for_type); + let matches_copy_ref_alias = impl_decl.trait_name == "Copy" + && trait_method.method_name == "copy" + && parameter.ty.ends_with("Ref"); + if !matches_expected && !matches_legacy_copy_by_value && !matches_copy_ref_alias { return Err(anyhow::anyhow!( "impl method {}.{} parameter {} type mismatch: expected {}, got {}", impl_decl.trait_name, @@ -3737,11 +4412,50 @@ fun main() -> I32 { } #[test] - fn resolve_rejects_non_extern_void_return() { + fn resolve_accepts_non_extern_void_return() { let source = r#" fun helper() -> Void { + Clib.free(i32_to_i64(0)) +} + +fun main() -> I32 { return 0 } +"# + .to_string(); + + let tokens = tokenizer::tokenize(source).expect("tokenize source"); + let ast = parser::parse(tokens).expect("parse source"); + resolve(ast).expect("non-extern Void return should be accepted"); + } + + #[test] + fn resolve_rejects_invalid_copy_trait_shape() { + let source = r#" +trait Copy { + fun clone(v: Self) -> Self +} + +fun main() -> I32 { + return 0 +} +"# + .to_string(); + + let tokens = tokenizer::tokenize(source).expect("tokenize source"); + let ast = parser::parse(tokens).expect("parse source"); + let err = resolve(ast).expect_err("invalid Copy trait shape should fail"); + assert!(err + .to_string() + .contains("trait Copy must define copy(v: Ref[Self]) -> Self")); + } + + #[test] + fn resolve_rejects_invalid_drop_trait_shape() { + let source = r#" +trait Drop { + fun drop(v: Self) -> Self +} fun main() -> I32 { return 0 @@ -3751,10 +4465,81 @@ fun main() -> I32 { let tokens = tokenizer::tokenize(source).expect("tokenize source"); let ast = parser::parse(tokens).expect("parse source"); - let err = resolve(ast).expect_err("non-extern Void return should fail"); + let err = resolve(ast).expect_err("invalid Drop trait shape should fail"); + assert!(err + .to_string() + .contains("trait Drop must define drop(v: Self) -> Void")); + } + + #[test] + fn resolve_copy_type_can_be_read_multiple_times() { + let source = r#" +fun main() -> I32 { + a = 7 + b = a + c = a + return b + c +} +"# + .to_string(); + + let tokens = tokenizer::tokenize(source).expect("tokenize source"); + let ast = parser::parse(tokens).expect("parse source"); + resolve(ast).expect("Copy-backed scalar reads should compile"); + } + + #[test] + fn resolve_rejects_use_after_move() { + let source = r#" +struct Box { + value: I32, +} + +fun consume(v: Box) -> I32 { + return v.value +} + +fun main() -> I32 { + a = Box struct { value: 1 } + x = consume(a) + y = consume(a) + return x + y +} +"# + .to_string(); + + let tokens = tokenizer::tokenize(source).expect("tokenize source"); + let ast = parser::parse(tokens).expect("parse source"); + let err = resolve(ast).expect_err("use-after-move should fail"); + assert!(err.to_string().contains("use of moved value a")); + } + + #[test] + fn resolve_rejects_move_from_uninitialized_binding() { + let source = r#" +struct Box { + value: I32, +} + +fun consume(v: Box) -> I32 { + return v.value +} + +fun main() -> I32 { + if 1 == 1 { + a = Box struct { value: 1 } + } + return consume(a) +} +"# + .to_string(); + + let tokens = tokenizer::tokenize(source).expect("tokenize source"); + let ast = parser::parse(tokens).expect("parse source"); + let err = resolve(ast).expect_err("uninitialized move should fail"); assert!(err .to_string() - .contains("only extern functions may return Void")); + .contains("cannot move from uninitialized value a")); } #[test] diff --git a/crates/oac/src/prove.rs b/crates/oac/src/prove.rs index 001064c..2a3b9af 100644 --- a/crates/oac/src/prove.rs +++ b/crates/oac/src/prove.rs @@ -445,6 +445,12 @@ struct Counter { max: I32, } +impl Copy for Counter { + fun copy(v: Counter) -> Counter { + return v + } +} + invariant value_non_negative "value non-negative" for (v: Counter) { return v.value >= 0 } @@ -519,6 +525,12 @@ struct Foo { x: I32, } +impl Copy for Foo { + fun copy(v: Foo) -> Foo { + return v + } +} + invariant "foo invariant" for (v: Foo) { w = helper(v) return w == w diff --git a/crates/oac/src/qbe_backend.rs b/crates/oac/src/qbe_backend.rs index c30fed2..d6141e3 100644 --- a/crates/oac/src/qbe_backend.rs +++ b/crates/oac/src/qbe_backend.rs @@ -1064,10 +1064,9 @@ fn compile_statement( ); } parser::Statement::Return { expr } => { - let (expr_var, expr_ty) = compile_expr(ctx, qbe_func, &expr, variables); - let return_var = maybe_clone_struct_value(ctx, qbe_func, &expr_var, &expr_ty); + let (expr_var, _expr_ty) = compile_expr(ctx, qbe_func, &expr, variables); trace!(%expr_var, "Emitting return instruction"); - qbe_func.add_instr(qbe::Instr::Ret(Some(qbe::Value::Temporary(return_var)))); + qbe_func.add_instr(qbe::Instr::Ret(Some(qbe::Value::Temporary(expr_var)))); } parser::Statement::Expression { expr } => { if compile_void_call_statement(ctx, qbe_func, expr, variables) { @@ -1223,22 +1222,21 @@ fn compile_assign_statement( trace!(%variable, "Compiling assignment"); let value_var = compile_expr(ctx, qbe_func, value, variables); - if let Some(existing_var) = variables.get(variable) { - let assigned_var = maybe_clone_struct_value(ctx, qbe_func, &value_var.0, &existing_var.1); - let existing_type = ctx + if let Some((existing_var, existing_ty)) = variables.get(variable).cloned() { + let existing_type_def = ctx .resolved .type_definitions - .get(&existing_var.1) + .get(&existing_ty) .expect("existing variable type should exist"); qbe_func.assign_instr( - qbe::Value::Temporary(existing_var.0.clone()), - type_to_qbe(existing_type), - qbe::Instr::Copy(qbe::Value::Temporary(assigned_var)), + qbe::Value::Temporary(existing_var.clone()), + type_to_qbe(existing_type_def), + qbe::Instr::Copy(qbe::Value::Temporary(value_var.0)), ); - } else { - let assigned_var = maybe_clone_struct_value(ctx, qbe_func, &value_var.0, &value_var.1); - variables.insert(variable.to_string(), (assigned_var, value_var.1)); + variables.insert(variable.to_string(), (existing_var, existing_ty)); + return; } + variables.insert(variable.to_string(), value_var); } fn compile_function(ctx: &mut CodegenCtx, func_def: ir::FunctionDefinition) { @@ -1262,12 +1260,17 @@ fn compile_function(ctx: &mut CodegenCtx, func_def: ir::FunctionDefinition) { .type_definitions .get(&func_def.sig.return_type) .unwrap(); + let returns_void = matches!(return_type_def, ir::TypeDef::BuiltIn(BuiltInType::Void)); let mut qbe_func = qbe::Function::new( qbe::Linkage::public(), func_def.name.clone(), qbe_args, - Some(type_to_qbe(return_type_def)), + if returns_void { + None + } else { + Some(type_to_qbe(return_type_def)) + }, ); qbe_func.add_block("start".to_string()); @@ -1287,6 +1290,9 @@ fn compile_function(ctx: &mut CodegenCtx, func_def: ir::FunctionDefinition) { &mut prove_site_counter, ); } + if returns_void && !qbe_func.blocks.last().unwrap().jumps() { + qbe_func.add_instr(qbe::Instr::Ret(None)); + } ctx.module.add_function(qbe_func); } @@ -1414,39 +1420,166 @@ fn alloc_struct_zeroed(func: &mut qbe::Function, size: u64) -> QbeAssignName { id } -fn clone_struct_bytes( +fn normalize_trait_target_type(ty: &str) -> &str { + match ty { + "Int" => "I32", + "PtrInt" => "I64", + _ => ty, + } +} + +fn copy_impl_target_for_type(ctx: &CodegenCtx, value_ty: &str) -> Option { + let impl_key = trait_impl_method_key("Copy", normalize_trait_target_type(value_ty), "copy"); + ctx.resolved.trait_impl_methods.get(&impl_key).cloned() +} + +fn emit_copy_ref_argument( ctx: &CodegenCtx, func: &mut qbe::Function, - source_ptr: &str, - struct_name: &str, + value_var: &str, + value_ty: &str, + ref_ty: &str, ) -> QbeAssignName { - let size = struct_size_bytes_by_name(ctx, struct_name); - let cloned_ptr = alloc_struct_zeroed(func, size); - func.add_instr(Instr::Call( - "memcpy".to_string(), - vec![ - (qbe::Type::Long, qbe::Value::Temporary(cloned_ptr.clone())), - ( - qbe::Type::Long, - qbe::Value::Temporary(source_ptr.to_string()), + let value_slot = new_id(&["implicit", "copy", "value", "slot"]); + func.assign_instr( + qbe::Value::Temporary(value_slot.clone()), + qbe::Type::Long, + qbe::Instr::Alloc8(non_zero_allocation_size(type_offset(ctx, value_ty))), + ); + func.add_instr(qbe::Instr::Store( + type_ref_to_qbe(ctx, value_ty), + qbe::Value::Temporary(value_slot.clone()), + qbe::Value::Temporary(value_var.to_string()), + )); + + let ref_def = match ctx + .resolved + .type_definitions + .get(ref_ty) + .unwrap_or_else(|| panic!("unknown Copy.copy receiver type {}", ref_ty)) + { + ir::TypeDef::Struct(def) => def, + _ => panic!( + "Copy.copy receiver type {} must be a ref-like struct wrapper", + ref_ty + ), + }; + let ptr_field = ref_def + .struct_fields + .iter() + .find(|field| field.name == "ptr") + .unwrap_or_else(|| { + panic!( + "Copy.copy receiver type {} must include ptr: PtrInt field", + ref_ty + ) + }); + if normalize_trait_target_type(&ptr_field.ty) != "I64" { + panic!( + "Copy.copy receiver type {} must include ptr field typed as PtrInt", + ref_ty + ); + } + + let ref_slot = new_id(&["implicit", "copy", "ref", "slot"]); + func.assign_instr( + qbe::Value::Temporary(ref_slot.clone()), + qbe::Type::Long, + qbe::Instr::Alloc8(non_zero_allocation_size(struct_size_bytes(ctx, ref_def))), + ); + + let mut ptr_offset = 0u64; + for field in &ref_def.struct_fields { + if field.name == "ptr" { + break; + } + ptr_offset += type_offset(ctx, &field.ty); + } + let ptr_addr = if ptr_offset == 0 { + ref_slot.clone() + } else { + let addr = new_id(&["implicit", "copy", "ref", "ptr", "addr"]); + func.assign_instr( + qbe::Value::Temporary(addr.clone()), + qbe::Type::Long, + qbe::Instr::Add( + qbe::Value::Temporary(ref_slot.clone()), + qbe::Value::Const(ptr_offset), ), - (qbe::Type::Long, qbe::Value::Const(size)), - ], - None, + ); + addr + }; + func.add_instr(qbe::Instr::Store( + qbe::Type::Long, + qbe::Value::Temporary(ptr_addr), + qbe::Value::Temporary(value_slot), )); - cloned_ptr + + ref_slot } -fn maybe_clone_struct_value( +fn maybe_emit_implicit_copy( ctx: &CodegenCtx, func: &mut qbe::Function, value_var: &str, value_ty: &str, ) -> QbeAssignName { - match ctx.resolved.type_definitions.get(value_ty) { - Some(ir::TypeDef::Struct(_)) => clone_struct_bytes(ctx, func, value_var, value_ty), - _ => value_var.to_string(), + let Some(copy_function) = copy_impl_target_for_type(ctx, value_ty) else { + return value_var.to_string(); + }; + + let copy_sig = ctx + .resolved + .function_sigs + .get(©_function) + .expect("copy impl function signature should exist"); + let copy_param_ty = copy_sig + .parameters + .first() + .map(|parameter| parameter.ty.clone()) + .expect("copy impl function should have one parameter"); + let copy_arg_var = + if normalize_trait_target_type(©_param_ty) == normalize_trait_target_type(value_ty) { + value_var.to_string() + } else { + emit_copy_ref_argument(ctx, func, value_var, value_ty, ©_param_ty) + }; + + let result = new_id(&["implicit", "copy"]); + let qbe_ret_ty = type_ref_to_qbe(ctx, value_ty); + let qbe_arg_ty = type_ref_to_qbe(ctx, ©_param_ty); + func.assign_instr( + qbe::Value::Temporary(result.clone()), + qbe_ret_ty, + qbe::Instr::Call( + call_target_symbol(©_function, copy_sig), + vec![(qbe_arg_ty, qbe::Value::Temporary(copy_arg_var))], + None, + ), + ); + result +} + +fn compile_variable_read( + ctx: &CodegenCtx, + func: &mut qbe::Function, + variables: &mut Variables, + variable_name: &str, +) -> (QbeAssignName, ir::TypeRef) { + let (stored_var, stored_ty) = variables + .get(variable_name) + .unwrap_or_else(|| panic!("unknown variable {}", variable_name)) + .clone(); + + if let Some(copy_impl_fn) = copy_impl_target_for_type(ctx, &stored_ty) { + if copy_impl_fn == func.name { + return (stored_var, stored_ty); + } + let copied = maybe_emit_implicit_copy(ctx, func, &stored_var, &stored_ty); + return (copied, stored_ty); } + + (stored_var, stored_ty) } fn emit_struct_memcmp( @@ -1547,16 +1680,12 @@ fn compile_void_call_statement( let mut lowered_args = vec![]; for arg in args { let (arg_var, arg_ty) = compile_expr(ctx, func, arg, variables); - let lowered_var = maybe_clone_struct_value(ctx, func, &arg_var, &arg_ty); let arg_type_def = ctx .resolved .type_definitions .get(&arg_ty) .expect("call argument type should exist"); - lowered_args.push(( - type_to_qbe(arg_type_def), - qbe::Value::Temporary(lowered_var), - )); + lowered_args.push((type_to_qbe(arg_type_def), qbe::Value::Temporary(arg_var))); } let sig = ctx @@ -1589,8 +1718,7 @@ fn compile_named_call( let mut arg_vars = vec![]; for arg in args { let (arg_var, arg_ty) = compile_expr(ctx, func, arg, variables); - let lowered_var = maybe_clone_struct_value(ctx, func, &arg_var, &arg_ty); - arg_vars.push((lowered_var, arg_ty)); + arg_vars.push((arg_var, arg_ty)); } let sig = ctx @@ -1712,10 +1840,11 @@ fn compile_expr( struct_variable, field: field_name, } => { - if let Some((struct_pointer_var, struct_name)) = variables.get(struct_variable.as_str()) - { + if variables.contains_key(struct_variable.as_str()) { + let (struct_pointer_var, struct_name) = + compile_variable_read(ctx, func, variables, struct_variable); let resolved = ctx.resolved.clone(); - let typedef = resolved.type_definitions.get(struct_name).unwrap(); + let typedef = resolved.type_definitions.get(&struct_name).unwrap(); let ir::TypeDef::Struct(structdef) = typedef else { panic!("Not really a struct: {struct_name}"); }; @@ -1738,7 +1867,7 @@ fn compile_expr( Value::Temporary(struct_field_address_id.clone()), qbe::Type::Long, Instr::Add( - Value::Temporary(struct_pointer_var.clone()), + Value::Temporary(struct_pointer_var), Value::Const(field_offset), ), ); @@ -2101,7 +2230,7 @@ fn compile_expr( (id, "Bool".to_string()) } parser::Expression::Variable(name) => { - return variables.get(name).unwrap().clone(); + return compile_variable_read(ctx, func, variables, name); } parser::Expression::UnaryOp(op, expr) => { let id = new_id(&["unary_op"]); @@ -2477,26 +2606,17 @@ fun main() -> I32 { } #[test] - fn qbe_codegen_structs_use_copy_barriers_and_memcmp_equality() { + fn qbe_codegen_structs_are_move_only_and_keep_memcmp_equality() { let source = r#" struct Box { value: I32, } -fun id(v: Box) -> Box { - return v -} - -fun consume(v: Box) -> I32 { - return v.value -} - fun main() -> I32 { a = Box struct { value: 7 } - b = a - c = id(b) - if a == c { - return consume(c) + b = Box struct { value: 7 } + if a == b { + return 1 } return 0 } @@ -2509,12 +2629,8 @@ fun main() -> I32 { "expected struct equality lowering via memcmp, got:\n{qbe_ir}" ); assert!( - qbe_ir.matches("call $memcpy").count() >= 4, - "expected struct copy barriers to emit memcpy calls, got:\n{qbe_ir}" - ); - assert!( - qbe_ir.matches("call $calloc").count() >= 3, - "expected struct allocations/clones to emit calloc calls, got:\n{qbe_ir}" + !qbe_ir.contains("cannot move from uninitialized value"), + "move-only struct program should still resolve and lower, got:\n{qbe_ir}" ); } diff --git a/crates/oac/src/std/std_traits.oa b/crates/oac/src/std/std_traits.oa index 5fc1c05..fe38454 100644 --- a/crates/oac/src/std/std_traits.oa +++ b/crates/oac/src/std/std_traits.oa @@ -6,6 +6,62 @@ trait Eq { fun equals(a: Self, b: Self) -> Bool } +trait Copy { + fun copy(v: Ref[Self]) -> Self +} + +trait Drop { + fun drop(v: Self) -> Void +} + +impl Copy for Bool { + fun copy(v: Bool) -> Bool { + return v + } +} + +impl Copy for U8 { + fun copy(v: U8) -> U8 { + return v + } +} + +impl Copy for I32 { + fun copy(v: I32) -> I32 { + return v + } +} + +impl Copy for I64 { + fun copy(v: I64) -> I64 { + return v + } +} + +impl Copy for FP32 { + fun copy(v: FP32) -> FP32 { + return v + } +} + +impl Copy for FP64 { + fun copy(v: FP64) -> FP64 { + return v + } +} + +impl Copy for Char { + fun copy(v: Char) -> Char { + return v + } +} + +impl Copy for AsciiChar { + fun copy(v: AsciiChar) -> AsciiChar { + return v + } +} + impl Hash for I32 { fun hash(v: I32) -> I32 { return v diff --git a/crates/oac/src/struct_invariants.rs b/crates/oac/src/struct_invariants.rs index 44a3891..530d690 100644 --- a/crates/oac/src/struct_invariants.rs +++ b/crates/oac/src/struct_invariants.rs @@ -1557,6 +1557,12 @@ struct Foo { x: I32, } +impl Copy for Foo { + fun copy(v: Foo) -> Foo { + return v + } +} + invariant "foo invariant" for (v: Foo) { return v.x == v.x } diff --git a/crates/oac/src/verification_outcomes.rs b/crates/oac/src/verification_outcomes.rs index c9c499e..0c8e3c3 100644 --- a/crates/oac/src/verification_outcomes.rs +++ b/crates/oac/src/verification_outcomes.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::sync::{Mutex, OnceLock}; @@ -55,11 +56,27 @@ struct ActiveCollector { } static COLLECTOR: OnceLock> = OnceLock::new(); +thread_local! { + static FIXTURE_CONTEXT: RefCell> = const { RefCell::new(None) }; +} fn collector() -> &'static Mutex { COLLECTOR.get_or_init(|| Mutex::new(ActiveCollector::default())) } +pub(crate) fn with_fixture_context(fixture: &str, run: impl FnOnce() -> T) -> T { + FIXTURE_CONTEXT.with(|slot| { + let previous = slot.replace(Some(fixture.to_string())); + let output = run(); + slot.replace(previous); + output + }) +} + +fn current_fixture_context() -> Option { + FIXTURE_CONTEXT.with(|slot| slot.borrow().clone()) +} + pub(crate) fn begin_outcome_collection(profile: VerificationProfile) -> anyhow::Result<()> { let mut guard = collector() .lock() @@ -80,7 +97,8 @@ pub(crate) fn record_outcome(mut record: VerificationOutcomeRecord) { return; }; if record.fixture.is_none() { - record.fixture = std::env::var("OAC_VERIFICATION_OUTCOME_FIXTURE").ok(); + record.fixture = current_fixture_context() + .or_else(|| std::env::var("OAC_VERIFICATION_OUTCOME_FIXTURE").ok()); } guard.records.push(record); }