Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ The generated loop is now written from the program's source alone, and the manif

### Fixed

- **`perf-shared-update` reports the copies generated code makes, and only those.** The check now reads the same field moves the Rust backend makes, from the module lowered as it compiles. `done = toppedUp(s.pool, s.book, s.nextKey)?` followed by `S.update(s, pool = done.pool, book = done.book, nextKey = done.nextKey)` moves the Book out of `s` and no longer warns. `match (s.window.created, s.height)` with `(created, _) -> f(s, absorbed(created, k))` copies the Map, since `s` still holds it, and now warns: "`absorbed` updates `created`, read from `s.window.created`, a Map that is still held by `s`". A field of a record a loop hands on unchanged is reported too.
- **Generated Rust moves the rest of a nested record into its update.** `Setting.update(setting, window = Window.update(setting.window, created = Map.set(setting.window.created, k, v)), height = setting.height + 1)` used to clone `setting.window` and `setting.window.created`, so every `Map.set` copied the Map. When nothing reads that part of `setting` again, the Map now moves into `Map.set` and the other fields of `setting.window` move into the new `Window`.
- **`check` no longer asks a verify block of a function taking a `Tcp.Socket` or a `Wait.Item`.** Every constructor of either carries a capability resource, so no verify case can write one, but the exemption only looked into the types of the function's own module. It now looks into a capability's own records and sums by the same rule.
- **Generated Rust builds when a record is updated after one of its fields was read.** `progress = flight.progress` followed by `Flight.update(flight, progress = f(progress))` inside a pair of functions that tail-call each other generated Rust that moved the field out and then moved the whole record, which rustc rejects (E0382). A loop that read a field in a `let` and later returned the whole record failed the same way. Such a field read now moves only when nothing reads that part of the record again and is copied otherwise, and the field read into `f` moves in both spellings (`f(progress)` and `f(flight.progress)`), so a Map `f` updates is not copied.
- **wasm-gc: `Map.set` no longer copies the map, and `Map.remove` no longer changes the map it was given.** `set` copied every bucket unless the compiler could prove the map had no other holder, which it cannot for a map held in a record field such as an answer module's state. A 100 000-entry state map served 2000 `Map.set` requests in 11 s under `aver run --wasm-gc` and 1.9 s on Node 26. `remove` wrote into the map it was given, so a caller that still held that map saw the key gone. Both now write into the map's arrays in place and return a new version. The version they were given stays valid, because a record of what the write replaced is kept with it. The same run now takes 0.4 s under `aver run --wasm-gc` and 0.3 s on Node. Reading an older version again costs one step for each write made since.
Expand Down
2 changes: 1 addition & 1 deletion docs/diagnostics-slugs.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ When a real signature shape is deliberately uninhabited, keep the suppression ex
| `perf-list-len` | warning | `List.len` called inside recursion, which adds up to O(n²). | Compute the length once, outside the recursion. |
| `perf-string-concat` | warning | String concatenation inside recursion. | Accumulate in a list and join once. |
| `perf-nested-match` | warning | Nested `match` on the same subject. | Combine into one `match`. |
| `perf-shared-update` | warning | A Map or Vector read out of a record (`setting.window.created`) is updated in place while the record still holds it, in a function that runs again and again (recursive, reached from a recursive function of its module, or part of an answer module). The update is `Map.set`, `Map.remove` or `Vector.set` on the field, or a call that hands the field to one of them and returns the updated collection (followed into the dependencies the call names). The record still holds it when it is read again after that call, or when it was already passed whole to the call or record the update is an argument of. Each such update copies the whole collection. A record update or literal that reads the field it replaces once, reads nothing else of the record but other fields, and is the record's last use is not reported: the VM takes the field out first. The same goes for a field further down (`setting.window.created`) read once in such a literal or update, inside updates of `setting.window` that write it. Not seen: a caller that keeps the record it passed, an alias made through a binding, one collection in two records. | Take the field out of the record before updating it (bind the parts with a `match` and carry on with a record that no longer holds them, or give it an empty collection in their place), or read it at the record's last use. |
| `perf-shared-update` | warning | A Map or Vector read out of a record (`setting.window.created`, or a local bound to such a field by a `let` or a `match`) is updated in place while the record still holds it, in a function that runs again and again (recursive, reached from a recursive function of its module, or part of an answer module). The update is `Map.set`, `Map.remove` or `Vector.set` on the value, or a call that hands it to one of them and returns the updated collection (followed into the dependencies the call names). The record still holds the field whenever the read does not move it, which is decided by the same field-move analysis generated code uses: a field read moves when every other read of the record runs in another branch, finished earlier, or reads a disjoint part (such as the rest of the record handed to the update that replaces the field), and nothing reads the record after. Each update of a field that does not move copies the whole collection. A record a loop hands on unchanged never gives up a field. Not seen: a caller that keeps the record it passed, a callee that borrows the value and copies it itself, an alias made through a binding, one collection in two records. | Read the field where nothing reads that part of the record again, for example in the update of the record that replaces it (`S.update(s, book = g(s.book))`, or `b = s.book` followed by `S.update(s, book = g(b))`), so it moves out of the record. Binding it with a `match` does not help while the record is still used whole afterwards. |
| `perf-loop-invariant` | warning | An expression is recomputed on every recursive call but does not depend on the recursion. | Hoist it outside the recursion. |
| `cse-match` | warning | Subexpression computed in both the match condition and an arm body. | Bind it once above the match. |
| `cse-duplicate` | warning | Expression computed more than once in one function. | Bind it and reuse it. |
Expand Down
2 changes: 1 addition & 1 deletion src/checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,6 @@ pub use module_effects::{collect_module_effects_warnings, collect_module_effects
pub use naming::{collect_naming_warnings, collect_naming_warnings_in};
pub use perf::{collect_perf_warnings, collect_perf_warnings_in};
pub use serve_path::{collect_serve_path_warnings, collect_serve_path_warnings_in};
pub use shared_update::{ModuleSource, collect_shared_update_warnings};
pub use shared_update::{ModuleSource, ProgramSymbols, collect_shared_update_warnings};
pub use traversal::collect_traversal_warnings_in;
pub use verify::{expr_to_str, merge_verify_blocks, verify_block_label};
Loading
Loading