Repository: pyodide/pyodide
Pull request #5671: Minor js perf improvements
Author: login: WebReflection
Created: 2025-05-29T08:29:59Z | Merged: 2025-05-30T19:29:36Z

<!-- Thank you for contributing to Pyodide! All improvements are welcome,
     so don't be afraid to make a PR. -->

### Description

The goal of this MR is to improve some low hanging fruits around TS/JS perf, most notably:

  * avoid unnecessary creation of descriptors to actually find if a property is *own* + use the modern `Object.hasOwn` when available and fallback with `hasOwnProperty` instead of `getOwnPropertyDescriptor` as that creates each time a new object even with the same ref and key
  * avoid runtime creation and crawl of always the same array of strings and use a set to have predictable *O(1)* checks for something super frequent/common (critical path)
  * avoid retrieving N times char points

**edit** tried the following, it doesn't work - feel free to ignore

- - -

About latest point though, I wonder if Python would handle just as fine `ASSIGN_U16` so that the whole JS to Python string conversion could be actually simplified as much as this:

```js
function js2python_string(value) {
  let result = _PyUnicode_New(value.length, 0xffff);
  if (result === 0) {
    throw new PropagateError();
  }

  for (let ptr = _PyUnicode_Data(result), i = 0; i < value.length; i++) {
    ASSIGN_U16(ptr, 0, value.charCodeAt(i));
    ptr += 2;
  }
  return result;
}
```

If that works, that might be not the most space-efficient way but surely it'll be the easiest/most straight forward way to go?

Anyway, happy to change/improve/ignore anything you like.



Diff:
diff --git a/src/core/js2python.js b/src/core/js2python.js
index e098d825500..2d92afeb8f2 100644
--- a/src/core/js2python.js
+++ b/src/core/js2python.js
@@ -6,13 +6,14 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
@@ -20,19 +21,16 @@ function js2python_string(value) {
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
-      ptr += 4;
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, i, code_points[i]);
     }
   } else if (max_code_point > 0xff) {
-    for (let c of value) {
-      ASSIGN_U16(ptr, 0, c.codePointAt(0));
-      ptr += 2;
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U16(ptr, i, code_points[i]);
     }
   } else {
-    for (let c of value) {
-      ASSIGN_U8(ptr, 0, c.codePointAt(0));
-      ptr += 1;
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U8(ptr, i, code_points[i]);
     }
   }
 
diff --git a/src/core/pre.js b/src/core/pre.js
index 264ea63458b..9345aee21b6 100644
--- a/src/core/pre.js
+++ b/src/core/pre.js
@@ -19,6 +19,8 @@ API.getTypeTag = getTypeTag;
  *
  * Observe whether a property exists or not without invoking getters.
  * Should never throw as long as arguments have the correct types.
+ * This check is better than `prop in obj` because it works also with
+ * primitives where `"length" in "str"`, as example, would throw.
  *
  * obj: an object
  * prop: a string or symbol
@@ -26,7 +28,7 @@ API.getTypeTag = getTypeTag;
 function hasProperty(obj, prop) {
   try {
     while (obj) {
-      if (Object.getOwnPropertyDescriptor(obj, prop)) {
+      if (Object.hasOwn(obj, prop)) {
         return true;
       }
       obj = Object.getPrototypeOf(obj);
@@ -70,7 +72,7 @@ const nullToUndefined = (x) => (x === null ? undefined : x);
 function isPromise(obj) {
   try {
     // clang-format off
-    return !!obj && typeof obj.then === "function";
+    return typeof obj?.then === "function";
     // clang-format on
   } catch (e) {
     return false;
@@ -95,7 +97,7 @@ API.typedArrayAsUint8Array = bufferAsUint8Array;
 
 Module.iterObject = function* (object) {
   for (let k in object) {
-    if (Object.prototype.hasOwnProperty.call(object, k)) {
+    if (Object.hasOwn(object, k)) {
       yield k;
     }
   }
diff --git a/src/core/pyproxy.ts b/src/core/pyproxy.ts
index b59ad763d65..5863ca66bbb 100644
--- a/src/core/pyproxy.ts
+++ b/src/core/pyproxy.ts
@@ -2146,6 +2146,13 @@ function python_pop(jsobj: any, pop_start: boolean): any {
   return res;
 }
 
+const filteredHasKeySet: Set<string | symbol> = new Set([
+  "name",
+  "length",
+  "caller",
+  "arguments",
+]);
+
 function filteredHasKey(
   jsobj: PyProxy,
   jskey: string | symbol,
@@ -2159,17 +2166,12 @@ function filteredHasKey(
     return (
       jskey in jsobj &&
       !(
-        [
-          "name",
-          "length",
-          "caller",
-          "arguments",
-          // we are required by JS law to return `true` for `"prototype" in pycallable`
-          // but we are allowed to return the value of `getattr(pycallable, "prototype")`.
-          // So we filter prototype out of the "get" trap but not out of the "has" trap
-          filterProto ? "prototype" : undefined,
-        ] as (string | symbol)[]
-      ).includes(jskey)
+        filteredHasKeySet.has(jskey) ||
+        // we are required by JS law to return `true` for `"prototype" in pycallable`
+        // but we are allowed to return the value of `getattr(pycallable, "prototype")`.
+        // So we filter prototype out of the "get" trap but not out of the "has" trap
+        (filterProto && jskey === "prototype")
+      )
     );
   } else {
     return jskey in jsobj;
@@ -2341,6 +2343,14 @@ const PyProxySequenceHandlers = {
   },
 };
 
+const PyProxyJsonAdaptorDictHandlersSet = new Set([
+  "copy",
+  "constructor",
+  "$$flags",
+  "toString",
+  "destroy",
+]);
+
 const PyProxyJsonAdaptorDictHandlers = {
   isExtensible(): boolean {
     return true;
@@ -2360,9 +2370,7 @@ const PyProxyJsonAdaptorDictHandlers = {
   get(jsobj: PyProxy, jskey: any): any {
     let result;
     if (
-      ["copy", "constructor", "$$flags", "toString", "destroy"].includes(
-        jskey,
-      ) ||
+      PyProxyJsonAdaptorDictHandlersSet.has(jskey) ||
       typeof jskey === "symbol"
     ) {
       // @ts-ignore


Reviews:
user: login: ryanking13
body: Thanks! It looks good to me.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-29T13:21:45Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T03:53:07Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T03:56:27Z
user: login: ryanking13
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T05:03:23Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T05:51:16Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T05:57:35Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T06:02:38Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T06:08:24Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T16:33:19Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T16:36:33Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T16:38:29Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T16:39:16Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T16:42:39Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:00:47Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:09:30Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:14:33Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T17:21:02Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:26:33Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T17:30:15Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T17:31:24Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:32:09Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T17:32:16Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:37:42Z
user: login: hoodmane
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T17:37:43Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:40:32Z
user: login: WebReflection
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-30T17:43:43Z

Review comments:
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: hoodmane
body: This adds an extra allocation doesn't it? I'd think the original allocation-free version would be better.
created_at: 2025-05-30T03:53:02Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,10 @@ function getTypeTag(x) {
 }
 API.getTypeTag = getTypeTag;
 
+const hasOwn =
+  Object.hasOwn ||
path: src/core/pre.js
user: login: hoodmane
body: I think we can assume that `Object.hasOwn()` is present.
created_at: 2025-05-30T03:55:30Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: ryanking13
body: But it will be cleaned up after the function isn't it?

While we don't have a thorough benchmark, when I wrote a short benchmark script comparing the two method, it seems like caching is quite better than calling `codePointAt` multiple times:

<details><summary>Script</summary>
<p>

```js
function benchmark(label, fn, iterations = 100000) {
  const { performance } = require('perf_hooks');
  const start = performance.now();
  let result = 0;
  for (let i = 0; i < iterations; i++) {
    result += fn();
  }
  const end = performance.now();
  console.log(`${label}: ${(end - start).toFixed(2)} ms`);
  return result;
}

function createLongString(length) {
  return 'Hello 🌍🚀'.repeat(length); // Mixed characters
}

// Short and long test strings
const shortString = "Hello 🌍🚀";
const longString = createLongString(100);

// Functions

function noCache(str) {
  let sum = 0;
  for (let i = 0; i < 3; i++) {
    for (const char of str) {
      sum += char.codePointAt(0) || 0;
    }
  }
  return sum;
}

function withCache(str) {
  const cache = [];
  for (const char of str) {
    cache.push(char.codePointAt(0) || 0);
  }

  let sum = 0;
  for (let i = 0; i < 3; i++) {
    for (const val of cache) {
      sum += val;
    }
  }
  return sum;
}

// Run benchmarks

console.log("=== Short String Benchmark ===");
const resultShortNoCache = benchmark("No Cache (Short)", () => noCache(shortString));
const resultShortWithCache = benchmark("With Cache (Short)", () => withCache(shortString));
console.log("Short Match:", resultShortNoCache === resultShortWithCache);

console.log("\n=== Long String Benchmark ===");
const resultLongNoCache = benchmark("No Cache (Long)", () => noCache(longString));
const resultLongWithCache = benchmark("With Cache (Long)", () => withCache(longString));
console.log("Long Match:", resultLongNoCache === resultLongWithCache);

```

</p>
</details> 

```
=== Short String Benchmark ===
No Cache (Short): 31.93 ms
With Cache (Short): 19.77 ms
Short Match: true

=== Long String Benchmark ===
No Cache (Long): 2703.89 ms
With Cache (Long): 1379.77 ms
Long Match: true
```
created_at: 2025-05-30T05:03:22Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: WebReflection
body: calling prototype methods multiple times is never faster than caching and all *JS* engines are super fast at collecting one-off scoped references + the code as it is passes through *JIT* or turbofan right away in both `push` and each loop later on: it's fast path by all means, it can hardly go faster.

I could've used that int to increment the index instead of using *push* but there is a known v8 limitation around that patter: it throws with arrays bigger than ~~1.8GB~~ 112813857 while `push` will keep going up to the array limit which is 4GB.

As summary: calling and resolving codepoints is expensive and in there unnecessarily repeated twice per each same code while caching via a plain array is cheap and fast and it doesn't cause any memory issue because it's freed instantly after.

**However** if it's the allocation the concern that could have an outer scope, always same, pointer to an array that is then reset via `cache.length = 0` but it's a rather "*dirty hack*" that shouldn't be used these days and I am not sure that ends up jitted right away anymore so I agree with @ryanking13 the current implementation wins for perf and simplicity (at the engine optimization level).
created_at: 2025-05-30T05:51:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: WebReflection
body: if anyone is interested in the `i++` jitted bug https://github.com/nodejs/node/issues/47928#issuecomment-2678282748 ... and the limit is not even 1.8GB, it's literally the number 112813858 which, once reached, makes *v8* crash with that pattern.
created_at: 2025-05-30T05:57:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,6 +14,10 @@ function getTypeTag(x) {
 }
 API.getTypeTag = getTypeTag;
 
+const hasOwn =
+  Object.hasOwn ||
path: src/core/pre.js
user: login: WebReflection
body: Samsung Internet and other WebKit based mobile variants might be late to the party https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn#browser_compatibility ... if browsers before 2023 are not meant to work I am more than happy to remove the fallback here, I wrote that just because `Object.getOwnPropertyDescriptor` predated `Object.hasOwn` by far so I didn't want to break that compatibility.
created_at: 2025-05-30T06:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: WebReflection
body: btw ... we were allocating dozen object descriptors up to every prototype chain before via `Object.getOwnPropertyDescriptor` so that having just an *array* to do the right thing doesn't seem to be worse than what this code was doing before this PR in terms of allocations + it is faster by design/logic 😉
created_at: 2025-05-30T06:08:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
path: src/core/js2python.js
user: login: hoodmane
body: Okay I'm convinced by @ryanking13's profiling data
created_at: 2025-05-30T16:33:19Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: What about:
```js
code_points.forEach((c, idx) => {
   ASSIGN_U32(ptr, idx, c);
});
```
and similarly in the other two cases.
created_at: 2025-05-30T16:36:32Z
author_association: MEMBER
diff_hunk: @@ -19,14 +19,16 @@ API.getTypeTag = getTypeTag;
  *
  * Observe whether a property exists or not without invoking getters.
  * Should never throw as long as arguments have the correct types.
+ * This check is better than `prop in obj` because it works also with
+ * primitives where `"length" in "str"`, as example, would throw.
path: src/core/pre.js
user: login: hoodmane
body: Thanks!
created_at: 2025-05-30T16:38:29Z
author_association: MEMBER
diff_hunk: @@ -19,14 +19,16 @@ API.getTypeTag = getTypeTag;
  *
  * Observe whether a property exists or not without invoking getters.
  * Should never throw as long as arguments have the correct types.
+ * This check is better than `prop in obj` because it works also with
+ * primitives where `"length" in "str"`, as example, would throw.
  *
  * obj: an object
  * prop: a string or symbol
  */
 function hasProperty(obj, prop) {
   try {
     while (obj) {
-      if (Object.getOwnPropertyDescriptor(obj, prop)) {
+      if (Object.hasOwn(obj, prop)) {
path: src/core/pre.js
user: login: hoodmane
body: So if I understand, this is a performance boost because it returns a boolean instead of having to make a temporary property descriptor and throw it away? Makes sense.
created_at: 2025-05-30T16:39:16Z
author_association: MEMBER
diff_hunk: @@ -14,6 +14,10 @@ function getTypeTag(x) {
 }
 API.getTypeTag = getTypeTag;
 
+const hasOwn =
+  Object.hasOwn ||
path: src/core/pre.js
user: login: hoodmane
body: Yeah we generally aim for ~2 years of backwards compatibility with mainstream browsers. If there were a bunch of iOS users stranded on an ancient version or something we might make special allowance for that. But I'm pretty sure this won't be the newest feature we're requiring.
created_at: 2025-05-30T16:42:39Z
author_association: MEMBER
diff_hunk: @@ -19,14 +19,16 @@ API.getTypeTag = getTypeTag;
  *
  * Observe whether a property exists or not without invoking getters.
  * Should never throw as long as arguments have the correct types.
+ * This check is better than `prop in obj` because it works also with
+ * primitives where `"length" in "str"`, as example, would throw.
  *
  * obj: an object
  * prop: a string or symbol
  */
 function hasProperty(obj, prop) {
   try {
     while (obj) {
-      if (Object.getOwnPropertyDescriptor(obj, prop)) {
+      if (Object.hasOwn(obj, prop)) {
path: src/core/pre.js
user: login: WebReflection
body: Yeah, creating a descriptor to use it just as truthy value is overkill and it allocates for no benefits whatsoever so this was more a GC/RAM/logic boost based on actual Boolean and zero waste of specs (the getOwnPropertyDescripor one vs just has own property)
created_at: 2025-05-30T17:00:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: you won't find a single benchmark out there where `forEach` is faster than a `for` loop but I am a bit confused ... you were worried about a single array allocation (for caching purpose that proved to be worth it) and now you want to create closures for no benefit/reason/performance gain? 🤔

for loops with a fixed length are JITted right away, closures and `forEach` are not ... before there was an iterator based loop which is also way slower than an index/length based one so to me that's a no?
created_at: 2025-05-30T17:09:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: also worth noting that your suggestion would break because the `ptr` needs to be incremented (1, 2 or 4) and the function call always needs to use `0` while assigning 'cause the `ptr` index value does not reflect the array index value so I think we are better off with the current approach which is both correct and faster and easier to read/reason about, imho (as it was before).
created_at: 2025-05-30T17:14:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: Okay how about this then?
```js
for (let i = 0; i < num_code_points; i++) {
   ASSIGN_U32(ptr, i, code_points[i]);
}
```
created_at: 2025-05-30T17:21:02Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: it won't work as explained ... I eventually need to write this instead:

```js
for (let i = 0; i < num_code_points; i++) {
   ASSIGN_U8(ptr, i, code_points[i]);
}
for (let i = 0; i < num_code_points; i++) {
   ASSIGN_U16(ptr, i * 2, code_points[i]);
}
for (let i = 0; i < num_code_points; i++) {
   ASSIGN_U32(ptr, i * 4, code_points[i]);
}
```

and hope all tests pass again

**edited** ... so, should I do that?
created_at: 2025-05-30T17:26:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: You don't need to multiply by 2 and 4, `ASSIGN_U16` and `ASSIGN_U32` already handle that.
created_at: 2025-05-30T17:30:14Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: `ASSIGN_U32(addr, offset, value)` is a macro which expands to:
```js
HEAPU32[(addr >> 2) + offset] = value
```

created_at: 2025-05-30T17:31:24Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: I see ... then why was the pointer incremented before? I'll try your suggestion, at that point it's a matter of just defining the function to call and use a single loop instead
created_at: 2025-05-30T17:32:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: See here:
https://github.com/pyodide/pyodide/blob/main/src/core/jsmemops.h?plain=1#L17
created_at: 2025-05-30T17:32:15Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: yeah ... it fails:

```
PythonError: pyodide.ffi.JsException: <exception str() failed>
```
created_at: 2025-05-30T17:37:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: hoodmane
body: > then why was the pointer incremented before?

Previously we didn't have a variable that was the index we were assigning to since we did `for (let c of str)`. Now we have the index in a variable so I think this reads a bit better.
created_at: 2025-05-30T17:37:43Z
author_association: MEMBER
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: ok ... it doesn't if I don't assign `ASSIGN_U32` or `ASSIGN_U16` or `ASSIGN_U8` ... I guess there's some processing incapable of dealing with runtime code in there, a bit weird ... but latest at least builds.

Pushing, I hope that's good enough now.
created_at: 2025-05-30T17:40:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -6,32 +6,33 @@ function js2python_string(value) {
   // to determine if is needs to be a 1-, 2- or 4-byte string, since
   // Python handles all 3.
   let max_code_point = 0;
-  let num_code_points = 0;
+  const code_points = [];
   for (let c of value) {
-    num_code_points++;
     let code_point = c.codePointAt(0);
+    code_points.push(code_point);
     max_code_point = code_point > max_code_point ? code_point : max_code_point;
   }
 
+  let num_code_points = code_points.length;
   let result = _PyUnicode_New(num_code_points, max_code_point);
   if (result === 0) {
     throw new PropagateError();
   }
 
   let ptr = _PyUnicode_Data(result);
   if (max_code_point > 0xffff) {
-    for (let c of value) {
-      ASSIGN_U32(ptr, 0, c.codePointAt(0));
+    for (let i = 0; i < num_code_points; i++) {
+      ASSIGN_U32(ptr, 0, code_points[i]);
path: src/core/js2python.js
user: login: WebReflection
body: done, let's see if this works 🤞
created_at: 2025-05-30T17:43:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: WebReflection
created_at: 2025-05-29T10:03:40Z
body: @ryanking13 @hoodmane btw ... the `hasProperty` crawls the prototype too and check each one up to `null` and passing, I believe, to `Object.prototype` ... accordingly, that looks to me like an overly-engineered `prop in obj` operation, ~~as that would basically do the same: check if a property is present down the prototypal chain~~ (nope, my bad) ... is there any background/reason that check needs to be orchestrated as such? 'cause while `hasOwn` as helper makes sense to me, the `hasProperty` utility itself could be just removed entirely, imho, unless there is some weird case I am not considering, thanks.

**edit**

as example, this just works the same but it moves much less code:

```js
function hasProperty(obj, prop) {
  return !!obj && prop in obj;
}
```

**edit** ... **A AH** ... that would throw with `string` or other primitives ... never mind then, it should be good as it is now.
author_association: CONTRIBUTOR
user: login: ryanking13
created_at: 2025-05-29T13:20:14Z
body: > About latest point though, I wonder if Python would handle just as fine ASSIGN_U16 so that the whole JS to Python string conversion could be actually simplified as much as this:

I am not 100% sure, as that code was [written 6 years ago](https://github.com/hoodmane/pyodide/commit/46f64b5c7cc6b318da9d4cb697eed5a6715b2540), but I think unicode is quite complex and that code if-else branch was required to support CJK strings and emojis. But I think you can try to see if it works. I believe we have some test cases for unicode.
author_association: MEMBER
user: login: WebReflection
created_at: 2025-05-29T13:55:34Z
body: @ryanking13 

> I think you can try to see if it works

I tried, it fails at building ... so I guess it doesn't work for complex points ... all good though, it's already slightly simplified now.
author_association: CONTRIBUTOR
user: login: hoodmane
created_at: 2025-05-30T03:56:48Z
body: Thanks for the improvements @WebReflection !
author_association: MEMBER
user: login: hoodmane
created_at: 2025-05-30T03:59:24Z
body: Maybe you could add a comment explaining why the `hasProperty()` code is right if it is right and you understand why it's right? This stuff can be a bit JavaScript voodoo so it'd be good to explain.
author_association: MEMBER
user: login: WebReflection
created_at: 2025-05-30T06:13:55Z
body: @hoodmane done (removed `hasOwn` fallback + added a reason `hasProperty` is good as it is compared to `in` operator).
author_association: CONTRIBUTOR
user: login: hoodmane
created_at: 2025-05-30T19:29:39Z
body: Thanks!
author_association: MEMBER