From 815ab6bbcd43d893ff7e4658e152f769c1edb763 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 3 Oct 2026 20:23:31 +0800 Subject: [PATCH] feat(webidl): support signed short argument conversion --- moli-webidl-derive/src/converter.rs | 8 +++ moli-webidl/src/convert.rs | 15 +++++ moli-webidl/src/lib.rs | 4 +- moli-webidl/src/types.rs | 9 +++ moli-webidl/tests/short.rs | 93 +++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 moli-webidl/tests/short.rs diff --git a/moli-webidl-derive/src/converter.rs b/moli-webidl-derive/src/converter.rs index 0db14acd50..20d78024ce 100644 --- a/moli-webidl-derive/src/converter.rs +++ b/moli-webidl-derive/src/converter.rs @@ -15,6 +15,7 @@ pub(crate) enum ConverterKind { CallbackInterface, Long, EnforceRangeLong, + Short, UnsignedShort, ClampedUnsignedShort, UnsignedLong, @@ -45,6 +46,7 @@ impl ConverterKind { "callback_interface" => Ok(Self::CallbackInterface), "long" => Ok(Self::Long), "enforce_range_long" => Ok(Self::EnforceRangeLong), + "short" => Ok(Self::Short), "unsigned_short" => Ok(Self::UnsignedShort), "clamped_unsigned_short" => Ok(Self::ClampedUnsignedShort), "unsigned_long" => Ok(Self::UnsignedLong), @@ -74,6 +76,7 @@ impl ConverterKind { Self::CallbackInterface => quote!(::moli_webidl::WebIdlCallbackInterface), Self::Long => quote!(::moli_webidl::Long), Self::EnforceRangeLong => quote!(::moli_webidl::EnforceRangeLong), + Self::Short => quote!(::moli_webidl::Short), Self::UnsignedShort => quote!(::moli_webidl::UnsignedShort), Self::ClampedUnsignedShort => quote!(::moli_webidl::ClampedUnsignedShort), Self::UnsignedLong => quote!(::moli_webidl::UnsignedLong), @@ -110,6 +113,7 @@ impl ConverterKind { Self::CallbackFunction | Self::CallbackInterface => quote!(#expr), Self::Long => quote!(::moli_webidl::Long(#expr)), Self::EnforceRangeLong => quote!(::moli_webidl::EnforceRangeLong(#expr)), + Self::Short => quote!(::moli_webidl::Short(#expr)), Self::UnsignedShort => quote!(::moli_webidl::UnsignedShort(#expr)), Self::ClampedUnsignedShort => { quote!(::moli_webidl::ClampedUnsignedShort(#expr)) @@ -141,6 +145,7 @@ impl ConverterKind { | Self::UsvString | Self::Long | Self::EnforceRangeLong + | Self::Short | Self::UnsignedShort | Self::ClampedUnsignedShort | Self::UnsignedLong @@ -242,6 +247,9 @@ fn infer_converter_kind(ty: &Type) -> Option { if is_type_ident(ty, "u16") { return Some(ConverterKind::UnsignedShort); } + if is_type_ident(ty, "i16") { + return Some(ConverterKind::Short); + } if is_type_ident(ty, "u64") { return Some(ConverterKind::UnsignedLongLong); } diff --git a/moli-webidl/src/convert.rs b/moli-webidl/src/convert.rs index f452a65068..d3fa83a5f8 100644 --- a/moli-webidl/src/convert.rs +++ b/moli-webidl/src/convert.rs @@ -261,6 +261,21 @@ impl<'s> WebIdlConverter<'s> for EnforceRangeUnsignedLong { } } +impl<'s> WebIdlConverter<'s> for crate::Short { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + _options: &Self::Options, + ) -> Result { + // Both 16-bit integer types truncate and reduce modulo 2^16; signed + // short then interprets the high bit as a negative value. + number_value(scope, value, context, "short").map(|value| Self(unsigned_short(value) as i16)) + } +} + impl<'s> WebIdlConverter<'s> for UnsignedShort { type Options = (); diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 2f02511201..c0f528a8d7 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -64,8 +64,8 @@ pub use traits::{ParseOutcome, WebIdlArguments, WebIdlConverter, WebIdlDictionar pub use types::{ Boolean, BufferSource, ByteString, ClampedUnsignedShort, DomString, DomString16, Double, EnforceRangeLong, EnforceRangeUnsignedLong, EnforceRangeUnsignedLongLong, EnumValue, - EventListenerOptions, Long, Record, Sequence, StringOptions, UnrestrictedDouble, UnsignedLong, - UnsignedLongLong, UnsignedShort, UsvString, + EventListenerOptions, Long, Record, Sequence, Short, StringOptions, UnrestrictedDouble, + UnsignedLong, UnsignedLongLong, UnsignedShort, UsvString, }; /// Restricted WebIDL float represented as a finite binary32 value. diff --git a/moli-webidl/src/types.rs b/moli-webidl/src/types.rs index dd23eb8c2f..74eed66928 100644 --- a/moli-webidl/src/types.rs +++ b/moli-webidl/src/types.rs @@ -227,6 +227,15 @@ impl From for u32 { } } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct Short(pub i16); + +impl From for i16 { + fn from(value: Short) -> Self { + value.0 + } +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct UnsignedShort(pub u16); diff --git a/moli-webidl/tests/short.rs b/moli-webidl/tests/short.rs new file mode 100644 index 0000000000..756a98e946 --- /dev/null +++ b/moli-webidl/tests/short.rs @@ -0,0 +1,93 @@ +use moli_webidl::WebIdlArgs; + +#[derive(WebIdlArgs)] +#[webidl(prefix = "Test.short")] +struct InferredShortArgs { + #[webidl(required)] + value: i16, +} + +#[derive(WebIdlArgs)] +#[webidl(prefix = "Test.explicitShort")] +struct ExplicitShortArgs { + #[webidl(required, converter = "short")] + value: i16, +} + +fn inferred_short<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + if let Some(parsed) = moli_webidl::parse_args::(scope, &args) { + rv.set(v8::Number::new(scope, f64::from(parsed.value)).into()); + } +} + +fn explicit_short<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + if let Some(parsed) = moli_webidl::parse_args::(scope, &args) { + rv.set(v8::Number::new(scope, f64::from(parsed.value)).into()); + } +} + +#[test] +fn signed_short_wraps_and_preserves_conversion_errors_in_the_callee_realm() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let callee = v8::Context::new(scope, Default::default()); + let values = { + let scope = &mut v8::ContextScope::new(scope, callee); + let inferred = v8::Function::new(scope, inferred_short).unwrap(); + let explicit = v8::Function::new(scope, explicit_short).unwrap(); + let key = v8::String::new(scope, "TypeError").unwrap(); + let error = callee.global(scope).get(scope, key.into()).unwrap(); + [ + ("inferred", inferred.into()), + ("explicit", explicit.into()), + ("CalleeTypeError", error), + ] + }; + let caller = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, caller); + for (name, value) in values { + let key = v8::String::new(scope, name).unwrap(); + assert_eq!( + caller.global(scope).set(scope, key.into(), value), + Some(true) + ); + } + let source = v8::String::new(scope, r#" + (() => { + const assert = (ok, message) => { if (!ok) throw Error(message); }; + const values = [[0,0],[-0,0],[3.9,3],[-3.9,-3],[32767,32767], + [32768,-32768],[65535,-1],[65536,0],[65537,1],[-65537,-1], + [4294967295,-1],[4294967297,1],[NaN,0],[Infinity,0],[-Infinity,0], + [undefined,0],[null,0],[true,1],['65535',-1]]; + for (const convert of [inferred, explicit]) { + for (const [value, expected] of values) assert(Object.is(convert(value),expected),'modulo conversion '+value); + for (const value of [Symbol('number'), 1n]) { + let error;try { convert(value); } catch(caught) { error=caught; } + assert(error instanceof CalleeTypeError && !(error instanceof TypeError),'callee realm TypeError'); + } + let reads=0;const value={[Symbol.toPrimitive](hint){reads++;assert(hint==='number','numeric hint');return 65535;}}; + assert(convert(value)===-1&&reads===1,'one observable conversion'); + const sentinel={};let error;try{convert({valueOf(){throw sentinel;}});}catch(caught){error=caught;}assert(error===sentinel,'exception identity'); + try { convert(); } catch(caught) { error=caught; }assert(error instanceof CalleeTypeError,'required argument'); + } + return true; + })() + "#).unwrap(); + let script = v8::Script::compile(scope, source, None).unwrap(); + assert!( + script + .run(scope) + .expect("signed short conversion matrix") + .is_true() + ); +}