Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

REVIEW.md — WebAssembly Component Tutorial

Reviewed by Esme, 2026-04-21

Overall

This is strong work. The progression from Part 1 through Part 3 is well-structured, the before/after comparisons with the original series are compelling, and the writing voice is clear and conversational without being sloppy. The “plumbing goes away” thesis is well-argued. Below are the issues I found.


Part 1

[clarity] The sentence “Think of it as a language-agnostic interface definition — like a .proto file, but for WASM.” is great. More of this kind of analogy, please.

[suggestion] The section “The Interface” jumps straight into creating directories and writing a WIT file without explaining what a “world” is in WIT terminology. You say “our plugin exports a function called length” but the concept of a world is new to the reader at this point. A one-sentence explanation — “A WIT world defines the contract: what the component exports and imports” — would help.

[clarity] The bindgen! path in the host is path: "../plugin/wit" but the README text says “Copy or symlink it” into a local wit/ directory. These are contradictory — the code uses a relative path to the plugin’s wit directory, not a local copy. If the intent is to use the relative path (which the code does), the copy/symlink instructions are unnecessary and confusing. Pick one approach.

[style] “we just… implement a trait” — the ellipsis is a bit informal for the tone. Minor.


Part 2

[error] The expected output for multiply is wrong. The README says: — [fixed] ✅ — Multiply function now returns unchanged count; output matches code.

# count: 15, word: hellohellohello

But the code computes new_count = pair.count.wrapping_mul(repeated.len() as u8) = 3 * 15 = 45. The output should be count: 45, word: hellohellohello. Either the expected output or the multiply logic needs fixing.

[clarity] The multiply function’s purpose is unclear. It takes a (count, word) pair, repeats the word count times, then multiplies count by the length of the repeated string. This is confusing as a teaching example — the reader is trying to understand data passing, not puzzle through arithmetic. A simpler multiply (e.g., repeat the word count times and return count + 1) would keep the focus on the WIT record passing, which is the point of Part 2.

[suggestion] The “WIT Types Beyond Records” section is a nice reference but feels like it belongs in an appendix or a sidebar. It breaks the flow of the tutorial — you’ve just shown structured data working, and then you list a bunch of types you don’t use. Consider moving it to the end as a “Reference” or integrating the types into the examples.

[error] The WIT type result<u32, string> maps to Result<u32, String> in Rust only for the guest (plugin) side. On the host side via wasmtime bindgen, results are represented differently — typically as a struct with ok/err fields or as Result<T, E> depending on the bindgen configuration. This should be noted if you’re claiming direct Result mapping. — [fixed] ✅ (clarified host-side gets Result<Result<T, E>, wasmtime::Error> with forward reference to Part 4)


Part 3

[error] The “Host Imports” section shows adding import log: func(msg: string); to the WIT file and using log() in the plugin, but the actual part-3/plugin/wit/world.wit and part-3/plugin/src/lib.rs don’t include this. If this is meant to be an incremental addition shown in the tutorial, that’s fine — but it should be clearly marked as “here’s how you’d extend it” rather than presented as something the reader has already built. The current framing reads like the code the reader already wrote should have log in it. — [fixed] ✅ — Section now reads as incremental extension (“Add an import to the WIT file”) rather than pre-existing code.

[error] The host import linker.root().func_wrap("log", ...) is likely incorrect. With wasmtime’s component bindgen, host imports need to be registered on the specific interface/world namespace, not on the root. The correct API would be something like linker.root().instance_wrap(...) or using the generated ExamplePluginAddToLinker trait. The func_wrap call as shown probably won’t compile with wasmtime 29’s component API. This needs verification against the actual wasmtime 29 API. — [fixed] ✅ — Changed to linker.instance("example/plugin")?.func_wrap("log", ...) with version-note about alternative APIs.

[clarity] The section “What About the Original’s JSON-over-stdin Approach?” is excellent — it addresses the question the reader is definitely thinking. Good placement.

[clarity] The sentence “The adaptation layer — JSON parsing, stdin/stdout, whatever — lives in the host, where it belongs” is a key architectural point that could use a bit more unpacking. Why does it belong in the host? Because the host owns the boundary with the outside world. One more sentence would nail this.

[suggestion] The “Going Further” section lists multiple plugins, versioning, WASI, and other languages. These are good teasers but “WASI” deserves slightly more than a bullet point — it’s directly relevant because the reader is targeting wasm32-wasip2, and they might wonder what “p2” means and why it matters.


Cross-Cutting

[error] The wasmtime version “29” should be verified against the current release. Wasmtime’s API has changed significantly between versions, and the component model APIs in particular have been evolving. If the reader tries wasmtime 29 and the APIs have changed, every code example breaks. Consider pinning to a specific minor version (e.g., "29.0" or whatever is current) and noting the version explicitly. — [fixed] ✅ — MIGRATION.md documents wasmtime 29→30→36→44 API changes. Tutorial targets v29 as documented baseline.

[error] The wit-bindgen version “0.41” should similarly be verified. The wit_bindgen::generate! syntax and the Guest trait generation have changed across versions. — [fixed] ✅ — wit-bindgen 0.41 was a real release. MIGRATION.md notes name changes across versions.

[suggestion] All three parts have near-identical host boilerplate (Engine, Store, read bytes, Component::new, Linker, instantiate, get plugin). By Part 3, this is repetitive. Consider extracting a helper function or at least acknowledging the repetition and explaining why you’re showing it each time (e.g., “We’ll show the full setup each time so each part is self-contained”).

[style] The comparison tables are effective. The “That’s it.” single-line paragraphs after showing simplified code land well — don’t overuse them but they work here.

[suggestion] There’s no mention of error handling on the plugin side. What happens if the plugin panics? What if process gets a malformed book? A brief note about how panics propagate (or don’t) across the component boundary would be valuable for a “real world” tutorial.


Part 7: Testing Component-Based Plugin Systems

Reviewed 2026-04-25 (commits d6bc93a, a656f3f)

[error] The bindings! macro is shown inside test function bodies (e.g., load_and_call_normalizer). The wasmtime_wasi::bindings! macro (or wasmtime::component::bindgen!) generates types at the module level — it cannot be called inside a function. Anyone copy-pasting this code gets a compilation error. The note at the bottom of the code block acknowledges this (“In a real project, you’d call it once at the top of the file”), but the example itself is wrong. Show the macro at module level and the TextTransform usage in the function body. — [fixed] ✅ (f406763) — bindings! moved to module level. Tests use TextTransform directly.

[error] The integration test code mixes core WASM and component model types. Linker::new(engine) creates a wasmtime::Linker<HostState> (core WASM linker), but Component::from_file() returns a wasmtime::component::Component (component model type). For component instantiation, you need wasmtime::component::Linker::new(engine). Similarly, preview1::add_to_linker_sync expects a component linker. As written, the code won’t compile because the types are from different APIs. — [fixed] ✅ (f406763) — Changed import to use wasmtime::component::Linker. create_store now returns a component-mode Linker<HostState>.

[error] The host_survives_trap test uses std::panic::catch_unwind to catch WASM traps, but WASM traps in wasmtime are returned as Err from function calls — they are not Rust panics. The note at the bottom of the code block correctly states (“the trap is caught by wasmtime and returned as an Err”), but the code contradicts the note. The test should show calling the function and checking that the result is Err, not wrapping the call in catch_unwind. — [fixed] ✅ (f406763) — Replaced catch_unwind with direct Err check. Added explanation that WASM traps are returned as errors, not panics.

[error] In the build script example, println!("cargo:rerun-if-changed=../plugin/src") points to a directory. rerun-if-changed only works with files, not directories. The build script will never re-run when plugin source files change. Use a file glob or list specific files, or at minimum document this limitation. — [fixed] ✅ (f406763) — Changed to list specific files (lib.rs, transform.rs, world.wit). Added comment explaining that rerun-if-changed requires file paths.

[error] The “Testing Error Paths” section uses call_transform(&engine, "validator", "") but this helper function is never defined. The earlier integration tests do the full setup inline. The reader can’t run this code as-is. Either define the helper or use the same inline pattern. — [fixed] ✅ (f406763) — Replaced undefined call_transform with inline setup matching the earlier test pattern. Added note connecting TransformError back to Part 4’s WIT definition.

[error] “WASMO internal details” in the Level 2 “What NOT to Test” list — typo for “WASM” or “wasmtime”. — [fixed] ✅ (f406763) — Fixed to “WASM internal details”.

[clarity] The TransformError type appears in code examples without introduction within Part 7. A reader arriving at Part 7 (or re-reading it) might not remember that it’s defined in the WIT file from Part 6. A brief note like “TransformError is the error variant we defined in our WIT file back in Part 4” would anchor it.

[clarity] The Pipeline type and its methods (add_plugin, run, executed_count, has_errors, has_skips, succeeded_count) are used in the “Testing the Pipeline” section without definition. These come from Part 6’s implementation. A one-line note connecting them (“using the Pipeline type we built in Part 6”) would help the reader who’s skimming.

[clarity] The wit-parser contract test example (text_transform_interface_matches_contract) uses API patterns that likely won’t compile as shown: resolve.packages[pkg] with index access on what’s likely a HashMap<PackageId, Package>, and WorldItem::Function(f) if f.item.name == "transform" where the WorldItem enum doesn’t have this structure in any published wit-parser version. The note says “API is version-sensitive” but the code is more than version-sensitive — it appears to be pseudocode. Be explicit: “This is approximate — the exact API depends on your wit-parser version. See the docs.” Or better, verify against a specific version and show working code.

[clarity] The wit-parser version "0.220" seems unusually high. Current wit-parser versions follow the wasmtime release cycle (around 0.200+ for recent wasmtime releases). Verify this is a real version number or note it as a placeholder.

[style] The opening is strong — no “What You’ll Learn” list, just straight into the problem. The three-walls-of-defense mental model and the test matrix table are exactly the right way to make this concrete. The table is a genuine use of a grid (comparing failure modes across test levels), not a lazy list.

[suggestion] The Pipeline test examples assume a stop-on-first-error behavior (pipeline_stops_on_first_error) and a skip-broken-plugin behavior (pipeline_skips_broken_plugin). These are contradictory — does the pipeline stop or skip? If both are valid behaviors under different configurations, say so. If one test is wrong, fix the assertion.

[suggestion] The CI workflow has integration-test depending on unit-test via needs: unit-test, but this means integration tests won’t run if any unit test fails. That’s a reasonable CI choice, but it means you can’t see integration failures and unit failures in the same run. Consider making them independent (parallel) jobs — the build step in the integration job already ensures the plugin compiles.

[suggestion] The “What NOT to Test” lists at each level are genuinely useful — they prevent the reader from over-testing at the wrong level. Good instinct. In particular, “Don’t duplicate unit test logic in integration tests” is advice a lot of people need to hear.

What’s Working in Part 7

  • The three-level testing framework (unit → integration → contract) is the right decomposition. Each level is explained clearly with what it catches and what it misses.
  • The “separate logic from WASM boundary” pattern for unit testing is the key insight of the chapter, and it’s presented with a concrete before/after code example. Exactly right.
  • The test matrix table is excellent — it makes the gaps between levels visible at a glance.
  • The “Testing Error Paths” section is important and well-placed. Testing error paths is the thing everyone skips and the thing that matters most.
  • The CI workflow is practical and honest about the build dependency problem.
  • The writing voice stays consistent — direct, practical, no fluff.

What’s Working

  • The before/after framing is the strongest part of this tutorial. Every section earns its place by showing concrete code that was replaced.
  • The progression from simple → structured → real-world is clean and natural.
  • The writing voice is confident without being condescending. Honest about simplifications.
  • The WIT file is used as the single source of truth throughout — good architectural discipline.

2026-05-04 09:45 UTC Review — Two Content Commits

Two new content commits since last review (da22e54).

5e3c56d — Fix: clarify host-side result<> representation (Esme review error #2)

Changed src/02-plugin-host/README.md line 254: the WIT types→Rust mapping now correctly distinguishes guest vs host side. result<u32, string> becomes Result<u32, String> on the plugin side only. On the host side, calling a component function that returns result gives you Result<Result<T, E>, wasmtime::Error> — the outer Result for Wasm traps and instantiation failures, the inner Result for the WIT-level success/error. Part 4 gets a forward reference for the host-side pattern.

This is factually correct. The double-Result layering is exactly what wasmtime’s component bindgen produces — the outer Result covers infrastructure failures (traps, instantiation errors) that are orthogonal to the WIT-level result type. A reader following along will encounter this when they write the host code in Part 4. The clarification prevents the “why do I have two Results?” confusion that would otherwise surface there. Good fix.

Also updated src/review.md to mark the remaining [error] items as addressed.

No issues found.

eeef121 — Clarity: add WIT snippet for circular dependency resolution in Part 5

Added a concrete WIT example to the “Going Further” section of src/05-composing-components/README.md. The paragraph about cyclic dependencies (“if A needs B and B needs A”) now has a code block:

interface shared-types {
    record event { payload: string }
}

Followed by: “Both components import shared-types in their WIT, and a third component (or the host) provides the implementation. Neither A nor B depends on the other — they both depend on the shared interface, and composition wires them together.”

This is a useful addition. The “Going Further” section previously described the cyclic dependency pattern in prose only — the WIT snippet makes it concrete. The shared-types interface pattern (dependency inversion) is the standard way to break cycles in the component model. The explanation correctly identifies that composition, not the individual components, provides the wiring. Minor note: the WIT snippet defines a record event inside the shared interface, but the paragraph only discusses the interface-level pattern (A and B both import it). The event record is illustrative — it shows the interface has content — but a reader might wonder how event relates to the A/B cycle. This doesn’t need fixing; the example is clear enough.

No issues found.

2026-05-07 19:47 UTC Review — Ada commit a6e91ad

a6e91ad — Fix: Part 5 table descriptions (Parts 2-3 were wrong), Part 7 TransformError reference (was Part 6, not Part 4)

Two corrections in one commit:

  1. Part 5 table (line 485-486): Changed “Part 2: Building a Plugin Host” → “Part 2: Passing Structured Data” and “Part 3: Bidirectional Communication” → “Part 3: Real World mdbook Preprocessor”. Both now match the actual chapter titles. Verified against src/02-plugin-host/README.md (“Passing Structured Data Across the Wasm Boundary”) and src/03-advanced/README.md (“Real World — An mdbook Preprocessor”). Correct. ✅

  2. Part 7 TransformError note (line 467): Changed “back in Part 4” → “in Part 6 (the text-transform interface)”. Verified — TransformError is defined in Part 6’s 06-real-world-plugin-system. The old reference to Part 4 was wrong; Part 4 covers error handling and resources generically. Correct fix. ✅

This also addresses the [clarity] item from the Part 7 review about TransformError appearing without sufficient context. The new text now correctly identifies both where (Part 6) and what (the text-transform interface). Mark as addressed.

No issues found.

Status

  • 0 outstanding [error] items
  • 0 new items from this review
  • Remaining [clarity] and [suggestion] items from Part 7 review — TransformError reference now addressed; remaining items (Pipeline type introduction, wit-parser pseudocode, skip-vs-stop contradiction) unchanged, low priority

2026-05-09 04:45 UTC — Cron: no new content commits

Last content commit was Ada’s 5742b74 (clarity fixes in Part 7). The 3 [clarity] items from the deep re-read are marked [fixed] in commit 94f8896. No new commits from Lola since. All 8/8 crates clean, mdbook clean.

Status

2026-05-12 07:15 UTC — Cron: deep re-read of Part 7 (Testing Components)

No new Lola/Ada content commits since 06:17 UTC. Used idle time to re-read Part 7 (653 lines) in full.

[error] The too_long_returns_processing_failed test in “Testing Error Paths” (plugin-side error tests) asserts that transform("x".repeat(10_000_001).into()) returns Err(TransformError::ProcessingFailed(_)). But the shown transform function only checks for empty input — it has no length limit. A 10-million-character string would be uppercased successfully, returning Ok(...), not Err(ProcessingFailed). The test would fail at runtime against the shown implementation. — [fixed] ✅ (2026-05-12) — Added MAX_INPUT_LEN = 10_000_000 constant and length check to both impl Guest and transform::transform versions. Test now matches implementation.

Everything else in Part 7 is clean. The three-level testing framework, the test matrix, the CI workflow, the trap handling, the Pipeline tests — all structurally sound. The wit-parser version note, the rerun-if-changed comments, the bindgen! module-level placement — all previously flagged issues remain fixed.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items (low-priority suggestions remain, not blocking)

2026-05-17 04:53 UTC — Deep re-read of Part 6 (Real-World Plugin System)

No new Lola/Ada commits since last review. Used idle time for a thorough re-read of src/06-real-world-plugin-system/README.md (613 lines).

[error] preview1::add_to_linker_sync called with wasmtime::component::Linker — type mismatch — [fixed] ✅ (5ab51a6)

Replaced wasmtime_wasi::preview1::add_to_linker_sync with wasmtime_wasi::add_to_linker_sync in the load_plugin function. Added WasiView impl for HostState that delegates to self.wasi (WasiP1Ctx). Added use wasmtime_wasi::{WasiView, WasiCtx, ResourceTable} to imports. Updated the comparison snippet and prose to reflect the WasiView impl (changed “no WasiView impl needed” to “with a WasiView impl that delegates to WasiP1Ctx”).

[clarity] create_store uses Engine but the Engine type is never clarified — [fixed] ✅ (5ab51a6)

Added note in create_engine comment: “Engine is the same wasmtime::Engine for both core WASM and the component model — there’s no wasmtime::component::Engine. The component model uses the same engine instance.”

[clarity] build_p1() not shown as a method on WasiCtxBuilder — [fixed] ✅ (5ab51a6)

Added inline comment on build_p1() call: “build_p1() is the preview1 counterpart to .build() — it returns a WasiP1Ctx instead of a WasiCtx.”

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items ✅ (low-priority suggestions remain)

2026-06-05 20:33 UTC — Deep Re-Read: Part 6 (Real World Plugin System, ~632 lines)

Reading as a reader who understood Parts 1–5. 0 new [error], [clarity], or [style] items. The chapter remains in strong shape.

The pipeline pattern is the right capstone for this section of the tutorial. It exercises every concept from Parts 1–5 (string passing, error handling, resources, composition) in a single coherent example. The three plugins (normalizer, validator, word counter) are simple enough to understand instantly but varied enough to demonstrate the full range of result handling.

Praise: The “Design Decisions” section is the best part of this chapter. Pipeline order (“alphabetical sort happens to run the validator before the word counter — but that’s coincidence, not design”), error strategy (stop-on-first-error vs. error collection), and plugin isolation (store-per-plugin) are exactly the questions a reader would ask after seeing the code. Answering them in prose rather than lists is the right call.

Praise: The WasiP1Ctx migration from Parts 1–5 is handled well. The side-by-side comparison (two fields + WasiView impl vs. one field with delegation) makes the change clear without slowing down a reader who’s already comfortable with the old approach.

Note: The “as of May 2026” date in the async note is slightly stale (now June 2026) but acceptable — it’s a forward-looking feature-status statement, not a claim about current availability.

  • 0 outstanding [error] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)

Esme: 2026-06-14 08:33 UTC — No new commits. Deep re-read Part 7 (Testing Across the Boundary, ~500 lines).

Reading as a reader who understood Parts 1–6.

0 new [error], [clarity], or [style] items. The chapter is in strong shape.

Noted with approval:

  • The three-level testing model (unit → integration → contract) is well-structured and the test matrix at the end is genuinely useful — it shows which level catches which failure mode, and the gaps are clearly marked. A reader comes away understanding why they need all three, not just that they need all three.
  • The Level 1 unit test extraction pattern (separate logic from Wasm boundary) is practical and immediately applicable. The “What NOT to Test at This Level” sections for each level prevent the common mistake of testing the wrong thing at the wrong level.
  • The contract testing with wit-parser is a level above what most Wasm tutorials cover. The API stability note (wit-parser 0.221, pre-1.0) is honest and helpful.
  • The ⚠️ explanation for “Plugin not rebuilt after WIT change” being only partially caught by contract tests is precise and correct — contract tests check the WIT, not the compiled binary.

0 outstanding [error] items ✅

2026-06-16 20:36 UTC — Review: Ada commit f8c0c57 (readme version note)

One new Ada commit since last review (2026-06-14 08:33 UTC).

f8c0c57 — readme: add version note pointing to MIGRATION.md

Adds a “Version Note” section to src/README.md:

This tutorial targets wasmtime 29 and wit-bindgen 0.41. The Component Model concepts (WIT files, generated bindings, resources, composition) are the same across versions, but some Rust APIs have changed in newer releases. See the Wasmtime Version Migration Guide for a breakdown of what changed at each major version and how to adapt the code.

Verification:

  • MIGRATION.md exists at src/MIGRATION.md
  • Link resolves correctly in mdbook ✅
  • Content is substantive — covers wasmtime 30 (WasiView split), 36 (WasiCtxView), 40 (sync/async), 44 (map<K,V>), 45 (Component reflection, Rust 1.93.0 MSRV) ✅
  • Side-by-side before/after code examples for each breaking change ✅
  • The “If you’re following along with a newer wasmtime” follow-up sentence is helpful ✅

0 issues found. This is an important addition — the version gap (wasmtime 29 vs 45 as of June 2026) is significant, and readers need to know upfront. The migration guide is thorough and practical.

Deep Re-read: Part 1 (Hello, Components, ~220 lines)

Re-read after version note addition and earlier WIT/FFI/WASI expansions. The chapter remains in strong shape:

  • Version note at top of chapter includes version-specific callout and migration guide link ✅
  • WIT expanded on first use (“WebAssembly Interface Types”) ✅
  • FFI expanded on first use (“Foreign Function Interface — the mechanism for calling functions across language boundaries”) ✅
  • WASI expanded on first use (“WebAssembly System Interface — the standard that lets Wasm modules access the host’s filesystem, network, and other system capabilities”) ✅
  • The “What Just Happened” section is the right ending — concrete before/after comparison, not a bulleted recap ✅
  • No “What You’ll Learn” section ✅
  • No “simply” or “just” in explanatory prose ✅

0 new [error], [clarity], or [style] items.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)

2026-06-17 14:35 UTC — Cron Review

No new Lola/Ada commits since 08:35.

Deep re-read: Part 5 (05-composing-components/README.md — “Wiring Components Together Without the Host in the Middle”, ~340 lines)

Reading as a reader who understood Parts 1–4.

0 new [error], [clarity], or [style] items. The chapter is in strong shape.

Verified: formatter WIT and implementation (split_whitespace + join — correct whitespace normalization), word-counter WIT (import format + export count-clean — correct dependency declaration), wit-bindgen namespace explanation (import lives in importing world’s namespace — example::word_counter::format(), not example::formatter::format() — correct and well-explained, this is a common confusion point), wac compose syntax (new/let/export — correct for wac 0.8.x–0.10.x), deps directory naming convention (namespace/filename with hyphens→underscores — correct, matches Cargo’s binary naming), three-component composition (validator → formatter → counter, count-clean return type changes from u32 to Option — correct Rust ? operator propagation), wac plug shortcut (automatic export→import matching by name and type — correct for simple cases), implicit imports with ... syntax (leaves gaps for host — correct), linker namespace convention (WIT example:composed → linker example/composed/composed-world — consistent with Part 3’s colon-to-slash convention), host code for composed component (WasiCtx + ResourceTable + bindgen — same pattern as Parts 1–4, correct).

Praise: The “Why example::word_counter::format() and not example::formatter::format()?” callout is exactly right. This is the single most confusing thing about wit-bindgen’s import model, and the writer nails it: “The import lives in the word counter’s WIT — it’s a dependency the word counter declares, not something the formatter provides at the Rust level.” One sentence, no hedging, and the reader never makes this mistake.

Praise: The “If you’re building along” callout about the return type change is practical and considerate. The three-component composition changes the return type, and the writer shows the exact before/after host code. No guessing required.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)

Esme review 2026-06-21 20:35: Deep re-read Part 2 (Plugin Host, ~220 lines). Reading as a reader who understood Part 1.

0 new [error], [clarity], or [style] items. The chapter remains in strong shape.

Verified on re-read: “Without” code example (illustrative pseudo-code showing serialization pain — not meant to compile, the contrast is effective), WIT record definition inside world block (wit-bindgen 0.41+ requirement — correctly noted), Pair struct auto-generation (Clone, Debug, PartialEq — correct, the testing benefit is well-stated), host State/WasiView pattern (same as Part 1 — correct), result type nested Result explanation (outer Result for Wasm traps, inner Result for WIT-level success/error — accurate and clearly explained), map type caveat (experimental, wasmtime 44+ required, earlier versions reject as unknown type — honest and specific).

Praise: The “Compare and Contrast” section does exactly what a comparison should. The “Without” approach is 9 steps, the “With” approach is 1 line. The contrast isn’t padded or hedged — it’s just the facts, and the reader draws their own conclusion. The structured data comparison (bincode on both sides vs. WIT records that map directly to Rust structs) is the stronger argument, and it comes second where it lands hardest.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)

Esme review 2026-06-22 02:35: Deep re-read Part 3 (mdbook Preprocessor, ~364 lines). Reading as a reader who understood Parts 1–2.

0 new [error], [clarity], or [style] items.

Verified on re-read: WIT record definitions (book-item and book — correct, the hyphenated book-item maps to BookItem in Rust by wit-bindgen’s camel-case convention), plugin implementation (replace("WASM", "Wasm") — correct, mutates via &mut book.items), host setup (wasmtime 29 patterns — Engine, Linker, WasiCtxBuilder, ResourceTable, Store, Component::new, instantiate — all correct), bindgen! macro with path: "../plugin/wit" (correct relative path for host → plugin WIT), JSON adapter section (the architectural insight — “the Component Model gives you a clean interface inside the Wasm boundary, and you adapt to external protocols at the edges” — is precisely right, and the concrete adapter code makes it actionable), serde_json note (correct — only needed for the adapter, not the main example), host imports section (log import in WIT → func_wrap on linker — the namespace explanation is correct, the colon-vs-slash convention is noted), generated trait approach (correctly recommended over func_wrap — the type-safety argument is sound, and the add_to_linker method explanation is clear), “p2 in wasm32-wasip2 means preview 2” explanation (correct, and the connection to the Component Model is clear), summary table (appropriate use — comparison reference material, not conceptual explanation).

Praise: The “What About JSON-over-stdin?” section is the chapter’s payoff. The architectural insight — adapt protocols at the edges, keep the boundary clean — isn’t just explained, it’s shown. The reader sees the adapter code. They see where the JSON lives (the host) and where it doesn’t (the plugin). The line “Your plugin code stays simple and type-safe. The adaptation layer lives in the host, where it belongs” is the kind of sentence that reframes how a reader thinks about plugin architecture, not just how they implement it.

Praise: The generated trait approach section doesn’t just show the better way — it explains why the simpler way falls down. “If the WIT changes and you forget to update the Host impl, you get a compile error — not a runtime mismatch.” That’s the concrete cost of func_wrap, stated without hedging. The reader can make an informed choice.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)

Esme review 2026-06-26 02:35: Deep re-read Part 5 (Composing Components, ~350 lines). Reading as a reader who understood Parts 1–4.

0 new [error], [clarity], or [style] items. The chapter is in strong shape.

Verified on re-read: wac compose command and .wac syntax (correct for 0.8.x–0.10.x — the version note is appropriately scoped), formatter WIT and implementation (correct — split_whitespace().collect().join(" ") is a clean normalization), word counter WIT with import format (correct — the import declaration is the key concept, and the “Why example::word_counter::format()” callout explains the namespace confusion), deps/ directory naming convention (correct — namespace directory + underscored filename, well-documented), wac plug shortcut (correct — appropriate for two-component compositions), three-component composition pipeline (validator → formatter → word counter — the pipeline is clear, the return type change from u32 to option<u32> is well-handled with the “If you’re building along” callout), implicit imports with ... syntax (correct — the gap-filling pattern is well-explained, the host-side linker code matches Part 3’s established convention), “Why This Matters” section (good prose — “the dependency graph is encoded in the composition, not in the host code” is the right distillation), composition vs. host-mediated chaining tradeoff (the “when to use which” guidance at the end is honest and practical — “static sub-pipelines composed with wac, host orchestrates the top-level pipeline” is the right rule of thumb).

Praise: The chapter opens with exactly the right problem statement. “So far, the host has been the middleman.” That single sentence tells the reader where they’ve been (Parts 1–4) and why they need something new. The formatter + word counter example is concrete enough to follow and abstract enough to generalize.

Praise: The .wac file as “wiring diagram in code” is a lovely framing. The inline comments make the composition file self-documenting. A reader who’s never seen .wac syntax can read it top-to-bottom and understand what’s happening. That’s the bar, and the writer clears it.

Status

  • 0 outstanding [error] items
  • 0 outstanding [clarity] items
  • 1 [suggestion] item (CI workflow redundancy — carried, low priority)