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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
635 changes: 236 additions & 399 deletions Cargo.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ clap = { version = "4", features = ["derive"] }
tokio = { version = "1", default-features = false, features = ["rt", "macros"] }
wasmtime = { version = "47", features = ["component-model", "async"] }
wasmtime-wasi = { version = "47", default-features = false, features = ["p2", "p3"] }
wit-parser = "0.256"
wit-parser = "0.258"

[package]
name = "componentize-qjs-cli"
Expand Down Expand Up @@ -57,11 +57,11 @@ componentize-qjs.workspace = true
anyhow.workspace = true
clap.workspace = true
tokio.workspace = true
oxc_minifier = "0.144.0"
oxc_allocator = "0.144.0"
oxc_codegen = "0.144.0"
oxc_parser = "0.144.0"
oxc_span = "0.144.0"
oxc_minifier = "0.147.0"
oxc_allocator = "0.147.0"
oxc_codegen = "0.147.0"
oxc_parser = "0.147.0"
oxc_span = "0.147.0"

[features]
default = ["component-model-async"]
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ a project root that contains shared files or `node_modules`.
|----------|---------|---------|
| `list<T>` | `Array` | `[1, 2, 3]` |
| `list<u8>` | `Uint8Array` or `Array` | `new Uint8Array([1, 2, 3])` |
| `map<K, V>` | `Map` | `new Map([["key", value]])` |
| `tuple<T, U, ...>` | `Array` | `[42, "hello"]` |
| `option<T>` | `T \| null` (nested: `{ tag: "some"\|"none", val }`) | `null` for none; `option<option<T>>` is wrapped |
| `result<T, E>` | top-level function result: return `T` or throw `E`; nested result: `{ tag: "ok"\|"err", val?: T\|E }` | `return 42` / `throw "error"` |
Expand Down
7 changes: 4 additions & 3 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ bytes = "1"
oxc_resolver = "11.21.0"
tokio.workspace = true
wit-parser.workspace = true
wit-component = { version = "0.256", features = ["dummy-module"] }
wit-dylib = "0.256"
wit-component = { version = "0.258", features = ["dummy-module"] }
wit-dylib = "0.258"
wasi-preview1-component-adapter-provider = "47"
wasmtime.workspace = true
wasmtime-wasi.workspace = true
wasmtime-wizer = { version = "47", features = ["component-model", "wasmtime"] }
wac-graph = "0.10"
wasm-compose = { version = "0.258", default-features = false }
indexmap = "2"
tempfile = "3"

[build-dependencies]
anyhow.workspace = true
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,11 @@ fn typedef_const_name(resolve: &Resolve, id: TypeId, style: ConstNameStyle) -> S
TypeDefKind::List(inner) => {
format!("LIST_{}", type_const_name(resolve, Some(inner), style))
}
TypeDefKind::Map(key, value) => {
let key = type_const_name(resolve, Some(key), style);
let value = type_const_name(resolve, Some(value), style);
format!("MAP_{key}_{value}")
}
TypeDefKind::Future(inner) => {
let inner = inner
.as_ref()
Expand Down
27 changes: 15 additions & 12 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,18 +136,20 @@ pub async fn componentize(opts: &ComponentizeOpts<'_>) -> Result<Vec<u8>> {
wit_component::StringEncoding::UTF8,
)?;

let pre_wizer_component = wit_component::Linker::default()
.validate(true)
.library(
"componentize_qjs_runtime.wasm",
runtime_wasm(opts.runtime),
false,
)?
.library("wit-dylib.wasm", &wit_dylib, false)?
.adapter(
"wasi_snapshot_preview1",
WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
)?
let mut linker = wit_component::Linker::default();
linker.encoder().validate(true);
linker.library(
"componentize_qjs_runtime.wasm",
runtime_wasm(opts.runtime),
false,
)?;
linker.library("wit-dylib.wasm", &wit_dylib, false)?;
linker.encoder().adapter(
"wasi_snapshot_preview1",
WASI_SNAPSHOT_PREVIEW1_REACTOR_ADAPTER,
)?;

let pre_wizer_component = linker
.encode()
.context("failed to link and encode component")?;

Expand Down Expand Up @@ -231,6 +233,7 @@ async fn wizer_init(
let mut config = Config::new();
config.wasm_component_model(true);
config.wasm_component_model_async(true);
config.wasm_component_model_map(true);

let engine = Engine::new(&config)?;
let mut store = Store::new(&engine, Ctx { wasi, table });
Expand Down
30 changes: 15 additions & 15 deletions crates/core/src/stubwasi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
//! 2. Create a "stub world" where the WASI imports become exports
//! 3. Use `dummy_module` to generate a core module with trap implementations
//! 4. Encode it as a stub component
//! 5. Use `wac-graph` to compose the stub into the original component
//! 5. Use `wasm-compose` to compose the stub into the original component

use anyhow::{Context, Result, bail};
use indexmap::IndexMap;
use wac_graph::{CompositionGraph, EncodeOptions, plug, types::Package};
use wasm_compose::{composer::ComponentComposer, config::Config as ComposeConfig};
use wit_component::{ComponentEncoder, StringEncoding, dummy_module, embed_component_metadata};
use wit_parser::decoding::{DecodedWasm, decode};
use wit_parser::{Docs, ManglingAndAbi, Resolve, Stability, World, WorldItem, WorldKey};
Expand Down Expand Up @@ -47,22 +47,22 @@ fn stub_imports(component: &[u8], should_stub: impl Fn(&str) -> bool) -> Result<
let stub_component =
make_stub_component(&resolve, world, &imports).context("failed to build stub component")?;

let mut graph = CompositionGraph::new();
let dir = tempfile::tempdir().context("failed to create composition directory")?;
let component_path = dir.path().join("original.wasm");
let stub_path = dir.path().join("stubs.wasm");

let orig_pkg = Package::from_bytes("original", None, component.to_vec(), graph.types_mut())
.context("failed to register original component")?;
std::fs::write(&component_path, component).context("failed to stage original component")?;
std::fs::write(&stub_path, stub_component).context("failed to stage stub component")?;

let stub_pkg = Package::from_bytes("stubs", None, stub_component, graph.types_mut())
.context("failed to register stub component")?;

let orig_id = graph.register_package(orig_pkg)?;
let stub_id = graph.register_package(stub_pkg)?;

plug(&mut graph, vec![stub_id], orig_id)?;
let config = ComposeConfig {
dir: dir.path().to_path_buf(),
definitions: vec!["stubs.wasm".into()],
..Default::default()
};

graph
.encode(EncodeOptions::default())
.context("failed to encode composed component")
ComponentComposer::new(&component_path, &config)
.compose()
.context("failed to compose stub component")
}

/// Build a component that exports trap implementations for the given imports.
Expand Down
4 changes: 2 additions & 2 deletions crates/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ crate-type = ["cdylib"]

[dependencies]
rquickjs = { version = "0.12", default-features = false, features = ["bindgen", "disable-assertions", "loader", "std", "macro"] }
wit-bindgen = "0.60"
wit-dylib-ffi = { version = "0.1.0", git = "https://github.com/bytecodealliance/wasm-tools", default-features = false, features = ["async-raw"] }
wit-bindgen = "0.61"
wit-dylib-ffi = { version = "0.1.0", git = "https://github.com/bytecodealliance/wasm-tools", tag = "v1.258.0", default-features = false, features = ["async-raw"] }
heck = "0.5"
num_enum = { version = "0.7", default-features = false }
smallvec = "1"
Expand Down
78 changes: 73 additions & 5 deletions crates/runtime/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ use crate::trivia::fn_lookup;
use crate::{BorrowedResource, QjsCallContext, with_ctx};

use rquickjs::class::Class;
use rquickjs::{Coerced, IntoJs, Persistent, Symbol, Value};
use rquickjs::function::This;
use rquickjs::{Coerced, Constructor, Function, IntoJs, Persistent, Symbol, Value};
use smallvec::SmallVec;
use wit_dylib_ffi::{
Call, Enum, Flags, Future, List, Record, Resource, Stream, Tuple, Type, Variant, WitOption,
WitResult,
Call, Enum, Flags, Future, List, Map, Record, Resource, Stream, Tuple, Type, Variant,
WitOption, WitResult,
};

use std::alloc::Layout;
Expand Down Expand Up @@ -202,7 +203,7 @@ impl Call for QjsCallContext {
})
}

fn pop_iter_next(&mut self, _ty: List) {
fn pop_list_iter_next(&mut self, _ty: List) {
let index = *self.iter_stack.last().expect("iter_stack underflow");
let arr_persistent = self.stack.last().expect("stack underflow").clone();

Expand All @@ -216,11 +217,53 @@ impl Call for QjsCallContext {
*self.iter_stack.last_mut().unwrap() = index + 1;
}

fn pop_iter(&mut self, _ty: List) {
fn pop_list_iter(&mut self, _ty: List) {
self.iter_stack.pop().expect("iter_stack underflow");
self.stack.pop().expect("stack underflow");
}

fn pop_map(&mut self, _ty: Map) -> usize {
let persistent = self.stack.pop().expect("stack underflow");

with_ctx(|ctx| {
let value = persistent.restore(ctx).unwrap();
let map = value.as_object().expect("expected Map");
let constructor: Constructor = ctx.globals().get("Map").expect("Map not found");
assert!(map.is_instance_of(&constructor), "expected Map");

let len = map.get("size").expect("expected Map size");
let entries: Function = map.get("entries").expect("Map.entries not found");
let iterator: Value = entries
.call((This(map.clone()),))
.expect("failed to create Map iterator");
self.stack.push(Persistent::save(ctx, iterator));
len
})
}

fn pop_map_iter_next(&mut self, _ty: Map) {
let persistent = self.stack.last().expect("stack underflow").clone();

with_ctx(|ctx| {
let value = persistent.restore(ctx).unwrap();
let iterator = value.as_object().expect("expected Map iterator");
let next: Function = iterator.get("next").expect("Map iterator.next not found");
let result: rquickjs::Object = next
.call((This(iterator.clone()),))
.expect("failed to advance Map iterator");
let entry: rquickjs::Array = result.get("value").expect("expected Map entry");
let key: Value = entry.get(0).expect("expected Map key");
let value: Value = entry.get(1).expect("expected Map value");

self.stack.push(Persistent::save(ctx, value));
self.stack.push(Persistent::save(ctx, key));
});
}

fn pop_map_iter(&mut self, _ty: Map) {
self.stack.pop().expect("stack underflow");
}

fn pop_option(&mut self, ty: WitOption) -> u32 {
let persistent = self.stack.pop().expect("stack underflow");

Expand Down Expand Up @@ -509,6 +552,31 @@ impl Call for QjsCallContext {
});
}

fn push_map(&mut self, _ty: Map, _capacity: usize) {
with_ctx(|ctx| {
let constructor: Constructor = ctx.globals().get("Map").expect("Map not found");
let map: Value = constructor.construct(()).expect("failed to create Map");
self.stack.push(Persistent::save(ctx, map));
});
}

fn map_append(&mut self, _ty: Map) {
let value = self.stack.pop().expect("stack underflow");
let key = self.stack.pop().expect("stack underflow");
let map_persistent = self.stack.last().expect("stack underflow").clone();

with_ctx(|ctx| {
let map_value = map_persistent.restore(ctx).unwrap();
let map = map_value.as_object().expect("expected Map");
let set: Function = map.get("set").expect("Map.set not found");
let key = key.restore(ctx).unwrap();
let value = value.restore(ctx).unwrap();
let _: Value = set
.call((This(map.clone()), key, value))
.expect("failed to append Map entry");
});
}

fn push_option(&mut self, ty: WitOption, is_some: bool) {
if option_is_nested(ty) {
// Nested option: wrap so `none` and `some(none)` stay distinct.
Expand Down
2 changes: 2 additions & 0 deletions docs/runtime-intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ stream<message>` adds `wit.Stream.PROMPT_STREAM`.
| Results | `RESULT_STRING_U32` | `result<string, u32>` |
| Tuples | `TUPLE_U32_STRING` | `tuple<u32, string>` |
| Lists | `LIST_U8` | `list<u8>` |
| Maps | `MAP_STRING_U32` | `map<string, u32>` |
| Nested streams/futures | `STREAM_U8`, `FUTURE_STRING` | `stream<u8>`, `future<string>` |
| Unit | `UNIT` | (no payload) |

Expand Down Expand Up @@ -268,6 +269,7 @@ represented directly by their values. Resources are exposed as classes.
| Variants | `{ tag, val }` (string tag) | `{ tag: "case-a", val }` |
| Flags | `{ name: boolean }` object | `{ flagA: true, flagB: false }` |
| Records | object, camelCase fields | `{ fieldName: value }` |
| Maps | `Map` | `new Map([["key", value]])` |
| Resources | class instance; methods on prototype | `input.blockingRead(n)` |

### Export Functions
Expand Down
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub async fn run(args: Vec<String>) -> Result<()> {
top_level: Some(false),
..Default::default()
}),
mangle_properties: None,
compress: Some(CompressOptions {
unused: CompressOptionsUnused::Keep,
keep_names: CompressOptionsKeepNames::all_false(),
Expand Down
28 changes: 28 additions & 0 deletions tests/async_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,34 @@ async fn test_stream_record_type_constant() {
assert_eq!(results.len(), 1);
}

#[tokio::test]
async fn test_stream_map_type_constant() {
let mut instance = TestCase::new()
.wit(
r#"
package test:stream-map;
world stream-map {
export make-stream: async func() -> stream<map<string, u32>>;
}
"#,
)
.script(
r#"
export async function makeStream() {
const { readable, writable } = wit.Stream(wit.Stream.MAP_STRING_U32);
writable.drop();
return readable;
}
"#,
)
.build_async()
.await
.unwrap();

let results = instance.call_async("make-stream", &[], 1).await.unwrap();
assert_eq!(results.len(), 1);
}

#[tokio::test]
async fn test_named_stream_alias_constants() {
let mut instance = TestCase::new()
Expand Down
2 changes: 2 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub fn engine() -> &'static Engine {
#[cfg(feature = "component-model-async")]
config.wasm_component_model_async(true);
config.wasm_component_model(true);
config.wasm_component_model_map(true);
Engine::new(&config).expect("Failed to create engine")
})
}
Expand All @@ -45,6 +46,7 @@ pub fn async_engine() -> &'static Engine {
config.wasm_component_model(true);
config.wasm_component_model_async(true);
config.wasm_component_model_async_stackful(true);
config.wasm_component_model_map(true);
Engine::new(&config).expect("Failed to create async engine")
})
}
Expand Down
Loading
Loading