diff --git a/crates/js-component-bindgen/src/esm_bindgen.rs b/crates/js-component-bindgen/src/esm_bindgen.rs index 915216f70..68108cbef 100644 --- a/crates/js-component-bindgen/src/esm_bindgen.rs +++ b/crates/js-component-bindgen/src/esm_bindgen.rs @@ -396,9 +396,6 @@ impl EsmBindgen { maybe_quote_member(specifier) ); for (external_name, local_name) in bound_external_names { - // For imports that are functions, ensure that they are noted as host provided - uwriteln!(output, "{local_name}._isHostProvided = true;"); - uwriteln!( output, r#" @@ -409,6 +406,9 @@ impl EsmBindgen { }} "#, ); + + // For imports that are functions, ensure that they are noted as host provided + uwriteln!(output, "{local_name}._isHostProvided = true;"); } } else if let Some(idl_binding) = idl_binding { uwrite!( @@ -480,9 +480,6 @@ impl EsmBindgen { // Process all external host-provided imports for (member_name, local_name) in generated_member_names { - // For imports that are functions, ensure that they are noted as host provided - uwriteln!(output, "{local_name}._isHostProvided = true;"); - // Ensure that the imports we destructured were defined // (if they were not, the user is likely missing an import @ instantiation time) uwriteln!( @@ -495,6 +492,8 @@ impl EsmBindgen { }} "#, ); + // For imports that are functions, ensure that they are noted as host provided + uwriteln!(output, "{local_name}._isHostProvided = true;"); } } } diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 9fd811c59..b84a4f1ec 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -4,7 +4,7 @@ use std::mem; use heck::{ToLowerCamelCase, ToUpperCamelCase}; use wasmtime_environ::component::{ - CanonicalOptions, ResourceIndex, TypeComponentLocalErrorContextTableIndex, + CanonicalOptions, InterfaceType, ResourceIndex, TypeComponentLocalErrorContextTableIndex, TypeFutureTableIndex, TypeResourceTableIndex, TypeStreamTableIndex, }; use wit_bindgen_core::abi::{Bindgen, Bitcast, Instruction}; @@ -64,16 +64,29 @@ pub enum ResourceData { }, } +#[derive(Clone, Debug, PartialEq)] +pub struct PayloadTypeMetadata { + pub(crate) ty: Type, + pub(crate) iface_ty: InterfaceType, + /// JS expression that serves as a function that lifts a given type + pub(crate) lift_js_expr: String, + /// JS expression that serves as a function that lowers a given type + pub(crate) lower_js_expr: String, + pub(crate) size32: u32, + pub(crate) align32: u32, + pub(crate) flat_count: Option, +} + /// Supplemental data kept along with [`ResourceData`] #[derive(Clone, Debug, PartialEq)] pub enum ResourceExtraData { Stream { table_idx: TypeStreamTableIndex, - elem_ty: Option, + elem_ty: Option, }, Future { table_idx: TypeFutureTableIndex, - elem_ty: Option, + elem_ty: Option, }, ErrorContext { table_idx: TypeComponentLocalErrorContextTableIndex, @@ -1140,10 +1153,6 @@ impl Bindgen for FunctionBindgen<'_> { let memory = self.memory.as_ref().unwrap(); let realloc = self.realloc.unwrap(); - // Gather metadata about list element - let size = self.sizes.size(element).size_wasm32(); - let align = ArchitectureSize::from(self.sizes.align(element)).size_wasm32(); - // Alias the list to a local variable uwriteln!(self.src, "var val{tmp} = {};", operands[0]); if matches!(element, Type::U8) { @@ -1155,6 +1164,10 @@ impl Bindgen for FunctionBindgen<'_> { uwriteln!(self.src, "var len{tmp} = val{tmp}.length;"); } + // Gather metadata about list element + let size = self.sizes.size(element).size_wasm32(); + let align = self.sizes.align(element).align_wasm32(); + // Allocate space for the type in question uwriteln!( self.src, @@ -1166,23 +1179,9 @@ impl Bindgen for FunctionBindgen<'_> { }, ); - // We may or may not be dealing with a buffer like object or a regular JS array, - // in which case we can detect and use the right value - // Determine what methods to use with a DataView when setting the data - let dataview_set_method = match element { - Type::Bool | Type::U8 => "setUint8", - Type::U16 => "setUint16", - Type::U32 => "setUint32", - Type::U64 => "setBigUint64", - Type::S8 => "setInt8", - Type::S16 => "setInt16", - Type::S32 => "setInt32", - Type::S64 => "setBigInt64", - Type::F32 => "setFloat32", - Type::F64 => "setFloat64", - _ => unreachable!("unsupported type [{element:?}] for canonical list lower"), - }; + let (dataview_set_method, check_fn_intrinsic) = + gen_dataview_set_and_check_fn_js_for_numeric_type(element); // Detect whether we're dealing with a regular array uwriteln!( @@ -1195,13 +1194,14 @@ impl Bindgen for FunctionBindgen<'_> { let offset = 0; const dv{tmp} = new DataView({memory}.buffer); for (const v of val{tmp}) {{ + {check_fn_intrinsic}(v); dv{tmp}.{dataview_set_method}(ptr{tmp} + offset, v, true); offset += {size}; }} }} else {{ // TypedArray / ArrayBuffer-like, direct copy valData{tmp} = new Uint8Array(val{tmp}.buffer || val{tmp}, val{tmp}.byteOffset, valLenBytes{tmp}); - const out{tmp} = new Uint8Array({memory}.buffer, ptr{tmp},valLenBytes{tmp}); + const out{tmp} = new Uint8Array({memory}.buffer, ptr{tmp}, valLenBytes{tmp}); out{tmp}.set(valData{tmp}); }} "#, @@ -2380,18 +2380,212 @@ impl Bindgen for FunctionBindgen<'_> { } } - Instruction::StreamLower { .. } => { - // TODO: convert this return of the lifted Future: - // ``` - // return BigInt(writeEndWaitableIdx) << 32n | BigInt(readEndWaitableIdx); - // ``` - // - // Into a component-local Future instance - // + Instruction::StreamLower { ty, .. } => { + let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); let stream_arg = operands .first() .expect("unexpectedly missing StreamLower arg"); - results.push(stream_arg.clone()); + let async_iterator_symbol = self.intrinsic(Intrinsic::SymbolAsyncIterator); + let iterator_symbol = self.intrinsic(Intrinsic::SymbolIterator); + let external_readable_stream_class = + self.intrinsic(Intrinsic::PlatformReadableStreamClass); + let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component( + ComponentIntrinsic::GetOrCreateAsyncState, + )); + + // Build the lowering function for the type produced by the stream + let type_id = &crate::dealias(self.resolve, *ty); + let ResourceTable { + imported: true, + data: + ResourceData::Guest { + extra: + Some(ResourceExtraData::Stream { + table_idx: stream_table_idx_ty, + elem_ty, + }), + .. + }, + } = self + .resource_map + .get(type_id) + .expect("missing resource mapping for stream lower") + else { + unreachable!("invalid resource table observed during stream lower"); + }; + + let component_idx = self.canon_opts.instance.as_u32(); + let stream_table_idx = stream_table_idx_ty.as_u32(); + + let ( + payload_type_name_js, + lift_fn_js, + lower_fn_js, + payload_is_none, + payload_is_numeric, + payload_is_borrow, + payload_is_async_value, + payload_size32_js, + payload_align32_js, + payload_flat_count_js, + ) = match elem_ty { + Some(PayloadTypeMetadata { + ty: _, + iface_ty, + lift_js_expr, + lower_js_expr, + size32, + align32, + flat_count, + }) => ( + format!("'{iface_ty:?}'"), + lift_js_expr.as_str(), + lower_js_expr.as_str(), + "false", + format!( + "{}", + matches!( + iface_ty, + InterfaceType::U8 + | InterfaceType::U16 + | InterfaceType::U32 + | InterfaceType::U64 + | InterfaceType::S8 + | InterfaceType::S16 + | InterfaceType::S32 + | InterfaceType::S64 + | InterfaceType::Float32 + | InterfaceType::Float64 + ) + ), + format!("{}", matches!(iface_ty, InterfaceType::Borrow(_))), + format!( + "{}", + matches!( + iface_ty, + InterfaceType::Stream(_) | InterfaceType::Future(_) + ) + ), + size32.to_string(), + align32.to_string(), + flat_count.unwrap_or(0).to_string(), + ), + None => ( + "null".into(), + "() => {{ throw new Error('no lift fn'); }}", + "() => {{ throw new Error('no lower fn'); }}", + "true", + "false".into(), + "false".into(), + "false".into(), + "null".into(), + "null".into(), + "0".into(), + ), + }; + + let tmp = self.tmp(); + let lowered_stream_waitable_idx = format!("streamWaitableIdx{tmp}"); + uwriteln!( + self.src, + r#" + if (!({async_iterator_symbol} in {stream_arg}) + && !({iterator_symbol} in {stream_arg}) + && !({stream_arg} instanceof {external_readable_stream_class})) {{ + {debug_log_fn}('[Instruction::StreamLower] object with no supported stream protocol', {{ {stream_arg} }}); + throw new Error('unrecognized stream object (no supported stream protocol)'); + }} + + const cstate{tmp} = {get_or_create_async_state_fn}({component_idx}); + if (!cstate{tmp}) {{ throw new Error(`missing component state for component [{component_idx}]`); }} + + const {{ writeEnd: hostWriteEnd{tmp}, readEnd: readEnd{tmp} }} = cstate{tmp}.createStream({{ + tableIdx: {stream_table_idx}, + elemMeta: {{ + liftFn: {lift_fn_js}, + lowerFn: {lower_fn_js}, + payloadTypeName: {payload_type_name_js}, + isNone: {payload_is_none}, + isNumeric: {payload_is_numeric}, + isBorrowed: {payload_is_borrow}, + isAsyncValue: {payload_is_async_value}, + flatCount: {payload_flat_count_js}, + align32: {payload_align32_js}, + size32: {payload_size32_js}, + // TODO(feat): facilitate non utf8 string encoding for lowered streams + stringEncoding: 'utf8', + }}, + }}); + + const doNothing{tmp} = () => {{}}; + const resetWriteEndToIdle{tmp} = () => {{ + // After the write is finished, we consume the event that was generated + // by the just-in-time write (and the subsequent read), if one was generated + if (hostWriteEnd{tmp}.hasPendingEvent()) {{ hostWriteEnd{tmp}.getPendingEvent(); }} + }}; + let genHostInjectFn = (readFn) => {{ + let done = false; + + return async (args) => {{ + let {{ count }} = args; + if (count < 0) {{ throw new Error('invalid count'); }} + if (count === 0) {{ return doNothing{tmp}; }} + + // If we get another read when done is already set, that was + // the case of a iterator that returned a final value + // along with `done: true` + if (done) {{ + hostWriteEnd{tmp}.getPendingEvent(); + hostWriteEnd{tmp}.drop(); + return doNothing{tmp}; + }} + + if (hostWriteEnd{tmp}.isDoneState()) {{ + return doNothing{tmp}; + }} + + const values = []; + while (count > 0 && !done) {{ + const res = await readFn(); + if (res.value !== undefined) {{ values.push(res.value); }} + done = res.done; + if (done) {{ break; }} + count -= 1; + }} + + // Iterator provided `done: true` with no final value + if (done && values.length === 0) {{ + hostWriteEnd{tmp}.getPendingEvent(); + hostWriteEnd{tmp}.drop(); + return doNothing{tmp}; + }} + + await hostWriteEnd{tmp}.write(values); + + return resetWriteEndToIdle{tmp}; + }}; + }}; + + let readFn; + if ({async_iterator_symbol} in {stream_arg}) {{ + let asyncIterator = {stream_arg}[{async_iterator_symbol}](); + readFn = () => asyncIterator.next(); + }} else if ({iterator_symbol} in {stream_arg}) {{ + let iterator = {stream_arg}[{iterator_symbol}](); + readFn = async () => iterator.next(); + }} else if ({stream_arg} instanceof {external_readable_stream_class}) {{ + // At this point we're dealing with a readable stream that *somehow *does not* + // implement the async iterator protocol. + const lockedReader = {stream_arg}.getReader(); + readFn = () => lockedReader.read(); + }} + + readEnd{tmp}.setHostInjectFn(genHostInjectFn(readFn)); + + const {lowered_stream_waitable_idx} = readEnd{tmp}.waitableIdx(); + "# + ); + results.push(lowered_stream_waitable_idx); } Instruction::StreamLift { payload, ty } => { @@ -2421,102 +2615,68 @@ impl Bindgen for FunctionBindgen<'_> { unreachable!("invalid resource table observed during stream lift"); }; - assert_eq!( - *stream_element_ty, **payload, - "stream element type mismatch" - ); - - let arg_stream_end_idx = operands - .first() - .expect("unexpectedly missing stream table idx arg in StreamLift"); - - let (payload_lift_fn, payload_lower_fn) = match payload { - None => ("null".into(), "null".into()), - Some(payload_ty) => { - match payload_ty { - // TODO: reuse existing lifts - Type::Bool - | Type::U8 - | Type::U16 - | Type::U32 - | Type::U64 - | Type::S8 - | Type::S16 - | Type::S32 - | Type::S64 - | Type::F32 - | Type::F64 - | Type::Char - | Type::String - | Type::ErrorContext => ( - format!( - "const payloadLiftFn = () => {{ throw new Error('lift for {payload_ty:?}'); }};" - ), - format!( - "const payloadLowerFn = () => {{ throw new Error('lower for {payload_ty:?}'); }};" - ), - ), - - Type::Id(payload_ty_id) => { - if let Some(ResourceTable { data, .. }) = - &self.resource_map.get(payload_ty_id) - { - let identifier = match data { - ResourceData::Host { local_name, .. } => local_name, - ResourceData::Guest { resource_name, .. } => resource_name, - }; - - ( - format!( - "const payloadLiftFn = () => {{ throw new Error('lift for {} (identifier {identifier})'); }};", - payload_ty_id.index(), - ), - format!( - "const payloadLowerFn = () => {{ throw new Error('lower for {} (identifier {identifier})'); }};", - payload_ty_id.index(), - ), - ) - } else { - ( - format!( - "const payloadLiftFn = () => {{ throw new Error('lift for missing type with type idx {payload_ty:?}'); }};", - ), - format!( - "const payloadLowerFn = () => {{ throw new Error('lower for missing type with type idx {payload_ty:?}'); }};", - ), - ) - } - } - } + // if a stream element is present, it should match the payload we're getting + let (lift_fn_js, lower_fn_js) = match stream_element_ty { + Some(PayloadTypeMetadata { + ty, + lift_js_expr, + lower_js_expr, + .. + }) => { + assert_eq!(Some(*ty), **payload, "stream element type mismatch"); + (lift_js_expr.to_string(), lower_js_expr.to_string()) } + None => ( + "() => {{ throw new Error('no lift fn'); }}".into(), + "() => {{ throw new Error('no lower fn'); }}".into(), + ), }; - - let payload_ty_size_js = if let Some(payload_ty) = payload { - self.sizes.size(payload_ty).size_wasm32().to_string() - } else { - "null".into() - }; - - let stream_table_idx = stream_table_idx_ty.as_u32(); - let is_unit_stream = payload.is_none(); + if let Some(PayloadTypeMetadata { ty, .. }) = stream_element_ty { + assert_eq!(Some(*ty), **payload, "stream element type mismatch"); + } let tmp = self.tmp(); let result_var = format!("streamResult{tmp}"); - uwriteln!( - self.src, - " - {payload_lift_fn} - {payload_lower_fn} - const {result_var} = {stream_new_from_lift_fn}({{ - componentIdx: {component_idx}, - streamTableIdx: {stream_table_idx}, - streamEndWaitableIdx: {arg_stream_end_idx}, - payloadLiftFn, - payloadTypeSize32: {payload_ty_size_js}, - payloadLowerFn, - isUnitStream: {is_unit_stream}, - }});", - ); + + // We only need to attempt to do an immediate lift in non-async cases, + // as the return of the function execution ('above' in the code) + // will be the stream idx + if !self.is_async { + // If we're dealing with a sync function, we can use the return directly + let arg_stream_end_idx = operands + .first() + .expect("unexpectedly missing stream end return arg in StreamLift"); + + let (payload_ty_size32_js, payload_ty_align32_js) = + if let Some(payload_ty) = payload { + ( + self.sizes.size(payload_ty).size_wasm32().to_string(), + self.sizes.align(payload_ty).align_wasm32().to_string(), + ) + } else { + ("null".into(), "null".into()) + }; + + let stream_table_idx = stream_table_idx_ty.as_u32(); + let is_unit_stream = payload.is_none(); + + uwriteln!( + self.src, + " + const {result_var} = {stream_new_from_lift_fn}({{ + componentIdx: {component_idx}, + streamTableIdx: {stream_table_idx}, + streamEndWaitableIdx: {arg_stream_end_idx}, + payloadLiftFn: {lift_fn_js}, + payloadLowerFn: {lower_fn_js}, + payloadTypeSize32: {payload_ty_size32_js}, + payloadTypeAlign32: {payload_ty_align32_js}, + isUnitStream: {is_unit_stream}, + }});", + ); + } + + // TODO(fix): in the async case we return an uninitialized var, which should not be necessary results.push(result_var.clone()); } @@ -2730,3 +2890,30 @@ pub fn js_array_ty(resolve: &Resolve, element_ty: &Type) -> Option<&'static str> }, } } + +/// Generate the JS `DataView` set and numeric checks for a given numeric type +/// +/// # Arguments +/// +/// * `ty` - the [`Type`] to check +/// +fn gen_dataview_set_and_check_fn_js_for_numeric_type(ty: &Type) -> (&str, String) { + let check_fn = Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + match ty { + // Unsigned Integers + Type::Bool => ("setUint8", format!("{check_fn}.bind(null, 'u8')",)), + Type::U8 => ("setUint8", format!("{check_fn}.bind(null, 'u8')",)), + Type::U16 => ("setUint16", format!("{check_fn}.bind(null, 'u16')",)), + Type::U32 => ("setUint32", format!("{check_fn}.bind(null, 'u32')",)), + Type::U64 => ("setBigUint64", format!("{check_fn}.bind(null, 'u64')",)), + // Signed integers + Type::S8 => ("setInt8", format!("{check_fn}.bind(null, 's8')",)), + Type::S16 => ("setInt16", format!("{check_fn}.bind(null, 's16')",)), + Type::S32 => ("setInt32", format!("{check_fn}.bind(null, 's32')",)), + Type::S64 => ("setBigInt64", format!("{check_fn}.bind(null, 's64')",)), + // Floating point + Type::F32 => ("setFloat32", format!("{check_fn}.bind(null, 'f32')",)), + Type::F64 => ("setFloat64", format!("{check_fn}.bind(null, 'f64')",)), + _ => unreachable!("unsupported type [{ty:?}] for canonical list lower"), + } +} diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 26ff10fc6..13ce120c8 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -130,6 +130,8 @@ impl ComponentIntrinsic { let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); let get_or_create_async_state_fn = Self::GetOrCreateAsyncState.name(); let promise_with_resolvers_fn = Intrinsic::PromiseWithResolversPonyfill.name(); + let stream_readable_end_class = + Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamReadableEndClass).name(); output.push_str(&format!( r#" @@ -466,9 +468,48 @@ impl ComponentIntrinsic { return new {waitable_class}({{ target: args?.target, }}); }} + createReadableStreamEnd(args) {{ + {debug_log_fn}('[{component_async_state_class}#createStreamEnd()] args', args); + const {{ tableIdx, elemMeta, hostInjectFn }} = args; + + const {{ table: localStreamTable, componentIdx }} = {global_stream_table_map}[tableIdx]; + if (!localStreamTable) {{ + throw new Error(`missing global stream table lookup for table [${{tableIdx}}] while creating stream`); + }} + if (componentIdx !== this.#componentIdx) {{ + throw new Error('component idx mismatch while creating stream'); + }} + + const waitable = this.createWaitable(); + const streamEnd = new {stream_readable_end_class}({{ + tableIdx, + elemMeta, + hostInjectFn, + pendingBufferMeta: {{}}, + target: `stream read end (lowered, @init)`, + waitable, + }}); + + streamEnd.setWaitableIdx(this.handles.insert(streamEnd)); + streamEnd.setHandle(localStreamTable.insert(streamEnd)); + if (streamEnd.streamTableIdx() !== tableIdx) {{ + throw new Error("unexpectedly mismatched stream table"); + }} + const streamEndWaitableIdx = streamEnd.waitableIdx(); + const streamEndHandle = streamEnd.handle(); + waitable.setTarget(`waitable for stream read end (lowered, waitable [${{streamEndWaitableIdx}}])`); + streamEnd.setTarget(`stream read end (lowered, waitable [${{streamEndWaitableIdx}}])`); + + return {{ + waitableIdx: streamEndWaitableIdx, + handle: streamEndHandle, + streamEnd, + }}; + }} + createStream(args) {{ {debug_log_fn}('[{component_async_state_class}#createStream()] args', args); - const {{ tableIdx, elemMeta }} = args; + const {{ tableIdx, elemMeta, hostInjectFn }} = args; if (tableIdx === undefined) {{ throw new Error("missing table idx while adding stream"); }} if (elemMeta === undefined) {{ throw new Error("missing element metadata while adding stream"); }} @@ -485,10 +526,10 @@ impl ComponentIntrinsic { const stream = new {internal_stream_class}({{ tableIdx, - componentIdx: this.#componentIdx, elemMeta, readWaitable, writeWaitable, + hostInjectFn, }}); stream.setGlobalStreamMapRep({global_stream_map}.insert(stream)); @@ -513,17 +554,21 @@ impl ComponentIntrinsic { readEnd.setTarget(`stream read end (waitable [${{readEndWaitableIdx}}])`); return {{ + writeEnd, writeEndWaitableIdx, writeEndHandle, readEndWaitableIdx, readEndHandle, + readEnd, }}; }} getStreamEnd(args) {{ {debug_log_fn}('[{component_async_state_class}#getStreamEnd()] args', args); const {{ tableIdx, streamEndHandle, streamEndWaitableIdx }} = args; - if (tableIdx === undefined) {{ throw new Error('missing table idx while getting stream end'); }} + if (tableIdx === undefined) {{ + throw new Error('missing table idx while getting stream end'); + }} const {{ table, componentIdx }} = {global_stream_table_map}[tableIdx]; const cstate = {get_or_create_async_state_fn}(componentIdx); diff --git a/crates/js-component-bindgen/src/intrinsics/conversion.rs b/crates/js-component-bindgen/src/intrinsics/conversion.rs index e6798f471..02c8f96fe 100644 --- a/crates/js-component-bindgen/src/intrinsics/conversion.rs +++ b/crates/js-component-bindgen/src/intrinsics/conversion.rs @@ -34,6 +34,12 @@ pub enum ConversionIntrinsic { ToUint8, ToResultString, + + /// Function that requires validity of various numeric primitive types (or throws a `TypeError`) + RequireValidNumericPrimitive, + + /// Function that checks validity of various numeric primitive types + IsValidNumericPrimitive, } impl ConversionIntrinsic { @@ -61,6 +67,8 @@ impl ConversionIntrinsic { "toUint64", "toUint64", "toUint8", + Self::RequireValidNumericPrimitive.name(), + Self::IsValidNumericPrimitive.name(), ] } @@ -82,6 +90,8 @@ impl ConversionIntrinsic { Self::I64ToF64 => "i64ToF64", Self::F32ToI32 => "f32ToI32", Self::F64ToI64 => "f64ToI64", + Self::RequireValidNumericPrimitive => "_requireValidNumericPrimitive", + Self::IsValidNumericPrimitive => "_isValidNumericPrimitive", } } @@ -199,6 +209,60 @@ impl ConversionIntrinsic { "); } + Self::RequireValidNumericPrimitive => { + let name = self.name(); + let is_valid_numeric_primitive_fn = Self::IsValidNumericPrimitive.name(); + + output.push_str(&format!(r#" + function {name}(ty, v) {{ + if (v === undefined || v === null || !{is_valid_numeric_primitive_fn}(ty, v)) {{ + throw new TypeError(`invalid ${{ty}} value [${{v}}]`); + }} + return true; + }} + "#)) + } + + Self::IsValidNumericPrimitive => { + let name = self.name(); + output.push_str(&format!(r#" + function {name}(ty, v) {{ + if (v === undefined || v === null) {{ return false; }} + switch (ty) {{ + case 'bool': + return v === 0 || v === 1; + break; + case 'u8': + return v >= 0 && v <= 255; + break; + case 's8': + return v >= -128 && v <= 127; + break; + case 'u16': + return v >= 0 && v <= 65535; + break; + case 's16': + return v >= -32768 && v <= 32767; + case 'u32': + return v >= 0 && v <= 4_294_967_295; + case 's32': + return v >= -2_147_483_648 && v <= 2_147_483_647; + case 'u64': + return typeof v === 'bigint' && v >= 0 && v <= 18_446_744_073_709_551_615n; + case 's64': + return typeof v === 'bigint' && v >= -9223372036854775808n && v <= 9223372036854775807n; + break; + case 'f32': + case 'f64': return typeof v === 'number'; + default: + return false; + }} + return true; + }} + "# + )); + } + } } } diff --git a/crates/js-component-bindgen/src/intrinsics/lift.rs b/crates/js-component-bindgen/src/intrinsics/lift.rs index ab2b52702..f46defcd1 100644 --- a/crates/js-component-bindgen/src/intrinsics/lift.rs +++ b/crates/js-component-bindgen/src/intrinsics/lift.rs @@ -151,6 +151,9 @@ pub enum LiftIntrinsic { /// Lift a char into provided storage given core type(s) that represent utf8 LiftFlatChar, + /// Lift a string from provided storage given core type(s), using encoding in lfit ctx + LiftFlatStringAny, + /// Lift a UTF8 string into provided storage given core type(s) LiftFlatStringUtf8, @@ -223,6 +226,7 @@ impl LiftIntrinsic { Self::LiftFlatFloat32 => "_liftFlatFloat32", Self::LiftFlatFloat64 => "_liftFlatFloat64", Self::LiftFlatChar => "_liftFlatChar", + Self::LiftFlatStringAny => "_liftFlatStringAny", Self::LiftFlatStringUtf8 => "_liftFlatStringUTF8", Self::LiftFlatStringUtf16 => "_liftFlatStringUTF16", Self::LiftFlatRecord => "_liftFlatRecord", @@ -259,8 +263,8 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 1) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 1) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (bool requires 1 byte)`); }} val = new DataView(ctx.memory.buffer).getUint8(ctx.storagePtr, true) === 1; @@ -288,9 +292,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 1) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 1) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (s8 requires 1 byte)`); }} + val = new DataView(ctx.memory.buffer).getInt8(ctx.storagePtr, true); ctx.storagePtr += 1; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 1; }} @@ -303,7 +308,7 @@ impl LiftIntrinsic { Self::LiftFlatU8 => { let debug_log_fn = Intrinsic::DebugLog.name(); let lift_flat_u8_fn = self.name(); - output.push_str(&format!(" + output.push_str(&format!(r#" function {lift_flat_u8_fn}(ctx) {{ {debug_log_fn}('[{lift_flat_u8_fn}()] args', {{ ctx }}); let val; @@ -315,8 +320,8 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 1) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 1) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (u8 requires 1 byte)`); }} val = new DataView(ctx.memory.buffer).getUint8(ctx.storagePtr, true); @@ -326,7 +331,7 @@ impl LiftIntrinsic { return [val, ctx]; }} - ")); + "#)); } Self::LiftFlatS16 => { @@ -341,15 +346,17 @@ impl LiftIntrinsic { if (ctx.params.length === 0) {{ throw new Error('expected at least a single i32 argument'); }} val = ctx.params[0]; ctx.params = ctx.params.slice(1); - }} else {{ - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 2) {{ - throw new Error('not enough storage remaining for lift'); - }} - val = new DataView(ctx.memory.buffer).getInt16(ctx.storagePtr, true); - ctx.storagePtr += 2; - if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 2; }} + return [val, ctx]; }} + if (ctx.storageLen !== undefined && ctx.storageLen < 2) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (s16 requires 2 bytes)`); + }} + + val = new DataView(ctx.memory.buffer).getInt16(ctx.storagePtr, true); + ctx.storagePtr += 2; + if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 2; }} + return [val, ctx]; }} ")); @@ -370,8 +377,8 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 2) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 2) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (u16 requires 2 bytes)`); }} val = new DataView(ctx.memory.buffer).getUint16(ctx.storagePtr, true); @@ -402,9 +409,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 4) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 4) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (s32 requires 4 bytes)`); }} + val = new DataView(ctx.memory.buffer).getInt32(ctx.storagePtr, true); ctx.storagePtr += 4; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 4; }} @@ -429,8 +437,8 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 4) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 4) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (u32 requires 4 bytes)`); }} val = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr, true); ctx.storagePtr += 4; @@ -457,9 +465,11 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 8) {{ - throw new Error('not enough storage remaining for lift'); + + if (ctx.storageLen !== undefined && ctx.storageLen < 8) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (s64 requires 8 bytes)`); }} + val = new DataView(ctx.memory.buffer).getBigInt64(ctx.storagePtr, true); ctx.storagePtr += 8; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 8; }} @@ -485,9 +495,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 8) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 8) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (u64 requires 8 bytes)`); }} + val = new DataView(ctx.memory.buffer).getBigUint64(ctx.storagePtr, true); ctx.storagePtr += 8; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 8; }} @@ -512,9 +523,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 4) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 4) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (f32 requires 4 bytes)`); }} + val = new DataView(ctx.memory.buffer).getFloat32(ctx.storagePtr, true); ctx.storagePtr += 4; @@ -540,9 +552,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 8) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 8) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (f64 requires 8 bytes)`); }} + val = new DataView(ctx.memory.buffer).getFloat64(ctx.storagePtr, true); ctx.storagePtr += 8; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 8; }} @@ -568,9 +581,10 @@ impl LiftIntrinsic { return [val, ctx]; }} - if (ctx.storageLen !== undefined && ctx.storageLen < ctx.storagePtr + 4) {{ - throw new Error('not enough storage remaining for lift'); + if (ctx.storageLen !== undefined && ctx.storageLen < 4) {{ + throw new Error(`insufficient storage ([${{ctx.storageLen}}] bytes) for lift (char requires 4 bytes)`); }} + val = {i32_to_char_fn}(new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr, true)); ctx.storagePtr += 4; if (ctx.storageLen !== undefined) {{ ctx.storageLen -= 4; }} @@ -580,6 +594,24 @@ impl LiftIntrinsic { ")); } + Self::LiftFlatStringAny => { + let lift_flat_string_any_fn = self.name(); + let lift_flat_string_utf8_fn = Self::LiftFlatStringUtf8.name(); + let lift_flat_string_utf16_fn = Self::LiftFlatStringUtf16.name(); + output.push_str(&format!(r#" + function {lift_flat_string_any_fn}(ctx) {{ + switch (ctx.stringEncoding) {{ + case 'utf8': + return {lift_flat_string_utf8_fn}(ctx); + case 'utf16': + return {lift_flat_string_utf16_fn}(ctx); + default: + throw new Error(`missing/unrecognized/unsupported string encoding [${{ctx.stringEncoding}}]`); + }} + }} + "#)); + } + Self::LiftFlatStringUtf8 => { let debug_log_fn = Intrinsic::DebugLog.name(); let decoder = Intrinsic::String(StringIntrinsic::GlobalTextDecoderUtf8).name(); @@ -601,14 +633,17 @@ impl LiftIntrinsic { return [val, ctx]; }} - const start = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr, true); - const codeUnits = new DataView(ctx.memory.buffer).getUint32(ctx.storagePtr + 4, true); + const rem = ctx.storagePtr % 4; + if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} + + const dv = new DataView(ctx.memory.buffer); + const start = dv.getUint32(ctx.storagePtr, true); + const codeUnits = dv.getUint32(ctx.storagePtr + 4, true); + val = {decoder}.decode(new Uint8Array(ctx.memory.buffer, start, codeUnits)); ctx.storagePtr += 8; - - const rem = ctx.storagePtr % 4; - if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} + if (ctx.storageLen !== undefined) {{ ctx.storagelen -= 8; }} return [val, ctx]; }} @@ -658,16 +693,14 @@ impl LiftIntrinsic { if (ctx.useDirectParams) {{ ctx.storagePtr = ctx.params[0]; + ctx.params = ctx.params.slice(1); }} const res = {{}}; - for (const [key, liftFn, _size32, align32] of keysAndLiftFns) {{ + for (const [key, liftFn, _size32, _align32] of keysAndLiftFns) {{ let [val, newCtx] = liftFn(ctx); res[key] = val; ctx = newCtx; - - const rem = ctx.storagePtr % align32; - if (rem !== 0) {{ ctx.storagePtr += align32 - rem; }} }} return [res, ctx]; @@ -747,7 +780,7 @@ impl LiftIntrinsic { output.push_str(&format!(r#" function {lift_flat_list_fn}(meta) {{ - const {{ elemLiftFn, align32, knownLen }} = meta; + const {{ elemLiftFn, elemSize32, elemAlign32, knownLen }} = meta; const readValuesAndReset = (ctx, originalPtr, dataPtr, len) => {{ ctx.storagePtr = dataPtr; @@ -757,37 +790,40 @@ impl LiftIntrinsic { val.push(res); ctx = nextCtx; - const rem = ctx.storagePtr % align32; - if (rem !== 0) {{ ctx.storagePtr += align32 - rem; }} + const rem = ctx.storagePtr % elemAlign32; + if (rem !== 0) {{ ctx.storagePtr += elemAlign32 - rem; }} }} if (originalPtr !== null) {{ ctx.storagePtr = originalPtr; }} return [val, ctx]; }}; + // TODO(fix): special case for u8/u16/etc into appropriate type + return function {lift_flat_list_fn}Inner(ctx) {{ {debug_log_fn}('[{lift_flat_list_fn}()] args', {{ ctx }}); let liftResults; - if (knownLen) {{ // list with known length + if (knownLen !== undefined) {{ // list with known length if (ctx.useDirectParams) {{ // list with known length w/ direct params const dataPtr = ctx.params[0]; ctx.params = ctx.params.slice(1); - // TODO: is it possible for all values to come in from params? + // TODO(???): is it possible for all values to come in from params? ctx.useDirectParams = false; const originalPtr = ctx.storagePtr; - ctx.storageLen = 8; + ctx.storageLen = knownLen * elemSize32; - liftResults = readValuesAndReset(ctx, originalPtr, dataPtr, len); + liftResults = readValuesAndReset(ctx, originalPtr, dataPtr, knownLen); ctx.useDirectParams = true; ctx.storagePtr = null; ctx.storageLen = null; }} else {{ + ctx.storageLen = knownLen * elemSize32; liftResults = readValuesAndReset(ctx, null, ctx.storagePtr, knownLen); }} @@ -801,7 +837,7 @@ impl LiftIntrinsic { ctx.useDirectParams = false; const originalPtr = ctx.storagePtr; - ctx.storageLen = 8; + ctx.storageLen = len * elemSize32; liftResults = readValuesAndReset(ctx, originalPtr, dataPtr, len); @@ -811,6 +847,8 @@ impl LiftIntrinsic { }} else {{ // unknown length list ptr w/ in-memory params + ctx.storageLen = 8; + const dataPtrLiftRes = {lift_u32}(ctx); const dataPtr = dataPtrLiftRes[0]; ctx = dataPtrLiftRes[1]; @@ -822,6 +860,7 @@ impl LiftIntrinsic { const originalPtr = ctx.storagePtr; ctx.storagePtr = dataPtr; + ctx.storageLen = len * elemSize32; liftResults = readValuesAndReset(ctx, originalPtr, dataPtr, len); }} }} @@ -920,7 +959,7 @@ impl LiftIntrinsic { output.push_str(&format!( r#" function {lift_flat_flags_fn}(meta) {{ - const {{ names, size32, align32, intSize }} = meta; + const {{ names, size32, align32, intSizeBytes }} = meta; return function {lift_flat_flags_fn}Inner(ctx) {{ {debug_log_fn}('[{lift_flat_flags_fn}()] args', {{ ctx }}); @@ -929,7 +968,7 @@ impl LiftIntrinsic { let liftRes; let align; - switch (intSize) {{ + switch (intSizeBytes) {{ case 1: liftRes = {lift_u8}(ctx); break; @@ -1063,7 +1102,7 @@ impl LiftIntrinsic { if (ctx.isBorrowed) {{ throw new Error('cannot lift flat stream of borrowed type'); }} if (streamEnd.isWritable()) {{ throw new Error('only readable streams can be lifted'); }} - if (!streamEnd.isIdle()) {{ throw new Error('streams must be in idle state'); }} + if (!streamEnd.isIdleState()) {{ throw new Error('streams must be in idle state'); }} const stream = new {external_stream_class}({{ globalRep: streamEnd.globalStreamMapRep(), diff --git a/crates/js-component-bindgen/src/intrinsics/lower.rs b/crates/js-component-bindgen/src/intrinsics/lower.rs index 83e1cb388..64efba7d3 100644 --- a/crates/js-component-bindgen/src/intrinsics/lower.rs +++ b/crates/js-component-bindgen/src/intrinsics/lower.rs @@ -150,6 +150,9 @@ pub enum LowerIntrinsic { /// Lower a char into provided storage given core type(s) LowerFlatChar, + /// Lower a string into provided storage given core type(s), using encoding in lower ctx + LowerFlatStringAny, + /// Lower a UTF8 string into provided storage given core type(s) LowerFlatStringUtf8, @@ -222,6 +225,7 @@ impl LowerIntrinsic { Self::LowerFlatFloat32 => "_lowerFlatFloat32", Self::LowerFlatFloat64 => "_lowerFlatFloat64", Self::LowerFlatChar => "_lowerFlatChar", + Self::LowerFlatStringAny => "_lowerFlatStringAny", Self::LowerFlatStringUtf8 => "_lowerFlatStringUTF8", Self::LowerFlatStringUtf16 => "_lowerFlatStringUTF16", Self::LowerFlatRecord => "_lowerFlatRecord", @@ -245,108 +249,145 @@ impl LowerIntrinsic { match self { Self::LowerFlatBool => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatBool(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatBool()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(r#" + function _lowerFlatBool(ctx) {{ + {debug_log_fn}('[_lowerFlatBool()] args', {{ ctx }}); + + if (!ctx.memory) {{ throw new Error("missing memory for lower"); }} + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] !== 0 && vals[0] !== 1) {{ throw new Error('invalid value for core value representing bool'); }} - new DataView(memory.buffer).setUint32(storagePtr, vals[0], true); - return 1; + + {require_valid_numeric_primitive_fn}.bind('bool', ctx.vals[0]); + new DataView(ctx.memory.buffer).setUint32(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 1; }} - ")); + "#)); } Self::LowerFlatS8 => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatS8(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatS8()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(r#" + function _lowerFlatS8(ctx) {{ + {debug_log_fn}('[_lowerFlatS8()] args', {{ ctx }}); + + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] > 127 || vals[0] < -128) {{ throw new Error('invalid value for core value representing s8'); }} - new DataView(memory.buffer).setInt32(storagePtr, vals[0], true); - return 8; + if (!ctx.memory) {{ throw new Error("missing memory for lower"); }} + + {require_valid_numeric_primitive_fn}.bind('s8', ctx.vals[0]); + new DataView(ctx.memory.buffer).setInt32(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 1; }} - ")); + "#)); } Self::LowerFlatU8 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_u8_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(r#" - function _lowerFlatU8(ctx) {{ - {debug_log_fn}('[_lowerFlatU8()] args', ctx); - const {{ memory, realloc, vals, storagePtr, storageLen }} = ctx; - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + function {lower_flat_u8_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_u8_fn}()] args', ctx); + + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] > 255 || vals[0] < 0) {{ throw new Error('invalid value for core value representing u8'); }} - if (!memory) {{ throw new Error("missing memory for lower"); }} - new DataView(memory.buffer).setUint32(storagePtr, vals[0], true); - // TODO: ALIGNMENT IS WRONG? + {require_valid_numeric_primitive_fn}.bind('u8', ctx.vals[0]); + + if (!ctx.memory) {{ throw new Error("missing memory for lower"); }} + new DataView(ctx.memory.buffer).setUint32(ctx.storagePtr, ctx.vals[0], true); - return 1; + ctx.storagePtr += 1; }} "#)); } - // TODO: alignment checks Self::LowerFlatS16 => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatS16(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatS16()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + let lower_flat_s16_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + + output.push_str(&format!(r#" + function {lower_flat_s16_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_s16_fn}()] args', {{ ctx }}); + + if (!ctx.memory) {{ throw new Error("missing memory for lower"); }} + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] > 32_767 || vals[0] < -32_768) {{ throw new Error('invalid value for core value representing s16'); }} - new DataView(memory.buffer).setInt16(storagePtr, vals[0], true); - return 2; + + const rem = ctx.storagePtr % 2; + if (rem !== 0) {{ ctx.storagePtr += (2 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('s16', ctx.vals[0]); + new DataView(ctx.memory.buffer).setInt16(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 2; }} - ")); + "#)); } Self::LowerFlatU16 => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatU16(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatU16()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + let lower_flat_u16_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + + output.push_str(&format!(r#" + function {lower_flat_u16_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_u16_fn}()] args', {{ ctx }}); + + if (!ctx.memory) {{ throw new Error("missing memory for lower"); }} + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] > 65_535 || vals[0] < 0) {{ throw new Error('invalid value for core value representing u16'); }} - new DataView(memory.buffer).setUint16(storagePtr, vals[0], true); - return 2; + + const rem = ctx.storagePtr % 2; + if (rem !== 0) {{ ctx.storagePtr += (2 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('u16', ctx.vals[0]); + new DataView(ctx.memory.buffer).setUint16(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 2; }} - ")); + "#)); } Self::LowerFlatS32 => { let debug_log_fn = Intrinsic::DebugLog.name(); let lower_flat_s32_fn = self.name(); - output.push_str(&format!(" + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + + output.push_str(&format!(r#" function {lower_flat_s32_fn}(ctx) {{ {debug_log_fn}('[{lower_flat_s32_fn}()] args', {{ ctx }}); - const {{ memory, realloc, vals, storagePtr, storageLen }} = ctx; - if (vals.length !== 1) {{ - throw new Error('unexpected number (' + vals.length + ') of core vals (expected 1)'); + if (ctx.vals.length !== 1) {{ + throw new Error(`unexpected number [${{ctx.vals.length}}] of vals (expected 1)`); }} - if (vals[0] > 2_147_483_647 || vals[0] < -2_147_483_648) {{ throw new Error('invalid value for core value representing s32'); }} - // TODO(refactor): fail loudly on misaligned flat lowers? const rem = ctx.storagePtr % 4; if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} - new DataView(memory.buffer).setInt32(storagePtr, vals[0], true); - return 4; - + {require_valid_numeric_primitive_fn}.bind('s32', ctx.vals[0]); + new DataView(ctx.memory.buffer).setInt32(ctx.storagePtr, ctx.vals[0], true); + ctx.storagePtr += 4; }} - ")); + "#)); } // TODO(fix) can u32s be lowered indirectly? maybe never? @@ -354,70 +395,118 @@ impl LowerIntrinsic { // to the function versus params that actually indicate where to write! Self::LowerFlatU32 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_u32_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(r#" - function _lowerFlatU32(ctx) {{ - {debug_log_fn}('[_lowerFlatU32()] args', {{ ctx }}); - const {{ memory, realloc, vals, storagePtr, storageLen }} = ctx; - if (vals.length !== 1) {{ throw new Error('expected single value to lower, got (' + vals.length + ')'); }} - if (vals[0] > 4_294_967_295 || vals[0] < 0) {{ throw new Error('invalid value for core value representing u32'); }} + function {lower_flat_u32_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_u32_fn}()] args', {{ ctx }}); + + if (ctx.vals.length !== 1) {{ + throw new Error(`expected single value to lower, got [${{ctx.vals.length}}]`); + }} - // TODO(refactor): fail loudly on misaligned flat lowers? const rem = ctx.storagePtr % 4; if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} - new DataView(memory.buffer).setUint32(storagePtr, vals[0], true); + {require_valid_numeric_primitive_fn}.bind('u32', ctx.vals[0]); + new DataView(ctx.memory.buffer).setUint32(ctx.storagePtr, ctx.vals[0], true); - return 4; + ctx.storagePtr += 4; }} "#)); } Self::LowerFlatS64 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_s64_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(" - function _lowerFlatS64(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatS64()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - if (vals[0] > 9_223_372_036_854_775_807n || vals[0] < -9_223_372_036_854_775_808n) {{ throw new Error('invalid value for core value representing s64'); }} - new DataView(memory.buffer).setBigInt64(storagePtr, vals[0], true); - return 8; + function {lower_flat_s64_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_s64_fn}()] args', {{ ctx }}); + + if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + + const rem = ctx.storagePtr % 8; + if (rem !== 0) {{ ctx.storagePtr += (8 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('s64', ctx.vals[0]); + new DataView(ctx.memory.buffer).setBigInt64(ctx.storagePtr, ctx.vals[0], true); + + + ctx.storagePtr += 8; }} ")); } Self::LowerFlatU64 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_u64_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(" - function _lowerFlatU64(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatU64()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - if (vals[0] > 18_446_744_073_709_551_615n || vals[0] < 0n) {{ throw new Error('invalid value for core value representing u64'); }} - new DataView(memory.buffer).setBigUint64(storagePtr, vals[0], true); - return 8; + function {lower_flat_u64_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_u64_fn}()] args', {{ ctx }}); + + if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + + const rem = ctx.storagePtr % 8; + if (rem !== 0) {{ ctx.storagePtr += (8 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('u64', ctx.vals[0]); + new DataView(ctx.memory.buffer).setBigUint64(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 8; }} ")); } Self::LowerFlatFloat32 => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatFloat32(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatFloat32()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - new DataView(memory.buffer).setFloat32(storagePtr, vals[0], true); - return 4; + let lower_flat_f32_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + + output.push_str(&format!(r#" + function {lower_flat_f32_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_f32_fn}()] args', {{ ctx }}); + + if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + + const rem = ctx.storagePtr % 4; + if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('f32', ctx.vals[0]); + new DataView(ctx.memory.buffer).setFloat32(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 4; }} - ")); + "#)); } Self::LowerFlatFloat64 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_f64_fn = self.name(); + let require_valid_numeric_primitive_fn = + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive).name(); + output.push_str(&format!(" - function _lowerFlatFloat64(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatFloat64()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - new DataView(memory.buffer).setFloat64(storagePtr, vals[0], true); - return 8; + function {lower_flat_f64_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_f64_fn}()] args', {{ ctx }}); + + if (vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + + const rem = ctx.storagePtr % 8; + if (rem !== 0) {{ ctx.storagePtr += (8 - rem); }} + + {require_valid_numeric_primitive_fn}.bind('f64', ctx.vals[0]); + new DataView(ctx.memory.buffer).setFloat64(ctx.storagePtr, ctx.vals[0], true); + + ctx.storagePtr += 8; }} ")); } @@ -426,82 +515,110 @@ impl LowerIntrinsic { let i32_to_char_fn = Intrinsic::Conversion(ConversionIntrinsic::I32ToChar).name(); let debug_log_fn = Intrinsic::DebugLog.name(); output.push_str(&format!(" - function _lowerFlatChar(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatChar()] args', {{ memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - new DataView(memory.buffer).setUint32(storagePtr, {i32_to_char_fn}(vals[0]), true); - return 4; + function _lowerFlatChar(ctx) {{ + {debug_log_fn}('[_lowerFlatChar()] args', {{ ctx }}); + + const rem = ctx.storagePtr % 4; + if (rem !== 0) {{ ctx.storagePtr += (4 - rem); }} + + if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + new DataView(ctx.memory.buffer).setUint32(ctx.storagePtr, {i32_to_char_fn}(ctx.vals[0]), true); + + ctx.storagePtr += 4; + }} + ")); + } + + Self::LowerFlatStringAny => { + let lower_flat_string_any_fn = self.name(); + let lower_flat_string_utf8_fn = Self::LowerFlatStringUtf8.name(); + let lower_flat_string_utf16_fn = Self::LowerFlatStringUtf16.name(); + output.push_str(&format!(" + function {lower_flat_string_any_fn}(ctx) {{ + switch (ctx.stringEncoding) {{ + case 'utf8': + return {lower_flat_string_utf8_fn}(ctx); + case 'utf16': + return {lower_flat_string_utf16_fn}(ctx); + default: + throw new Error(`missing/unrecognized/unsupported string encoding [${{ctx.stringEncoding}}]`); + }} }} ")); } Self::LowerFlatStringUtf16 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_string_utf16_fn = self.name(); + let utf16_encode_fn = Intrinsic::String(StringIntrinsic::Utf16Encode).name(); + output.push_str(&format!(" - function _lowerFlatStringUTF16(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatStringUTF16()] args', {{ memory, vals, storagePtr, storageLen }}); - const start = new DataView(memory.buffer).getUint32(storagePtr, vals[0], true); - const codeUnits = new DataView(memory.buffer).getUint32(storagePtr, vals[0] + 4, true); - var bytes = new Uint16Array(memory.buffer, start, codeUnits); - if (memory.buffer.byteLength < start + bytes.byteLength) {{ + function {lower_flat_string_utf16_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_string_utf16_fn}()] args', {{ ctx }}); + if (!ctx.realloc) {{ throw new Error('missing realloc during flat string lower'); }} + + const s = ctx.vals[0]; + const {{ ptr, len, codepoints }} = {utf16_encode_fn}(ctx.vals[0], ctx.realloc, ctx.memory); + + const view = new DataView(ctx.memory.buffer); + view.setUint32(ctx.storagePtr, ptr, true); + view.setUint32(ctx.storagePtr + 4, codepoints, true); + + const bytes = new Uint16Array(ctx.memory.buffer, start, codeUnits); + if (ctx.memory.buffer.byteLength < start + bytes.byteLength) {{ throw new Error('memory out of bounds'); }} - if (storageLen !== undefined && storageLen !== bytes.byteLength) {{ - throw new Error('storage length (' + storageLen + ') != (' + bytes.byteLength + ')'); + if (ctx.storageLen !== undefined && ctx.storageLen !== bytes.byteLength) {{ + throw new Error(`storage length [${{ctx.storageLen}}] != [${{bytes.byteLength}}])`); }} - new Uint16Array(memory.buffer, storagePtr).set(bytes); - return bytes.byteLength; + new Uint16Array(ctx.memory.buffer, ctx.storagePtr).set(bytes); + + ctx.storagePtr += len; }} ")); } Self::LowerFlatStringUtf8 => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_string_utf8_fn = self.name(); let utf8_encode_fn = Intrinsic::String(StringIntrinsic::Utf8Encode).name(); - output.push_str(&format!(" - function _lowerFlatStringUTF8(ctx) {{ - {debug_log_fn}('[_lowerFlatStringUTF8()] args', ctx); - const {{ memory, realloc, vals, storagePtr, storageLen }} = ctx; + output.push_str(&format!(r#" + function {lower_flat_string_utf8_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_string_utf8_fn}()] args', ctx); + if (!ctx.realloc) {{ throw new Error('missing realloc during flat string lower'); }} - const s = vals[0]; - const {{ ptr, len, codepoints }} = {utf8_encode_fn}(vals[0], realloc, memory); + const s = ctx.vals[0]; + const {{ ptr, codepoints }} = {utf8_encode_fn}(ctx.vals[0], ctx.realloc, ctx.memory); - const view = new DataView(memory.buffer); - view.setUint32(storagePtr, ptr, true); - view.setUint32(storagePtr + 4, codepoints, true); + const view = new DataView(ctx.memory.buffer); + view.setUint32(ctx.storagePtr, ptr, true); + view.setUint32(ctx.storagePtr + 4, codepoints, true); - return len; + ctx.storagePtr += 8; }} - ")); + "#)); } Self::LowerFlatRecord => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatRecord(fieldMetas) {{ - return (size, memory, vals, storagePtr, storageLen) => {{ - const params = [...arguments].slice(5); - {debug_log_fn}('[_lowerFlatRecord()] args', {{ - size, - memory, - vals, - storagePtr, - storageLen, - params, - fieldMetas - }}); - - const [start] = vals; - if (storageLen !== undefined && size !== undefined && size > storageLen) {{ - throw new Error('not enough storage remaining for record flat lower'); + let lower_flat_record_fn = self.name(); + + output.push_str(&format!( + r#" + function {lower_flat_record_fn}(fieldMetas) {{ + return function {lower_flat_record_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_record_fn}()] args', {{ ctx }}); + + const r = ctx.vals[0]; + for (const [tag, lowerFn, size32, align32 ] of fieldMetas) {{ + ctx.vals = [r[tag]]; + lowerFn(ctx); }} - const data = new Uint8Array(memory.buffer, start, size); - new Uint8Array(memory.buffer, storagePtr, size).set(data); - return data.byteLength; }} }} - ")); + "# + )); } Self::LowerFlatVariant => { @@ -513,22 +630,25 @@ impl LowerIntrinsic { output.push_str(&format!(r#" function {lower_flat_variant_fn}(lowerMetas) {{ - return function {lower_flat_variant_fn}Inner(ctx) {{ - {debug_log_fn}('[{lower_flat_variant_fn}()] args', ctx); + let caseLookup = {{}}; + for (const [idx, meta] of lowerMetas.entries()) {{ + let tag = meta[0]; + caseLookup[tag] = {{ discriminant: idx, meta }}; + }} - const {{ memory, realloc, vals, storageLen, componentIdx }} = ctx; - let storagePtr = ctx.storagePtr; + return function {lower_flat_variant_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_variant_fn}()] args', {{ ctx }}); - const {{ tag, val }} = vals[0]; - const disc = lowerMetas.findIndex(m => m[0] === tag); - if (disc === -1) {{ - throw new Error(`invalid variant tag/discriminant [${{tag}}] (valid tags: ${{variantMetas.map(m => m[0])}})`); + const {{ tag, val }} = ctx.vals[0]; + const variantCase = caseLookup[tag]; + if (!variantCase) {{ + throw new Error(`missing tag [${{tag}}] (valid tags: ${{Object.keys(caseLookup)}})`); }} - const [ _tag, lowerFn, size32, align32, payloadOffset32 ] = lowerMetas[disc]; + const [ _tag, lowerFn, size32, align32, payloadOffset32 ] = variantCase.meta; - const originalPtr = ctx.resultPtr; - ctx.vals = [disc]; + const originalPtr = ctx.storagePtr; + ctx.vals = [variantCase.discriminant]; let discLowerRes; if (lowerMetas.length < 256) {{ discLowerRes = {lower_u8_fn}(ctx); @@ -537,23 +657,15 @@ impl LowerIntrinsic { }} else if (lowerMetas.length >= 65536 && lowerMetas.length < 4_294_967_296) {{ discLowerRes = {lower_u32_fn}(ctx); }} else {{ - throw new Error('unsupported number of cases [' + lowerMetas.legnth + ']'); + throw new Error(`unsupported number of cases [${{lowerMetas.length}}]`); }} - ctx.resultPtr = originalPtr + payloadOffset32; + const payloadOffsetPtr = originalPtr + payloadOffset32; + ctx.storagePtr = payloadOffsetPtr; + ctx.vals = [val]; + if (lowerFn) {{ lowerFn(ctx); }} - let payloadBytesWritten = 0; - if (lowerFn) {{ - lowerFn({{ - memory, - realloc, - vals: [val], - storagePtr, - storageLen, - componentIdx, - }}); - }} - let bytesWritten = payloadOffset + payloadBytesWritten; + let bytesWritten = ctx.storagePtr - payloadOffsetPtr; const rem = ctx.storagePtr % align32; if (rem !== 0) {{ @@ -562,7 +674,7 @@ impl LowerIntrinsic { bytesWritten += pad; }} - return bytesWritten; + ctx.storagePtr += bytesWritten; }} }} "#)); @@ -571,47 +683,97 @@ impl LowerIntrinsic { Self::LowerFlatList => { let debug_log_fn = Intrinsic::DebugLog.name(); let lower_flat_list_fn = self.name(); + let lower_u32_fn = Self::LowerFlatU32.name(); + output.push_str(&format!(r#" - function {lower_flat_list_fn}(args) {{ - const {{ elemLowerFn }} = args; + function {lower_flat_list_fn}(meta) {{ + const {{ + elemLowerFn, + knownLen, + size32, + align32, + elemSize32, + elemAlign32, + }} = meta; + if (!elemLowerFn) {{ throw new TypeError("missing/invalid element lower fn for list"); }} return function {lower_flat_list_fn}Inner(ctx) {{ - {debug_log_fn}('[_lowerFlatList()] args', {{ ctx }}); - - if (ctx.params.length < 2) {{ throw new Error('insufficient params left to lower list'); }} - const storagePtr = ctx.params[0]; - const elemCount = ctx.params[1]; - ctx.params = ctx.params.slice(2); + {debug_log_fn}('[{lower_flat_list_fn}()] args', {{ ctx }}); if (ctx.useDirectParams) {{ + if (ctx.params.length < 2) {{ throw new Error('insufficient params left to lower list'); }} + const storagePtr = ctx.params[0]; + const elemCount = ctx.params[1]; + ctx.params = ctx.params.slice(2); + const list = ctx.vals[0]; if (!list) {{ throw new Error("missing direct param value"); }} - const elemLowerCtx = {{ storagePtr, memory: ctx.memory }}; + const lowerCtx = {{ + storagePtr, + memory: ctx.memory, + stringEncoding: ctx.stringEncoding, + }}; for (let idx = 0; idx < list.length; idx++) {{ - elemLowerCtx.vals = list.slice(idx, idx+1); - elemLowerCtx.storagePtr += elemLowerFn(elemLowerCtx); + lowerCtx.vals = list.slice(idx, idx+1); + elemLowerFn(lowerCtx); }} - const bytesLowered = elemLowerCtx.storagePtr - ctx.storagePtr; - ctx.storagePtr = elemLowerCtx.storagePtr; - return bytesLowered; + const bytesLowered = lowerCtx.storagePtr - ctx.storagePtr; + ctx.storagePtr = lowerCtx.storagePtr; + + // TODO: implement parma-only known-length processing + + ctx.storagePtr += bytesLowered; + return; }} - if (ctx.vals.length !== 2) {{ - throw new Error('indirect parameter loading must have a pointer and length as vals'); + // TODO(fix): is it possible to get a vals that are a addr and length here from + // a component lower? + + const elems = ctx.vals[0]; + if (knownLen === undefined) {{ + // unknown length + if (!ctx.realloc) {{ throw new Error('missing realloc during flat string lower'); }} + const dataPtr = ctx.realloc(0, 0, elemAlign32, elemSize32 * elems.length); + + ctx.vals[0] = dataPtr; + {lower_u32_fn}(ctx); + + ctx.vals[0] = elems.length; + {lower_u32_fn}(ctx); + + const origPtr = ctx.storagePtr; + ctx.storagePtr = dataPtr; + + ctx.storagePtr = dataPtr; + for (const elem of elems) {{ + ctx.vals = [elem]; + elemLowerFn(ctx); + }} + + ctx.storagePtr = origPtr; + + }} else {{ + // known length + + if (elems.length !== knownLen) {{ + throw new TypeError(`invalid list input of length [${{elems.length}}], must be length [${{knownLen}}]`); + }} + + for (const elem of elems) {{ + ctx.vals = [elem]; + elemLowerFn(ctx); + }} }} - let [valStartPtr, valLen] = ctx.vals; - const totalSizeBytes = valLen * size; + + // TODO(fix): special case for u8/u16/etc, we can do a direct copy + + const totalSizeBytes = elems.length * size32; if (ctx.storageLen !== undefined && totalSizeBytes > ctx.storageLen) {{ throw new Error('not enough storage remaining for list flat lower'); }} - - const data = new Uint8Array(memory.buffer, valStartPtr, totalSizeBytes); - new Uint8Array(memory.buffer, storagePtr, totalSizeBytes).set(data); - - return totalSizeBytes; }} }} "#)); @@ -619,65 +781,122 @@ impl LowerIntrinsic { Self::LowerFlatTuple => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatTuple(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatTuple()] args', {{ size, memory, vals, storagePtr, storageLen }}); - let [start, len] = vals; - if (storageLen !== undefined && len > storageLen) {{ - throw new Error('not enough storage remaining for tuple flat lower'); + let lower_flat_tuple_fn = self.name(); + + output.push_str(&format!( + r#" + function {lower_flat_tuple_fn}(elemLowerMetas) {{ + return function {lower_flat_tuple_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_tuple_fn}()] args', {{ ctx }}); + const tuple = ctx.vals[0]; + for (const [idx, [ lowerFn, size32, align32 ]] of elemLowerMetas.entries()) {{ + ctx.vals = [tuple[idx]]; + lowerFn(ctx); + }} }} - const data = new Uint8Array(memory.buffer, start, len); - new Uint8Array(memory.buffer, storagePtr, len).set(data); - return data.byteLength; }} - ")); + "# + )); } Self::LowerFlatFlags => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatFlags(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatFlags()] args', {{ size, memory, vals, storagePtr, storageLen }}); - if (vals.length !== 1) {{ throw new Error('unexpected number of core vals'); }} - new DataView(memory.buffer).setInt32(storagePtr, vals[0], true); - return 4; + let lower_flat_flags_fn = self.name(); + + output.push_str(&format!(r#" + function {lower_flat_flags_fn}(meta) {{ + const {{ names, size32, align32, intSizeBytes }} = meta; + + return function {lower_flat_flags_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_flags_fn}()] args', {{ ctx }}); + if (ctx.vals.length !== 1) {{ throw new Error('unexpected number of vals'); }} + + let flagObj = ctx.vals[0]; + let flagValue = 0; + for (const [idx, name] of names.entries()) {{ + if (flagObj[name] === true) {{ + flagValue |= 1 << idx; + }} + }} + + const rem = ctx.storagePtr % align32; + if (rem !== 0) {{ ctx.storagePtr += (align32 - rem); }} + + const dv = new DataView(ctx.memory.buffer); + if (intSizeBytes === 1) {{ + dv.setUint8(ctx.storagePtr, flagValue); + }} else if (intSizeBytes === 2) {{ + dv.setUint16(ctx.storagePtr, flagValue); + }} else if (intSizeBytes === 4) {{ + dv.setUint32(ctx.storagePtr, flagValue); + }} else {{ + throw new Error(`unrecognized flag size [${{intSizeBytes}} bytes]`); + }} + + ctx.storagePtr += intSizeBytes; + }} }} - ")); + "#)); } Self::LowerFlatEnum => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatEnum(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatEnum()] args', {{ size, memory, vals, storagePtr, storageLen }}); - let [start] = vals; - if (storageLen !== undefined && size !== undefined && size > storageLen) {{ - throw new Error('not enough storage remaining for enum flat lower'); + let lower_flat_enum_fn = self.name(); + let lower_variant_fn = Self::LowerFlatVariant.name(); + + output.push_str(&format!( + r#" + function {lower_flat_enum_fn}(lowerMetas) {{ + return function {lower_flat_enum_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_enum_fn}()] args', {{ ctx }}); + + const v = ctx.vals[0]; + const isNotEnumObject = typeof v !== 'object' + || Object.keys(v).length !== 2 + || !('tag' in v); + if (isNotEnumObject) {{ + ctx.vals[0] = {{ tag: v }}; + }} + + {lower_variant_fn}(lowerMetas)(ctx); }} - const data = new Uint8Array(memory.buffer, start, size); - new Uint8Array(memory.buffer, storagePtr, size).set(data); - return data.byteLength; }} - ")); + "# + )); } Self::LowerFlatOption => { let debug_log_fn = Intrinsic::DebugLog.name(); let lower_flat_option_fn = self.name(); let lower_variant_fn = Self::LowerFlatVariant.name(); + output.push_str(&format!( " function {lower_flat_option_fn}(lowerMetas) {{ - function {lower_flat_option_fn}Inner(ctx) {{ + return function {lower_flat_option_fn}Inner(ctx) {{ {debug_log_fn}('[{lower_flat_option_fn}()] args', {{ ctx }}); - return {lower_variant_fn}(lowerMetas)(ctx); + + const v = ctx.vals[0]; + if (v === null) {{ + ctx.vals[0] = {{ tag: 'none' }}; + }} else {{ + const isNotOptionObject = typeof v !== 'object' + || Object.keys(v).length !== 2 + || !('tag' in v) + || !(v.tag === 'some' || v.tag === 'none') + || !('val' in v); + if (isNotOptionObject) {{ + ctx.vals[0] = {{ tag: 'some', val: v }}; + }} + }} + + {lower_variant_fn}(lowerMetas)(ctx); }} }} " )); } - // Results are just a special case of lowering variants Self::LowerFlatResult => { let debug_log_fn = Intrinsic::DebugLog.name(); let lower_flat_result_fn = self.name(); @@ -687,89 +906,114 @@ impl LowerIntrinsic { function {lower_flat_result_fn}(lowerMetas) {{ return function {lower_flat_result_fn}Inner(ctx) {{ {debug_log_fn}('[{lower_flat_result_fn}()] args', {{ lowerMetas }}); - return {lower_variant_fn}(lowerMetas)(ctx); + + const v = ctx.vals[0]; + const isNotResultObject = typeof v !== 'object' + || Object.keys(v).length !== 2 + || !('tag' in v) + || !('ok' === v.tag || 'err' === v.tag) + || !('val' in v); + if (isNotResultObject) {{ + ctx.vals[0] = {{ tag: 'ok', val: v }}; + }} + + {lower_variant_fn}(lowerMetas)(ctx); }}; }} "# )); } + // TODO: implement lower flat own Self::LowerFlatOwn => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatOwn(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatOwn()] args', {{ size, memory, vals, storagePtr, storageLen }}); + let lower_flat_own_fn = self.name(); + output.push_str(&format!( + " + function {lower_flat_own_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_own_fn}()] args', {{ ctx }}); throw new Error('flat lower for owned resources not yet implemented!'); }} - ")); + " + )); } Self::LowerFlatBorrow => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatBorrow(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatBorrow()] args', {{ size, memory, vals, storagePtr, storageLen }}); - throw new Error('flat lower for borrowed resources not yet implemented!'); + let lower_flat_borrow_fn = self.name(); + output.push_str(&format!( + " + function {lower_flat_borrow_fn}(ctx) {{ + {debug_log_fn}('[{lower_flat_borrow_fn}()] args', {{ ctx }}); + throw new Error('flat lower for borrowed resources is not supported!'); }} - ")); + " + )); } Self::LowerFlatFuture => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatFuture(memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatFuture()] args', {{ size, memory, vals, storagePtr, storageLen }}); + let lower_flat_future_fn = self.name(); + output.push_str(&format!( + " + function {lower_flat_future_fn}(futureTableIdx, ctx) {{ + {debug_log_fn}('[{lower_flat_future_fn}()] args', {{ ctx }}); throw new Error('flat lower for futures not yet implemented!'); }} - ")); + " + )); } Self::LowerFlatStream => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_stream_fn = self.name(); let global_stream_map = AsyncStreamIntrinsic::GlobalStreamMap.name(); let external_stream_class = AsyncStreamIntrinsic::ExternalStreamClass.name(); let internal_stream_class = AsyncStreamIntrinsic::InternalStreamClass.name(); - // TODO: fix writable is getting dropped before it can be read!! - // We need to do some waiting? - // Last write should have been triggering the reader to progress... - // Then the last reads return all the data??? - output.push_str(&format!( r#" - function _lowerFlatStream(streamTableIdx, ctx) {{ - {debug_log_fn}('[_lowerFlatStream()] args', {{ streamTableIdx, ctx }}); + function {lower_flat_stream_fn}(meta) {{ const {{ - memory, - realloc, - vals, - storagePtr: resultPtr, - }} = ctx; - - const externalStream = vals[0]; - if (!externalStream || !(externalStream instanceof {external_stream_class})) {{ - throw new Error("invalid external stream value"); - }} + streamTableIdx, + componentIdx, + isBorrowedType, + isNoneType, + isNumericTypeJs, + }} = meta; + + return function {lower_flat_stream_fn}Inner(ctx) {{ + {debug_log_fn}('[{lower_flat_stream_fn}()] args', {{ ctx }}); + + // TODO(fix): This stream could be a stream from the host, which is + // any async-iterator capable thing (see Instruction::StreamLower), + // not only an ExternalStream which is used by the guest??? + + const externalStream = ctx.vals[0]; + if (!externalStream || !(externalStream instanceof {external_stream_class})) {{ + throw new Error("invalid external stream value"); + }} - const globalRep = externalStream.globalRep(); - const internalStream = {global_stream_map}.get(globalRep); - if (!internalStream || !(internalStream instanceof {internal_stream_class})) {{ - throw new Error(`failed to find internal stream with rep [${{globalRep}}]`); - }} + const globalRep = externalStream.globalRep(); + const internalStream = {global_stream_map}.get(globalRep); + if (!internalStream || !(internalStream instanceof {internal_stream_class})) {{ + throw new Error(`failed to find internal stream with rep [${{globalRep}}]`); + }} - const readEnd = internalStream.readEnd(); - const waitableIdx = readEnd.waitableIdx(); + const readEnd = internalStream.readEnd(); + const waitableIdx = readEnd.waitableIdx(); - // Write the idx of the waitable to memory (a waiting async task or caller) - if (resultPtr) {{ - new DataView(memory.buffer).setUint32(resultPtr, waitableIdx, true); - }} + // Write the idx of the waitable to memory (a waiting async task or caller) + if (ctx.storagePtr) {{ + new DataView(ctx.memory.buffer).setUint32(ctx.storagePtr, waitableIdx, true); + }} - // TODO: if we flat lower another way (host -> guest async) we need to actually - // modify the guests table's afresh, we can't just use the global rep! - // (can detect this by whether the external stream has a rep or not) + // TODO: if we flat lower another way (host -> guest async) we need to actually + // modify the guests table's afresh, we can't just use the global rep! + // (can detect this by whether the external stream has a rep or not) - return waitableIdx + return waitableIdx; + }} }} "# )); @@ -784,6 +1028,7 @@ impl LowerIntrinsic { // see: `LiftIntrinsic::LiftFlatErrorContext` Self::LowerFlatErrorContext => { let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_flat_error_context_fn = self.name(); let lower_u32_fn = Self::LowerFlatU32.name(); let create_local_handle_fn = ErrCtxIntrinsic::CreateLocalHandle.name(); let err_ctx_global_ref_count_add_fn = ErrCtxIntrinsic::GlobalRefCountAdd.name(); @@ -794,8 +1039,8 @@ impl LowerIntrinsic { // NOTE: at this point the error context has already been lowered into the appropriate // place for us via error context transfer. output.push_str(&format!(r#" - function _lowerFlatErrorContext(errCtxTableIdx, ctx) {{ - {debug_log_fn}('[_lowerFlatErrorContext()] args', {{ errCtxTableIdx, ctx }}); + function {lower_flat_error_context_fn}(errCtxTableIdx, ctx) {{ + {debug_log_fn}('[{lower_flat_error_context_fn}()] args', {{ errCtxTableIdx, ctx }}); const {{ memory, realloc, vals, storagePtr, storageLen, componentIdx }} = ctx; const errCtxGlobalRep = vals[0]; diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index ac82b5d0b..cc8f869b0 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -89,10 +89,17 @@ pub enum Intrinsic { SymbolResourceHandle, SymbolResourceRep, SymbolDispose, + SymbolAsyncIterator, + SymbolIterator, ScopeId, DefinedResourceTables, HandleTables, + /// Class that conforms to a `ReadableStreams`-like interface and is usable externally + /// + /// This is normally the `ReadableStream` class provided by the platform itself. + PlatformReadableStreamClass, + // Global Initializers FinalizationRegistryCreate, @@ -384,11 +391,23 @@ impl Intrinsic { ", ), - Intrinsic::SymbolDispose => output.push_str( - " - const symbolDispose = Symbol.dispose || Symbol.for('dispose'); - ", - ), + Intrinsic::SymbolDispose => { + let var_name = self.name(); + uwriteln!( + output, + "const {var_name} = Symbol.dispose || Symbol.for('dispose');" + ); + } + + Intrinsic::SymbolAsyncIterator => { + let var_name = self.name(); + uwriteln!(output, "const {var_name} = Symbol.asyncIterator;"); + } + + Intrinsic::SymbolIterator => { + let var_name = self.name(); + uwriteln!(output, "const {var_name} = Symbol.iterator;"); + } Intrinsic::ThrowInvalidBool => output.push_str( " @@ -525,7 +544,12 @@ impl Intrinsic { this.#ptr = this.#start; this.capacity = args.capacity; this.#elemMeta = args.elemMeta; + + if (args.data !== undefined && !Array.isArray(args.data)) {{ + throw new TypeError('host-only data must be an array'); + }} this.#hostOnlyData = args.data; + this.target = args.target; }} @@ -563,10 +587,14 @@ impl Intrinsic { }} else {{ let currentCount = count; let startPtr = this.#ptr; + if (this.#elemMeta.stringEncoding === undefined) {{ + throw new Error('string encoding unknown during read'); + }} let liftCtx = {{ storagePtr: startPtr, memory: this.#memory, componentIdx: this.#componentIdx, + stringEncoding: this.#elemMeta.stringEncoding, }}; if (currentCount < 0) {{ throw new Error('unexpectedly invalid count'); }} while (currentCount > 0) {{ @@ -600,15 +628,23 @@ impl Intrinsic { this.#hostOnlyData.push(...values); }} else {{ let startPtr = this.#ptr; + if (this.#elemMeta.stringEncoding === undefined) {{ + throw new Error('string encoding unknown during write'); + }} + + const lowerCtx = {{ + memory: this.#memory, + storagePtr: startPtr, + componentIdx: this.#componentIdx, + stringEncoding: this.#elemMeta.stringEncoding, + realloc: this.#elemMeta.reallocFn, + }} for (const v of values) {{ - startPtr += this.#elemMeta.lowerFn({{ - memory: this.#memory, - storagePtr: startPtr, - componentIdx: this.#componentIdx, - vals: [v], - }}); + lowerCtx.vals = [v]; + this.#elemMeta.lowerFn(lowerCtx); }} - this.#ptr = startPtr; + + this.#ptr = lowerCtx.storagePtr; }} }} @@ -673,6 +709,7 @@ impl Intrinsic { elemMeta: args.elemMeta, data: args.data, target: args.target, + stringEncoding: args.stringEncoding, }}); if (instanceBuffers.has(nextBufID)) {{ @@ -959,7 +996,7 @@ impl Intrinsic { const {{ taskID, componentIdx }} = args; const meta = {global_current_task_meta_obj}[componentIdx]; - if (!meta) {{ throw new Error(`missing current task meta for component idx [${{componentIdx}}]`); }} + if (!meta) {{ throw new Error(`missing current task meta for component idx [${{componentIdx}}]n`); }} if (meta.taskID !== taskID) {{ throw new Error(`task ID [${{meta.taskID}}] != requested ID [${{taskID}}]`); @@ -973,6 +1010,20 @@ impl Intrinsic { "#, )); } + + // TODO(feat): customizable stream classes + Intrinsic::PlatformReadableStreamClass => { + let name = self.name(); + uwriteln!( + output, + r#" + if (!ReadableStream) {{ + throw new Error('builtin stream class [ReadableStream] is not available'); + }} + const {name} = ReadableStream; + "# + ); + } } } } @@ -1015,9 +1066,11 @@ pub struct RenderIntrinsicsArgs<'a> { } /// Intrinsics that should be rendered as early as possible -const EARLY_INTRINSICS: [Intrinsic; 32] = [ +const EARLY_INTRINSICS: [Intrinsic; 36] = [ Intrinsic::PromiseWithResolversPonyfill, Intrinsic::SymbolDispose, + Intrinsic::SymbolAsyncIterator, + Intrinsic::SymbolIterator, Intrinsic::DebugLog, Intrinsic::GlobalAsyncDeterminism, Intrinsic::GlobalComponentMemoryMap, @@ -1032,10 +1085,14 @@ const EARLY_INTRINSICS: [Intrinsic; 32] = [ Intrinsic::RepTableClass, Intrinsic::CoinFlip, Intrinsic::ScopeId, + // Type checking helpers Intrinsic::ConstantI32Min, Intrinsic::ConstantI32Max, + Intrinsic::Conversion(ConversionIntrinsic::IsValidNumericPrimitive), + Intrinsic::Conversion(ConversionIntrinsic::RequireValidNumericPrimitive), Intrinsic::TypeCheckValidI32, Intrinsic::TypeCheckAsyncFn, + // Async helpers Intrinsic::AsyncFunctionCtor, Intrinsic::AsyncTask(AsyncTaskIntrinsic::ClearCurrentTask), Intrinsic::AsyncTask(AsyncTaskIntrinsic::CurrentTaskMayBlock), @@ -1043,10 +1100,13 @@ const EARLY_INTRINSICS: [Intrinsic; 32] = [ Intrinsic::AsyncTask(AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs), Intrinsic::AsyncTask(AsyncTaskIntrinsic::UnpackCallbackResult), Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncSubtaskClass), + // Host helpers Intrinsic::Host(HostIntrinsic::PrepareCall), Intrinsic::Host(HostIntrinsic::AsyncStartCall), Intrinsic::Host(HostIntrinsic::SyncStartCall), + // Waitable helpers Intrinsic::Waitable(WaitableIntrinsic::WaitableClass), + // Error context helpers Intrinsic::ErrCtx(ErrCtxIntrinsic::GlobalErrCtxTableMap), ]; @@ -1118,9 +1178,24 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { if args .intrinsics .contains(&Intrinsic::String(StringIntrinsic::Utf8Encode)) + || args + .intrinsics + .contains(&Intrinsic::String(StringIntrinsic::Utf8EncodeAsync)) { - args.intrinsics - .extend([&Intrinsic::String(StringIntrinsic::GlobalTextEncoderUtf8)]); + args.intrinsics.extend([ + &Intrinsic::IsLE, + &Intrinsic::String(StringIntrinsic::GlobalTextEncoderUtf8), + ]); + } + + if args + .intrinsics + .contains(&Intrinsic::String(StringIntrinsic::Utf16Encode)) + || args + .intrinsics + .contains(&Intrinsic::String(StringIntrinsic::Utf16EncodeAsync)) + { + args.intrinsics.extend([&Intrinsic::IsLE]); } // Attempting to perform a debug message hoist will require string encoding to memory @@ -1298,6 +1373,16 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { ]); } + if args + .intrinsics + .contains(&Intrinsic::Lift(LiftIntrinsic::LiftFlatStringAny)) + { + args.intrinsics.extend([ + &Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8), + &Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16), + ]); + } + if args .intrinsics .contains(&Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8)) @@ -1306,6 +1391,16 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { .insert(Intrinsic::String(StringIntrinsic::GlobalTextDecoderUtf8)); } + if args + .intrinsics + .contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatStringAny)) + { + args.intrinsics.extend([ + &Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8), + &Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf16), + ]); + } + if args .intrinsics .contains(&Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8)) @@ -1366,6 +1461,7 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { args.intrinsics.extend([ &Intrinsic::AsyncStream(AsyncStreamIntrinsic::InternalStreamClass), &Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamEndClass), + &Intrinsic::AsyncEventCodeEnum, ]); } @@ -1451,6 +1547,8 @@ impl Intrinsic { "symbolCabiDispose", "symbolCabiLower", "symbolDispose", + "symbolAsyncIterator", + "symbolIterator", "symbolRscHandle", "symbolRscRep", "T_FLAG", @@ -1512,11 +1610,15 @@ impl Intrinsic { Intrinsic::InstantiateCore => "instantiateCore", Intrinsic::IsLE => "isLE", Intrinsic::ScopeId => "SCOPE_ID", + Intrinsic::SymbolCabiDispose => "symbolCabiDispose", Intrinsic::SymbolCabiLower => "symbolCabiLower", Intrinsic::SymbolDispose => "symbolDispose", + Intrinsic::SymbolAsyncIterator => "symbolAsyncIterator", + Intrinsic::SymbolIterator => "symbolIterator", Intrinsic::SymbolResourceHandle => "symbolRscHandle", Intrinsic::SymbolResourceRep => "symbolRscRep", + Intrinsic::ThrowInvalidBool => "throwInvalidBool", Intrinsic::ThrowUninitialized => "throwUninitialized", @@ -1532,6 +1634,9 @@ impl Intrinsic { Intrinsic::TypeCheckAsyncFn => "_typeCheckAsyncFn", Intrinsic::AsyncFunctionCtor => "ASYNC_FN_CTOR", + // Streams + Intrinsic::PlatformReadableStreamClass => "_PlatformReadableStream", + // Async Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", Intrinsic::CoinFlip => "_coinFlip", diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs index f45e1ddcb..e2164f5ac 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs @@ -375,6 +375,7 @@ impl AsyncFutureIntrinsic { writable, readable, target: `future read/write`, + stringEncoding, }}); const processFn = (result) => {{ diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs index e9bc4555b..692a1d2bb 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -281,11 +281,13 @@ impl AsyncStreamIntrinsic { #idx = null; // stream end index in the table #componentIdx = null; - #dropped = false; - #onDrop; #copyState = {stream_end_class}.CopyState.IDLE; + #dropped; + #setDroppedFn; + #isDroppedFn; + target; constructor(args) {{ @@ -300,7 +302,17 @@ impl AsyncStreamIntrinsic { this.#tableIdx = args.tableIdx; this.#waitable = args.waitable; - this.#onDrop = args.onDrop; + + if (args.setDroppedFn && args.isDroppedFn) {{ + this.#setDroppedFn = args.setDroppedFn; + this.#isDroppedFn = args.isDroppedFn; + }} else if (args.setDroppedFn === undefined && args.isDroppedFn === undefined) {{ + this.#setDroppedFn = (v) => {{ this.#dropped = v; }}; + this.#isDroppedFn = () => {{ return this.#dropped; }}; + }} else {{ + throw new TypeError('setDroppedFn and isDroppedFn must both be specified or neither'); + }} + this.target = args.target; }} @@ -359,7 +371,8 @@ impl AsyncStreamIntrinsic { return event; }} - isDropped() {{ return this.#dropped; }} + isDropped() {{ return this.#isDroppedFn(); }} + setDropped() {{ return this.#setDroppedFn(); }} drop() {{ {debug_log_fn}('[{stream_end_class}#drop()]', {{ @@ -368,7 +381,7 @@ impl AsyncStreamIntrinsic { componentIdx: this.#waitable.componentIdx(), }}); - if (this.#dropped) {{ + if (this.isDropped()) {{ {debug_log_fn}('[{stream_end_class}#drop()] already dropped', {{ waitable: this.#waitable, waitableinSet: this.#waitable.isInSet(), @@ -382,7 +395,7 @@ impl AsyncStreamIntrinsic { w.drop(); }} - this.#dropped = true; + this.setDropped(); }} }} "# @@ -414,7 +427,14 @@ impl AsyncStreamIntrinsic { let copy_setup_impl = format!( r#" setupCopy(args) {{ - const {{ memory, ptr, count, eventCode, componentIdx, skipStateCheck }} = args; + const {{ + memory, + ptr, + count, + eventCode, + componentIdx, + skipStateCheck, + }} = args; if (eventCode === undefined) {{ throw new Error("missing/invalid event code"); }} let buffer = args.buffer; @@ -499,7 +519,7 @@ impl AsyncStreamIntrinsic { let (rw_fn_name, inner_rw_impl) = match self { // Internal implementation for writing to internal buffer after reading from a provided managed buffers // - // This _write() function is primarily called by guests. + // This is called by both the host and the guest Self::StreamWritableEndClass => ( "write", format!( @@ -517,7 +537,7 @@ impl AsyncStreamIntrinsic { const pendingElemMeta = this.#pendingBufferMeta.buffer.getElemMeta(); const newBufferElemMeta = buffer.getElemMeta(); - if (pendingElemMeta.typeIdx !== newBufferElemMeta.typeIdx) {{ + if (pendingElemMeta.payloadTypeName !== newBufferElemMeta.payloadTypeName) {{ throw new Error("trap: stream end type does not match internal buffer"); }} @@ -563,7 +583,7 @@ impl AsyncStreamIntrinsic { // Internal implementation for reading from an internal buffer and writing to a provided managed buffer // - // This _read() function is primarily called by guests. + // This is called by both the host and the guest Self::StreamReadableEndClass => ( "read", format!( @@ -587,10 +607,20 @@ impl AsyncStreamIntrinsic { const pendingElemMeta = this.#pendingBufferMeta.buffer.getElemMeta(); const newBufferElemMeta = buffer.getElemMeta(); - if (pendingElemMeta.typeIdx !== newBufferElemMeta.typeIdx) {{ + if (pendingElemMeta.payloadTypeName !== newBufferElemMeta.payloadTypeName) {{ throw new Error("trap: stream end type does not match internal buffer"); }} + // Since we do not know the string encoding until a write is performed, it is possible that + // one end (i.e. the read end) does not yet know the appropriate string encoding to use when + // lifting/lowering. + if (newBufferElemMeta.stringEncoding === undefined || pendingElemMeta.stringEncoding === undefined) {{ + const encoding = pendingElemMeta.stringEncoding ?? newBufferElemMeta.stringEncoding; + if (encoding === undefined) {{ throw new Error('both writer & reader missing string encoding'); }} + newBufferElemMeta.stringEncoding = encoding; + pendingElemMeta.stringEncoding = encoding; + }} + // If the buffer came from the same component that is currently doing the operation // we're doing a inter-component read, and only unit or numeric types are allowed const pendingElemIsNoneOrNumeric = pendingElemMeta.isNone || pendingElemMeta.isNumeric; @@ -643,9 +673,22 @@ impl AsyncStreamIntrinsic { eventCode, initial, skipStateCheck, + stringEncoding, + reallocFn, }} = args; if (eventCode === undefined) {{ throw new TypeError('missing/invalid event code'); }} + if (this.#elemMeta.stringEncoding === undefined && stringEncoding) {{ + this.#elemMeta.stringEncoding = stringEncoding; + }} + if (this.#elemMeta.stringEncoding && stringEncoding && this.#elemMeta.stringEncoding !== stringEncoding) {{ + throw new Error(`inconsistent string encoding (previously [${{this.#elemMeta.stringEncoding}}], now [${{stringEncoding}}])`); + }} + + if (this.#elemMeta.reallocFn === undefined && reallocFn) {{ + this.#elemMeta.reallocFn = reallocFn; + }} + if (this.isDropped()) {{ if (this.#pendingBufferMeta?.onCopyDoneFn) {{ const f = this.#pendingBufferMeta.onCopyDoneFn; @@ -667,6 +710,20 @@ impl AsyncStreamIntrinsic { skipStateCheck, }}); + // If the stream is readable and was lowered from the host, + // when the component is doing a read (i.e. `stream.read`), + // the writer is host-side and may have already written. + // + // We effectively do a just-in-time "write" of the external value, + // if one is present, because what we got from the outside world + // was a reader + // + let onReadFinishFn; + const injectHostWrite = this.isReadable() && !!this.#hostInjectFn; + if (injectHostWrite) {{ + onReadFinishFn = await this.#hostInjectFn({{ count }}); + }} + // Perform the read/write this._{rw_fn_name}({{ buffer, @@ -677,24 +734,36 @@ impl AsyncStreamIntrinsic { // If sync, wait forever but allow task to do other things if (!this.hasPendingEvent()) {{ - if (isAsync) {{ - this.setCopyState({stream_end_class}.CopyState.ASYNC_COPYING); - {debug_log_fn}('[{stream_end_class}#copy()] blocked', {{ componentIdx, eventCode, self: this }}); - return {async_blocked_const}; - }} else {{ - this.setCopyState({stream_end_class}.CopyState.SYNC_COPYING); - - const taskMeta = {current_task_get_fn}(componentIdx); - if (!taskMeta) {{ throw new Error(`missing task meta for component idx [${{componentIdx}}]`); }} - - const task = taskMeta.task; - if (!task) {{ throw new Error('missing task task from task meta'); }} - - const streamEnd = this; - await task.suspendUntil({{ - readyFn: () => streamEnd.hasPendingEvent(), - }}); - }} + if (injectHostWrite) {{ + throw new Error('reader unexpectedly blocked after injected write'); + }} + + if (isAsync) {{ + this.setCopyState({stream_end_class}.CopyState.ASYNC_COPYING); + {debug_log_fn}('[{stream_end_class}#copy()] blocked', {{ componentIdx, eventCode, self: this }}); + return {async_blocked_const}; + }} else {{ + this.setCopyState({stream_end_class}.CopyState.SYNC_COPYING); + + const taskMeta = {current_task_get_fn}(componentIdx); + if (!taskMeta) {{ throw new Error(`missing task meta for component idx [${{componentIdx}}]`); }} + + const task = taskMeta.task; + if (!task) {{ throw new Error('missing task task from task meta'); }} + + const streamEnd = this; + await task.suspendUntil({{ + readyFn: () => streamEnd.hasPendingEvent(), + }}); + }} + }} + + // If we injected a write and the read has completed, we should reset + // we can skip the rest of the async machinery since there the host controlled + // write end does not need to use the pending event machinery + if (injectHostWrite) {{ + if (!onReadFinishFn) {{ throw new Error('missing read finish fn'); }} + onReadFinishFn(); }} const event = this.getPendingEvent(); @@ -778,6 +847,10 @@ impl AsyncStreamIntrinsic { const {{ promise, resolve, reject }} = newResult; const count = 1; + if (this.#elemMeta.stringEncoding === undefined) {{ + this.#elemMeta.string = 'utf8'; + }} + try {{ const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ componentIdx: -1, @@ -799,7 +872,19 @@ impl AsyncStreamIntrinsic { componentIdx: -1, }}); - if (packedResult === {async_blocked_const}) {{ + // If we are dealing with a blocked component write operation, we do an immedaite wait + // on the host side to pause the host until the write can be completed. + // + // We do not do this if we're dealing with a host injection, + // (i.e. a lowered read end into a component does a read() and forces + // data to be read from the host side), we must signal the write is completed + // and we are waiting for the read. + // + // In the host injection case, it is OK that the write is blocked, because we + // know the read is about to occur (we control the writes to the stream to be + // just-before reads, no matter what the user does on the other end). + // + if (packedResult === {async_blocked_const} && !this.#isHostOwned) {{ // If the write was blocked, we can only make progress when // the read side notifies us of a read, then we must attempt the copy again @@ -825,7 +910,7 @@ impl AsyncStreamIntrinsic { }}); const copied = packedResult >> 4; - if (copied === 0 && this.isDone()) {{ + if (copied === 0 && this.isDoneState()) {{ reject(new Error("read end dropped during write")); }} @@ -834,6 +919,18 @@ impl AsyncStreamIntrinsic { }} }} + + // Host owned writes were not necessarily unblocked, but are always blocked + // because they happen just-before a component read (via a lowered end). + // + // In this case, we cant to declare the copy state back to idle + // for the next write that is performed, assuming there may be more writes + // to do. + // + // if (this.#hostOwned) {{ + // this.setCopyState({stream_end_class}.CopyState.IDLE); + // }} + // If the write was not blocked, we can resolve right away this.#result = null; resolve(); @@ -880,6 +977,9 @@ impl AsyncStreamIntrinsic { }} const {{ promise, resolve, reject }} = newResult; + // TODO(fix): when we do a read, we need to GET the string encoding from the + // other side, via the lift/lower fn? + const count = 1; try {{ const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ @@ -928,7 +1028,7 @@ impl AsyncStreamIntrinsic { }}); const copied = packedResult >> 4; - if (copied === 0 && this.isDone()) {{ + if (copied === 0 && this.isDoneState()) {{ reject(new Error("write end dropped during read")); }} @@ -947,7 +1047,8 @@ impl AsyncStreamIntrinsic { reject(err); }} - return await promise; + const res = await promise; + return {{ value: res, done: res === undefined }}; }} "# ), @@ -960,12 +1061,21 @@ impl AsyncStreamIntrinsic { #done = false; #elemMeta = null; - #pendingBufferMeta = null; // held by both write and read ends + // held by both write and read ends + #pendingBufferMeta = null; + + // table index that the stream is in (can change after a stream transfer) + #streamTableIdx; + // handle (index) inside the given table (can change after a stream transfer) + #handle; - #streamTableIdx; // table index that the stream is in (can change after a stream transfer) - #handle; // handle (index) inside the given table (can change after a stream transfer) + // internal stream (which has both ends) rep + #globalStreamMapRep; - #globalStreamMapRep; // internal stream (which has both ends) rep + // only populated for lowered (read) stream ends + #hostInjectFn; + // only populated for the write side of a lowered read stream end + #isHostOwned; #result = null; @@ -981,6 +1091,9 @@ impl AsyncStreamIntrinsic { if (args.tableIdx === undefined) {{ throw new Error('missing index for stream table idx'); }} this.#streamTableIdx = args.tableIdx; + + this.#hostInjectFn = args.hostInjectFn; + this.#isHostOwned = args.hostOwned; }} streamTableIdx() {{ return this.#streamTableIdx; }} @@ -999,14 +1112,18 @@ impl AsyncStreamIntrinsic { w.setTarget(`waitable for {rw_fn_name} end (waitable [${{idx}}])`); }} + setHostInjectFn(f) {{ + if (this.#hostInjectFn) {{ throw new Error('host injection fn is already set'); }} + this.#hostInjectFn = f; + }} + getElemMeta() {{ return {{...this.#elemMeta}}; }} {type_getter_impl} - isDone() {{ return this.getCopyState() === {stream_end_class}.CopyState.DONE; }} - isCompleted() {{ return this.getCopyState() === {stream_end_class}.CopyState.COMPLETED; }} - isDropped() {{ return this.getCopyState() === {stream_end_class}.CopyState.DROPPED; }} - isIdle() {{ return this.getCopyState() === {stream_end_class}.CopyState.IDLE; }} + isDoneState() {{ return this.getCopyState() === {stream_end_class}.CopyState.DONE; }} + isCancelledState() {{ return this.getCopyState() === {stream_end_class}.CopyState.CANCELLED; }} + isIdleState() {{ return this.getCopyState() === {stream_end_class}.CopyState.IDLE; }} {action_impl} {inner_rw_impl} @@ -1041,10 +1158,10 @@ impl AsyncStreamIntrinsic { drop() {{ {debug_log_fn}('[{stream_end_class}#drop()]'); if (this.isDropped()) {{ return; }} + super.drop(); if (this.#pendingBufferMeta) {{ this.resetAndNotifyPending({stream_end_class}.CopyResult.DROPPED); }} - super.drop(); }} }} "#)); @@ -1085,12 +1202,22 @@ impl AsyncStreamIntrinsic { this.#elemMeta = elemMeta; + let dropped = false; + const setDroppedFn = () => {{ dropped = true }}; + const isDroppedFn = () => dropped; + this.#readEnd = new {read_end_class}({{ tableIdx, elemMeta: this.#elemMeta, pendingBufferMeta: this.#pendingBufferMeta, target: "stream read end (@ init)", waitable: readWaitable, + // Only in-component read-ends need the host inject fn if provided, + // as that function will *inject* a write when a read is performed + // from inside the guest. + hostInjectFn: args.hostInjectFn, + setDroppedFn, + isDroppedFn, }}); this.#writeEnd = new {write_end_class}({{ @@ -1099,6 +1226,9 @@ impl AsyncStreamIntrinsic { pendingBufferMeta: this.#pendingBufferMeta, target: "stream write end (@ init)", waitable: writeWaitable, + hostOwned: true, + setDroppedFn, + isDroppedFn, }}); }} @@ -1170,16 +1300,19 @@ impl AsyncStreamIntrinsic { this.#isUnitStream = args.isUnitStream; }} - setRep(r) {{ this.#rep = r; }} + setRep(rep) {{ this.#rep = rep; }} - createUserStream(args) {{ + createUserStream() {{ if (this.#userStream) {{ return this.#userStream; }} if (this.#rep === null) {{ throw new Error("unexpectedly missing rep for host stream"); }} const cstate = {get_or_create_async_state_fn}(this.#componentIdx); if (!cstate) {{ throw new Error(`missing async state for component [${{this.#componentIdx}}]`); }} - const streamEnd = cstate.getStreamEnd({{ tableIdx: this.#streamTableIdx, streamEndWaitableIdx: this.#streamEndWaitableIdx }}); + const streamEnd = cstate.getStreamEnd({{ + tableIdx: this.#streamTableIdx, + streamEndWaitableIdx: this.#streamEndWaitableIdx + }}); if (!streamEnd) {{ throw new Error(`missing stream [${{this.#streamEndWaitableIdx}}] (table [${{this.#streamTableIdx}}], component [${{this.#componentIdx}}]`); }} @@ -1216,6 +1349,7 @@ impl AsyncStreamIntrinsic { let debug_log_fn = Intrinsic::DebugLog.name(); let external_stream_class_name = self.name(); let symbol_dispose = Intrinsic::SymbolDispose.name(); + let symbol_async_iterator = Intrinsic::SymbolAsyncIterator.name(); output.push_str(&format!( r#" @@ -1249,6 +1383,8 @@ impl AsyncStreamIntrinsic { globalRep() {{ return this.#globalRep; }} + [{symbol_async_iterator}]() {{ return this; }} + async next() {{ {debug_log_fn}('[{external_stream_class_name}#next()]'); if (!this.#isReadable) {{ throw new Error("stream is not marked as readable and cannot be written from"); }} @@ -1403,6 +1539,7 @@ impl AsyncStreamIntrinsic { let stream_op_fn = self.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let may_block = AsyncTaskIntrinsic::CurrentTaskMayBlock.name(); let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); let managed_buffer_class = Intrinsic::ManagedBufferClass.name(); let (event_code, stream_end_class) = match self { @@ -1446,7 +1583,9 @@ impl AsyncStreamIntrinsic { const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} - // TODO(fix): check for may block & async + if (!{may_block} && !isAsync) {{ + throw new Error('trap: only async tasks or otherwise blocking-allowed tasks my stream.{stream_op_fn}'); + }} const streamEnd = cstate.getStreamEnd({{ tableIdx: streamTableIdx, streamEndWaitableIdx }}); if (!streamEnd) {{ @@ -1466,6 +1605,8 @@ impl AsyncStreamIntrinsic { count, eventCode: {event_code}, componentIdx, + stringEncoding, + reallocFn: getReallocFn(), }}); return result; @@ -1627,7 +1768,7 @@ impl AsyncStreamIntrinsic { if (!streamEnd.isReadable()) {{ throw new Error("writable stream ends cannot be moved"); }} - if (streamEnd.isDone()) {{ + if (streamEnd.isDoneState()) {{ throw new Error('readable ends cannot be moved once writable ends are dropped'); }} diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs index 70136f28c..0bd2c4d6c 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -447,7 +447,8 @@ impl AsyncTaskIntrinsic { memoryIdx, callbackFnIdx, liftFns, - lowerFns + lowerFns, + stringEncoding, }} = ctx; const params = [...arguments].slice(1); const memory = getMemoryFn(); @@ -496,7 +497,7 @@ impl AsyncTaskIntrinsic { throw new Error('memory must be present if more than max async flat lifts are performed'); }} - let liftCtx = {{ memory, useDirectParams, params, componentIdx }}; + let liftCtx = {{ memory, useDirectParams, params, componentIdx, stringEncoding }}; if (!useDirectParams) {{ liftCtx.storagePtr = params[0]; liftCtx.storageLen = params[1]; @@ -1680,6 +1681,7 @@ impl AsyncTaskIntrinsic { realloc, vals: [subtaskValue], storagePtr: resultPtr, + stringEncoding: callMetadata.stringEncoding, }}); }} }} @@ -2030,6 +2032,7 @@ impl AsyncTaskIntrinsic { memoryIdx, getMemoryFn, getReallocFn, + stringEncoding, importFn, }} = args; @@ -2068,6 +2071,7 @@ impl AsyncTaskIntrinsic { realloc: getReallocFn(), resultPtr: params[0], lowers: resultLowerFns, + stringEncoding, }} }}); task.setReturnMemoryIdx(memoryIdx); @@ -2213,6 +2217,7 @@ impl AsyncTaskIntrinsic { getMemoryFn, getReallocFn, importFn, + stringEncoding, }} = args; let meta = {get_global_current_task_meta_fn}(componentIdx); @@ -2292,6 +2297,7 @@ impl AsyncTaskIntrinsic { realloc: getReallocFn(), resultPtr: params[0], lowers: resultLowerFns, + stringEncoding, }} }}); task.setReturnMemoryIdx(memoryIdx); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index be9572082..b97cebcb4 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -224,6 +224,7 @@ impl HostIntrinsic { resultPtr, returnFn, startFn, + stringEncoding, }} }}); @@ -423,7 +424,8 @@ impl HostIntrinsic { memory: callerMemory, vals: [res], storagePtr: subtaskCallMeta.resultPtr, - componentIdx: callerComponentIdx + componentIdx: callerComponentIdx, + stringEncoding: subtaskCallMeta.stringEncoding, }}); }}); diff --git a/crates/js-component-bindgen/src/intrinsics/string.rs b/crates/js-component-bindgen/src/intrinsics/string.rs index 92e68c89f..20288245f 100644 --- a/crates/js-component-bindgen/src/intrinsics/string.rs +++ b/crates/js-component-bindgen/src/intrinsics/string.rs @@ -73,6 +73,7 @@ impl StringIntrinsic { Self::Utf16Encode | Self::Utf16EncodeAsync => { let is_le = Intrinsic::IsLE.name(); + let (fn_preamble, realloc_call) = match self { Self::Utf16Encode => ("", "realloc"), Self::Utf16EncodeAsync => ("async ", "await realloc"), diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 8f0885b53..2f0b7048d 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -31,7 +31,8 @@ use wit_parser::{ use crate::esm_bindgen::EsmBindgen; use crate::files::Files; use crate::function_bindgen::{ - ErrHandling, FunctionBindgen, ResourceData, ResourceExtraData, ResourceMap, ResourceTable, + ErrHandling, FunctionBindgen, PayloadTypeMetadata, ResourceData, ResourceExtraData, + ResourceMap, ResourceTable, }; use crate::intrinsics::component::ComponentIntrinsic; use crate::intrinsics::js_helper::JsHelperIntrinsic; @@ -355,8 +356,6 @@ impl JsBindgen<'_> { "{local_name} = WebAssembly.promising({core_export_fn});", ); } else { - // TODO: run may be sync lifted, but it COULD call an async lowered function! - uwriteln!(core_exported_funcs, "{local_name} = {core_export_fn};",); } } @@ -1286,9 +1285,17 @@ impl<'a> Instantiator<'a, '_> { // Get to the payload type for the given stream table idx let table_ty = &self.types[*ty]; let stream_ty_idx = table_ty.ty; - let stream_ty_idx_js = stream_ty_idx.as_u32(); let stream_ty = &self.types[stream_ty_idx]; + // TODO(???): do we have no way to go from interface type to in-component type idx? + // TODO(???): does this work under type aliases?? we need the type def? + // TODO(???): can the stream type be treated as a unique indicator of the payload type? maybe not? + // need a way to go from iface type + stream type -> payload type idx? + let payload_ty_name_js = stream_ty + .payload + .map(|iface_ty| format!("'{iface_ty:?}'")) + .unwrap_or_else(|| "null".into()); + // Gather type metadata let ( align_32_js, @@ -1322,17 +1329,8 @@ impl<'a> Instantiator<'a, '_> { .flat_count .map(|v| v.to_string()) .unwrap_or_else(|| "null".into()), - gen_flat_lift_fn_js_expr( - self, - &ty, - &wasmtime_environ::component::StringEncoding::Utf8, - ), - gen_flat_lower_fn_js_expr( - self, - self.types, - &ty, - &wasmtime_environ::component::StringEncoding::Utf8, - ), + gen_flat_lift_fn_js_expr(self, &ty), + gen_flat_lower_fn_js_expr(self, &ty), "false", format!( "{}", @@ -1366,7 +1364,7 @@ impl<'a> Instantiator<'a, '_> { elemMeta: {{ liftFn: {lift_fn_js}, lowerFn: {lower_fn_js}, - typeIdx: {stream_ty_idx_js}, + payloadTypeName: {payload_ty_name_js}, isNone: {is_none_js}, isNumeric: {is_numeric_type_js}, isBorrowed: {is_borrow_js}, @@ -1412,7 +1410,7 @@ impl<'a> Instantiator<'a, '_> { let v = v.as_u32().to_string(); (v.to_string(), format!("() => realloc{v}")) } - None => ("null".into(), "null".into()), + None => ("null".into(), "() => null".into()), }; let component_instance_id = instance.as_u32(); @@ -2024,16 +2022,12 @@ impl<'a> Instantiator<'a, '_> { // Build list of lift functions for the params of the lowered import let param_types = &self.types.index(func_ty.params).types; let param_lift_fns_js = - gen_flat_lift_fn_list_js_expr(self, param_types.iter().as_slice(), canon_opts); + gen_flat_lift_fn_list_js_expr(self, param_types.iter().as_slice()); // Build list of lower functions for the results of the lowered import let result_types = &self.types.index(func_ty.results).types; - let result_lower_fns_js = gen_flat_lower_fn_list_js_expr( - self, - self.types, - result_types.iter().as_slice(), - &canon_opts.string_encoding, - ); + let result_lower_fns_js = + gen_flat_lower_fn_list_js_expr(self, result_types.iter().as_slice()); let get_callback_fn_js = canon_opts .callback @@ -2066,6 +2060,7 @@ impl<'a> Instantiator<'a, '_> { let (memory_idx_js, memory_expr_js) = memory_exprs.unwrap_or_else(|| ("null".into(), "() => null".into())); let realloc_expr_js = realloc_expr_js.unwrap_or_else(|| "() => null".into()); + let string_encoding_js = string_encoding_js_literal(&canon_opts.string_encoding); // Build the lower import call that will wrap the actual trampoline let func_ty_async = func_ty.async_; @@ -2084,6 +2079,7 @@ impl<'a> Instantiator<'a, '_> { getPostReturnFn: {get_post_return_fn_js}, isCancellable: {cancellable}, memoryIdx: {memory_idx_js}, + stringEncoding: {string_encoding_js}, getMemoryFn: {memory_expr_js}, getReallocFn: {realloc_expr_js}, importFn: _trampoline{i}, @@ -2397,6 +2393,7 @@ impl<'a> Instantiator<'a, '_> { CanonicalOptionsDataModel::LinearMemory(LinearMemoryOptions { memory, realloc }), callback, post_return, + string_encoding, .. } = canon_opts else { @@ -2445,11 +2442,7 @@ impl<'a> Instantiator<'a, '_> { // that are actually being passed through task.return let mut lift_fns: Vec = Vec::with_capacity(result_types.len()); for result_ty in result_types { - lift_fns.push(gen_flat_lift_fn_js_expr( - self, - result_ty, - &canon_opts.string_encoding, - )); + lift_fns.push(gen_flat_lift_fn_js_expr(self, result_ty)); } let lift_fns_js = format!("[{}]", lift_fns.join(",")); @@ -2460,12 +2453,7 @@ impl<'a> Instantiator<'a, '_> { // (i.e. via prepare & async start call) let mut lower_fns: Vec = Vec::with_capacity(result_types.len()); for result_ty in result_types { - lower_fns.push(gen_flat_lower_fn_js_expr( - self.bindgen, - self.types, - result_ty, - &canon_opts.string_encoding, - )); + lower_fns.push(gen_flat_lower_fn_js_expr(self, result_ty)); } let lower_fns_js = format!("[{}]", lower_fns.join(",")); @@ -2482,6 +2470,7 @@ impl<'a> Instantiator<'a, '_> { let callback_fn_idx = callback .map(|v| v.as_u32().to_string()) .unwrap_or_else(|| "null".into()); + let string_encoding_js = string_encoding_js_literal(string_encoding); uwriteln!( self.src.js, @@ -2495,6 +2484,7 @@ impl<'a> Instantiator<'a, '_> { callbackFnIdx: {callback_fn_idx}, liftFns: {lift_fns_js}, lowerFns: {lower_fns_js}, + stringEncoding: {string_encoding_js}, }}, );", ); @@ -3239,7 +3229,24 @@ impl<'a> Instantiator<'a, '_> { prefix: Some(format!("${}", table_idx.as_u32())), extra: Some(ResourceExtraData::Future { table_idx: *table_idx, - elem_ty: *maybe_elem_ty, + elem_ty: maybe_elem_ty.map(|ty| { + let table_ty = &self.types[*table_idx]; + let future_ty_idx = table_ty.ty; + let future_ty = &self.types[future_ty_idx]; + let iface_ty = future_ty + .payload + .expect("missing future payload despite elem type being present"); + let abi = self.types.canonical_abi(&iface_ty); + PayloadTypeMetadata { + ty, + iface_ty, + lift_js_expr: gen_flat_lift_fn_js_expr(self, &iface_ty), + lower_js_expr: gen_flat_lower_fn_js_expr(self, &iface_ty), + size32: abi.size32, + align32: abi.align32, + flat_count: abi.flat_count, + } + }), }), }, }, @@ -3250,7 +3257,24 @@ impl<'a> Instantiator<'a, '_> { prefix: Some(format!("${}", table_idx.as_u32())), extra: Some(ResourceExtraData::Stream { table_idx: *table_idx, - elem_ty: *maybe_elem_ty, + elem_ty: maybe_elem_ty.map(|ty| { + let table_ty = &self.types[*table_idx]; + let stream_ty_idx = table_ty.ty; + let stream_ty = &self.types[stream_ty_idx]; + let iface_ty = stream_ty + .payload + .expect("missing payload despite elem type being present"); + let abi = self.types.canonical_abi(&iface_ty); + PayloadTypeMetadata { + ty, + iface_ty, + lift_js_expr: gen_flat_lift_fn_js_expr(self, &iface_ty), + lower_js_expr: gen_flat_lower_fn_js_expr(self, &iface_ty), + size32: abi.size32, + align32: abi.align32, + flat_count: abi.flat_count, + } + }), }), }, }, @@ -4475,15 +4499,10 @@ fn string_encoding_js_literal(val: &wasmtime_environ::component::StringEncoding) pub fn gen_flat_lift_fn_list_js_expr( intrinsic_mgr: &mut Instantiator, types: &[InterfaceType], - canon_opts: &CanonicalOptions, ) -> String { let mut lift_fns: Vec = Vec::with_capacity(types.len()); for ty in types.iter() { - lift_fns.push(gen_flat_lift_fn_js_expr( - intrinsic_mgr, - ty, - &canon_opts.string_encoding, - )); + lift_fns.push(gen_flat_lift_fn_js_expr(intrinsic_mgr, ty)); } format!("[{}]", lift_fns.join(",")) } @@ -4502,11 +4521,7 @@ pub fn gen_flat_lift_fn_list_js_expr( /// /// The intrinsic it guaranteed to be in scope once execution time because it wlil be used in the relevant branch. /// -pub fn gen_flat_lift_fn_js_expr( - intrinsic_mgr: &mut Instantiator, - ty: &InterfaceType, - string_encoding: &wasmtime_environ::component::StringEncoding, -) -> String { +pub fn gen_flat_lift_fn_js_expr(intrinsic_mgr: &mut Instantiator, ty: &InterfaceType) -> String { let component_types = intrinsic_mgr.types; match ty { @@ -4574,23 +4589,12 @@ pub fn gen_flat_lift_fn_js_expr( Intrinsic::Lift(LiftIntrinsic::LiftFlatChar).name().into() } - InterfaceType::String => match string_encoding { - wasmtime_environ::component::StringEncoding::Utf8 => { - intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8)); - Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8) - .name() - .into() - } - wasmtime_environ::component::StringEncoding::Utf16 => { - intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16)); - Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16) - .name() - .into() - } - wasmtime_environ::component::StringEncoding::CompactUtf16 => { - todo!("latin1+utf8 not supported") - } - }, + InterfaceType::String => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringAny)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatStringAny) + .name() + .into() + } InterfaceType::Record(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatRecord)); @@ -4604,7 +4608,7 @@ pub fn gen_flat_lift_fn_js_expr( keys_and_lifts_expr.push_str(&format!( "['{}', {}, {}, {}],", f.name.to_lower_camel_case(), - gen_flat_lift_fn_js_expr(intrinsic_mgr, &f.ty, string_encoding), + gen_flat_lift_fn_js_expr(intrinsic_mgr, &f.ty), component_types.canonical_abi(ty).size32, component_types.canonical_abi(ty).align32, )); @@ -4624,7 +4628,7 @@ pub fn gen_flat_lift_fn_js_expr( Some(ty) => { format!( "['{name}', {}, {}, {}, {}],", - gen_flat_lift_fn_js_expr(intrinsic_mgr, ty, string_encoding), + gen_flat_lift_fn_js_expr(intrinsic_mgr, ty), variant_ty.abi.size32, variant_ty.abi.align32, variant_ty.info.payload_offset32, @@ -4641,26 +4645,39 @@ pub fn gen_flat_lift_fn_js_expr( intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatList)); let f = Intrinsic::Lift(LiftIntrinsic::LiftFlatList).name(); let list_ty = &component_types[*ty_idx]; - let lift_fn_expr = - gen_flat_lift_fn_js_expr(intrinsic_mgr, &list_ty.element, string_encoding); + let lift_fn_expr = gen_flat_lift_fn_js_expr(intrinsic_mgr, &list_ty.element); let elem_cabi = component_types.canonical_abi(&list_ty.element); - let align_32 = elem_cabi.align32; - let size_32 = elem_cabi.size32; - format!("{f}({{ elemLiftFn: {lift_fn_expr}, align32: {align_32}, size32: {size_32} }})") + let elem_align32 = elem_cabi.align32; + let elem_size32 = elem_cabi.size32; + format!( + "{f}({{ + elemLiftFn: {lift_fn_expr}, + elemAlign32: {elem_align32}, + elemSize32: {elem_size32}, + }})" + ) } InterfaceType::FixedLengthList(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatList)); let f = Intrinsic::Lift(LiftIntrinsic::LiftFlatList).name(); let list_ty = &component_types[*ty_idx]; - let lift_fn_expr = - gen_flat_lift_fn_js_expr(intrinsic_mgr, &list_ty.element, string_encoding); + let list_size32 = list_ty.abi.size32; + let list_align32 = list_ty.abi.align32; + let lift_fn_expr = gen_flat_lift_fn_js_expr(intrinsic_mgr, &list_ty.element); let list_len = list_ty.size; let elem_cabi = component_types.canonical_abi(&list_ty.element); - let align_32 = elem_cabi.align32; - let size_32 = elem_cabi.size32; + let elem_align32 = elem_cabi.align32; + let elem_size32 = elem_cabi.size32; format!( - "{f}({{ elemLiftFn: {lift_fn_expr}, align32: {align_32}, size32: {size_32}, knownLen: {list_len} }})" + "{f}({{ + elemLiftFn: {lift_fn_expr}, + elemAlign32: {elem_align32}, + elemSize32: {elem_size32}, + listSize32: {list_size32}, + listAlign32: {list_align32}, + knownLen: {list_len}, + }})" ) } @@ -4673,7 +4690,7 @@ pub fn gen_flat_lift_fn_js_expr( let mut elem_lifts_expr = String::from("["); for ty in &tuple_ty.types { - let lift_fn_js = gen_flat_lift_fn_js_expr(intrinsic_mgr, ty, string_encoding); + let lift_fn_js = gen_flat_lift_fn_js_expr(intrinsic_mgr, ty); elem_lifts_expr.push_str(&format!("[{lift_fn_js}, {size_u32}, {align_u32}],")); } elem_lifts_expr.push(']'); @@ -4704,8 +4721,9 @@ pub fn gen_flat_lift_fn_js_expr( } else { 4 }; + format!( - "{f}({{ names: {names_expr}, size32: {size_u32}, align32: {align_u32}, intSize: {elem_size} }})" + "{f}({{ names: {names_expr}, size32: {size_u32}, align32: {align_u32}, intSizeBytes: {elem_size} }})" ) } @@ -4735,8 +4753,7 @@ pub fn gen_flat_lift_fn_js_expr( let payload_offset_32 = option_ty.info.payload_offset32; let align_32 = option_ty.abi.align32; let size_32 = option_ty.abi.size32; - let lift_fn_js = - gen_flat_lift_fn_js_expr(intrinsic_mgr, &option_ty.ty, string_encoding); + let lift_fn_js = gen_flat_lift_fn_js_expr(intrinsic_mgr, &option_ty.ty); // NOTE: options are treated as variants format!( "{f}([ @@ -4755,7 +4772,7 @@ pub fn gen_flat_lift_fn_js_expr( if let Some(ok_ty) = result_ty.ok { cases_and_lifts_expr.push_str(&format!( "['ok', {}, {}, {}, {}],", - gen_flat_lift_fn_js_expr(intrinsic_mgr, &ok_ty, string_encoding), + gen_flat_lift_fn_js_expr(intrinsic_mgr, &ok_ty), result_ty.abi.size32, result_ty.abi.align32, result_ty.info.payload_offset32, @@ -4767,7 +4784,7 @@ pub fn gen_flat_lift_fn_js_expr( if let Some(err_ty) = &result_ty.err { cases_and_lifts_expr.push_str(&format!( "['err', {}, {}, {}, {}],", - gen_flat_lift_fn_js_expr(intrinsic_mgr, err_ty, string_encoding), + gen_flat_lift_fn_js_expr(intrinsic_mgr, err_ty), result_ty.abi.size32, result_ty.abi.align32, result_ty.info.payload_offset32, @@ -4902,6 +4919,9 @@ pub fn gen_flat_lift_fn_js_expr( let stream_ty_idx = table_ty.ty; let stream_ty = &component_types[stream_ty_idx]; let payload = stream_ty.payload; + + // TODO(fix): payload u8 should be special cased here + let (is_borrowed, is_none_type_js, is_numeric_type_js) = match payload { None => (false, true, false), Some(t) => ( @@ -4945,19 +4965,12 @@ pub fn gen_flat_lift_fn_js_expr( /// Generate the javascript that corresponds to a list of lowering functions for a given list of types pub fn gen_flat_lower_fn_list_js_expr( - intrinsic_mgr: &mut impl ManagesIntrinsics, - component_types: &ComponentTypes, + intrinsic_mgr: &mut Instantiator, types: &[InterfaceType], - string_encoding: &wasmtime_environ::component::StringEncoding, ) -> String { let mut lower_fns: Vec = Vec::with_capacity(types.len()); for ty in types.iter() { - lower_fns.push(gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - ty, - string_encoding, - )); + lower_fns.push(gen_flat_lower_fn_js_expr(intrinsic_mgr, ty)); } format!("[{}]", lower_fns.join(",")) } @@ -4976,12 +4989,8 @@ pub fn gen_flat_lower_fn_list_js_expr( /// /// The intrinsic it guaranteed to be in scope once execution time because it wlil be used in the relevant branch. /// -pub fn gen_flat_lower_fn_js_expr( - intrinsic_mgr: &mut impl ManagesIntrinsics, - component_types: &ComponentTypes, - ty: &InterfaceType, - string_encoding: &wasmtime_environ::component::StringEncoding, -) -> String { +pub fn gen_flat_lower_fn_js_expr(intrinsic_mgr: &mut Instantiator, ty: &InterfaceType) -> String { + let component_types = intrinsic_mgr.types; match ty { InterfaceType::Bool => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatBool)); @@ -5051,23 +5060,12 @@ pub fn gen_flat_lower_fn_js_expr( .into() } - InterfaceType::String => match string_encoding { - wasmtime_environ::component::StringEncoding::Utf8 => { - intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8)); - Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8) - .name() - .into() - } - wasmtime_environ::component::StringEncoding::Utf16 => { - intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf16)); - Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf16) - .name() - .into() - } - wasmtime_environ::component::StringEncoding::CompactUtf16 => { - todo!("latin1+utf8 not supported") - } - }, + InterfaceType::String => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStringAny)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatStringAny) + .name() + .into() + } InterfaceType::Record(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatRecord)); @@ -5081,18 +5079,13 @@ pub fn gen_flat_lower_fn_js_expr( keys_and_lowers_expr.push_str(&format!( "['{}', {}, {}, {} ],", f.name.to_lower_camel_case(), - gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &f.ty, - string_encoding - ), + gen_flat_lower_fn_js_expr(intrinsic_mgr, &f.ty), component_types.canonical_abi(ty).size32, component_types.canonical_abi(ty).align32, )); } keys_and_lowers_expr.push(']'); - format!("{lower_fn}.bind(null, {keys_and_lowers_expr})") + format!("{lower_fn}({keys_and_lowers_expr})") } InterfaceType::Variant(ty_idx) => { @@ -5108,12 +5101,7 @@ pub fn gen_flat_lower_fn_js_expr( lower_metas_expr.push_str(&format!( "[ '{name}', {}, {size32}, {align32}, {payload_offset32} ],", maybe_ty - .map(|ty| gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &ty, - string_encoding, - )) + .map(|ty| gen_flat_lower_fn_js_expr(intrinsic_mgr, &ty)) .unwrap_or_else(|| "null".into()), )); } @@ -5124,79 +5112,127 @@ pub fn gen_flat_lower_fn_js_expr( InterfaceType::List(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatList)); - let list_ty = &component_types[*ty_idx]; - let elem_ty_lower_expr = gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &list_ty.element, - string_encoding, - ); let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatList).name(); - let ty_idx = ty_idx.as_u32(); - format!("{f}({{ elemLowerFn: {elem_ty_lower_expr}, typeIdx: {ty_idx} }})") + let list_ty = &component_types[*ty_idx]; + let elem_ty_lower_expr = gen_flat_lower_fn_js_expr(intrinsic_mgr, &list_ty.element); + let elem_cabi = component_types.canonical_abi(&list_ty.element); + let elem_align32 = elem_cabi.align32; + let elem_size32 = elem_cabi.size32; + + format!( + "{f}({{ + elemLowerFn: {elem_ty_lower_expr}, + elemSize32: {elem_size32}, + elemAlign32: {elem_align32}, + }})" + ) } InterfaceType::FixedLengthList(ty_idx) => { - // TODO(fix): add more robust handling for fixed length lists intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatList)); - let list_ty = &component_types[*ty_idx]; - let elem_ty_lower_expr = gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &list_ty.element, - string_encoding, - ); let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatList).name(); - let ty_idx = ty_idx.as_u32(); - format!("{f}({{ elemLowerFn: {elem_ty_lower_expr}, typeIdx: {ty_idx} }})") + let list_ty = &component_types[*ty_idx]; + let elem_ty_lower_expr = gen_flat_lower_fn_js_expr(intrinsic_mgr, &list_ty.element); + let list_len = list_ty.size; + let list_align32 = list_ty.abi.size32; + let list_size32 = list_ty.abi.size32; + let elem_cabi = component_types.canonical_abi(&list_ty.element); + let elem_align32 = elem_cabi.align32; + let elem_size32 = elem_cabi.size32; + + format!( + r#"{f}({{ + elemLowerFn: {elem_ty_lower_expr}, + elemAlign32: {elem_align32}, + elemSize32: {elem_size32}, + align32: {list_align32}, + size32: {list_size32}, + knownLen: {list_len}, + }})"# + ) } InterfaceType::Tuple(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatTuple)); - let ty_idx = ty_idx.as_u32(); let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatTuple).name(); - format!("{f}.bind(null, {ty_idx})") + let tuple_ty = &component_types[*ty_idx]; + let size_u32 = tuple_ty.abi.size32; + let align_u32 = tuple_ty.abi.align32; + + let mut elem_lowers_expr = String::from("["); + for ty in &tuple_ty.types { + let lower_fn_js = gen_flat_lower_fn_js_expr(intrinsic_mgr, ty); + elem_lowers_expr.push_str(&format!("[{lower_fn_js}, {size_u32}, {align_u32}],")); + } + elem_lowers_expr.push(']'); + + format!("{f}({elem_lowers_expr})") } InterfaceType::Flags(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatFlags)); - let ty_idx = ty_idx.as_u32(); let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatFlags).name(); - format!("{f}.bind(null, {ty_idx})") + let flags_ty = &component_types[*ty_idx]; + let size32 = flags_ty.abi.size32; + let align32 = flags_ty.abi.align32; + let names_list_js = format!( + "[{}]", + flags_ty + .names + .iter() + .map(|s| format!("'{s}'")) + .collect::>() + .join(",") + ); + let num_flags = flags_ty.names.len(); + let elem_size = if num_flags <= 8 { + 1 + } else if num_flags <= 16 { + 2 + } else { + 4 + }; + + format!( + "{f}({{ names: {names_list_js}, size32: {size32}, align32: {align32}, intSizeBytes: {elem_size} }})" + ) } InterfaceType::Enum(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatEnum)); - let ty_idx = ty_idx.as_u32(); let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatEnum).name(); - format!("{f}.bind(null, {ty_idx})") + let enum_ty = &component_types[*ty_idx]; + let size32 = enum_ty.abi.size32; + let align32 = enum_ty.abi.align32; + let payload_offset32 = enum_ty.info.payload_offset32; + + let mut elem_lowers_expr = String::from("["); + for name in &enum_ty.names { + elem_lowers_expr.push_str(&format!( + "['{name}', null, {size32}, {align32}, {payload_offset32}]," + )); + } + elem_lowers_expr.push(']'); + + format!("{f}({elem_lowers_expr})") } InterfaceType::Option(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatOption)); + let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatOption).name(); let option_ty = &component_types[*ty_idx]; let size32 = option_ty.abi.size32; let align32 = option_ty.abi.align32; let payload_offset32 = option_ty.info.payload_offset32; + let lower_fn_js = gen_flat_lower_fn_js_expr(intrinsic_mgr, &option_ty.ty); - let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatOption).name(); - - let mut cases_and_lowers_expr = String::from("["); - cases_and_lowers_expr.push_str(&format!( - "[ 'some', {}, {size32}, {align32}, {payload_offset32} ],", - gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &option_ty.ty, - string_encoding, - ) - )); - cases_and_lowers_expr.push_str(&format!( - "[ 'none', null, {size32}, {align32}, {payload_offset32} ],", - )); - cases_and_lowers_expr.push(']'); - - format!("{f}({cases_and_lowers_expr})") + format!( + r#"{f}([ + [ 'none', null, {size32}, {align32}, {payload_offset32} ], + [ 'some', {lower_fn_js}, {size32}, {align32}, {payload_offset32} ], + ]) + "# + ) } InterfaceType::Result(ty_idx) => { @@ -5206,34 +5242,22 @@ pub fn gen_flat_lower_fn_js_expr( let size32 = result_ty.abi.size32; let align32 = result_ty.abi.align32; let payload_offset32 = result_ty.info.payload_offset32; + let ok_lower_fn_js = result_ty + .ok + .map(|ty| gen_flat_lower_fn_js_expr(intrinsic_mgr, &ty)) + .unwrap_or_else(|| "null".into()); + let err_lower_fn_js = result_ty + .err + .map(|ty| gen_flat_lower_fn_js_expr(intrinsic_mgr, &ty)) + .unwrap_or_else(|| "null".into()); - let mut cases_and_lowers_expr = String::from("["); - cases_and_lowers_expr.push_str(&format!( - "[ 'ok', {}, {size32}, {align32}, {payload_offset32} ],", - result_ty - .ok - .map(|ty| gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &ty, - string_encoding, - )) - .unwrap_or_else(|| "null".into()) - )); - cases_and_lowers_expr.push_str(&format!( - "[ 'err', {}, {size32}, {align32}, {payload_offset32} ],", - result_ty - .err - .map(|ty| gen_flat_lower_fn_js_expr( - intrinsic_mgr, - component_types, - &ty, - string_encoding, - )) - .unwrap_or_else(|| "null".into()) - )); - cases_and_lowers_expr.push(']'); - format!("{lower_fn}({cases_and_lowers_expr})") + format!( + r#"{lower_fn}([ + [ 'ok', {ok_lower_fn_js}, {size32}, {align32}, {payload_offset32} ], + [ 'err', {err_lower_fn_js}, {size32}, {align32}, {payload_offset32} ], + ]) + "# + ) } InterfaceType::Own(ty_idx) => { @@ -5261,8 +5285,46 @@ pub fn gen_flat_lower_fn_js_expr( InterfaceType::Stream(ty_idx) => { intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStream)); let table_idx = ty_idx.as_u32(); - let lower_flat_stream_fn = Intrinsic::Lower(LowerIntrinsic::LowerFlatStream).name(); - format!("{lower_flat_stream_fn}.bind(null, {table_idx})") + let f = Intrinsic::Lower(LowerIntrinsic::LowerFlatStream).name(); + let table_ty = &component_types[*ty_idx]; + let component_idx = table_ty.instance.as_u32(); + let stream_ty_idx = table_ty.ty; + let stream_ty = &component_types[stream_ty_idx]; + let payload = stream_ty.payload; + + // TODO(fix): payload u8 should be special cased here + + let (is_borrowed, is_none_type_js, is_numeric_type_js) = match payload { + None => (false, true, false), + Some(t) => ( + matches!(t, InterfaceType::Borrow(_)), + false, + matches!( + ty, + InterfaceType::U8 + | InterfaceType::U16 + | InterfaceType::U32 + | InterfaceType::U64 + | InterfaceType::S8 + | InterfaceType::S16 + | InterfaceType::S32 + | InterfaceType::S64 + | InterfaceType::Float32 + | InterfaceType::Float64 + ), + ), + }; + + format!( + r#"{f}({{ + streamTableIdx: {table_idx}, + componentIdx: {component_idx}, + isBorrowedType: {is_borrowed}, + isNoneType: {is_none_type_js}, + isNumericTypeJs: {is_numeric_type_js}, + }}) + "# + ) } InterfaceType::ErrorContext(ty_idx) => { diff --git a/crates/test-components/src/bin/stream_rx.rs b/crates/test-components/src/bin/stream_rx.rs new file mode 100644 index 000000000..00db1ccb3 --- /dev/null +++ b/crates/test-components/src/bin/stream_rx.rs @@ -0,0 +1,137 @@ +mod bindings { + use super::Component; + wit_bindgen::generate!({ + world: "stream-rx", + }); + export!(Component); +} + +use wit_bindgen::StreamReader; + +use bindings::exports::jco::test_components::use_stream_async; +use bindings::exports::jco::test_components::use_stream_sync; + +use bindings::exports::jco::test_components::use_stream_async::{ + ExampleEnum, ExampleFlags, ExampleRecord, ExampleVariant, +}; + +struct Component; + +impl use_stream_sync::Guest for Component { + fn stream_passthrough(rx: StreamReader) -> StreamReader { + rx + } +} + +impl use_stream_async::Guest for Component { + async fn stream_passthrough(rx: StreamReader) -> StreamReader { + rx + } + + async fn read_stream_values_bool(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_u8(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_s8(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_u16(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_s16(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_u32(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_s32(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_u64(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_s64(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_f32(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_f64(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_string(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_record(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_variant(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_option_string( + rx: StreamReader>, + ) -> Vec> { + read_async_values(rx).await + } + + async fn read_stream_values_result_string( + rx: StreamReader>, + ) -> Vec> { + read_async_values(rx).await + } + + async fn read_stream_values_tuple( + rx: StreamReader<(u32, i32, String)>, + ) -> Vec<(u32, i32, String)> { + read_async_values(rx).await + } + + async fn read_stream_values_flags(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_enum(rx: StreamReader) -> Vec { + read_async_values(rx).await + } + + async fn read_stream_values_list_u8(rx: StreamReader>) -> Vec> { + read_async_values(rx).await + } + + async fn read_stream_values_list_string(rx: StreamReader>) -> Vec> { + read_async_values(rx).await + } + + async fn read_stream_values_fixed_list_u32( + rx: StreamReader>, + ) -> Vec> { + read_async_values(rx).await + } +} + +async fn read_async_values(mut rx: StreamReader) -> Vec { + let mut vals = Vec::new(); + while let Some(v) = rx.next().await { + vals.push(v); + } + vals +} + +// Stub only to ensure this works as a binary +fn main() {} diff --git a/crates/test-components/wit/all.wit b/crates/test-components/wit/all.wit index ea7589adc..f7a5d9fbd 100644 --- a/crates/test-components/wit/all.wit +++ b/crates/test-components/wit/all.wit @@ -23,20 +23,12 @@ interface resources { } } -interface get-stream-async { - use resources.{example-resource}; - - resource example-guest-resource { - constructor(id: u32); - get-id: func() -> u32; - get-id-async: async func() -> u32; - } - +interface example-types { variant example-variant { num(u32), str(string), float(f32), - maybe-bool(option), + maybe-u32(option), } enum example-enum { @@ -55,6 +47,17 @@ interface get-stream-async { second, third, } +} + +interface get-stream-async { + use resources.{example-resource}; + use example-types.{example-variant, example-enum, example-record, example-flags}; + + resource example-guest-resource { + constructor(id: u32); + get-id: func() -> u32; + get-id-async: async func() -> u32; + } get-stream-u32: async func(vals: list) -> result, string>; get-stream-s32: async func(vals: list) -> result, string>; @@ -173,3 +176,74 @@ world basic-run-string { export get-string; export local-run-string; } + +////////////////// +// Stream Usage // +////////////////// + +interface use-stream-sync { + // TODO(fix): optimize host-only stream usage -- detect when the read & write are held by the host + stream-passthrough: func(s: stream) -> stream; +} + +interface use-stream-async { + // use resources.{example-resource}; + use example-types.{example-variant, example-enum, example-record, example-flags}; + + stream-passthrough: async func(s: stream) -> stream; + + read-stream-values-bool: async func(s: stream) -> list; + + read-stream-values-u8: async func(s: stream) -> list; + read-stream-values-s8: async func(s: stream) -> list; + + read-stream-values-u16: async func(s: stream) -> list; + read-stream-values-s16: async func(s: stream) -> list; + + read-stream-values-u32: async func(s: stream) -> list; + read-stream-values-s32: async func(s: stream) -> list; + + read-stream-values-u64: async func(s: stream) -> list; + read-stream-values-s64: async func(s: stream) -> list; + + read-stream-values-f32: async func(s: stream) -> list; + + read-stream-values-f64: async func(s: stream) -> list; + + read-stream-values-string: async func(s: stream) -> list; + + read-stream-values-record: async func(s: stream) -> list; + + read-stream-values-variant: async func(s: stream) -> list; + + read-stream-values-tuple: async func(s: stream>) -> list>; + + read-stream-values-flags: async func(s: stream) -> list; + + read-stream-values-enum: async func(s: stream) -> list; + + read-stream-values-option-string: async func(s: stream>) -> list>; + + read-stream-values-result-string: async func(s: stream>) -> list>; + + read-stream-values-list-u8: async func(s: stream>) -> list>; + read-stream-values-list-string: async func(s: stream>) -> list>; + read-stream-values-fixed-list-u32: async func(s: stream>>) -> list>>; + // read-stream-values-list-record: async func(s: stream>) -> list>; + + // // NOTE: the check here will be whether the resources are disposed inside the component properly + // read-stream-values-example-resource-own: async func(s: stream); + + // read-stream-values-example-resource-own-attr: async func(s: stream) -> list; + + // read-stream-values-stream-string: async func(s: stream>) -> list>; + + //read-stream-values-future-string: async func(vals: list>) -> result>, string>; +} + +world stream-rx { + // import resources; + + export use-stream-sync; + export use-stream-async; +} \ No newline at end of file diff --git a/packages/jco/test/common.js b/packages/jco/test/common.js index 42db2e153..58a5cd6f3 100644 --- a/packages/jco/test/common.js +++ b/packages/jco/test/common.js @@ -2,6 +2,9 @@ import { env } from "node:process"; import { readdir } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath, URL } from "node:url"; +import { ReadableStream } from "node:stream/web"; + +import { assert } from "vitest"; /** Path to a linter as installed by npm-compatible tooling */ export const LINTER_PATH = fileURLToPath(new URL("../../../node_modules/oxlint/bin/oxlint", import.meta.url)); @@ -48,3 +51,38 @@ export async function getDefaultComponentFixtures() { .filter((f) => f.isFile() && f.name !== "dummy_reactor.component.wasm") .map((f) => f.name); } + +/** Check the values of a given stream (normally returned from a component) */ +export async function checkStreamValues(args) { + const { stream, vals, typeName, assertEqFn, partial } = args ?? {}; + const expectedValues = args.expectedValues ?? []; + + // Ensure the values produced match expected + const eq = assertEqFn ?? assert.equal; + let iteratorRes; + for (const [idx, v] of vals.entries()) { + const expected = expectedValues[idx] ?? v; + iteratorRes = await stream.next(); + assert.isFalse(iteratorRes.done); + eq(iteratorRes.value, expected, `${typeName} [${idx}] read is incorrect`); + } + + // If dealing with a partial list of values from the stream, do not attempt to read the last value + if (partial) { + return; + } + + // Ensure the next value is undefined (and the iterator is done) + iteratorRes = await stream.next(); + assert.isUndefined(iteratorRes.value); + assert.isTrue(iteratorRes.done); +} + +export function createReadableStreamFromValues(vals) { + return new ReadableStream({ + start(ctrl) { + vals.forEach((v) => ctrl.enqueue(v)); + ctrl.close(); + }, + }); +} diff --git a/packages/jco/test/fixtures/components/p3/general/async-intertask-communication.wasm b/packages/jco/test/fixtures/components/p3/general/async-intertask-communication.wasm index 36cb82c29..b21d6baf2 100644 Binary files a/packages/jco/test/fixtures/components/p3/general/async-intertask-communication.wasm and b/packages/jco/test/fixtures/components/p3/general/async-intertask-communication.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/general/async-short-reads.wasm b/packages/jco/test/fixtures/components/p3/general/async-short-reads.wasm index 7e8de5e12..2aac62adb 100644 Binary files a/packages/jco/test/fixtures/components/p3/general/async-short-reads.wasm and b/packages/jco/test/fixtures/components/p3/general/async-short-reads.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/streams/async-closed-stream.wasm b/packages/jco/test/fixtures/components/p3/streams/async-closed-stream.wasm index 808728ee7..6dec195ba 100644 Binary files a/packages/jco/test/fixtures/components/p3/streams/async-closed-stream.wasm and b/packages/jco/test/fixtures/components/p3/streams/async-closed-stream.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/streams/async-closed-streams.wasm b/packages/jco/test/fixtures/components/p3/streams/async-closed-streams.wasm index d15165e09..d977d0d1b 100644 Binary files a/packages/jco/test/fixtures/components/p3/streams/async-closed-streams.wasm and b/packages/jco/test/fixtures/components/p3/streams/async-closed-streams.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/streams/async-read-resource-stream.wasm b/packages/jco/test/fixtures/components/p3/streams/async-read-resource-stream.wasm index 088dce033..1215a8175 100644 Binary files a/packages/jco/test/fixtures/components/p3/streams/async-read-resource-stream.wasm and b/packages/jco/test/fixtures/components/p3/streams/async-read-resource-stream.wasm differ diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/closed-stream.js b/packages/jco/test/p3/ported/wasmtime/component-async/closed-stream.js new file mode 100644 index 000000000..5188cf75d --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/closed-stream.js @@ -0,0 +1,38 @@ +import { join } from "node:path"; + +import { assert, suite, test } from "vitest"; + +import { buildAndTranspile, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/streams.rs +// +suite("closed stream scenario", () => { + test("sync instantly dropped stream", async () => { + const componentPath = join(COMPONENT_FIXTURES_DIR, "p3/streams/async-closed-stream.wasm"); + let cleanup; + try { + const res = await buildAndTranspile({ + componentPath, + transpile: { + extraArgs: { + minify: false, + }, + }, + }); + const instance = res.instance; + cleanup = res.cleanup; + + const stream = instance["local:local/closed-stream"].get(); + const { value, done } = await stream.next(); + assert.strictEqual(value, undefined); + assert.isTrue(done, undefined); + } finally { + if (cleanup) { + await cleanup(); + } + } + }); +}); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/closed-streams.js b/packages/jco/test/p3/ported/wasmtime/component-async/closed-streams.js new file mode 100644 index 000000000..d2fd283a2 --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/closed-streams.js @@ -0,0 +1,54 @@ +import { join } from "node:path"; + +import { suite, test } from "vitest"; + +import { buildAndTranspile, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/streams.rs +// +suite.skip("closed streams scenario", () => { + test("host->host", async () => { + // TODO: use direct & indirect producer, direct & indirect consumer (all combinations) + // TODO: create 2 host streams which will send 1 object + // TODO: create an array of values to send (3) + // TODO: create an single value + // TODO: at the same time: + // - send all the values on the stream, then drop the stream + // - wait to get the values out (options that contain a value), then get an null + // TODO: Wait for both to complete + }); + + test("host->guest", async () => { + const componentPath = join(COMPONENT_FIXTURES_DIR, "p3/streams/async-closed-streams.wasm"); + let cleanup; + try { + const res = await buildAndTranspile({ + componentPath, + component: { + path: componentPath, + skipInstantiation: true, + }, + transpile: { + extraArgs: { + minify: false, + }, + }, + }); + cleanup = res.cleanup; + + // TODO: create single element stream on host side (tx, rx) + // TODO: create listof values (3 u32s is fine) + // TODO: At the same time: + // - send all values down the stream + // - call the read_stream guest export (local:local/closed#read-stream), w/ values + // TODO: Wait for both to complete + } finally { + if (cleanup) { + await cleanup(); + } + } + }); +}); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/inter-task-communication.js b/packages/jco/test/p3/ported/wasmtime/component-async/inter-task-communication.js new file mode 100644 index 000000000..b0fef645b --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/inter-task-communication.js @@ -0,0 +1,38 @@ +import { join } from "node:path"; + +import { suite, test } from "vitest"; + +import { buildAndTranspile, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/streams.rs +// +suite.skip("inter-task communications scenario", () => { + test("component", async () => { + const componentPath = join(COMPONENT_FIXTURES_DIR, "p3/general/async-intertask-communication.wasm"); + let cleanup; + try { + const res = await buildAndTranspile({ + componentPath, + // transpile: { + // extraArgs: { + // minify: false, + // } + // } + }); + const instance = res.instance; + cleanup = res.cleanup; + + // TODO(fix): requires futures! + + await instance["local:local/run"].run(); + await instance["local:local/run"].run(); + } finally { + if (cleanup) { + await cleanup(); + } + } + }); +}); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/read-resource-stream.js b/packages/jco/test/p3/ported/wasmtime/component-async/read-resource-stream.js new file mode 100644 index 000000000..2c0089325 --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/read-resource-stream.js @@ -0,0 +1,55 @@ +import { join } from "node:path"; + +import { suite, test } from "vitest"; + +const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); + +import { COMPONENT_FIXTURES_DIR } from "./common.js"; +import { createReadableStreamFromValues } from "../../../../common.js"; +import { setupAsyncTest } from "../../../../helpers.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/streams.rs +// +suite("read resource stream", () => { + test.skip("component", async () => { + class X { + foo() {} + } + + const name = "async-read-resource-stream"; + const { esModule, cleanup } = await setupAsyncTest({ + asyncMode: "jspi", + component: { + name, + path: join(COMPONENT_FIXTURES_DIR, `p3/streams/${name}.wasm`), + skipInstantiation: true, + }, + // jco: { + // transpile: { + // extraArgs: { + // minify: false, + // }, + // }, + // }, + }); + + const instance = await esModule.instantiate(undefined, { + ...new WASIShim().getImportObject(), + "local:local/resource-stream": { + X, + foo: (count) => { + return createReadableStreamFromValues([...new Array(count)].map(() => new X())); + }, + }, + }); + + // TODO(fix): broken due to lower stream from host being broken + + await instance["local:local/run"].run(); + + await cleanup(); + }); +}); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/short-reads.js b/packages/jco/test/p3/ported/wasmtime/component-async/short-reads.js new file mode 100644 index 000000000..342733544 --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/short-reads.js @@ -0,0 +1,45 @@ +import { join } from "node:path"; + +import { suite, test } from "vitest"; + +import { buildAndTranspile, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/streams.rs +// +suite.skip("short reads scenario", () => { + test("component", async () => { + const componentPath = join(COMPONENT_FIXTURES_DIR, "p3/streams/async-short-reads.wasm"); + let cleanup; + try { + const res = await buildAndTranspile({ + componentPath, + transpile: { + extraArgs: { + minify: false, + }, + }, + }); + // const instance = res.instance; + cleanup = res.cleanup; + + // TODO: get Thing class from instance + // TODO: create strings = ["a", "b", "c", "d", "e"] + // TODO: create things array that is the same length, fill with things with every string as input + // TODO: create host stream of things array + // TODO: call short reads passing in a stream, will get a stream back out + // TODO: read every item of stream one at a time + + // TODO: wait until we've received N things + // TODO: Get all values out of the things + + // TODO: compare with the strings array (order matters) + } finally { + if (cleanup) { + await cleanup(); + } + } + }); +}); diff --git a/packages/jco/test/p3/stream.js b/packages/jco/test/p3/stream-lifts.js similarity index 76% rename from packages/jco/test/p3/stream.js rename to packages/jco/test/p3/stream-lifts.js index 7425f6de3..5f9a3c1a3 100644 --- a/packages/jco/test/p3/stream.js +++ b/packages/jco/test/p3/stream-lifts.js @@ -1,9 +1,9 @@ import { join } from "node:path"; -import { suite, test, assert, expect, vi, beforeAll, beforeEach, afterAll } from "vitest"; +import { suite, test, assert, beforeAll, beforeEach, afterAll, expect } from "vitest"; import { setupAsyncTest } from "../helpers.js"; -import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR } from "../common.js"; +import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR, checkStreamValues } from "../common.js"; import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"; suite("stream lifts", () => { @@ -54,19 +54,6 @@ suite("stream lifts", () => { }); }); - test.concurrent("u32/s32", async () => { - assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU32, AsyncFunction); - assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS32, AsyncFunction); - - let vals = [11, 22, 33]; - let stream = await instance["jco:test-components/get-stream-async"].getStreamU32(vals); - await checkStreamValues({ stream, vals, typeName: "u32" }); - - vals = [-11, -22, -33]; - stream = await instance["jco:test-components/get-stream-async"].getStreamS32(vals); - await checkStreamValues({ stream, vals, typeName: "s32" }); - }); - test.concurrent("bool", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamBool, AsyncFunction); const vals = [true, false]; @@ -74,6 +61,33 @@ suite("stream lifts", () => { await checkStreamValues({ stream, vals, typeName: "bool" }); }); + test.concurrent("u8/s8", async () => { + assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU8, AsyncFunction); + assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS8, AsyncFunction); + + let vals = [0, 1, 255]; + let stream = await instance["jco:test-components/get-stream-async"].getStreamU8(vals); + await checkStreamValues({ stream, vals, typeName: "u8" }); + + let invalidVals = [-1, 256]; + for (const invalid of invalidVals) { + await expect(() => instance["jco:test-components/get-stream-async"].getStreamU8([invalid])).rejects.toThrow( + /invalid u8 value/, + ); + } + + vals = [-11, -22, -33, -128, 127]; + stream = await instance["jco:test-components/get-stream-async"].getStreamS8(vals); + await checkStreamValues({ stream, vals, typeName: "s8" }); + + invalidVals = [-129, 128]; + for (const invalid of invalidVals) { + await expect(() => instance["jco:test-components/get-stream-async"].getStreamS8([invalid])).rejects.toThrow( + /invalid s8 value/, + ); + } + }); + test.concurrent("u16/s16", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU16, AsyncFunction); assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS16, AsyncFunction); @@ -87,6 +101,19 @@ suite("stream lifts", () => { await checkStreamValues({ stream, vals, typeName: "u16" }); }); + test.concurrent("u32/s32", async () => { + assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU32, AsyncFunction); + assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS32, AsyncFunction); + + let vals = [11, 22, 33]; + let stream = await instance["jco:test-components/get-stream-async"].getStreamU32(vals); + await checkStreamValues({ stream, vals, typeName: "u32" }); + + vals = [-11, -22, -33]; + stream = await instance["jco:test-components/get-stream-async"].getStreamS32(vals); + await checkStreamValues({ stream, vals, typeName: "s32" }); + }); + test.concurrent("u64/s64", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU64, AsyncFunction); assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS64, AsyncFunction); @@ -95,26 +122,23 @@ suite("stream lifts", () => { let stream = await instance["jco:test-components/get-stream-async"].getStreamU64(vals); await checkStreamValues({ stream, vals, typeName: "u64" }); + let invalidVals = [-1n, 18446744073709551616n]; + for (const invalid of invalidVals) { + await expect(() => + instance["jco:test-components/get-stream-async"].getStreamU64([invalid]), + ).rejects.toThrow(/invalid u64 value/); + } + vals = [-32_768n, 0n, 32_767n]; stream = await instance["jco:test-components/get-stream-async"].getStreamS64(vals); await checkStreamValues({ stream, vals, typeName: "s64" }); - }); - test.concurrent("u8/s8", async () => { - assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamU8, AsyncFunction); - assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamS8, AsyncFunction); - - let vals = [0, 1, 255]; - let stream = await instance["jco:test-components/get-stream-async"].getStreamU8(vals); - await checkStreamValues({ stream, vals, typeName: "u8" }); - - vals = [-11, -22, -33, -128, 127, 128]; - stream = await instance["jco:test-components/get-stream-async"].getStreamS8(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); - assert.equal(vals[3], await stream.next()); - assert.equal(vals[4], await stream.next()); + invalidVals = [-9223372036854775809n, 9223372036854775808n]; + for (const invalid of invalidVals) { + await expect(() => + instance["jco:test-components/get-stream-async"].getStreamS64([invalid]), + ).rejects.toThrow(/invalid s64 value/); + } }); test.concurrent("f32/f64", async () => { @@ -123,21 +147,25 @@ suite("stream lifts", () => { let vals = [-300.01235, -1.5, -0.0, 0.0, 1.5, 300.01235]; let stream = await instance["jco:test-components/get-stream-async"].getStreamF32(vals); - assert.closeTo(vals[0], await stream.next(), 0.00001); - assert.closeTo(vals[1], await stream.next(), 0.00001); - assert.closeTo(vals[2], await stream.next(), 0.00001); - assert.closeTo(vals[3], await stream.next(), 0.00001); - assert.closeTo(vals[4], await stream.next(), 0.00001); - assert.closeTo(vals[5], await stream.next(), 0.00001); + await checkStreamValues({ + stream, + vals, + typeName: "f32", + assertEqFn: (value, expected) => { + assert.closeTo(value, expected, 0.00001); + }, + }); vals = [-300.01235, -1.5, -0.0, 0.0, 1.5, -300.01235]; stream = await instance["jco:test-components/get-stream-async"].getStreamF64(vals); - assert.closeTo(vals[0], await stream.next(), 0.00001); - assert.closeTo(vals[1], await stream.next(), 0.00001); - assert.closeTo(vals[2], await stream.next(), 0.00001); - assert.closeTo(vals[3], await stream.next(), 0.00001); - assert.closeTo(vals[4], await stream.next(), 0.00001); - assert.closeTo(vals[5], await stream.next(), 0.00001); + await checkStreamValues({ + stream, + vals, + typeName: "f64", + assertEqFn: (value, expected) => { + assert.closeTo(value, expected, 0.00001); + }, + }); }); test.concurrent("string", async () => { @@ -164,20 +192,49 @@ suite("stream lifts", () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamVariant, AsyncFunction); const vals = [ - { tag: "maybe-bool", val: 123 }, // NOTE: non-nullable option values are *not* wrapped as objects - { tag: "maybe-bool", val: null }, + { tag: "maybe-u32", val: 123 }, + { tag: "maybe-u32", val: null }, { tag: "float", val: 123.1 }, { tag: "str", val: "string-value" }, { tag: "num", val: 1 }, ]; const stream = await instance["jco:test-components/get-stream-async"].getStreamVariant(vals); - assert.deepEqual(await stream.next(), { tag: "maybe-bool", val: { tag: "some", val: 123 } }); - assert.deepEqual(await stream.next(), { tag: "maybe-bool", val: { tag: "none" } }); - const floatMember = await stream.next(); - assert.equal(floatMember.tag, "float"); - assert.closeTo(floatMember.val, 123.1, 0.00001); - assert.deepEqual(await stream.next(), vals[3]); - assert.deepEqual(await stream.next(), vals[4]); + + // Ensure first two values match + await checkStreamValues({ + stream, + vals: [ + // TODO: wit type representation smoothing mismatch, + // non-nullable option values are *not* wrapped as objects + { tag: "maybe-u32", val: { tag: "some", val: 123 } }, + { tag: "maybe-u32", val: { tag: "none" } }, + ], + partial: true, + typeName: "variant", + assertEqFn: assert.deepEqual, + }); + + // Check float member + await checkStreamValues({ + stream, + vals: vals.slice(2, 3), + typeName: "variant", + partial: true, + assertEqFn: (value, expected) => { + { + assert.equal(value.tag, expected.tag); + assert.closeTo(value.val, expected.val, 0.00001); + } + }, + }); + + // Check rest of values + await checkStreamValues({ + stream, + vals: vals.slice(3), + typeName: "variant", + assertEqFn: assert.deepEqual, + }); }); test.concurrent("tuple", async () => { @@ -234,6 +291,7 @@ suite("stream lifts", () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamListU8, AsyncFunction); let vals = [[0x01, 0x02, 0x03, 0x04, 0x05], new Uint8Array([0x05, 0x04, 0x03, 0x02, 0x01]), []]; let stream = await instance["jco:test-components/get-stream-async"].getStreamListU8(vals); + await checkStreamValues({ stream, vals, @@ -248,6 +306,8 @@ suite("stream lifts", () => { }); }); + // TODO(fix): add tests for optimized UintXArrays (js_array_ty) + test.concurrent("list", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamListString, AsyncFunction); let vals = [["first", "second", "third"], []]; @@ -295,7 +355,7 @@ suite("stream lifts", () => { await checkStreamValues({ stream, vals, typeName: "result", assertEqFn: assert.deepEqual }); }); - test("example-resource", async () => { + test.concurrent("example-resource", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamExampleResourceOwn, AsyncFunction); const disposeSymbol = Symbol.dispose || Symbol.for("dispose"); @@ -303,13 +363,18 @@ suite("stream lifts", () => { let stream = await instance["jco:test-components/get-stream-async"].getStreamExampleResourceOwn(vals); const resources = []; for (const expectedResourceId of vals) { - const resource = await stream.next(); + const { value: resource, done } = await stream.next(); + assert.isFalse(done); assert.isNotNull(resource); assert.instanceOf(resource, instance["jco:test-components/get-stream-async"].ExampleGuestResource); assert.strictEqual(resource.getId(), expectedResourceId); resources.push(resource); } + const finished = await stream.next(); + assert.isTrue(finished.done); + assert.isUndefined(finished.value); + // NOTE: we have to pull all objects out of the stream and drop the stream, // *before* attempting to call async functions on the resources, to avoid // recursive re-entrancy into the same component instance @@ -330,7 +395,7 @@ suite("stream lifts", () => { } }); - test("example-resource#get-id", async () => { + test.concurrent("example-resource#get-id", async () => { assert.instanceOf( instance["jco:test-components/get-stream-async"].getStreamExampleResourceOwnAttr, AsyncFunction, @@ -346,39 +411,24 @@ suite("stream lifts", () => { }); }); - test("stream", async () => { + test.concurrent("stream", async () => { assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamStreamString, AsyncFunction); let vals = ["first", "third", "second"]; let stream = await instance["jco:test-components/get-stream-async"].getStreamStreamString(vals); for (const v of vals) { - const nestedStream = await stream.next(); - assert.strictEqual(v, await nestedStream.next()); + const { value: nestedStream, done } = await stream.next(); + assert.isFalse(done); + let nestedRes = await nestedStream.next(); + assert.isFalse(nestedRes.done); + assert.strictEqual(nestedRes.value, v); + nestedRes = await nestedStream.next(); + assert.isTrue(nestedRes.done); + assert.isUndefined(nestedRes.value); } + + // The stream should be done after expected streams are produced + const { value, done } = await stream.next(); + assert.isTrue(done); + assert.isUndefined(value); }); }); - -async function checkStreamValues(args) { - const { stream, vals, typeName, assertEqFn } = args ?? {}; - const expectedValues = args.expectedValues ?? []; - - const eq = assertEqFn ?? assert.equal; - for (const [idx, v] of vals.entries()) { - const expected = expectedValues[idx] ?? v; - eq(await stream.next(), expected, `${typeName} [${idx}] read is incorrect`); - } - - // If we get this far, the fourth read will do one of the following: - // - time out (hung during wait for writer that will never come) - // - report write end was dropped during the read (guest finished writing) - // - read end is fully closed after write - // - await expect( - vi.waitUntil( - async () => { - await stream.next(); - return true; // we should never get here, as we s error should occur - }, - { timeout: 500, interval: 0 }, - ), - ).rejects.toThrowError(/timed out|read end is closed|write end dropped during read/i); -} diff --git a/packages/jco/test/p3/stream-lowers.js b/packages/jco/test/p3/stream-lowers.js new file mode 100644 index 000000000..b5f2076e4 --- /dev/null +++ b/packages/jco/test/p3/stream-lowers.js @@ -0,0 +1,375 @@ +import { join } from "node:path"; +import { ReadableStream } from "node:stream/web"; + +import { suite, test, assert, beforeAll, beforeEach, afterAll } from "vitest"; + +import { setupAsyncTest } from "../helpers.js"; +import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR, createReadableStreamFromValues } from "../common.js"; +import { WASIShim } from "@bytecodealliance/preview2-shim/instantiation"; + +suite("stream lowers", () => { + let esModule, cleanup, instance; + + beforeAll(async () => { + const name = "stream-rx"; + const setupRes = await setupAsyncTest({ + asyncMode: "jspi", + component: { + name, + path: join(LOCAL_TEST_COMPONENTS_DIR, `${name}.wasm`), + skipInstantiation: true, + }, + jco: { + transpile: { + extraArgs: { + minify: false, + }, + }, + }, + }); + + esModule = setupRes.esModule; + cleanup = setupRes.cleanup; + }); + + afterAll(async () => { + await cleanup(); + }); + + beforeEach(async () => { + instance = await esModule.instantiate(undefined, { + ...new WASIShim().getImportObject(), + }); + }); + + test.concurrent("sync passthrough", async () => { + assert.notInstanceOf(instance["jco:test-components/use-stream-sync"].streamPassthrough, AsyncFunction); + + let vals = [0, 5, 10]; + const readerStream = new ReadableStream({ + start(ctrl) { + vals.forEach((v) => ctrl.enqueue(v)); + ctrl.close(); + }, + }); + + let returnedStream = instance["jco:test-components/use-stream-sync"].streamPassthrough(readerStream); + + // NOTE: Returned streams conform to the async iterator protocol -- they *do not* confirm to + // any other interface, though an object that is a ReadableStream may have been passed in. + // + let returnedVals = []; + for await (const v of returnedStream) { + returnedVals.push(v); + } + assert.deepEqual(vals, returnedVals); + + // Test late writer -- component should block until a value is written, + // and we should handle a final value + done from an iterator properly + const lateStream = { + [Symbol.asyncIterator]() { + let returned = 0; + return { + async next() { + await new Promise((resolve) => setTimeout(resolve, 300)); + if (returned === 2) { + return { value: 42, done: true }; + } + returned += 1; + return { value: 42, done: false }; + }, + }; + }, + }; + returnedStream = instance["jco:test-components/use-stream-sync"].streamPassthrough(lateStream); + + returnedVals = []; + for await (const v of returnedStream) { + returnedVals.push(v); + } + assert.deepEqual([42, 42, 42], returnedVals); + }); + + test.concurrent("async passthrough", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].streamPassthrough, AsyncFunction); + + let vals = [10, 5, 0]; + const readerStream = new ReadableStream({ + start(ctrl) { + vals.forEach((v) => ctrl.enqueue(v)); + ctrl.close(); + }, + }); + + let stream = await instance["jco:test-components/use-stream-async"].streamPassthrough(readerStream); + let returnedVals = []; + for await (const v of stream) { + returnedVals.push(v); + } + assert.deepEqual(vals, returnedVals); + }); + + test.concurrent("bool", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesBool, AsyncFunction); + + let vals = [true, false]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesBool( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("u8/s8", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesU8, AsyncFunction); + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesS8, AsyncFunction); + + let vals = [0, 1, 255]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesU8( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + + vals = [-128, 0, 1, 127]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesS8( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("u16/s16", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesU16, AsyncFunction); + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesS16, AsyncFunction); + + let vals = [0, 100, 65535]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesU16( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + + vals = [-32_768, 0, 32_767]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesS16( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("u32/s32", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesU32, AsyncFunction); + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesS32, AsyncFunction); + + let vals = [10, 5, 0]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesU32( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + + vals = [-32, 90001, 3200000]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesS32( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("u64/s64", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesU64, AsyncFunction); + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesS64, AsyncFunction); + + let vals = [0n, 100n, 65535n]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesU64( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + + vals = [-32_768n, 0n, 32_767n]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesS64( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("f32/f64", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesF32, AsyncFunction); + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesF64, AsyncFunction); + + let vals = [-300.01235, -1.5, -0.0, 0.0, 1.5, 300.01235]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesF32( + createReadableStreamFromValues(vals), + ); + vals.entries().forEach(([idx, v]) => assert.closeTo(v, returnedVals[idx], 0.01)); + + vals = [-60000.01235, -1.5, -0.0, 0.0, 1.5, -60000.01235]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesF32( + createReadableStreamFromValues(vals), + ); + vals.entries().forEach(([idx, v]) => assert.closeTo(v, returnedVals[idx], 0.01)); + }); + + test.concurrent("string", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesString, AsyncFunction); + + let vals = ["hello", "world", "!"]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesString( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("record", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesRecord, AsyncFunction); + + let vals = [ + { id: 3, idStr: "three" }, + { id: 2, idStr: "two" }, + { id: 1, idStr: "one" }, + ]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesRecord( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("variant", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesVariant, AsyncFunction); + + let vals = [ + { tag: "maybe-u32", val: 123 }, + { tag: "maybe-u32", val: null }, + { tag: "str", val: "string-value" }, + { tag: "num", val: 1 }, + ]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesVariant( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, [ + // TODO: wit type representation smoothing mismatch + { tag: "maybe-u32", val: { tag: "some", val: 123 } }, + { tag: "maybe-u32", val: { tag: "none" } }, + { tag: "str", val: "string-value" }, + { tag: "num", val: 1 }, + ]); + + vals = [{ tag: "float", val: 123.1 }]; + returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesVariant( + createReadableStreamFromValues(vals), + ); + assert.closeTo(returnedVals[0].val, 123.1, 0.01); + }); + + test.concurrent("tuple", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesTuple, AsyncFunction); + + let vals = [ + [1, -1, "one"], + [2, -2, "two"], + [3, -3, "two"], + ]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesTuple( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("flags", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesFlags, AsyncFunction); + + let vals = [ + { first: true, second: false, third: false }, + { first: false, second: true, third: false }, + { first: false, second: false, third: true }, + ]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesFlags( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("enum", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesEnum, AsyncFunction); + + let vals = ["first", "second", "third"]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesEnum( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, ["first", "second", "third"]); + }); + + test.concurrent("option", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesOptionString, AsyncFunction); + + let vals = ["present string", null]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesOptionString( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, [ + // TODO: wit type representation smoothing mismatch + { tag: "some", val: "present string" }, + { tag: "none" }, + ]); + }); + + test.concurrent("result", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesResultString, AsyncFunction); + + let vals = [{ tag: "ok", val: "present string" }, { tag: "err", val: "nope" }, "bare string (ok)"]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesResultString( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, [ + // TODO: wit type representation smoothing mismatch + { tag: "ok", val: "present string" }, + { tag: "err", val: "nope" }, + { tag: "ok", val: "bare string (ok)" }, + ]); + }); + + test.concurrent("list", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesListU8, AsyncFunction); + + let vals = [[0x01, 0x02, 0x03, 0x04, 0x05], new Uint8Array([0x05, 0x04, 0x03, 0x02, 0x01]), []]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesListU8( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, [ + // TODO: wit type representation smoothing mismatch + vals[0], + [...vals[1]], + [], + ]); + }); + + test.concurrent("list", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesListString, AsyncFunction); + + let vals = [["first", "second", "third"], []]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesListString( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, vals); + }); + + test.concurrent("list>", async () => { + assert.instanceOf(instance["jco:test-components/use-stream-async"].readStreamValuesFixedListU32, AsyncFunction); + + let vals = [ + [ + [1, 2, 3, 4, 5], + [0, 0, 0, 0, 0], + ], + [[0, 0, 0, 0, 0], new Uint32Array([0x05, 0x04, 0x03, 0x02, 0x01])], + ]; + let returnedVals = await instance["jco:test-components/use-stream-async"].readStreamValuesFixedListU32( + createReadableStreamFromValues(vals), + ); + assert.deepEqual(returnedVals, [ + // TODO(fix): wit type representation smoothing mismatch + [ + [1, 2, 3, 4, 5], + [0, 0, 0, 0, 0], + ], + [ + [0, 0, 0, 0, 0], + [0x05, 0x04, 0x03, 0x02, 0x01], + ], + ]); + }); +});