diff --git a/changes/cache-nested-none-markers.polish.md b/changes/cache-nested-none-markers.polish.md new file mode 100644 index 0000000000..0e19961041 --- /dev/null +++ b/changes/cache-nested-none-markers.polish.md @@ -0,0 +1 @@ +- 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. diff --git a/packages/@rescript/runtime/Primitive_option.res b/packages/@rescript/runtime/Primitive_option.res index 88579266df..05b4e65f71 100644 --- a/packages/@rescript/runtime/Primitive_option.res +++ b/packages/@rescript/runtime/Primitive_option.res @@ -13,12 +13,32 @@ let isNested = (x: Obj.t): bool => { Obj.repr((Obj.magic(x): nested).depth) !== Obj.repr(Primitive_js_extern.undefined) } +external getUnsafe: (array<'a>, int) => 'a = "%array_unsafe_get" +external setUnsafe: (array<'a>, int, 'a) => unit = "%array_unsafe_set" +@scope("Object") external freeze: nested => nested = "freeze" + +/* One frozen representation per depth, created on first use, so that + wrapping and unwrapping a nested [None] does not allocate and + [Some(None) === Some(None)] holds */ +let markers: array = [] + +let marker = (depth: int): Obj.t => { + let m = getUnsafe(markers, depth) + if Obj.repr(m) !== Obj.repr(Primitive_js_extern.undefined) { + Obj.repr(m) + } else { + let m = freeze({depth: depth}) + setUnsafe(markers, depth, m) + Obj.repr(m) + } +} + let some = (x: Obj.t): Obj.t => if Obj.magic(x) == None { - Obj.repr({depth: 0}) + marker(0) } /* [x] is neither None nor null so it is safe to do property access */ else if x !== Obj.repr(Primitive_js_extern.null) && isNested(x) { - Obj.repr({depth: (Obj.magic(x): nested).depth + 1}) + marker((Obj.magic(x): nested).depth + 1) } else { x } @@ -48,7 +68,7 @@ let valFromOption = (x: Obj.t): Obj.t => if depth == 0 { Obj.magic(None) } else { - Obj.repr({depth: depth - 1}) + marker(depth - 1) } } else { Obj.magic(x) diff --git a/packages/@rescript/runtime/lib/es6/Primitive_option.mjs b/packages/@rescript/runtime/lib/es6/Primitive_option.mjs index 5258627a18..9fe7522ae0 100644 --- a/packages/@rescript/runtime/lib/es6/Primitive_option.mjs +++ b/packages/@rescript/runtime/lib/es6/Primitive_option.mjs @@ -5,15 +5,25 @@ function isNested(x) { return x.BS_PRIVATE_NESTED_SOME_NONE !== undefined; } +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; } @@ -43,9 +53,7 @@ function valFromOption(x) { if (depth === 0) { return; } else { - return { - BS_PRIVATE_NESTED_SOME_NONE: depth - 1 | 0 - }; + return marker(depth - 1 | 0); } } diff --git a/packages/@rescript/runtime/lib/js/Primitive_option.cjs b/packages/@rescript/runtime/lib/js/Primitive_option.cjs index 349e1de8bb..5b50ebc668 100644 --- a/packages/@rescript/runtime/lib/js/Primitive_option.cjs +++ b/packages/@rescript/runtime/lib/js/Primitive_option.cjs @@ -5,15 +5,25 @@ function isNested(x) { return x.BS_PRIVATE_NESTED_SOME_NONE !== undefined; } +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; } @@ -43,9 +53,7 @@ function valFromOption(x) { if (depth === 0) { return; } else { - return { - BS_PRIVATE_NESTED_SOME_NONE: depth - 1 | 0 - }; + return marker(depth - 1 | 0); } } diff --git a/tests/tests/src/option_nested_marker_test.mjs b/tests/tests/src/option_nested_marker_test.mjs new file mode 100644 index 0000000000..bcacbd8275 --- /dev/null +++ b/tests/tests/src/option_nested_marker_test.mjs @@ -0,0 +1,72 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.mjs"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.mjs"; + +function wrap(x) { + return Primitive_option.some(x); +} + +function unwrap(x) { + if (x !== undefined) { + return Primitive_option.valFromOption(x); + } + throw { + RE_EXN_ID: "Assert_failure", + _1: [ + "option_nested_marker_test.res", + 11, + 12 + ], + Error: new Error() + }; +} + +function depth(x) { + if (x === undefined) { + return 0; + } + let match = Primitive_option.valFromOption(x); + if (match !== undefined) { + if (Primitive_option.valFromOption(match) !== undefined) { + return 3; + } else { + return 2; + } + } else { + return 1; + } +} + +Mocha.describe("Option_nested_marker_test", () => { + Mocha.test("a nested None is the same object at each depth", () => { + Test_utils.eq("File \"option_nested_marker_test.res\", line 25, characters 7-14", true, Primitive_option.some(undefined) === Primitive_option.some(undefined)); + Test_utils.eq("File \"option_nested_marker_test.res\", line 26, characters 7-14", true, Primitive_option.some(Primitive_option.some(undefined)) === Primitive_option.some(Primitive_option.some(undefined))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 27, characters 7-14", false, Primitive_option.some(undefined) === Primitive_option.some(Primitive_option.some(undefined))); + }); + Mocha.test("unwrapping gives the marker one level down", () => { + Test_utils.eq("File \"option_nested_marker_test.res\", line 31, characters 7-14", true, unwrap(Primitive_option.some(Primitive_option.some(undefined))) === Primitive_option.some(undefined)); + Test_utils.eq("File \"option_nested_marker_test.res\", line 32, characters 7-14", true, unwrap(unwrap(Primitive_option.some(Primitive_option.some(undefined)))) === undefined); + }); + Mocha.test("markers are frozen", () => { + Test_utils.eq("File \"option_nested_marker_test.res\", line 36, characters 7-14", true, Object.isFrozen(Primitive_option.some(undefined))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 37, characters 7-14", true, Object.isFrozen(Primitive_option.some(Primitive_option.some(undefined)))); + }); + Mocha.test("matching and equality are unchanged", () => { + Test_utils.eq("File \"option_nested_marker_test.res\", line 42, characters 7-14", 0, depth(undefined)); + Test_utils.eq("File \"option_nested_marker_test.res\", line 43, characters 7-14", 1, depth(Primitive_option.some(undefined))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 44, characters 7-14", 2, depth(Primitive_option.some(Primitive_option.some(undefined)))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 45, characters 7-14", 3, depth(Primitive_option.some(Primitive_option.some(1)))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 46, characters 7-14", Primitive_option.some(Primitive_option.some(undefined)), Primitive_option.some(Primitive_option.some(undefined))); + Test_utils.eq("File \"option_nested_marker_test.res\", line 47, characters 7-14", true, Primitive_object.equal(Primitive_option.some(Primitive_option.some(undefined)), Primitive_option.some(Primitive_option.some(undefined)))); + }); +}); + +export { + wrap, + unwrap, + depth, +} +/* Not a pure module */ diff --git a/tests/tests/src/option_nested_marker_test.res b/tests/tests/src/option_nested_marker_test.res new file mode 100644 index 0000000000..a2005a5113 --- /dev/null +++ b/tests/tests/src/option_nested_marker_test.res @@ -0,0 +1,49 @@ +open Mocha +open Test_utils + +@scope("Object") external isFrozen: 'a => bool = "isFrozen" + +let wrap = x => Some(x) + +let unwrap = (x: option<'a>) => + switch x { + | Some(v) => v + | None => assert(false) + } + +let depth = (x: option>>) => + switch x { + | None => 0 + | Some(None) => 1 + | Some(Some(None)) => 2 + | Some(Some(Some(_))) => 3 + } + +describe(__MODULE__, () => { + test("a nested None is the same object at each depth", () => { + let none: option = None + eq(__LOC__, true, wrap(none) === wrap(none)) + eq(__LOC__, true, wrap(wrap(none)) === wrap(wrap(none))) + eq(__LOC__, false, wrap(none) === Obj.magic(wrap(wrap(none)))) + }) + test("unwrapping gives the marker one level down", () => { + let none: option = None + eq(__LOC__, true, unwrap(wrap(wrap(none))) === wrap(none)) + eq(__LOC__, true, unwrap(unwrap(wrap(wrap(none)))) === none) + }) + test("markers are frozen", () => { + let none: option = None + eq(__LOC__, true, isFrozen(wrap(none))) + eq(__LOC__, true, isFrozen(wrap(wrap(none)))) + }) + test("matching and equality are unchanged", () => { + let none: option = None + let none2: option> = None + eq(__LOC__, 0, depth(None)) + eq(__LOC__, 1, depth(wrap(none2))) + eq(__LOC__, 2, depth(wrap(wrap(none)))) + eq(__LOC__, 3, depth(wrap(wrap(Some(1))))) + eq(__LOC__, Some(Some(None)), wrap(wrap(none))) + eq(__LOC__, true, wrap(wrap(none)) == Some(Some(None))) + }) +})