Skip to content

Reuse one frozen object per depth for a nested None - #9025

Merged
cknitt merged 1 commit into
masterfrom
cache-nested-none-markers
Oct 24, 2026
Merged

cknitt merged 1 commit into
masterfrom
cache-nested-none-markers

Conversation

@cknitt

@cknitt cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member

Primitive_option.some and valFromOption created a new {BS_PRIVATE_NESTED_SOME_NONE: depth} object on every call. This had two effects:

  • Wrapping or unwrapping a nested None allocated each time. Generic code such as Option.map over an option<option<_>> runs through these functions.
  • Two equal values were different objects: Some(None) === Some(None) was false, while Some(1) === Some(1) is true.

Now each depth has one frozen object, created on first use and returned by both functions:

let wrap = x => Some(x)
let none: option<int> = None
wrap(none) === wrap(none) // before: false, after: true

Generated code doesn't change: it calls the same two functions. Values are still recognised by the BS_PRIVATE_NESTED_SOME_NONE property, so two copies of the runtime, e.g. in two bundles, still work with each other's values. Only === between their objects stays false, as before. The objects are frozen because they are now shared, so a write through Obj.magic would otherwise affect every nested None of that depth.

New test option_nested_marker_test.res: identity at each depth, unwrapping to the marker one level down, frozen markers, and unchanged matching and equality.

🤖 Generated with Claude Code

https://claude.ai/code/session_015yE4gCmTmJKVecrVt1PU8C

Primitive_option.some and valFromOption created a new
{BS_PRIVATE_NESTED_SOME_NONE: depth} object on every call. That allocates
each time a nested None is wrapped or unwrapped, and makes two equal
values different objects: Some(None) === Some(None) was false while
Some(1) === Some(1) is true. Each depth now has one frozen object,
created on first use.

Representation modified (nested None):
Before (representation):
    None ↦ undefined
    Some(None) ↦ a new {BS_PRIVATE_NESTED_SOME_NONE: 0} per call
    Some(Some(None)) ↦ a new {BS_PRIVATE_NESTED_SOME_NONE: 1} per call
    note: Some(None) === Some(None) is false
After (representation):
    None ↦ undefined
    Some(None) ↦ the shared frozen {BS_PRIVATE_NESTED_SOME_NONE: 0}
    Some(Some(None)) ↦ the shared frozen {BS_PRIVATE_NESTED_SOME_NONE: 1}
    note: Some(None) === Some(None) is true

JS modified (Primitive_option.some):
Before (js):
    function some(x) {
      if (x === undefined) {
        return {
          BS_PRIVATE_NESTED_SOME_NONE: 0
        };
      } else if (x !== null && x.BS_PRIVATE_NESTED_SOME_NONE !== undefined) {
        return {
          BS_PRIVATE_NESTED_SOME_NONE: x.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0
        };
      } else {
        return x;
      }
    }
After (js):
    let markers = [];

    function marker(depth) {
      let m = markers[depth];
      if (m !== undefined) {
        return m;
      }
      let m$1 = Object.freeze({
        BS_PRIVATE_NESTED_SOME_NONE: depth
      });
      markers[depth] = m$1;
      return m$1;
    }

    function some(x) {
      if (x === undefined) {
        return marker(0);
      } else if (x !== null && x.BS_PRIVATE_NESTED_SOME_NONE !== undefined) {
        return marker(x.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0);
      } else {
        return x;
      }
    }

Review: Does this work with two copies of the runtime, e.g. in two
bundles? Yes: a value is recognised by its property, not by identity, so
each copy's objects work with the other copy; only === between them stays
false, as before.
Review: Why freeze? The objects are now shared, so a write through
Obj.magic would change every nested None of that depth.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yE4gCmTmJKVecrVt1PU8C
Signed-off-by: Christoph Knittel <ck@cca.io>
@github-actions

Copy link
Copy Markdown

Changelog

💅 Polish

  • Reuse one object per depth for nested None (Some(None), Some(Some(None)), …), so creating one no longer allocates and Some(None) === Some(None) holds.

Semantic Diff

1 commit · Full page

Reuse one frozen object per depth for a nested None

fbb13e4 · cknitt + Claude · Oct 24, 2026, 17:15 UTC

Primitive_option.some and valFromOption created a new {BS_PRIVATE_NESTED_SOME_NONE: depth} object on every call. That allocates each time a nested None is wrapped or unwrapped, and makes two equal values different objects: Some(None) === Some(None) was false while Some(1) === Some(1) is true. Each depth now has one frozen object, created on first use.

Representation modified (nested None)

representation

 None             ↦ undefined
-Some(None)       ↦ a new {BS_PRIVATE_NESTED_SOME_NONE: 0} per call
-Some(Some(None)) ↦ a new {BS_PRIVATE_NESTED_SOME_NONE: 1} per call
-note: Some(None) === Some(None) is false
+Some(None)       ↦ the shared frozen {BS_PRIVATE_NESTED_SOME_NONE: 0}
+Some(Some(None)) ↦ the shared frozen {BS_PRIVATE_NESTED_SOME_NONE: 1}
+note: Some(None) === Some(None) is true

JS modified (Primitive_option.some)

js

+let markers = [];
+
+function marker(depth) {
+  let m = markers[depth];
+  if (m !== undefined) {
+    return m;
+  }
+  let m$1 = Object.freeze({
+    BS_PRIVATE_NESTED_SOME_NONE: depth
+  });
+  markers[depth] = m$1;
+  return m$1;
+}
+
 function some(x) {
   if (x === undefined) {
-    return {
-      BS_PRIVATE_NESTED_SOME_NONE: 0
-    };
+    return marker(0);
   } else if (x !== null && x.BS_PRIVATE_NESTED_SOME_NONE !== undefined) {
-    return {
-      BS_PRIVATE_NESTED_SOME_NONE: x.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0
-    };
+    return marker(x.BS_PRIVATE_NESTED_SOME_NONE + 1 | 0);
   } else {
     return x;
   }
 }

Review

  • Does this work with two copies of the runtime, e.g. in two bundles? Yes: a value is recognised by its property, not by identity, so each copy's objects work with the other copy; only === between them stays false, as before.
  • Why freeze? The objects are now shared, so a write through Obj.magic would change every nested None of that depth.

@cknitt
cknitt marked this pull request as ready for review October 24, 2026 17:20
@cknitt

cknitt commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T17:22:27.478956Z fbb13e4 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: fbb13e432d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 81.31%. Comparing base (d8db3c6) to head (fbb13e4).
⚠️ Report is 8 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9025      +/-   ##
==========================================
+ Coverage   81.29%   81.31%   +0.02%     
==========================================
  Files         456      456              
  Lines       63836    64006     +170     
==========================================
+ Hits        51893    52047     +154     
- Misses      11943    11959      +16     

see 38 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cknitt
cknitt requested a review from cristianoc October 24, 2026 17:38
@pkg-pr-new

pkg-pr-new Bot commented Oct 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9025

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9025

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9025

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9025

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9025

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9025

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9025

commit: fbb13e4

@github-actions

Copy link
Copy Markdown

@cknitt
cknitt enabled auto-merge (squash) October 24, 2026 17:50
@cknitt
cknitt merged commit 188e24e into master Oct 24, 2026
36 of 38 checks passed
@cknitt
cknitt deleted the cache-nested-none-markers branch October 24, 2026 18:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants