lazycodex-ai 5.0.0-beta.85 → 5.0.0-beta.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/index.js +68 -41
- package/dist/cli-node/index.js +68 -41
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -0
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +63 -89
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +2 -2
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +63 -89
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/define-goal.md +2 -3
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +8 -2
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +2 -2
- package/packages/omo-codex/plugin/skills/browser/ATTRIBUTION.md +26 -14
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +65 -52
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +81 -66
- package/packages/omo-codex/plugin/skills/browser/references/install.md +31 -34
- package/packages/omo-codex/plugin/skills/browser/references/owned-engine/README.md +41 -21
- package/packages/omo-codex/plugin/skills/browser/references/owned-engine/frames-and-humans.md +33 -20
- package/packages/omo-codex/plugin/skills/browser/references/owned-engine/ladder.md +24 -22
- package/packages/omo-codex/plugin/skills/browser/references/owned-engine/network.md +35 -15
- package/packages/omo-codex/plugin/skills/browser/references/recipes/1password.md +13 -11
- package/packages/omo-codex/plugin/skills/browser/references/remote.md +5 -4
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/index.js +1534 -0
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +9 -0
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/page-bundle.js +1395 -0
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-doctor.mjs +37 -32
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-install.mjs +31 -44
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +25 -0
- package/packages/omo-codex/plugin/skills/debugging/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/06-fix.md +3 -3
- package/packages/omo-codex/plugin/skills/debugging/references/methodology/08-qa.md +1 -1
- package/packages/omo-codex/plugin/skills/debugging/references/tools/browser-qa.md +104 -0
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/SKILL.md +12 -18
- package/packages/omo-codex/plugin/skills/programming/references/rust/README.md +43 -15
- package/packages/omo-codex/plugin/skills/programming/references/rust/api-design.md +81 -0
- package/packages/omo-codex/plugin/skills/programming/references/rust/async-tokio.md +60 -28
- package/packages/omo-codex/plugin/skills/programming/references/rust/axum-stack.md +1 -13
- package/packages/omo-codex/plugin/skills/programming/references/rust/cargo-strict.md +44 -8
- package/packages/omo-codex/plugin/skills/programming/references/rust/clap-stack.md +8 -3
- package/packages/omo-codex/plugin/skills/programming/references/rust/concurrency.md +66 -52
- package/packages/omo-codex/plugin/skills/programming/references/rust/libraries.md +35 -25
- package/packages/omo-codex/plugin/skills/programming/references/rust/macros.md +63 -0
- package/packages/omo-codex/plugin/skills/programming/references/rust/one-liners.md +5 -3
- package/packages/omo-codex/plugin/skills/programming/references/rust/proptest-insta.md +8 -0
- package/packages/omo-codex/plugin/skills/programming/references/rust/type-state.md +50 -12
- package/packages/omo-codex/plugin/skills/programming/references/rust/unsafe-discipline.md +34 -6
- package/packages/omo-codex/plugin/skills/programming/references/rust/zero-cost-safety.md +62 -52
- package/packages/omo-codex/plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md +1 -1
- package/packages/omo-codex/plugin/skills/programming/references/rust-ub/ub-taxonomy.md +6 -3
- package/packages/omo-codex/plugin/skills/programming/scripts/rust/check-no-excuse-rules.sh +86 -75
- package/packages/omo-codex/plugin/skills/programming/scripts/rust/new-project.py +31 -28
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/SKILL.md +27 -20
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +32 -100
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/README.md +5 -11
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +20 -37
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +63 -89
- package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +4 -4
- package/packages/omo-codex/plugin/skills/ulw-loop/references/define-goal.md +2 -3
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +2 -2
- package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/visual-qa/references/browser-setup.md +46 -46
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +2 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -2
- package/packages/prompts-core/prompts/ultrawork/codex.md +63 -89
- package/packages/shared-skills/skills/browser/ATTRIBUTION.md +26 -14
- package/packages/shared-skills/skills/browser/SKILL.md +65 -52
- package/packages/shared-skills/skills/browser/references/commands.md +81 -66
- package/packages/shared-skills/skills/browser/references/install.md +31 -34
- package/packages/shared-skills/skills/browser/references/owned-engine/README.md +41 -21
- package/packages/shared-skills/skills/browser/references/owned-engine/frames-and-humans.md +33 -20
- package/packages/shared-skills/skills/browser/references/owned-engine/ladder.md +24 -22
- package/packages/shared-skills/skills/browser/references/owned-engine/network.md +35 -15
- package/packages/shared-skills/skills/browser/references/recipes/1password.md +13 -11
- package/packages/shared-skills/skills/browser/references/remote.md +5 -4
- package/packages/shared-skills/skills/browser/runtime/omowright/index.js +1534 -0
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +9 -0
- package/packages/shared-skills/skills/browser/runtime/omowright/page-bundle.js +1395 -0
- package/packages/shared-skills/skills/browser/scripts/browser-doctor.mjs +37 -32
- package/packages/shared-skills/skills/browser/scripts/browser-install.mjs +31 -44
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +25 -0
- package/packages/shared-skills/skills/debugging/SKILL.md +2 -2
- package/packages/shared-skills/skills/debugging/references/methodology/06-fix.md +3 -3
- package/packages/shared-skills/skills/debugging/references/methodology/08-qa.md +1 -1
- package/packages/shared-skills/skills/debugging/references/tools/browser-qa.md +104 -0
- package/packages/shared-skills/skills/frontend/SKILL.md +1 -1
- package/packages/shared-skills/skills/frontend/references/design/clone-from-url.md +1 -1
- package/packages/shared-skills/skills/programming/SKILL.md +12 -18
- package/packages/shared-skills/skills/programming/references/rust/README.md +43 -15
- package/packages/shared-skills/skills/programming/references/rust/api-design.md +81 -0
- package/packages/shared-skills/skills/programming/references/rust/async-tokio.md +60 -28
- package/packages/shared-skills/skills/programming/references/rust/axum-stack.md +1 -13
- package/packages/shared-skills/skills/programming/references/rust/cargo-strict.md +44 -8
- package/packages/shared-skills/skills/programming/references/rust/clap-stack.md +8 -3
- package/packages/shared-skills/skills/programming/references/rust/concurrency.md +66 -52
- package/packages/shared-skills/skills/programming/references/rust/libraries.md +35 -25
- package/packages/shared-skills/skills/programming/references/rust/macros.md +63 -0
- package/packages/shared-skills/skills/programming/references/rust/one-liners.md +5 -3
- package/packages/shared-skills/skills/programming/references/rust/proptest-insta.md +8 -0
- package/packages/shared-skills/skills/programming/references/rust/type-state.md +50 -12
- package/packages/shared-skills/skills/programming/references/rust/unsafe-discipline.md +34 -6
- package/packages/shared-skills/skills/programming/references/rust/zero-cost-safety.md +62 -52
- package/packages/shared-skills/skills/programming/references/rust-ub/miri-sanitizers-loom.md +1 -1
- package/packages/shared-skills/skills/programming/references/rust-ub/ub-taxonomy.md +6 -3
- package/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.sh +86 -75
- package/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.test.ts +83 -0
- package/packages/shared-skills/skills/programming/scripts/rust/new-project.py +31 -28
- package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/SKILL.md +27 -20
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +32 -100
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/README.md +5 -11
- package/packages/shared-skills/skills/ultimate-browsing/references/insane-search/playwright.md +20 -37
- package/packages/shared-skills/skills/ulw-execute/SKILL.md +4 -4
- package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
- package/packages/shared-skills/skills/visual-qa/references/browser-setup.md +46 -46
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-env.mjs +0 -41
- package/packages/omo-codex/plugin/skills/debugging/references/tools/playwright-cli.md +0 -112
- package/packages/omo-codex/plugin/skills/programming/scripts/rust/check-no-excuse-rules.py +0 -296
- package/packages/shared-skills/skills/browser/scripts/browser-env.mjs +0 -41
- package/packages/shared-skills/skills/debugging/references/tools/playwright-cli.md +0 -112
- package/packages/shared-skills/skills/programming/scripts/rust/check-no-excuse-rules.py +0 -296
|
@@ -11,8 +11,8 @@ Highest level tokio::sync::mpsc / broadcast / watch
|
|
|
11
11
|
Arc<Mutex<T>> / Arc<RwLock<T>>
|
|
12
12
|
(shared mutable state — common, easy to get right)
|
|
13
13
|
|
|
14
|
-
parking_lot::{Mutex, RwLock, Condvar}
|
|
15
|
-
(
|
|
14
|
+
std::sync / parking_lot::{Mutex, RwLock, Condvar}
|
|
15
|
+
(sync locks for critical sections without .await)
|
|
16
16
|
|
|
17
17
|
Atomics (AtomicUsize, AtomicBool, AtomicPtr)
|
|
18
18
|
(single-word lock-free state)
|
|
@@ -43,10 +43,10 @@ Need to share state between tasks?
|
|
|
43
43
|
│ └── AtomicBool / OnceLock<T> / OnceCell<T>
|
|
44
44
|
├── State needs mutation across many tasks/threads, cheap critical sections
|
|
45
45
|
│ ├── async context → tokio::sync::Mutex<T>
|
|
46
|
-
│ └── sync context (no .await held) → parking_lot::Mutex<T>
|
|
46
|
+
│ └── sync context (no .await held) → std::sync::Mutex<T> / parking_lot::Mutex<T>
|
|
47
47
|
├── State needs mutation, many readers, few writers
|
|
48
48
|
│ ├── async context → tokio::sync::RwLock<T>
|
|
49
|
-
│ └── sync context → parking_lot::RwLock<T>
|
|
49
|
+
│ └── sync context → std::sync::RwLock<T> / parking_lot::RwLock<T>
|
|
50
50
|
└── State is a custom lock-free primitive (channels, hazard pointers)
|
|
51
51
|
└── UnsafeCell + atomics + loom-tested + miri-tested + a co-author
|
|
52
52
|
```
|
|
@@ -83,29 +83,20 @@ let n = c.load(Ordering::Relaxed);
|
|
|
83
83
|
|
|
84
84
|
### Publish-then-load pattern
|
|
85
85
|
|
|
86
|
+
Write data, then `store(true, Release)` a flag; a reader that `load(Acquire)`s `true` is guaranteed to see the data written before the store. Do not hand-roll it around a `static mut` (the 2024 edition denies references to one, and every access is `unsafe`): `OnceLock<T>` is exactly this pattern behind a safe API.
|
|
87
|
+
|
|
86
88
|
```rust
|
|
87
|
-
static
|
|
88
|
-
static mut DATA: Option<Config> = None;
|
|
89
|
-
|
|
90
|
-
// Producer thread:
|
|
91
|
-
unsafe { DATA = Some(load_config()); }
|
|
92
|
-
READY.store(true, Ordering::Release);
|
|
93
|
-
|
|
94
|
-
// Consumer thread:
|
|
95
|
-
if READY.load(Ordering::Acquire) {
|
|
96
|
-
// SAFETY: producer's Release pairs with our Acquire; if we see READY=true,
|
|
97
|
-
// we are guaranteed to also see the DATA write that happened-before it.
|
|
98
|
-
let cfg = unsafe { DATA.as_ref().unwrap() };
|
|
99
|
-
}
|
|
100
|
-
```
|
|
89
|
+
static CONFIG: std::sync::OnceLock<Config> = std::sync::OnceLock::new();
|
|
101
90
|
|
|
102
|
-
|
|
91
|
+
// Producer, once: CONFIG.set(cfg) returns Err(cfg) if already set.
|
|
92
|
+
// Consumer: CONFIG.get() is Some only after the publishing set completed.
|
|
93
|
+
```
|
|
103
94
|
|
|
104
95
|
## Std vs parking_lot vs tokio for locks
|
|
105
96
|
|
|
106
97
|
| | std::sync::Mutex | parking_lot::Mutex | tokio::sync::Mutex |
|
|
107
98
|
|---|---|---|---|
|
|
108
|
-
| Speed |
|
|
99
|
+
| Speed | Fast (futex-based since 1.62) | Fast (adaptive spinning, smaller) | Slower (await-aware) |
|
|
109
100
|
| Poisoning | Yes (`PoisonError`) | No | No |
|
|
110
101
|
| Hold across `.await` | Dangerous (deadlock under current-thread runtime) | Dangerous | Safe |
|
|
111
102
|
| Drop guard releases | Yes | Yes | Yes |
|
|
@@ -115,17 +106,17 @@ This is the canonical Release/Acquire pattern. **Use `OnceLock<Config>` instead*
|
|
|
115
106
|
|
|
116
107
|
**Rule of thumb:**
|
|
117
108
|
|
|
118
|
-
-
|
|
109
|
+
- Short critical section, no await inside → `std::sync::Mutex`, or `parking_lot::Mutex` when the crate already depends on it or you want no poisoning. Neither is universally faster; measure before switching for speed.
|
|
119
110
|
- Shared state held across `.await` → `tokio::sync::Mutex`.
|
|
120
111
|
- Static init / app config → `OnceLock` or `LazyLock`.
|
|
121
|
-
-
|
|
112
|
+
- A `PoisonError` from `std` means another thread panicked mid-update; propagate it or recover the data deliberately with `into_inner()`, never `unwrap()` it away.
|
|
122
113
|
|
|
123
114
|
### Common deadlock — async + sync mutex
|
|
124
115
|
|
|
125
116
|
```rust
|
|
126
|
-
let m =
|
|
127
|
-
let guard = m.lock()
|
|
128
|
-
something_async().await; //
|
|
117
|
+
let m = parking_lot::Mutex::new(0u64);
|
|
118
|
+
let mut guard = m.lock();
|
|
119
|
+
something_async().await; // WRONG: guard is held across await
|
|
129
120
|
*guard += 1;
|
|
130
121
|
```
|
|
131
122
|
|
|
@@ -135,7 +126,7 @@ Fix:
|
|
|
135
126
|
|
|
136
127
|
```rust
|
|
137
128
|
{
|
|
138
|
-
let mut guard = m.lock()
|
|
129
|
+
let mut guard = m.lock();
|
|
139
130
|
*guard += 1;
|
|
140
131
|
} // guard released
|
|
141
132
|
something_async().await;
|
|
@@ -172,12 +163,12 @@ tx.send(new_config)?;
|
|
|
172
163
|
// Consumer:
|
|
173
164
|
loop {
|
|
174
165
|
rx.changed().await?;
|
|
175
|
-
let cfg = rx.
|
|
176
|
-
apply(&cfg);
|
|
166
|
+
let cfg = rx.borrow_and_update().clone(); // release the read lock before any .await
|
|
167
|
+
apply(&cfg).await;
|
|
177
168
|
}
|
|
178
169
|
```
|
|
179
170
|
|
|
180
|
-
Receivers see only the latest value (older updates are dropped). Perfect for config reload, leadership changes, "current time" propagation.
|
|
171
|
+
Receivers see only the latest value (older updates are dropped). Perfect for config reload, leadership changes, "current time" propagation. The `borrow()` guard blocks the sender while held; never keep it across an `.await`.
|
|
181
172
|
|
|
182
173
|
### Broadcast — fanout queue
|
|
183
174
|
|
|
@@ -209,10 +200,11 @@ Bound concurrent operations:
|
|
|
209
200
|
|
|
210
201
|
```rust
|
|
211
202
|
let sem = Arc::new(tokio::sync::Semaphore::new(10));
|
|
203
|
+
let mut set = tokio::task::JoinSet::new();
|
|
212
204
|
|
|
213
205
|
for task in tasks {
|
|
214
|
-
let permit =
|
|
215
|
-
|
|
206
|
+
let permit = Arc::clone(&sem).acquire_owned().await?;
|
|
207
|
+
set.spawn(async move {
|
|
216
208
|
let _hold = permit; // released when task exits
|
|
217
209
|
process(task).await
|
|
218
210
|
});
|
|
@@ -232,13 +224,14 @@ A semaphore with `permits=1` is a mutex. Use the actual `Mutex` for that — cle
|
|
|
232
224
|
|
|
233
225
|
```rust
|
|
234
226
|
let shared = Arc::new(BigData::new());
|
|
227
|
+
let mut set = tokio::task::JoinSet::new();
|
|
235
228
|
for _ in 0..workers {
|
|
236
|
-
let s =
|
|
237
|
-
|
|
229
|
+
let s = Arc::clone(&shared);
|
|
230
|
+
set.spawn(async move { use_data(&s).await });
|
|
238
231
|
}
|
|
239
232
|
```
|
|
240
233
|
|
|
241
|
-
`Arc::clone(&s)` is just a reference-count increment; the data is not copied.
|
|
234
|
+
`Arc::clone(&s)` is just a reference-count increment; the data is not copied. Write it as `Arc::clone(&x)`, never `x.clone()` (`clone_on_ref_ptr`), so a deep clone never hides behind the same spelling.
|
|
242
235
|
|
|
243
236
|
**Do not clone in hot loops** if you can pass a reference. `&Arc<T>` is fine to pass; only call `Arc::clone` when you need to move ownership across a thread/task boundary.
|
|
244
237
|
|
|
@@ -247,27 +240,25 @@ for _ in 0..workers {
|
|
|
247
240
|
## Once-init primitives
|
|
248
241
|
|
|
249
242
|
```rust
|
|
250
|
-
use std::sync::
|
|
251
|
-
|
|
252
|
-
// Lazy initialization, computed on first read
|
|
253
|
-
static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load_from_env().unwrap());
|
|
243
|
+
use std::sync::LazyLock;
|
|
254
244
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
static DB: OnceLock<sqlx::PgPool> = OnceLock::new();
|
|
245
|
+
// Lazy initialization of an infallible value, computed on first read
|
|
246
|
+
static WORD_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
|
|
247
|
+
#[expect(clippy::expect_used, reason = "literal pattern; covered by a unit test")]
|
|
248
|
+
regex::Regex::new(r"\w+").expect("valid regex literal")
|
|
249
|
+
});
|
|
261
250
|
|
|
262
251
|
#[tokio::main]
|
|
263
|
-
async fn main() {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
252
|
+
async fn main() -> anyhow::Result<()> {
|
|
253
|
+
// Fallible setup happens in main, where `?` reports it; pass the result down
|
|
254
|
+
// as a parameter instead of reaching for a global.
|
|
255
|
+
let config = Config::load_from_env()?;
|
|
256
|
+
let pool = sqlx::PgPool::connect(&config.database_url).await?;
|
|
257
|
+
run(&config, &pool).await
|
|
267
258
|
}
|
|
268
259
|
```
|
|
269
260
|
|
|
270
|
-
`
|
|
261
|
+
`LazyLock` runs its closure on first access, where no caller can handle an error, so it holds only values that cannot fail at runtime. `OnceLock` and `LazyLock` are in `std::sync` (1.70 / 1.80); avoid `once_cell` and `lazy_static!` for new code.
|
|
271
262
|
|
|
272
263
|
## Loom — model-checking lock-free code
|
|
273
264
|
|
|
@@ -306,8 +297,8 @@ mod loom_tests {
|
|
|
306
297
|
fn concurrent_push_pop_preserves_order() {
|
|
307
298
|
loom::model(|| {
|
|
308
299
|
let queue = Arc::new(MyQueue::new());
|
|
309
|
-
let q1 =
|
|
310
|
-
let q2 =
|
|
300
|
+
let q1 = Arc::clone(&queue);
|
|
301
|
+
let q2 = Arc::clone(&queue);
|
|
311
302
|
let h1 = thread::spawn(move || q1.push(1));
|
|
312
303
|
let h2 = thread::spawn(move || q2.pop());
|
|
313
304
|
h1.join().unwrap();
|
|
@@ -324,7 +315,7 @@ Run:
|
|
|
324
315
|
RUSTFLAGS="--cfg loom" cargo test --release -- --test-threads 1
|
|
325
316
|
```
|
|
326
317
|
|
|
327
|
-
Loom explores
|
|
318
|
+
Loom systematically explores the thread interleavings its model allows (bounded by `LOOM_MAX_PREEMPTIONS`), including those a real scheduler would rarely produce, and replays a failing schedule deterministically. It is strong evidence within that bound, not a proof over unbounded executions.
|
|
328
319
|
|
|
329
320
|
### Loom's limits
|
|
330
321
|
|
|
@@ -333,6 +324,29 @@ Loom explores every legal scheduling of the threads, including those a real sche
|
|
|
333
324
|
- Doesn't catch UB inside `unsafe` blocks the way miri does. **Run both: miri for memory safety, loom for thread schedules.**
|
|
334
325
|
- Doesn't handle `tokio` directly. Loom replaces stdlib's sync primitives; tokio's are independent.
|
|
335
326
|
|
|
327
|
+
## Scoped threads and per-thread state
|
|
328
|
+
|
|
329
|
+
For a fixed number of short-lived threads that borrow local data, `std::thread::scope` joins every thread before it returns, so no `Arc` or `'static` bound is needed:
|
|
330
|
+
|
|
331
|
+
```rust
|
|
332
|
+
let (left, right) = data.split_at(data.len() / 2);
|
|
333
|
+
let (a, b) = std::thread::scope(|s| {
|
|
334
|
+
let a = s.spawn(|| checksum(left));
|
|
335
|
+
let b = s.spawn(|| checksum(right));
|
|
336
|
+
(a.join(), b.join())
|
|
337
|
+
});
|
|
338
|
+
// a and b are Result<_, panic payload>: a panicked thread surfaces here
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
For data-parallel work over a collection, `rayon`'s `par_iter()` is simpler. Per-thread scratch state (a reusable buffer, a per-thread RNG) is `thread_local!` with `Cell` / `RefCell`, never a `static mut`:
|
|
342
|
+
|
|
343
|
+
```rust
|
|
344
|
+
thread_local! {
|
|
345
|
+
static SCRATCH: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new()) };
|
|
346
|
+
}
|
|
347
|
+
SCRATCH.with_borrow_mut(|buf| { buf.clear(); encode_into(buf, msg) });
|
|
348
|
+
```
|
|
349
|
+
|
|
336
350
|
## Send and Sync — what they mean
|
|
337
351
|
|
|
338
352
|
- `T: Send` — `T` can be moved between threads safely.
|
|
@@ -4,10 +4,10 @@ The opinionated, audited-in-prod stack for 2026 Rust. Every entry has a one-line
|
|
|
4
4
|
|
|
5
5
|
## Async runtime — `tokio`
|
|
6
6
|
|
|
7
|
-
The default. Use `tokio` for new work. Multi-thread runtime unless you have a measured reason
|
|
7
|
+
The default. Use `tokio` for new work. Multi-thread runtime with the default worker count unless you have a measured reason otherwise ([async-tokio.md](async-tokio.md)).
|
|
8
8
|
|
|
9
9
|
```rust
|
|
10
|
-
#[tokio::main
|
|
10
|
+
#[tokio::main]
|
|
11
11
|
async fn main() -> anyhow::Result<()> {
|
|
12
12
|
tracing_subscriber::fmt::init();
|
|
13
13
|
run().await
|
|
@@ -49,7 +49,7 @@ pub enum ParseError {
|
|
|
49
49
|
}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`#[non_exhaustive]` on enums prevents downstream `match` from breaking when you add variants. `#[error(transparent)]` on a wrapper variant forwards Display + cause to the inner error.
|
|
52
|
+
`#[non_exhaustive]` on enums prevents downstream `match` from breaking when you add variants. `#[error(transparent)]` on a wrapper variant forwards Display + cause to the inner error. Keep the cause chain: a variant that wraps another error marks it `#[from]` or `#[source]` so `anyhow`'s `{error:#}` and reporters print every layer. Error messages are lowercase with no trailing period, because they get composed into `context: cause` chains.
|
|
53
53
|
|
|
54
54
|
## CLI — `clap` with derive
|
|
55
55
|
|
|
@@ -108,7 +108,7 @@ fn init_tracing() {
|
|
|
108
108
|
.init();
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
#[instrument(
|
|
111
|
+
#[instrument(skip_all, fields(user_id = %user.id))]
|
|
112
112
|
async fn process_user(db: &Pool, user: &User) -> anyhow::Result<()> {
|
|
113
113
|
info!("processing user");
|
|
114
114
|
if user.is_banned() {
|
|
@@ -122,6 +122,8 @@ async fn process_user(db: &Pool, user: &User) -> anyhow::Result<()> {
|
|
|
122
122
|
|
|
123
123
|
Replace `println!` with `info!`/`warn!`/`error!`. Replace `eprintln!` with `tracing::error!`.
|
|
124
124
|
|
|
125
|
+
`#[instrument]` records every argument it does not skip with `Debug`, so a `&User` argument lands in the span whole, PII included. Default to `skip_all` and whitelist fields explicitly, as above; secrets live in `secrecy::SecretString` so a stray `?value` prints `[REDACTED]`. A **library** emits `tracing` events and spans but never installs a subscriber (`tracing_subscriber::…::init()`): only the binary chooses the output format and filter.
|
|
126
|
+
|
|
125
127
|
## Error reporting (binaries) — `color-eyre`
|
|
126
128
|
|
|
127
129
|
For binary `main()`, hook `color-eyre` to give pretty panics + nice `Result` printing:
|
|
@@ -141,19 +143,29 @@ Library code stays on `anyhow`/`thiserror`. `color-eyre` is purely a display lay
|
|
|
141
143
|
The default for any data crossing a process boundary (file, network, IPC, database column).
|
|
142
144
|
|
|
143
145
|
```rust
|
|
144
|
-
|
|
146
|
+
// Input we own the schema of (config, our own API): reject typos.
|
|
147
|
+
#[derive(Debug, serde::Deserialize)]
|
|
145
148
|
#[serde(deny_unknown_fields, rename_all = "snake_case")]
|
|
146
|
-
pub struct
|
|
149
|
+
pub struct CreateUser {
|
|
150
|
+
pub email: Email, // validated newtype, see type-state.md
|
|
151
|
+
#[serde(default)]
|
|
152
|
+
pub display_name: Option<String>,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Payload a newer peer may extend: keep unknown fields instead of rejecting them.
|
|
156
|
+
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
|
157
|
+
#[serde(rename_all = "snake_case")]
|
|
158
|
+
pub struct Event {
|
|
147
159
|
pub user_id: UserId,
|
|
148
160
|
pub created_at: jiff::Timestamp,
|
|
149
|
-
#[serde(
|
|
161
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
150
162
|
pub display_name: Option<String>,
|
|
151
163
|
#[serde(flatten)]
|
|
152
164
|
pub extra: HashMap<String, serde_json::Value>,
|
|
153
165
|
}
|
|
154
166
|
```
|
|
155
167
|
|
|
156
|
-
`deny_unknown_fields`
|
|
168
|
+
Choose per type: `deny_unknown_fields` for input whose schema you own (a typoed config key fails loudly), `#[serde(flatten)]` catch-all for payloads a newer peer may extend. The two do not combine: serde does not support `deny_unknown_fields` on a struct with a `flatten` field. `rename_all` matches the wire schema you are given, not a house style. Enums get an explicit representation (`#[serde(tag = "type")]`, or `tag` + `content`); `#[serde(untagged)]` tries variants in order and reports only "did not match any variant", so reserve it for shapes that are unambiguous by construction. Validated newtypes deserialize through their checked constructor with `#[serde(try_from = "...")]` ([type-state.md](type-state.md)). For a field whose wire format differs from its Rust type (a duration as seconds, a timestamp as a string), use `#[serde(with = "module")]` or the type's own serde feature instead of a hand-written parallel struct.
|
|
157
169
|
|
|
158
170
|
Alternatives:
|
|
159
171
|
- `serde_yaml` (YAML — note: YAML's "deserialize anything" surface is a security trap; prefer JSON/TOML where possible)
|
|
@@ -306,7 +318,7 @@ type WorldPoint = Point2D<f32, WorldSpace>;
|
|
|
306
318
|
let cursor: ScreenPoint = Point2D::new(120.0, 240.0);
|
|
307
319
|
let player: WorldPoint = Point2D::new(3.5, 1.2);
|
|
308
320
|
|
|
309
|
-
// let mistake = cursor + player; //
|
|
321
|
+
// let mistake = cursor + player; // compile error: different coordinate spaces
|
|
310
322
|
```
|
|
311
323
|
|
|
312
324
|
Generalize the pattern to your own domains (see `references/type-state.md`).
|
|
@@ -350,11 +362,12 @@ fn serializes_well() {
|
|
|
350
362
|
Stable Rust friendly (no nightly `#[bench]`).
|
|
351
363
|
|
|
352
364
|
```rust
|
|
353
|
-
use criterion::{
|
|
365
|
+
use criterion::{criterion_group, criterion_main, Criterion};
|
|
366
|
+
use std::hint::black_box;
|
|
354
367
|
|
|
355
368
|
fn bench_parse(c: &mut Criterion) {
|
|
356
|
-
let input =
|
|
357
|
-
c.bench_function("parse_large", |b| b.iter(|| parse(black_box(
|
|
369
|
+
let input = include_str!("../samples/large.txt");
|
|
370
|
+
c.bench_function("parse_large", |b| b.iter(|| parse(black_box(input))));
|
|
358
371
|
}
|
|
359
372
|
|
|
360
373
|
criterion_group!(benches, bench_parse);
|
|
@@ -363,6 +376,8 @@ criterion_main!(benches);
|
|
|
363
376
|
|
|
364
377
|
Run with `cargo bench`. HTML reports under `target/criterion/`. Pair with `cargo bench -- --save-baseline main` then `--baseline main` for comparison.
|
|
365
378
|
|
|
379
|
+
**Profile first, then optimize.** A change made for speed ships with its measurement: the benchmark or profile (`cargo flamegraph`, `samply`) that located the hot spot, and the before/after numbers on a representative input. Source shape alone proves nothing: iterator chains, `#[inline]`, bounds checks, and `Iterator::chain` usually compile to the same code as the "optimized" rewrite. Attributes like `#[inline(always)]` and `#[cold]` go in only when a measurement shows they help, and the `std::hint::black_box` above keeps the compiler from deleting the work being timed.
|
|
380
|
+
|
|
366
381
|
## Concurrency model — `loom`
|
|
367
382
|
|
|
368
383
|
For lock-free or atomic-heavy code (channels, refcounts, hazard pointers). See `references/concurrency.md` for the full pattern.
|
|
@@ -378,7 +393,7 @@ let s: &str = bump.alloc_str("hello");
|
|
|
378
393
|
// All allocations freed at once when `bump` drops.
|
|
379
394
|
```
|
|
380
395
|
|
|
381
|
-
For parser nodes, AST construction, per-request scratch
|
|
396
|
+
For parser nodes, AST construction, per-request scratch: one bump pointer per allocation and one free for the whole arena. Measure the gain on your workload before claiming one.
|
|
382
397
|
|
|
383
398
|
## Web client (browser, WASM-bound) — `gloo` ecosystem
|
|
384
399
|
|
|
@@ -386,26 +401,21 @@ If targeting WASM browser, use `gloo-net` for fetch and `gloo-storage` for local
|
|
|
386
401
|
|
|
387
402
|
## Lazy statics — `std::sync::LazyLock` (since 1.80)
|
|
388
403
|
|
|
389
|
-
|
|
390
|
-
use std::sync::LazyLock;
|
|
391
|
-
static CONFIG: LazyLock<Config> = LazyLock::new(|| Config::load_from_env().unwrap());
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
Avoid `lazy_static!` (macro-heavy, predates std), `once_cell` (now in std as `LazyLock`/`OnceLock`).
|
|
404
|
+
`LazyLock` for values that cannot fail at runtime (a compiled literal regex, a static table); fallible setup such as loading config belongs in `main`, where `?` reports it. Pattern → [concurrency.md](concurrency.md#once-init-primitives). Avoid `lazy_static!` (macro-heavy, predates std), `once_cell` (now in std as `LazyLock`/`OnceLock`).
|
|
395
405
|
|
|
396
|
-
## Hash maps — `std::collections::HashMap
|
|
406
|
+
## Hash maps — `std::collections::HashMap`, a faster hasher only where measured
|
|
397
407
|
|
|
398
408
|
```rust
|
|
399
409
|
use std::collections::HashMap;
|
|
400
|
-
use ahash::RandomState;
|
|
401
410
|
|
|
402
|
-
|
|
411
|
+
// Keys come from trusted code and a profile shows hashing is hot:
|
|
412
|
+
type FastMap<K, V> = HashMap<K, V, foldhash::fast::RandomState>;
|
|
403
413
|
let mut counters: FastMap<String, u64> = FastMap::default();
|
|
404
414
|
```
|
|
405
415
|
|
|
406
|
-
|
|
416
|
+
std's default hasher (SipHash-1-3 with a random key) resists hash-flooding from attacker-chosen keys; keep it for any map fed by input. For trusted keys on a measured hot path, `foldhash` (the hasher `hashbrown` defaults to) or `rustc-hash` (`FxHashMap`, integer keys) are faster; `rustc-hash` is predictable, so never feed it untrusted keys. Update-or-insert goes through `map.entry(key).or_insert(..)` / `.and_modify(..)`, one lookup instead of `get` + `insert`.
|
|
407
417
|
|
|
408
|
-
For sorted iteration, use `BTreeMap`. For small keys with known small N, `Vec<(K, V)>` may beat both.
|
|
418
|
+
For sorted iteration, use `BTreeMap`; for insertion order that is part of the contract, `indexmap::IndexMap`. For small keys with known small N, `Vec<(K, V)>` may beat both.
|
|
409
419
|
|
|
410
420
|
## File I/O — `tokio::fs` (async) or `std::fs` (sync utility)
|
|
411
421
|
|
|
@@ -413,7 +423,7 @@ For sorted iteration, use `BTreeMap`. For small keys with known small N, `Vec<(K
|
|
|
413
423
|
let contents = tokio::fs::read_to_string("data.json").await?;
|
|
414
424
|
```
|
|
415
425
|
|
|
416
|
-
For large files: `tokio::fs::File` + `tokio::io::BufReader`. For random access, `memmap2` (with the unsafe-discipline wrappers).
|
|
426
|
+
For large files: `tokio::fs::File` + `tokio::io::BufReader`. For random access, `memmap2` (with the unsafe-discipline wrappers). Many small reads or writes (line by line, record by record) go through `BufReader` / `BufWriter`, sync or async. Call `flush()` on a `BufWriter` before dropping it: `Drop` flushes too but has to discard the error.
|
|
417
427
|
|
|
418
428
|
## Decision tree
|
|
419
429
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Macros
|
|
2
|
+
|
|
3
|
+
A macro is the last rung of the ladder: reach for one only when a function, a generic, or a trait cannot express the thing (variadic input, new syntax, generating items, compile-time code from a type's shape). Macros cost compile time, error-message quality, and IDE support.
|
|
4
|
+
|
|
5
|
+
## `macro_rules!`
|
|
6
|
+
|
|
7
|
+
```rust
|
|
8
|
+
/// Builds a `HashMap` from `key => value` pairs.
|
|
9
|
+
#[macro_export]
|
|
10
|
+
macro_rules! hashmap {
|
|
11
|
+
($($key:expr => $value:expr),* $(,)?) => {{
|
|
12
|
+
let mut map = $crate::__private::HashMap::new();
|
|
13
|
+
$( map.insert($key, $value); )*
|
|
14
|
+
map
|
|
15
|
+
}};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#[doc(hidden)]
|
|
19
|
+
pub mod __private {
|
|
20
|
+
pub use std::collections::HashMap;
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **`$crate::` for every item of the defining crate.** Callers may rename your crate or lack the import; `$crate` always resolves to the crate that defined the macro.
|
|
25
|
+
- **Narrowest fragment specifier.** `expr`, `ty`, `ident`, `path`, `pat`, `literal` give callers real error messages and parse precedence correctly; `tt` accepts anything and defers the error to a confusing expansion.
|
|
26
|
+
- **Helpers the expansion needs** live in a `#[doc(hidden)] pub mod __private` and are reached through `$crate::__private::...`; they are not API.
|
|
27
|
+
- **`#[macro_export]` puts the macro at the crate root** (`my_crate::hashmap!`). Import it by path (`use my_crate::hashmap;`), not `#[macro_use] extern crate`.
|
|
28
|
+
- Hygiene covers local variables, not items or method calls: a name the expansion uses unqualified resolves at the call site. Qualify everything.
|
|
29
|
+
|
|
30
|
+
## Procedural macros
|
|
31
|
+
|
|
32
|
+
A proc macro lives in its own crate (`[lib] proc-macro = true`), which can export nothing else. Ship it as `my-crate-derive` and re-export it from `my-crate` so users depend on one crate and the macro's generated code can refer to `::my_crate::...`.
|
|
33
|
+
|
|
34
|
+
```rust
|
|
35
|
+
use proc_macro::TokenStream;
|
|
36
|
+
use quote::{format_ident, quote};
|
|
37
|
+
use syn::{parse_macro_input, Data, DeriveInput};
|
|
38
|
+
|
|
39
|
+
#[proc_macro_derive(Builder)]
|
|
40
|
+
pub fn derive_builder(input: TokenStream) -> TokenStream {
|
|
41
|
+
let input = parse_macro_input!(input as DeriveInput);
|
|
42
|
+
expand(&input).unwrap_or_else(syn::Error::into_compile_error).into()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn expand(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
|
|
46
|
+
let Data::Struct(_) = &input.data else {
|
|
47
|
+
return Err(syn::Error::new_spanned(&input.ident, "Builder supports structs only"));
|
|
48
|
+
};
|
|
49
|
+
let name = &input.ident;
|
|
50
|
+
let builder = format_ident!("{}Builder", name); // quote! cannot paste identifiers
|
|
51
|
+
Ok(quote! {
|
|
52
|
+
impl #name {
|
|
53
|
+
pub fn builder() -> #builder { #builder::default() }
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Parse with `syn`, generate with `quote`, work in `proc_macro2` types so the logic is unit-testable outside the compiler.
|
|
60
|
+
- **Never panic.** Report problems as `syn::Error::new_spanned(the_offending_tokens, "...")` and return `into_compile_error()`: the compiler underlines the exact input the user must change. A panic produces one message at the derive site with no location.
|
|
61
|
+
- Build new identifiers with `format_ident!`; `quote!` has no token pasting (`#name##Builder` is not valid).
|
|
62
|
+
- Generated code uses fully qualified paths (`::core::option::Option`, `::my_crate::Trait`) so it compiles regardless of what the caller imported or shadowed.
|
|
63
|
+
- Test expansions with `trybuild` (pass and compile-fail cases, including the error message spans) rather than by asserting on generated token text.
|
|
@@ -124,9 +124,9 @@ rust-script --build-only --base-path . ./script.rs
|
|
|
124
124
|
|
|
125
125
|
This drops a `target/` next to the script with the prebuilt binary.
|
|
126
126
|
|
|
127
|
-
## `cargo-script` (RFC 3424,
|
|
127
|
+
## `cargo-script` (RFC 3424, nightly-only)
|
|
128
128
|
|
|
129
|
-
The official replacement
|
|
129
|
+
The official replacement being built into cargo. Same idea, slightly different syntax:
|
|
130
130
|
|
|
131
131
|
```rust
|
|
132
132
|
#!/usr/bin/env -S cargo +nightly -Zscript
|
|
@@ -140,6 +140,8 @@ dependencies:
|
|
|
140
140
|
reqwest = { version = "0.12", features = ["blocking"] }
|
|
141
141
|
---
|
|
142
142
|
|
|
143
|
+
use anyhow::Context as _;
|
|
144
|
+
|
|
143
145
|
fn main() -> anyhow::Result<()> {
|
|
144
146
|
let url = std::env::args().nth(1).context("url required")?;
|
|
145
147
|
println!("{}", reqwest::blocking::get(&url)?.text()?.len());
|
|
@@ -147,7 +149,7 @@ fn main() -> anyhow::Result<()> {
|
|
|
147
149
|
}
|
|
148
150
|
```
|
|
149
151
|
|
|
150
|
-
|
|
152
|
+
Stable cargo (1.97) still rejects `-Zscript` and refuses to run a `.rs` file directly. Use `rust-script` now; migrate once `cargo script` reaches the stable channel of every toolchain your scripts run on.
|
|
151
153
|
|
|
152
154
|
## Strict mode for scripts
|
|
153
155
|
|
|
@@ -15,6 +15,14 @@ Two test types every Rust project should have alongside unit tests. Proptest hun
|
|
|
15
15
|
|
|
16
16
|
Use all three. They cover different bug classes.
|
|
17
17
|
|
|
18
|
+
## Where tests live
|
|
19
|
+
|
|
20
|
+
- **Unit tests of private behavior**: `#[cfg(test)] mod tests { use super::*; ... }` at the bottom of the module they test.
|
|
21
|
+
- **Public-API behavior**: `tests/*.rs`. Each file is a separate crate that sees only `pub` items, so it fails when the public contract breaks, not when an internal changes.
|
|
22
|
+
- **Examples in rustdoc**: doctests. They compile and run, so a documented example that drifts from the API fails the build. `cargo nextest run` skips them; the gate runs `cargo test --doc` too. Write examples with `?` and hide setup behind `# ` lines ([api-design.md](api-design.md#rustdoc-sections)).
|
|
23
|
+
- **Panics**: `#[should_panic(expected = "...")]` only when panicking is the documented contract (an out-of-bounds index on a slice-like type). An invalid input that returns `Err` is asserted as that `Err` value.
|
|
24
|
+
- **Environment and globals**: `std::env::set_var` is `unsafe` in the 2024 edition because another thread may read the environment at the same moment, and tests run in parallel threads. Inject configuration as a parameter instead of mutating process state; a test that must touch the real environment runs in its own process (`tests/` binary, or nextest's process-per-test).
|
|
25
|
+
|
|
18
26
|
## Proptest — setup
|
|
19
27
|
|
|
20
28
|
`Cargo.toml`:
|
|
@@ -92,9 +92,8 @@ Now:
|
|
|
92
92
|
```rust
|
|
93
93
|
let distance: Quantity<Meters> = Quantity::new(100.0);
|
|
94
94
|
let height: Quantity<Feet> = Quantity::new(50.0);
|
|
95
|
-
let combined = distance + height;
|
|
96
|
-
let combined = distance +
|
|
97
|
-
let combined = distance + distance; // ✅
|
|
95
|
+
let combined = distance + height; // compile error: Quantity<Meters> + Quantity<Feet>
|
|
96
|
+
let combined = distance + distance; // compiles
|
|
98
97
|
```
|
|
99
98
|
|
|
100
99
|
The agent cannot accidentally mix units. Refactors that change a quantity's underlying unit are caught at compile time everywhere the type flows.
|
|
@@ -109,12 +108,13 @@ pub struct ByteOffset(pub u32);
|
|
|
109
108
|
pub struct CharOffset(pub u32);
|
|
110
109
|
|
|
111
110
|
impl ByteOffset {
|
|
112
|
-
pub fn
|
|
111
|
+
pub fn checked_add(self, delta: u32) -> Option<Self> { self.0.checked_add(delta).map(Self) }
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
// Converting between them is a function on the actual text.
|
|
116
115
|
pub fn byte_to_char(text: &str, byte: ByteOffset) -> Option<CharOffset> {
|
|
117
|
-
text.get(..byte.0
|
|
116
|
+
let prefix = text.get(..usize::try_from(byte.0).ok()?)?;
|
|
117
|
+
u32::try_from(prefix.chars().count()).ok().map(CharOffset)
|
|
118
118
|
}
|
|
119
119
|
```
|
|
120
120
|
|
|
@@ -193,6 +193,41 @@ impl ProjectRel {
|
|
|
193
193
|
|
|
194
194
|
The agent's path-handling code now distinguishes between project-relative and home-relative paths at the type level. A function taking `ProjectRel` cannot be called with a `HomeRel`.
|
|
195
195
|
|
|
196
|
+
### Validated Newtypes — Parse Once, Every Path
|
|
197
|
+
|
|
198
|
+
A newtype that carries an invariant has exactly one way in: a fallible constructor over a private field. Every other entry point (`FromStr`, serde, `TryFrom`) routes through it, so no path can build an unchecked value.
|
|
199
|
+
|
|
200
|
+
```rust
|
|
201
|
+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
202
|
+
#[serde(try_from = "String", into = "String")]
|
|
203
|
+
pub struct Email(String); // private field: no `Email(raw)` from outside
|
|
204
|
+
|
|
205
|
+
#[derive(Debug, thiserror::Error)]
|
|
206
|
+
#[error("not an email address: {0:?}")]
|
|
207
|
+
pub struct InvalidEmail(String);
|
|
208
|
+
|
|
209
|
+
impl TryFrom<String> for Email {
|
|
210
|
+
type Error = InvalidEmail;
|
|
211
|
+
fn try_from(raw: String) -> Result<Self, Self::Error> {
|
|
212
|
+
match raw.split_once('@') {
|
|
213
|
+
Some((user, host)) if !user.is_empty() && host.contains('.') => Ok(Self(raw)),
|
|
214
|
+
_ => Err(InvalidEmail(raw)),
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
impl std::str::FromStr for Email {
|
|
220
|
+
type Err = InvalidEmail;
|
|
221
|
+
fn from_str(raw: &str) -> Result<Self, Self::Err> { Self::try_from(raw.to_owned()) }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
impl From<Email> for String {
|
|
225
|
+
fn from(email: Email) -> Self { email.0 }
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`#[serde(try_from = "String")]` makes deserialization run the same check, so JSON cannot smuggle in an invalid `Email`. Never add `impl From<String> for Email`: an infallible conversion into an invariant-bearing type is a bypass.
|
|
230
|
+
|
|
196
231
|
## Type-State State Machines
|
|
197
232
|
|
|
198
233
|
Encode the lifecycle of a value as a sequence of types. Each transition consumes the previous state and returns the next.
|
|
@@ -313,6 +348,7 @@ Downstream code cannot add new `impl Renderer for Whatever` because they cannot
|
|
|
313
348
|
## NonEmpty Collections
|
|
314
349
|
|
|
315
350
|
```rust
|
|
351
|
+
#[derive(Debug)]
|
|
316
352
|
pub struct NonEmptyVec<T> {
|
|
317
353
|
head: T,
|
|
318
354
|
tail: Vec<T>,
|
|
@@ -322,16 +358,16 @@ pub struct NonEmptyVec<T> {
|
|
|
322
358
|
#[error("vector was empty")]
|
|
323
359
|
pub struct Empty;
|
|
324
360
|
|
|
361
|
+
#[expect(clippy::len_without_is_empty, reason = "never empty by construction")]
|
|
325
362
|
impl<T> NonEmptyVec<T> {
|
|
326
|
-
pub fn try_from_vec(
|
|
327
|
-
|
|
328
|
-
let
|
|
329
|
-
|
|
330
|
-
Ok(Self { head, tail })
|
|
363
|
+
pub fn try_from_vec(v: Vec<T>) -> Result<Self, Empty> {
|
|
364
|
+
let mut items = v.into_iter();
|
|
365
|
+
let Some(head) = items.next() else { return Err(Empty) };
|
|
366
|
+
Ok(Self { head, tail: items.collect() })
|
|
331
367
|
}
|
|
332
368
|
|
|
333
|
-
pub fn first(&self) -> &T { &self.head }
|
|
334
|
-
pub fn len(&self) -> usize { self.tail.len()
|
|
369
|
+
pub const fn first(&self) -> &T { &self.head }
|
|
370
|
+
pub fn len(&self) -> usize { self.tail.len().saturating_add(1) }
|
|
335
371
|
}
|
|
336
372
|
```
|
|
337
373
|
|
|
@@ -343,6 +379,8 @@ Functions taking `NonEmptyVec<T>` cannot receive an empty vector. The `first()`
|
|
|
343
379
|
- When the wrapper does not change behavior or invariants vs. the underlying type (e.g., a `struct Count(u32)` that is only ever used in one struct).
|
|
344
380
|
- When `From`/`Into` conversions would be ergonomic but would defeat the purpose (if you find yourself wanting `impl From<UserId> for Uuid`, you do not want a newtype - you want a type alias).
|
|
345
381
|
|
|
382
|
+
A domain newtype never implements `Deref` to its inner type: that re-exposes every method the wrapper exists to fence off, and method resolution silently picks the inner one. `Deref` is for pointer-like wrappers (`Box`, guards, smart handles); a newtype exposes the operations it means to allow.
|
|
383
|
+
|
|
346
384
|
The cost of a newtype is one tuple struct + the impls you need. The break-even is around three uses across different functions, or any use that crosses an API boundary.
|
|
347
385
|
|
|
348
386
|
## When NOT to Use Type-State
|