diff --git a/.isu/issues.json b/.isu/issues.json
index da26677..248e0a2 100644
--- a/.isu/issues.json
+++ b/.isu/issues.json
@@ -1,5 +1,5 @@
{
- "next_id": 249,
+ "next_id": 251,
"issues": [
{
"id": 1,
@@ -2914,7 +2914,7 @@
],
"assigned": [],
"author": "piefev",
- "state": "open",
+ "state": "closed",
"created_at": "2026-05-21T16:05:09Z"
},
{
@@ -2998,6 +2998,28 @@
"author": "piefev",
"state": "open",
"created_at": "2026-05-21T18:43:29Z"
+ },
+ {
+ "id": 249,
+ "repo": "we",
+ "title": "Remove CPU renderer",
+ "body": null,
+ "labels": [],
+ "assigned": [],
+ "author": "piefev",
+ "state": "open",
+ "created_at": "2026-05-21T19:18:53Z"
+ },
+ {
+ "id": 250,
+ "repo": "we",
+ "title": "macos native Keyboard shortcuts with menu bar options (File, Edit, View, Window, Help)",
+ "body": null,
+ "labels": [],
+ "assigned": [],
+ "author": "piefev",
+ "state": "open",
+ "created_at": "2026-05-21T19:22:06Z"
}
]
}
diff --git a/crates/e2e/pages/32_instagram_mutation_observer.html b/crates/e2e/pages/32_instagram_mutation_observer.html
new file mode 100644
index 0000000..10fe0cd
--- /dev/null
+++ b/crates/e2e/pages/32_instagram_mutation_observer.html
@@ -0,0 +1,36 @@
+
+
+
+ Instagram MutationObserver regression
+
+
+ not-run
+
+
+
diff --git a/crates/e2e/scenarios/instagram_mutation_observer.we b/crates/e2e/scenarios/instagram_mutation_observer.we
new file mode 100644
index 0000000..362b5fa
--- /dev/null
+++ b/crates/e2e/scenarios/instagram_mutation_observer.we
@@ -0,0 +1,4 @@
+viewport 800 600
+goto crates/e2e/pages/32_instagram_mutation_observer.html
+dump_dom instagram_mutation_observer_dom.txt
+assert_dom_contains "function:0:ready"
diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs
index 9375533..593bfb7 100644
--- a/crates/js/src/builtins.rs
+++ b/crates/js/src/builtins.rs
@@ -857,7 +857,17 @@ fn make_string_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[
/// Create a JS array from a slice of Values.
fn make_value_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[Value]) -> Value {
+ make_value_array_with_proto(gc, shapes, items, None)
+}
+
+fn make_value_array_with_proto(
+ gc: &mut Gc,
+ shapes: &mut ShapeTable,
+ items: &[Value],
+ prototype: Option,
+) -> Value {
let mut obj = ObjectData::new();
+ obj.prototype = prototype;
for (i, v) in items.iter().enumerate() {
obj.insert_property(i.to_string(), Property::data(v.clone()), shapes);
}
@@ -874,6 +884,22 @@ fn make_value_array(gc: &mut Gc, shapes: &mut ShapeTable, items: &[V
Value::Object(gc.alloc(HeapObject::Object(obj)))
}
+fn array_prototype_from_context(ctx: &NativeContext) -> Option {
+ let callee = ctx.callee?;
+ match ctx.gc.get(callee) {
+ Some(HeapObject::Function(fdata)) => fdata.prototype_obj.or_else(|| {
+ fdata
+ .properties
+ .get("__array_prototype__")
+ .and_then(|prop| match &prop.value {
+ Value::Object(r) => Some(*r),
+ _ => None,
+ })
+ }),
+ _ => None,
+ }
+}
+
// ── Array.prototype ──────────────────────────────────────────
fn init_array_prototype(gc: &mut Gc, shapes: &mut ShapeTable, proto: GcRef) {
@@ -1391,19 +1417,23 @@ fn init_array_constructor(gc: &mut Gc, arr_proto: GcRef) -> GcRef {
set_func_prop(gc, ctor, "isArray", Value::Function(is_array));
let from = make_native(gc, "from", array_from);
+ set_func_prop(gc, from, "__array_prototype__", Value::Object(arr_proto));
set_func_prop(gc, ctor, "from", Value::Function(from));
let of = make_native(gc, "of", array_of);
+ set_func_prop(gc, of, "__array_prototype__", Value::Object(arr_proto));
set_func_prop(gc, ctor, "of", Value::Function(of));
ctor
}
fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result {
+ let prototype = array_prototype_from_context(ctx);
if args.len() == 1 {
if let Value::Number(n) = &args[0] {
let len = *n as usize;
let mut obj = ObjectData::new();
+ obj.prototype = prototype;
obj.insert_property(
"length".to_string(),
Property {
@@ -1417,7 +1447,9 @@ fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result Result {
@@ -1436,6 +1468,7 @@ fn array_is_array(args: &[Value], ctx: &mut NativeContext) -> Result Result {
let iterable = args.first().cloned().unwrap_or(Value::Undefined);
+ let prototype = array_prototype_from_context(ctx);
match iterable {
Value::Object(r) => {
let len = array_length(ctx.gc, ctx.shapes, r);
@@ -1443,18 +1476,30 @@ fn array_from(args: &[Value], ctx: &mut NativeContext) -> Result {
let chars: Vec = s.chars().map(|c| Value::String(c.to_string())).collect();
- Ok(make_value_array(ctx.gc, ctx.shapes, &chars))
+ Ok(make_value_array_with_proto(
+ ctx.gc, ctx.shapes, &chars, prototype,
+ ))
}
- _ => Ok(make_value_array(ctx.gc, ctx.shapes, &[])),
+ _ => Ok(make_value_array_with_proto(
+ ctx.gc,
+ ctx.shapes,
+ &[],
+ prototype,
+ )),
}
}
fn array_of(args: &[Value], ctx: &mut NativeContext) -> Result {
- Ok(make_value_array(ctx.gc, ctx.shapes, args))
+ let prototype = array_prototype_from_context(ctx);
+ Ok(make_value_array_with_proto(
+ ctx.gc, ctx.shapes, args, prototype,
+ ))
}
// ── Error constructors ───────────────────────────────────────
diff --git a/crates/js/src/compiler.rs b/crates/js/src/compiler.rs
index 2743211..d5969b0 100644
--- a/crates/js/src/compiler.rs
+++ b/crates/js/src/compiler.rs
@@ -2345,10 +2345,6 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(),
fc.free_reg(key_reg);
}
- // Set `this` to the receiver object before calling.
- let this_ni = fc.builder.add_name("this");
- fc.builder.emit_store_global(this_ni, obj_reg);
-
let args_start = fc.next_reg;
let arg_count = arguments.len().min(255) as u8;
for arg in arguments {
@@ -2356,6 +2352,11 @@ fn compile_expr(fc: &mut FunctionCompiler, expr: &Expr, dst: Reg) -> Result<(),
compile_expr(fc, arg, arg_reg)?;
}
+ // Set `this` after compiling arguments so nested method calls
+ // inside the argument list cannot clobber the receiver.
+ let this_ni = fc.builder.add_name("this");
+ fc.builder.emit_store_global(this_ni, obj_reg);
+
fc.builder.emit_call(dst, func_reg, args_start, arg_count);
// Free in LIFO order: args, func_reg, obj_reg.
diff --git a/crates/js/src/dom_bridge.rs b/crates/js/src/dom_bridge.rs
index 4d48d42..4dbca9a 100644
--- a/crates/js/src/dom_bridge.rs
+++ b/crates/js/src/dom_bridge.rs
@@ -28,6 +28,7 @@ type NativeMethod = (
// ── Font registry (lazy-initialized, single-threaded) ─────────────────
use std::cell::RefCell;
+use std::collections::HashMap;
// ── ImageData / Uint8ClampedArray store ──────────────────────────────
@@ -939,6 +940,228 @@ fn make_empty_array(gc: &mut Gc, shapes: &mut ShapeTable) -> Value {
Value::Object(gc.alloc(HeapObject::Object(obj)))
}
+// ── MutationObserver ────────────────────────────────────────────────
+
+const MUTATION_OBSERVER_CALLBACK_KEY: &str = "__mutation_observer_callback__";
+const MUTATION_OBSERVER_ACTIVE_KEY: &str = "__mutation_observer_active__";
+const MUTATION_OBSERVER_TARGET_KEY: &str = "__mutation_observer_target__";
+const MUTATION_OBSERVER_CHILD_LIST_KEY: &str = "__mutation_observer_child_list__";
+const MUTATION_OBSERVER_ATTRIBUTES_KEY: &str = "__mutation_observer_attributes__";
+const MUTATION_OBSERVER_SUBTREE_KEY: &str = "__mutation_observer_subtree__";
+
+fn bool_option(
+ gc: &Gc,
+ shapes: &ShapeTable,
+ options: Option,
+ key: &str,
+) -> bool {
+ options
+ .and_then(|opts_ref| match gc.get(opts_ref) {
+ Some(HeapObject::Object(opts)) => opts.get_property(key, shapes),
+ _ => None,
+ })
+ .map(|prop| prop.value.to_boolean())
+ .unwrap_or(false)
+}
+
+fn mutation_observer_constructor(
+ args: &[Value],
+ ctx: &mut NativeContext,
+) -> Result {
+ let callback = match args.first() {
+ Some(Value::Function(r)) => *r,
+ _ => {
+ return Err(RuntimeError::type_error(
+ "MutationObserver callback must be a function",
+ ))
+ }
+ };
+
+ let mut data = ObjectData::new();
+ data.prototype = ctx.callee.and_then(|callee| match ctx.gc.get(callee) {
+ Some(HeapObject::Function(fdata)) => fdata.prototype_obj,
+ _ => None,
+ });
+ data.insert_property(
+ MUTATION_OBSERVER_CALLBACK_KEY.to_string(),
+ Property::builtin(Value::Function(callback)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_ACTIVE_KEY.to_string(),
+ Property::builtin(Value::Boolean(false)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_TARGET_KEY.to_string(),
+ Property::builtin(Value::Null),
+ ctx.shapes,
+ );
+ Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(data))))
+}
+
+fn mutation_observer_observe(
+ args: &[Value],
+ ctx: &mut NativeContext,
+) -> Result {
+ let observer_ref = match ctx.this {
+ Value::Object(r) => r,
+ _ => {
+ return Err(RuntimeError::type_error(
+ "observe called on non-MutationObserver",
+ ))
+ }
+ };
+ let is_observer = matches!(
+ ctx.gc.get(observer_ref),
+ Some(HeapObject::Object(data))
+ if data
+ .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes)
+ .is_some()
+ );
+ if !is_observer {
+ return Err(RuntimeError::type_error(
+ "observe called on non-MutationObserver",
+ ));
+ }
+
+ let target = match args.first() {
+ Some(Value::Object(target_ref)) => get_node_id(ctx.gc, ctx.shapes, *target_ref),
+ _ => None,
+ }
+ .ok_or_else(|| RuntimeError::type_error("MutationObserver.observe target must be a Node"))?;
+
+ let options = match args.get(1) {
+ Some(Value::Object(opts_ref)) => Some(*opts_ref),
+ _ => None,
+ };
+ let child_list = bool_option(ctx.gc, ctx.shapes, options, "childList");
+ let attributes = bool_option(ctx.gc, ctx.shapes, options, "attributes");
+ let subtree = bool_option(ctx.gc, ctx.shapes, options, "subtree");
+ if !(child_list || attributes) {
+ return Err(RuntimeError::type_error(
+ "MutationObserver.observe requires childList or attributes",
+ ));
+ }
+
+ if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(observer_ref) {
+ data.insert_property(
+ MUTATION_OBSERVER_ACTIVE_KEY.to_string(),
+ Property::builtin(Value::Boolean(true)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_TARGET_KEY.to_string(),
+ Property::builtin(Value::Number(target.index() as f64)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_CHILD_LIST_KEY.to_string(),
+ Property::builtin(Value::Boolean(child_list)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_ATTRIBUTES_KEY.to_string(),
+ Property::builtin(Value::Boolean(attributes)),
+ ctx.shapes,
+ );
+ data.insert_property(
+ MUTATION_OBSERVER_SUBTREE_KEY.to_string(),
+ Property::builtin(Value::Boolean(subtree)),
+ ctx.shapes,
+ );
+ }
+ Ok(Value::Undefined)
+}
+
+fn mutation_observer_disconnect(
+ _args: &[Value],
+ ctx: &mut NativeContext,
+) -> Result {
+ let observer_ref = match ctx.this {
+ Value::Object(r) => r,
+ _ => {
+ return Err(RuntimeError::type_error(
+ "disconnect called on non-MutationObserver",
+ ))
+ }
+ };
+ if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(observer_ref) {
+ if data
+ .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes)
+ .is_none()
+ {
+ return Err(RuntimeError::type_error(
+ "disconnect called on non-MutationObserver",
+ ));
+ }
+ data.insert_property(
+ MUTATION_OBSERVER_ACTIVE_KEY.to_string(),
+ Property::builtin(Value::Boolean(false)),
+ ctx.shapes,
+ );
+ }
+ Ok(Value::Undefined)
+}
+
+fn mutation_observer_take_records(
+ _args: &[Value],
+ ctx: &mut NativeContext,
+) -> Result {
+ match ctx.this {
+ Value::Object(observer_ref) => match ctx.gc.get(observer_ref) {
+ Some(HeapObject::Object(data))
+ if data
+ .get_property(MUTATION_OBSERVER_CALLBACK_KEY, ctx.shapes)
+ .is_some() => {}
+ _ => {
+ return Err(RuntimeError::type_error(
+ "takeRecords called on non-MutationObserver",
+ ))
+ }
+ },
+ _ => {
+ return Err(RuntimeError::type_error(
+ "takeRecords called on non-MutationObserver",
+ ))
+ }
+ }
+ Ok(make_empty_array(ctx.gc, ctx.shapes))
+}
+
+pub fn init_mutation_observer(vm: &mut Vm) {
+ let mut proto = ObjectData::new();
+ proto.prototype = vm.object_prototype;
+ let proto_ref = vm.gc.alloc(HeapObject::Object(proto));
+
+ let methods: &[NativeMethod] = &[
+ ("observe", mutation_observer_observe),
+ ("disconnect", mutation_observer_disconnect),
+ ("takeRecords", mutation_observer_take_records),
+ ];
+ for &(name, callback) in methods {
+ let func = make_native(&mut vm.gc, name, callback);
+ set_builtin_prop(
+ &mut vm.gc,
+ &mut vm.shapes,
+ proto_ref,
+ name,
+ Value::Function(func),
+ );
+ }
+
+ let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
+ name: "MutationObserver".to_string(),
+ kind: FunctionKind::Native(NativeFunc {
+ callback: mutation_observer_constructor,
+ }),
+ prototype_obj: Some(proto_ref),
+ properties: HashMap::new(),
+ upvalues: Vec::new(),
+ })));
+ vm.set_global("MutationObserver", Value::Function(ctor));
+}
+
// ── Method registration on wrappers ──────────────────────────────────
/// Register DOM methods on a wrapper object based on node type.
@@ -7071,6 +7294,25 @@ mod tests {
}
}
+ #[test]
+ fn test_mutation_observer_constructor_and_methods() {
+ let result = eval_with_doc(
+ "",
+ r#"
+ var o = new MutationObserver(function(records) {});
+ o.observe(document.documentElement, { childList: true, subtree: true });
+ var records = o.takeRecords();
+ o.disconnect();
+ typeof o.observe + ":" + typeof o.disconnect + ":" + records.length
+ "#,
+ )
+ .unwrap();
+ match result {
+ Value::String(s) => assert_eq!(s, "function:function:0"),
+ v => panic!("expected MutationObserver method summary, got {v:?}"),
+ }
+ }
+
#[test]
fn test_element_node_type() {
let result = eval_with_doc("", "document.body.nodeType").unwrap();
diff --git a/crates/js/src/streams.rs b/crates/js/src/streams.rs
index f2c3b0a..40c5df4 100644
--- a/crates/js/src/streams.rs
+++ b/crates/js/src/streams.rs
@@ -1294,7 +1294,17 @@ function ReadableStream_pipeTo(_t, destination, options) {
};
if (signal) {
- if (signal.aborted) { onAbortFn(); return; }
+ if (signal.aborted) {
+ var preReason = signal.reason;
+ if (!preventAbort) {
+ try { writer.abort(preReason); } catch (e) {}
+ }
+ if (!preventCancel) {
+ try { reader.cancel(preReason); } catch (e) {}
+ }
+ cleanup(false, preReason);
+ return;
+ }
signal.addEventListener('abort', onAbortFn);
}
diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs
index 1694bd7..a424e92 100644
--- a/crates/js/src/vm.rs
+++ b/crates/js/src/vm.rs
@@ -1279,6 +1279,7 @@ impl Vm {
crate::dom_bridge::init_storage_objects(self);
crate::dom_bridge::init_canvas_path_api(self);
crate::dom_bridge::init_image_data_api(self);
+ crate::dom_bridge::init_mutation_observer(self);
crate::indexeddb::init_indexeddb(self);
crate::service_worker::init_service_worker_api(self);
crate::cache_api::init_cache_api(self);
@@ -7064,6 +7065,44 @@ mod tests {
}
}
+ #[test]
+ fn test_array_from_result_has_array_prototype_methods() {
+ let src = r#"
+ var sum = 0;
+ var arrayLike = { length: 3 };
+ arrayLike[0] = 2;
+ arrayLike[1] = 4;
+ arrayLike[2] = 6;
+ Array.from(arrayLike).forEach(function(x) {
+ sum = sum + x;
+ });
+ sum
+ "#;
+ match eval(src).unwrap() {
+ Value::Number(n) => assert_eq!(n, 12.0),
+ v => panic!("expected 12, got {v:?}"),
+ }
+ }
+
+ #[test]
+ fn test_method_call_this_survives_nested_argument_calls() {
+ let src = r#"
+ var receiver = {
+ value: 10,
+ method: function(x) { return this.value + x; }
+ };
+ var argument = {
+ value: 5,
+ get: function() { return this.value; }
+ };
+ receiver.method(argument.get())
+ "#;
+ match eval(src).unwrap() {
+ Value::Number(n) => assert_eq!(n, 15.0),
+ v => panic!("expected 15, got {v:?}"),
+ }
+ }
+
#[test]
fn test_array_flat() {
let src = r#"