Skip to content

Add WASM reference type support and fix placeholder implementations - #4

Merged
LayerDynamics merged 3 commits into
mainfrom
feature/wasm-reftype-support
Dec 15, 2025
Merged

LayerDynamics merged 3 commits into
mainfrom
feature/wasm-reftype-support

Conversation

@LayerDynamics

@LayerDynamics LayerDynamics commented Dec 15, 2025 •

Copy link
Copy Markdown
Owner

Summary

  • WASM externref/funcref support: Full implementation with handle registry for storing and retrieving function references across the JS/WASM boundary
  • forge-weld-macro type parser: Comprehensive Rust-to-WeldType parsing supporting all primitive types, containers, references, tuples, and arrays
  • Placeholder fixes: Removed empty resolver.ts, pinned deno_core versions, extracted window manager code

Changes

WASM Reference Types (crates/ext_wasm/src/lib.rs)

  • Added FuncRef(Option<String>) and ExternRef(Option<String>) variants to WasmValue enum
  • Implemented handle registry in WasmInstance with func_refs: HashMap<String, Func>
  • to_wasmtime() now looks up funcref handles from the instance registry
  • from_wasmtime() stores refs in registry with unique handles like funcref_1
  • Removed unused externref registry (externrefs are opaque/non-roundtrippable)
  • Simplified store borrow pattern in op_wasm_call (combined function lookup and type info)
  • Optimized args conversion to single-pass validation+conversion
  • Added tests: test_wasm_funcref_registry, test_wasm_null_refs

Type Parser (crates/forge-weld-macro/src/type_parser.rs)

  • New module for parsing Rust types to WeldType
  • Supports: primitives, Option<T>, Vec<T>, Result<T, E>, HashMap<K, V>, references, tuples, arrays
  • Returns syn::Error instead of panic! for better compile-time diagnostics with source location

Window Manager (crates/ext_window/src/manager.rs)

  • Added context menu timeout (30s) for when user dismisses menu without selecting
  • Changed verbose info! logs to debug! for routine operations (window/tray/menu lifecycle)

Other Fixes

  • Delete empty resolver.ts placeholder
  • Pin deno_core = "0.373" across all extension crates
  • Add crossbeam-channel dependency for menu timeout handling
  • Fix typo: "deno" -> "Deno" in Lies.md
  • Add Lies.md to track placeholder implementations
  • Update site docs: architecture, forge-weld docs, roadmap

Test plan

  • cargo build - Compiles successfully
  • cargo test - All 107 tests pass
  • cargo clippy - No warnings
  • cargo fmt --check - Properly formatted
  • Manual testing of WASM modules with reference types

Code Review Feedback Addressed

  • Remove unused WASM externref registry
  • Replace panic! with syn::Error in type_parser
  • Add context menu timeout for dismissed menus
  • Simplify store borrow pattern in op_wasm_call
  • Optimize args conversion (single-pass)
  • Change verbose info! to debug! for routine logs
  • Fix "deno" typo in Lies.md
  • Apply cargo fmt formatting

WASM externref/funcref support:
- Add FuncRef and ExternRef variants to WasmValue enum
- Implement handle registry in WasmInstance for storing/retrieving funcrefs
- Update to_wasmtime() to look up handles from registry
- Update from_wasmtime() to store refs in registry with unique handles
- Update op_wasm_call to pass instance for proper ref handling
- Add comprehensive tests for funcref registry and null refs

forge-weld-macro improvements:
- Add type_parser.rs with comprehensive Rust-to-WeldType parsing
- Support all primitive types, containers, references, tuples, arrays
- Strict mode panics on unparseable types to prevent unknown types

Other fixes:
- Delete empty resolver.ts placeholder
- Pin deno_core to 0.373 across all crates
- Extract window manager code to separate module
- Add Lies.md to track placeholder implementations
- Update site documentation for architecture and roadmap
@sourcery-ai

sourcery-ai Bot commented Dec 15, 2025 •

Copy link
Copy Markdown

Reviewer's Guide

Implements full WASM reference type (externref/funcref) support with a handle registry, centralizes window management logic into a reusable WindowManager abstraction used by forge-host, introduces a strict Rust→WeldType type parser shared by forge-weld macros, and cleans up placeholders while pinning deno_* dependencies and updating docs/roadmap.

Sequence diagram for wasm.call with funcref and handle registry

sequenceDiagram
    participant JS as JsCaller
    participant Ext as ext_wasm_op_wasm_call
    participant Inst as WasmInstance
    participant WT as wasmtime_Func

    JS->>Ext: wasm.call(instance_id, func_name, [FuncRef("funcref_1")])
    Ext->>Inst: lock(instance_id) -> &mut WasmInstance
    Ext->>Inst: get_func_ref("funcref_1")
    alt handle found
        Inst-->>Ext: Some(Func)
        Ext->>WT: call(store, args=[Val::FuncRef(Some(Func))])
    else handle missing
        Inst-->>Ext: None
        Ext->>WT: call(store, args=[Val::null_func_ref()])
    end

    WT-->>Ext: results [Val]
    loop for each Val result
        Ext->>Inst: WasmValue::from_wasmtime(val, &mut WasmInstance)
        alt FuncRef
            Inst->>Inst: store_func_ref(Func) -> "funcref_N"
        else ExternRef
            Inst->>Inst: store_extern_ref(id) -> "externref_N"
        end
    end
    Ext-->>JS: Vec<WasmValue> (with FuncRef/ExternRef handles)
Loading

Class diagram for updated WASM reference handling (WasmValue and WasmInstance)

classDiagram
    class WasmValue {
      +I32(i32)
      +I64(i64)
      +F32(f32)
      +F64(f64)
      +FuncRef(Option~String~)
      +ExternRef(Option~String~)
      +to_wasmtime(instance: Option~WasmInstance~) Val
      +from_wasmtime(val: Val, instance: WasmInstance) Option~WasmValue~
      +valtype() ValType
    }

    class WasmInstance {
      +store: Store~WasmStoreData~
      +instance: Instance
      +module_id: String
      +func_refs: HashMap~String, Func~
      +next_func_ref_id: u64
      +extern_ref_ids: HashMap~String, u64~
      +next_extern_ref_id: u64
      +store_func_ref(func: Func) String
      +get_func_ref(handle: str) Option~Func~
      +store_extern_ref(id: u64) String
      +get_extern_ref_id(handle: str) Option~u64~
    }

    class WasmStoreData {
      <<struct>>
      +... (extension state)
    }

    class Val {
      <<from wasmtime>>
      +I32(i32)
      +I64(i64)
      +F32(u32)
      +F64(u64)
      +FuncRef(Option~Func~)
      +ExternRef(Option~ExternRef~)
    }

    class Func {
      <<from wasmtime>>
    }

    class ValType {
      <<from wasmtime>>
      +I32
      +I64
      +F32
      +F64
      +FUNCREF
      +EXTERNREF
    }

    WasmInstance "1" o-- "*" WasmValue : uses
    WasmInstance "1" o-- "*" Func : stores
    WasmInstance "1" --> "1" WasmStoreData : owns
    WasmValue ..> Val : converts_to
    WasmValue ..> ValType : type_of
Loading

File-Level Changes

Change Details Files
Add full WASM reference type support with handle registries and round‑trip conversion in ext_wasm
  • Extend WasmValue enum with funcref/externref variants and include their ValType mappings
  • Change WasmValue::to_wasmtime/from_wasmtime to take a WasmInstance, storing and resolving function and extern references via registries
  • Augment WasmInstance with func_refs/extern_ref_ids maps and ID counters plus helper methods to store and retrieve handles
  • Refactor op_wasm_call to use the instance-aware conversions without violating Rust borrowing rules
  • Add tests covering primitive conversions, funcref registry behavior, and null reference handling
crates/ext_wasm/src/lib.rs
Extract and centralize window, menu, tray, and dialog logic into a dedicated WindowManager used by forge-host and ext_window
  • Add a new manager module that encapsulates tao/wry window creation, WebView setup, protocol handler, dialogs, menus, tray icons, and native handles, with injectable AssetProvider and ChannelChecker traits
  • Refactor forge-host main event loop to use WindowManager APIs for window lifecycle, IPC routing, menus, trays, and window events instead of managing multiple ad‑hoc HashMaps
  • Change the UserEvent enum and WindowCmd forwarding to route a single WindowCmd variant into WindowManager::handle_cmd
  • Adapt app menu and tray usage to fetch shared state (menu_id_map, pending_ctx_menu, trays, tao_windows) from WindowManager instead of local state
crates/forge-host/src/main.rs
crates/ext_window/src/lib.rs
crates/ext_window/src/manager.rs
crates/ext_window/Cargo.toml
Introduce a strict Rust→WeldType type parser and reuse it across weld_op and weld_struct macros, updating forge-weld docs to match
  • Create type_parser.rs implementing rust_type_to_weld_type with exhaustive handling of primitives, containers, smart pointers, references, tuples, arrays, and special types, panicking on unsupported forms
  • Remove duplicated ad‑hoc type parsing logic from weld_op.rs and weld_struct.rs and replace with calls into the shared type parser
  • Add unit tests for the type parser covering common and nested type shapes
  • Update forge-weld and forge-weld-macro documentation to describe the new IR layout, WeldType variants, codegen modules, and supported Rust type mappings
crates/forge-weld-macro/src/type_parser.rs
crates/forge-weld-macro/src/lib.rs
crates/forge-weld-macro/src/weld_op.rs
crates/forge-weld-macro/src/weld_struct.rs
site/src/content/docs/crates/forge-weld.md
site/src/content/docs/crates/forge-weld-macro.md
crates/forge-weld/Cargo.toml
crates/forge-weld/README.md
Pin deno_core/deno_ast/deno_error versions and clean up placeholder or legacy pieces across extensions and docs
  • Replace wildcard deno_core/deno_error/deno_ast dependency versions in forge-host, ext_* crates, and forge-weld with pinned versions (deno_core 0.373, deno_ast 0.52, deno_error 0.7)
  • Wire ext_window to ext_ipc for IpcEvent and add Linux GTK/GDK dependencies where needed for window manager functionality
  • Remove the empty resolver.ts placeholder from forge-host and document outstanding and fixed placeholders in a new Lies.md
  • Add or refine architecture and roadmap docs, including updated extension tables, dependency lists, and high‑level diagrams for forge-weld and forge-weld-macro, plus site nav entry for the roadmap
crates/forge-host/Cargo.toml
crates/ext_fs/Cargo.toml
crates/ext_ipc/Cargo.toml
crates/ext_net/Cargo.toml
crates/ext_process/Cargo.toml
crates/ext_sys/Cargo.toml
crates/ext_ui/Cargo.toml
crates/ext_window/Cargo.toml
crates/ext_wasm/Cargo.toml
site/src/content/docs/architecture.md
site/src/content/docs/crates/forge-weld.md
site/src/content/docs/crates/forge-weld-macro.md
site/src/content/docs/roadmap.md
site/astro.config.mjs
README.md
Lies.md
crates/forge-host/src/resolver.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In the WASM externref handling, extern_ref_ids / store_extern_ref / get_extern_ref_id are currently never used and store_extern_ref is called with next_extern_ref_id instead of any property of the ExternRef itself; consider either wiring this up to actual ExternRef identity or removing the registry until there is a concrete use case to avoid misleading state.
  • The new mime_for helper in ext_window::manager duplicates the logic already present in forge-host; it might be worth centralizing this utility to a shared module to avoid divergence in MIME handling over time.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the WASM externref handling, `extern_ref_ids` / `store_extern_ref` / `get_extern_ref_id` are currently never used and `store_extern_ref` is called with `next_extern_ref_id` instead of any property of the `ExternRef` itself; consider either wiring this up to actual `ExternRef` identity or removing the registry until there is a concrete use case to avoid misleading state.
- The new `mime_for` helper in `ext_window::manager` duplicates the logic already present in `forge-host`; it might be worth centralizing this utility to a shared module to avoid divergence in MIME handling over time.

## Individual Comments

### Comment 1
<location> `crates/ext_window/src/manager.rs:363-372` </location>
<code_context>
+        wv_builder = wv_builder.with_custom_protocol("app".into(), move |_ctx, request| {
</code_context>

<issue_to_address>
**issue:** AssetProvider is bypassed when assets are not embedded, which contradicts its abstraction and ForgeAssetProvider implementation.

In the protocol handler, `asset_provider.get_asset(path)` is only called when `asset_provider.is_embedded()` is true. However, `ForgeAssetProvider.get_asset` already falls back to the filesystem when `ASSET_EMBEDDED` is false. Gating on `is_embedded()` disables the provider in the non-embedded case and reimplements that fallback in `WindowManager`, weakening the abstraction and coupling it to a specific provider. Instead, always call `asset_provider.get_asset(path)` and only read from the filesystem if it returns `None`, so the provider keeps full control over how assets are resolved.
</issue_to_address>

### Comment 2
<location> `crates/ext_window/src/manager.rs:1041-172` </location>
<code_context>
+    // =========================================================================
+
+    /// Handle window close requested event
+    pub fn handle_close_requested(&mut self, tao_window_id: WindowId) -> bool {
+        if let Some(win_id) = self.windows.remove(&tao_window_id) {
+            let _ = self.to_deno_tx.try_send(IpcEvent {
+                window_id: win_id.clone(),
+                channel: "__window__".to_string(),
+                payload: serde_json::json!({}),
+                event_type: Some("close".to_string()),
+            });
+            self.webviews.remove(&win_id);
+            self.tao_windows.remove(&win_id);
+            self.window_channels.remove(&win_id);
+            tracing::info!("Window {} closed", win_id);
+            true
+        } else {
+            false
</code_context>

<issue_to_address>
**suggestion:** Window close logic is duplicated between forge-host and WindowManager, increasing the chance of inconsistent behavior.

`handle_close_requested` already encapsulates closing a window (emitting the `__window__` close event and cleaning up all maps), but `forge-host` still performs similar close logic inline in the tao event loop (`get_window_id` + `remove_window`). This duplication risks divergence if one path gains extra cleanup or events. Please consolidate close handling into a single code path (e.g., always calling `window_manager.handle_close_requested` from the host loop, or making it a thin wrapper over a shared `remove_window`), so window lifecycle changes stay consistent and easier to maintain.

Suggested implementation:

```rust

    /// Handle window close requested event

        }
    }
=======
    // Window Events (called from event loop)
    // =========================================================================

    /// Handle window close requested event
    ///
    /// This encapsulates all window-close behavior so that callers (e.g. the
    /// forge-host tao event loop) can simply delegate to this method to ensure
    /// consistent cleanup and event emission.
    pub fn handle_close_requested(&mut self, tao_window_id: WindowId) -> bool {
        if let Some(win_id) = self.windows.remove(&tao_window_id) {
            // Emit a __window__ "close" event to the Deno side so userland
            // can react to the close.
            let _ = self.to_deno_tx.try_send(IpcEvent {
                window_id: win_id.clone(),
                channel: "__window__".to_string(),
                payload: serde_json::json!({}),
                event_type: Some("close".to_string()),
            });

            // Ensure all internal maps are cleaned up for this window ID.
            self.webviews.remove(&win_id);
            self.tao_windows.remove(&win_id);
            self.window_channels.remove(&win_id);

            tracing::info!("Window {} closed", win_id);
            true
        } else {
            false
        }
    }

        }
    }

```

To fully consolidate close logic and remove duplication with forge-host:
1. In the forge-host tao event loop, replace any inline close handling that:
   - looks up an internal window id from a tao `WindowId` (e.g. via `get_window_id`) and
   - manually calls something like `remove_window` / removes from maps
   with a single call to:
   ```rust
   if !window_manager.handle_close_requested(tao_window_id) {
       tracing::warn!("Close requested for unknown tao window: {:?}", tao_window_id);
   }
   ```
2. If there is an existing public `remove_window` (by internal window id), consider either:
   - marking it `pub(crate)` or keeping it for non-tao-driven closes, but have its implementation delegate to a shared internal helper used by `handle_close_requested`, or
   - updating any callers that respond to tao events to use `handle_close_requested` instead, so all tao-driven closes follow the same code path.
3. Ensure that any future cleanup or additional events on window close are added only in `handle_close_requested` (or its shared helper), so behavior remains consistent between forge-host and other call sites.
</issue_to_address>

### Comment 3
<location> `crates/forge-weld-macro/src/type_parser.rs:27-36` </location>
<code_context>
+pub fn rust_type_to_weld_type(ty: &Type) -> TokenStream {
</code_context>

<issue_to_address>
**suggestion:** Using `panic!` in the type parser for unsupported types degrades macro error UX; `compile_error!` or `syn::Error` would be clearer.

The strictness of `rust_type_to_weld_type` makes sense, but panicking inside a proc macro usually produces poor diagnostics (often pointing inside the macro). For unsupported forms (`impl Trait`, `dyn Trait`, `BareFn`, `Infer`, `Macro`, `Verbatim`, and the final catch‑all), consider emitting a `compile_error!("…")` `TokenStream` or using `syn::Error::to_compile_error()`. This preserves the “no unknown types” invariant while giving users a clear error anchored on the offending type instead of a panic backtrace.

Suggested implementation:

```rust
pub fn rust_type_to_weld_type(ty: &Type) -> syn::Result<TokenStream> {
    match ty {
        Type::Path(type_path) => parse_path_type(type_path),
        Type::Reference(type_ref) => {
            let inner = rust_type_to_weld_type(&type_ref.elem)?;
            let mutable = type_ref.mutability.is_some();
            Ok(quote! {
                forge_weld::WeldType::Reference {
                    inner: Box::new(#inner),
                    mutable: #mutable,
                }
            })

```

To fully implement the “no `panic!` in the type parser” suggestion and give better macro diagnostics, the following additional edits are needed in this file:

1. **Import `syn::Error` (and `Result` if you prefer a type alias)**:
   - At the top of `type_parser.rs`, add:
     ```rust
     use syn::Error;
     ```
     and optionally:
     ```rust
     type Result<T> = std::result::Result<T, Error>;
     ```
     Then you can change the signature to `pub fn rust_type_to_weld_type(ty: &Type) -> Result<TokenStream>` and adjust the `syn::Result` in the edit above accordingly.

2. **Update all callers of `rust_type_to_weld_type`**:
   - Every place that currently calls `rust_type_to_weld_type(...)` and expects a `TokenStream` must now handle a `Result<TokenStream>`, typically by propagating the error (`?`) inside other parsing helpers, and only converting to tokens at the outermost proc-macro boundary:
     ```rust
     let ty_tokens = rust_type_to_weld_type(&some_type)?;
     ```
   - At the top-level proc macro function (the one returning `TokenStream`), convert errors to `TokenStream`:
     ```rust
     match rust_type_to_weld_type(&ty) {
         Ok(tokens) => tokens,
         Err(err) => err.to_compile_error(),
     }
     ```

3. **Change all `panic!` branches for unsupported types to `syn::Error`**:
   - For each unsupported `Type` variant in the `match ty` (and any helper functions like `parse_path_type`) that currently uses `panic!` or `unimplemented!`, replace with something like:
     ```rust
     Type::ImplTrait(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: `impl Trait` is not supported in Weld type mapping",
         ));
     }
     Type::TraitObject(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: `dyn Trait` is not supported in Weld type mapping",
         ));
     }
     Type::BareFn(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: bare function types are not supported in Weld type mapping",
         ));
     }
     Type::Infer(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: inferred types (`_`) are not supported in Weld type mapping",
         ));
     }
     Type::Macro(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: type macros are not supported in Weld type mapping",
         ));
     }
     Type::Verbatim(t) => {
         return Err(Error::new_spanned(
             t,
             "forge-weld: verbatim types are not supported in Weld type mapping",
         ));
     }
     _ => {
         return Err(Error::new_spanned(
             ty,
             "forge-weld: this Rust type is not supported in Weld type mapping",
         ));
     }
     ```
   - These `Error::new_spanned(...).to_compile_error()` will automatically be turned into a `compile_error!` at the top-level when you use `err.to_compile_error()` as described above.

4. **Adjust other helper functions that return `TokenStream`**:
   - Any helper like `parse_path_type`, `parse_tuple_type`, etc., that currently returns `TokenStream` and may `panic!` on unsupported constructs should be changed to return `syn::Result<TokenStream>` (or the `Result<T>` alias) and use `Error::new_spanned(...)` instead of `panic!`.
   - Calls to these helpers inside `rust_type_to_weld_type` and each other should be updated to use `?` for error propagation:
     ```rust
     Type::Path(type_path) => parse_path_type(type_path)?,
     ```

These changes preserve the “no unknown types” invariant, but instead of panicking, the proc macro will emit a precise `compile_error!` attached to the offending type via `syn::Error::new_spanned`, which gives much clearer diagnostics to users.
</issue_to_address>

### Comment 4
<location> `Lies.md:28` </location>
<code_context>
+
+### ~~Dependency Version Not Pinned~~ FIXED
+
+**Resolution**: All deno dependencies are now pinned:
+- `deno_core = "0.373"` (all crates)
+- `deno_ast = { version = "0.52", features = ["transpiling"] }`
</code_context>

<issue_to_address>
**nitpick (typo):** Capitalize "Deno" as a proper noun for consistency with the rest of the docs.

For consistency with the rest of the docs, update this line to `All Deno dependencies are now pinned:`.

```suggestion
**Resolution**: All Deno dependencies are now pinned:
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread crates/ext_window/src/manager.rs Resolved
Comment thread crates/ext_window/src/manager.rs Resolved
Comment thread Lies.md Resolved
Comment thread crates/ext_wasm/src/lib.rs Fixed
Comment thread crates/ext_wasm/src/lib.rs Fixed
Comment thread crates/ext_wasm/src/lib.rs Fixed
- Remove unused WASM externref registry (funcref registry retained)
- Replace panic! with syn::Error in type_parser for better diagnostics
- Add context menu timeout (30s) for dismissed menus
- Simplify store borrow pattern in op_wasm_call
- Optimize args conversion to single-pass validation+conversion
- Change verbose info! logs to debug! for routine operations
- Fix typo: "deno" -> "Deno" in Lies.md
- Add crossbeam-channel dependency for menu timeout handling
@LayerDynamics
LayerDynamics merged commit d5dd441 into main Dec 15, 2025
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants