From 3ed0e72dd726be7b0be48aa483f7483cf4f07cb4 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sat, 20 Sep 2025 11:07:09 +0200 Subject: [PATCH 1/3] Add Parsetree-level Option stdlib optimizations (forEach/map/flatMap) --- compiler/frontend/ast_option_optimizations.ml | 126 ++++++ .../frontend/ast_option_optimizations.mli | 1 + compiler/frontend/bs_builtin_ppx.ml | 3 +- tests/tests/src/core/Core_ObjectTests.mjs | 12 +- tests/tests/src/core/Core_TempTests.mjs | 10 +- tests/tests/src/core/intl/Core_IntlTests.mjs | 4 +- .../src/option_stdlib_optimization_test.mjs | 290 +++++++++++++ .../src/option_stdlib_optimization_test.res | 388 ++++++++++++++++++ tests/tests/src/reactTestUtils.mjs | 12 +- 9 files changed, 830 insertions(+), 16 deletions(-) create mode 100644 compiler/frontend/ast_option_optimizations.ml create mode 100644 compiler/frontend/ast_option_optimizations.mli create mode 100644 tests/tests/src/option_stdlib_optimization_test.mjs create mode 100644 tests/tests/src/option_stdlib_optimization_test.res diff --git a/compiler/frontend/ast_option_optimizations.ml b/compiler/frontend/ast_option_optimizations.ml new file mode 100644 index 0000000000..4ca14651db --- /dev/null +++ b/compiler/frontend/ast_option_optimizations.ml @@ -0,0 +1,126 @@ +open Parsetree +open Longident + +(* + Optimise calls to Option.forEach/map/flatMap so they produce the same switch + structure as handwritten code. We only rewrite calls whose callback is a + simple literal lambda or identifier; more complex callbacks are left intact + to preserve ReScript's call-by-value semantics. +*) + +let value_name = "__res_option_value" + +type option_call = ForEach | Map | FlatMap + +(* Inlineable callbacks are bare identifiers (possibly wrapped in coercions or + type annotations). Those can be applied directly inside the emitted switch + without introducing a let-binding that might change evaluation behaviour. *) +let rec callback_is_inlineable expr = + match expr.pexp_desc with + | Pexp_ident _ -> true + | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) -> + callback_is_inlineable inner + | _ -> false + +(* Detect literal lambdas (ignoring type annotations) so we can reuse their + argument binder in the rewritten switch. *) +let rec inline_lambda expr = + match expr.pexp_desc with + | Pexp_constraint (inner, _) | Pexp_coerce (inner, _, _) -> + inline_lambda inner + | Pexp_fun {arg_label = Asttypes.Nolabel; lhs; rhs; async = false} -> + Some (lhs, rhs) + | _ -> None + +let transform (expr : Parsetree.expression) : Parsetree.expression = + match expr.pexp_desc with + | Pexp_apply + { + funct = + { + pexp_desc = + Pexp_ident + {txt = Ldot (Lident ("Option" | "Stdlib_Option"), fname)}; + }; + args = [(_, opt_expr); (_, func_expr)]; + } -> ( + let call_kind = + match fname with + | "forEach" -> Some ForEach + | "map" -> Some Map + | "flatMap" -> Some FlatMap + | _ -> None + in + match call_kind with + | None -> expr + | Some call_kind -> ( + let loc_ghost = {expr.pexp_loc with loc_ghost = true} in + let emit_option_match value_pat result_expr = + let some_rhs = + match call_kind with + | ForEach | FlatMap -> result_expr + | Map -> + Ast_helper.Exp.construct ~loc:loc_ghost + {txt = Lident "Some"; loc = loc_ghost} + (Some result_expr) + in + let none_rhs = + match call_kind with + | ForEach -> + Ast_helper.Exp.construct ~loc:loc_ghost + {txt = Lident "()"; loc = loc_ghost} + None + | Map | FlatMap -> + Ast_helper.Exp.construct ~loc:loc_ghost + {txt = Lident "None"; loc = loc_ghost} + None + in + let mk_case ctor payload rhs = + { + Parsetree.pc_bar = None; + pc_lhs = + Ast_helper.Pat.construct ~loc:loc_ghost + {txt = Lident ctor; loc = loc_ghost} + payload; + pc_guard = None; + pc_rhs = rhs; + } + in + let some_case = mk_case "Some" (Some value_pat) some_rhs in + let none_case = mk_case "None" None none_rhs in + let transformed = + Ast_helper.Exp.match_ ~loc:loc_ghost opt_expr [some_case; none_case] + in + { + transformed with + pexp_loc = expr.pexp_loc; + pexp_attributes = expr.pexp_attributes; + } + in + match inline_lambda func_expr with + (* Literal lambda with a simple binder: reuse the binder directly inside + the generated switch, so the body runs exactly once with the option's + payload. *) + | Some ({ppat_desc = Parsetree.Ppat_var {txt}}, body) -> + let value_pat = + Ast_helper.Pat.var ~loc:loc_ghost {txt; loc = loc_ghost} + in + emit_option_match value_pat body + (* Callback is a simple identifier (possibly annotated). Apply it inside + the switch so evaluation order matches handwritten code. *) + | _ when callback_is_inlineable func_expr -> + let value_pat = + Ast_helper.Pat.var ~loc:loc_ghost {txt = value_name; loc = loc_ghost} + in + let value_ident = + Ast_helper.Exp.ident ~loc:loc_ghost + {txt = Lident value_name; loc = loc_ghost} + in + let apply_callback = + Ast_helper.Exp.apply ~loc:loc_ghost func_expr + [(Asttypes.Nolabel, value_ident)] + in + emit_option_match value_pat apply_callback + (* Complex callbacks are left as-is so we don't change when they run. *) + | _ -> expr)) + | _ -> expr diff --git a/compiler/frontend/ast_option_optimizations.mli b/compiler/frontend/ast_option_optimizations.mli new file mode 100644 index 0000000000..84ee077695 --- /dev/null +++ b/compiler/frontend/ast_option_optimizations.mli @@ -0,0 +1 @@ +val transform : Parsetree.expression -> Parsetree.expression diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 08ec91fc62..3777aa37c6 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -112,7 +112,8 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) body; pexp_attributes; }) - | Pexp_apply _ -> Ast_exp_apply.app_exp_mapper e self + | Pexp_apply _ -> + Ast_exp_apply.app_exp_mapper e self |> Ast_option_optimizations.transform | Pexp_match ( b, [ diff --git a/tests/tests/src/core/Core_ObjectTests.mjs b/tests/tests/src/core/Core_ObjectTests.mjs index 8c7770f929..31b7c9569d 100644 --- a/tests/tests/src/core/Core_ObjectTests.mjs +++ b/tests/tests/src/core/Core_ObjectTests.mjs @@ -4,6 +4,7 @@ import * as Test from "./Test.mjs"; import * as Stdlib_BigInt from "@rescript/runtime/lib/es6/Stdlib_BigInt.js"; import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; let eq = Primitive_object.equal; @@ -530,10 +531,13 @@ runGetTest({ 3 ] }), - get: i => Stdlib_Option.getOr(Stdlib_Option.map(i["a"], i => i.concat([ - 4, - 5 - ])), []), + get: i => { + let i$1 = i["a"]; + return Stdlib_Option.getOr(i$1 !== undefined ? Primitive_option.valFromOption(i$1).concat([ + 4, + 5 + ]) : undefined, []); + }, expected: [ 1, 2, diff --git a/tests/tests/src/core/Core_TempTests.mjs b/tests/tests/src/core/Core_TempTests.mjs index 519540f601..ae5605a103 100644 --- a/tests/tests/src/core/Core_TempTests.mjs +++ b/tests/tests/src/core/Core_TempTests.mjs @@ -205,13 +205,11 @@ console.log(regex.test(string)); let result = regex.exec(string); -let result$1 = (result == null) ? undefined : Primitive_option.some(result); - -console.log(Stdlib_Option.map(result$1, prim => prim.input)); +console.log(!(result == null) ? result.input : undefined); -console.log(Stdlib_Option.map(result$1, prim => prim.index)); +console.log(!(result == null) ? result.index : undefined); -console.log(Stdlib_Option.map(result$1, prim => prim.slice(1))); +console.log(!(result == null) ? result.slice(1) : undefined); console.info(""); @@ -322,6 +320,8 @@ let formatter = Core_IntlTests.formatter; let segments = Core_IntlTests.segments; +let result$1 = (result == null) ? undefined : Primitive_option.some(result); + export { _collator, collator, diff --git a/tests/tests/src/core/intl/Core_IntlTests.mjs b/tests/tests/src/core/intl/Core_IntlTests.mjs index 2f718598eb..b74bc130f1 100644 --- a/tests/tests/src/core/intl/Core_IntlTests.mjs +++ b/tests/tests/src/core/intl/Core_IntlTests.mjs @@ -1,7 +1,6 @@ // Generated by ReScript, PLEASE EDIT WITH CARE import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js"; -import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; import * as Core_Intl_LocaleTest from "./Core_Intl_LocaleTest.mjs"; import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js"; @@ -58,7 +57,8 @@ try { let e$2 = Primitive_exceptions.internalToException(raw_e$2); if (e$2.RE_EXN_ID === "JsExn") { let e$3 = e$2._1; - let message = Stdlib_Option.map(Stdlib_JsExn.message(e$3), prim => prim.toLowerCase()); + let __res_option_value = Stdlib_JsExn.message(e$3); + let message = __res_option_value !== undefined ? __res_option_value.toLowerCase() : undefined; let exit = 0; if (message === "invalid key : someinvalidkey") { console.log("Caught expected error"); diff --git a/tests/tests/src/option_stdlib_optimization_test.mjs b/tests/tests/src/option_stdlib_optimization_test.mjs new file mode 100644 index 0000000000..9d630dbd7b --- /dev/null +++ b/tests/tests/src/option_stdlib_optimization_test.mjs @@ -0,0 +1,290 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; +import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; +import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; + +function testPrimitive() { + console.log(42); +} + +function testPrimitiveRef() { + console.log(42); +} + +function testNone() { + +} + +function testNoneRef() { + +} + +function testQualified() { + console.log("hello"); +} + +function testQualifiedRef() { + console.log("hello"); +} + +function testComplexExpr() { + console.log("true"); +} + +function testComplexExprRef() { + console.log("true"); +} + +function testNestedCalls() { + let opt = [ + 1, + 2, + 3 + ]; + opt.forEach(item => { + console.log(item); + }); +} + +function testNestedCallsRef() { + let opt = [ + 1, + 2, + 3 + ]; + opt.forEach(item => { + console.log(item); + }); +} + +function testNamedCallback() { + console.log(42); +} + +function testNamedCallbackRef() { + console.log(42); +} + +function testMultiple() { + console.log("first"); + console.log("second"); +} + +function testMultipleRef() { + console.log("first"); + console.log("second"); +} + +function testNonPrimitive() { + console.log("hello"); +} + +function testNonPrimitiveRef() { + console.log("hello"); +} + +let ForEach = { + testPrimitive: testPrimitive, + testPrimitiveRef: testPrimitiveRef, + testNone: testNone, + testNoneRef: testNoneRef, + testQualified: testQualified, + testQualifiedRef: testQualifiedRef, + testComplexExpr: testComplexExpr, + testComplexExprRef: testComplexExprRef, + testNestedCalls: testNestedCalls, + testNestedCallsRef: testNestedCallsRef, + testNamedCallback: testNamedCallback, + testNamedCallbackRef: testNamedCallbackRef, + testMultiple: testMultiple, + testMultipleRef: testMultipleRef, + testNonPrimitive: testNonPrimitive, + testNonPrimitiveRef: testNonPrimitiveRef +}; + +function testPrimitive$1() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testPrimitiveRef$1() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testNone$1() { + let result; + console.log(result); +} + +function testNoneRef$1() { + let result; + console.log(result); +} + +function testQualified$1() { + let result = "hello" + " world"; + console.log(result); +} + +function testQualifiedRef$1() { + let result = "hello" + " world"; + console.log(result); +} + +function testComplex() { + let result = "true"; + console.log(result); +} + +function testComplexRef() { + let result = "true"; + console.log(result); +} + +function testNamedCallback$1() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testNamedCallbackRef$1() { + let result = 42 + 1 | 0; + console.log(result); +} + +let $$Map = { + testPrimitive: testPrimitive$1, + testPrimitiveRef: testPrimitiveRef$1, + testNone: testNone$1, + testNoneRef: testNoneRef$1, + testQualified: testQualified$1, + testQualifiedRef: testQualifiedRef$1, + testComplex: testComplex, + testComplexRef: testComplexRef, + testNamedCallback: testNamedCallback$1, + testNamedCallbackRef: testNamedCallbackRef$1 +}; + +function testPrimitive$2() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testPrimitiveRef$2() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testNone$2() { + let result; + console.log(result); +} + +function testNoneRef$2() { + let result; + console.log(result); +} + +function testQualified$2() { + let result = "hello" + " world"; + console.log(result); +} + +function testQualifiedRef$2() { + let result = "hello" + " world"; + console.log(result); +} + +function testComplex$1() { + let result = "true"; + console.log(result); +} + +function testComplexRef$1() { + let result = "true"; + console.log(result); +} + +function testNamedCallback$2() { + let result = 42 + 1 | 0; + console.log(result); +} + +function testNamedCallbackRef$2() { + let result = 42 + 1 | 0; + console.log(result); +} + +let FlatMap = { + testPrimitive: testPrimitive$2, + testPrimitiveRef: testPrimitiveRef$2, + testNone: testNone$2, + testNoneRef: testNoneRef$2, + testQualified: testQualified$2, + testQualifiedRef: testQualifiedRef$2, + testComplex: testComplex$1, + testComplexRef: testComplexRef$1, + testNamedCallback: testNamedCallback$2, + testNamedCallbackRef: testNamedCallbackRef$2 +}; + +Mocha.describe("Scope preservation in Option optimizations", () => { + Mocha.test("Option.forEach evaluates callback argument even when option is None", () => { + let invocations = { + contents: 0 + }; + let makeCallback = () => { + invocations.contents = invocations.contents + 1 | 0; + return _value => {}; + }; + Stdlib_Option.forEach(undefined, makeCallback()); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 337, characters 7-14", invocations.contents, 1); + }); + Mocha.test("Option.forEach does not shadow surrounding bindings", () => { + let result; + result = 89 + 1 | 0; + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 350, characters 7-14", result, 90); + }); + Mocha.test("Option.map evaluates callback argument even when option is None", () => { + let invocations = { + contents: 0 + }; + let makeCallback = () => { + invocations.contents = invocations.contents + 1 | 0; + return value => value; + }; + Stdlib_Option.map(undefined, makeCallback()); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 362, characters 7-14", invocations.contents, 1); + }); + Mocha.test("Option.map does not shadow surrounding bindings", () => { + let result = 89 + 1 | 0; + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 368, characters 7-14", result, 90); + }); + Mocha.test("Option.flatMap evaluates callback argument even when option is None", () => { + let invocations = { + contents: 0 + }; + let makeCallback = () => { + invocations.contents = invocations.contents + 1 | 0; + return value => Primitive_option.some(value); + }; + Stdlib_Option.flatMap(undefined, makeCallback()); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 380, characters 7-14", invocations.contents, 1); + }); + Mocha.test("Option.flatMap does not shadow surrounding bindings", () => { + let result = 89 + 1 | 0; + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 386, characters 7-14", result, 90); + }); +}); + +let globalValue = 89; + +export { + globalValue, + ForEach, + $$Map, + FlatMap, +} +/* Not a pure module */ diff --git a/tests/tests/src/option_stdlib_optimization_test.res b/tests/tests/src/option_stdlib_optimization_test.res new file mode 100644 index 0000000000..ab81a8b6d8 --- /dev/null +++ b/tests/tests/src/option_stdlib_optimization_test.res @@ -0,0 +1,388 @@ +open Mocha +open Test_utils + +// Test Option.forEach/Option.map optimizations generate optimal JavaScript +// These tests verify that the optimized calls are transformed to efficient +// switch statements that match handwritten switch patterns + +let globalValue = 89 + +module ForEach = { + let testPrimitive = () => { + let opt = Some(42) + Option.forEach(opt, value => Console.log(value)) + } + + let testPrimitiveRef = () => { + let opt = Some(42) + switch opt { + | Some(value) => Console.log(value) + | None => () + } + } + + let testNone = () => { + let opt = None + Option.forEach(opt, value => Console.log(globalValue + value)) + } + + let testNoneRef = () => { + let opt = None + switch opt { + | Some(value) => Console.log(globalValue + value) + | None => () + } + } + + let testQualified = () => { + let opt = Some("hello") + Stdlib_Option.forEach(opt, value => Console.log(value)) + } + + let testQualifiedRef = () => { + let opt = Some("hello") + switch opt { + | Some(value) => Console.log(value) + | None => () + } + } + + let testComplexExpr = () => { + let opt = Some(true) + Option.forEach(opt, value => + if value { + Console.log("true") + } else { + Console.log("false") + } + ) + } + + let testComplexExprRef = () => { + let opt = Some(true) + switch opt { + | Some(value) => + if value { + Console.log("true") + } else { + Console.log("false") + } + | None => () + } + } + + let testNestedCalls = () => { + let opt = Some([1, 2, 3]) + Option.forEach(opt, value => Array.forEach(value, item => Console.log(item))) + } + + let testNestedCallsRef = () => { + let opt = Some([1, 2, 3]) + switch opt { + | Some(value) => Array.forEach(value, item => Console.log(item)) + | None => () + } + } + + let testNamedCallback = () => { + let opt = Some(42) + let log = value => Console.log(value) + Option.forEach(opt, log) + } + + let testNamedCallbackRef = () => { + let opt = Some(42) + let log = value => Console.log(value) + switch opt { + | Some(value) => log(value) + | None => () + } + } + + let testMultiple = () => { + let opt1 = Some("first") + let opt2 = Some("second") + let opt3 = None + + Option.forEach(opt1, value => Console.log(value)) + Option.forEach(opt2, value => Console.log(value)) + Option.forEach(opt3, value => Console.log(value)) + } + + let testMultipleRef = () => { + let opt1 = Some("first") + let opt2 = Some("second") + let opt3 = None + + switch opt1 { + | Some(value) => Console.log(value) + | None => () + } + switch opt2 { + | Some(value) => Console.log(value) + | None => () + } + switch opt3 { + | Some(value) => Console.log(value) + | None => () + } + } + + let testNonPrimitive = () => { + let opt = Some("hello") + Option.forEach(opt, value => Console.log(value)) + } + + let testNonPrimitiveRef = () => { + let opt = Some("hello") + switch opt { + | Some(value) => Console.log(value) + | None => () + } + } +} + +module Map = { + let testPrimitive = () => { + let opt = Some(42) + let result = Option.map(opt, value => value + 1) + Console.log(result) + } + + let testPrimitiveRef = () => { + let opt = Some(42) + let result = switch opt { + | Some(value) => Some(value + 1) + | None => None + } + Console.log(result) + } + + let testNone = () => { + let opt = None + let result = Option.map(opt, value => globalValue + value) + Console.log(result) + } + + let testNoneRef = () => { + let opt = None + let result = switch opt { + | Some(value) => Some(globalValue + value) + | None => None + } + Console.log(result) + } + + let testQualified = () => { + let opt = Some("hello") + let result = Stdlib_Option.map(opt, value => value ++ " world") + Console.log(result) + } + + let testQualifiedRef = () => { + let opt = Some("hello") + let result = switch opt { + | Some(value) => Some(value ++ " world") + | None => None + } + Console.log(result) + } + + let testComplex = () => { + let opt = Some(true) + let result = Option.map(opt, value => + if value { + "true" + } else { + "false" + } + ) + Console.log(result) + } + + let testComplexRef = () => { + let opt = Some(true) + let result = switch opt { + | Some(value) => + Some( + if value { + "true" + } else { + "false" + }, + ) + | None => None + } + Console.log(result) + } + + let testNamedCallback = () => { + let opt = Some(42) + let add = value => value + 1 + let result = Option.map(opt, add) + Console.log(result) + } + + let testNamedCallbackRef = () => { + let opt = Some(42) + let add = value => value + 1 + let result = switch opt { + | Some(value) => Some(add(value)) + | None => None + } + Console.log(result) + } +} + +module FlatMap = { + let testPrimitive = () => { + let opt = Some(42) + let result = Option.flatMap(opt, value => Some(value + 1)) + Console.log(result) + } + + let testPrimitiveRef = () => { + let opt = Some(42) + let result = switch opt { + | Some(value) => Some(value + 1) + | None => None + } + Console.log(result) + } + + let testNone = () => { + let opt = None + let result = Option.flatMap(opt, value => Some(globalValue + value)) + Console.log(result) + } + + let testNoneRef = () => { + let opt = None + let result = switch opt { + | Some(value) => Some(globalValue + value) + | None => None + } + Console.log(result) + } + + let testQualified = () => { + let opt = Some("hello") + let result = Stdlib_Option.flatMap(opt, value => Some(value ++ " world")) + Console.log(result) + } + + let testQualifiedRef = () => { + let opt = Some("hello") + let result = switch opt { + | Some(value) => Some(value ++ " world") + | None => None + } + Console.log(result) + } + + let testComplex = () => { + let opt = Some(true) + let result = Option.flatMap(opt, value => + if value { + Some("true") + } else { + None + } + ) + Console.log(result) + } + + let testComplexRef = () => { + let opt = Some(true) + let result = switch opt { + | Some(value) => + if value { + Some("true") + } else { + None + } + | None => None + } + Console.log(result) + } + + let testNamedCallback = () => { + let opt = Some(42) + let add = value => Some(value + 1) + let result = Option.flatMap(opt, add) + Console.log(result) + } + + let testNamedCallbackRef = () => { + let opt = Some(42) + let add = value => Some(value + 1) + let result = switch opt { + | Some(value) => add(value) + | None => None + } + Console.log(result) + } +} + +describe("Scope preservation in Option optimizations", () => { + test("Option.forEach evaluates callback argument even when option is None", () => { + let invocations = ref(0) + let makeCallback = () => { + invocations.contents = invocations.contents + 1 + (_value: string) => () + } + + Option.forEach(None, makeCallback()) + + eq(__LOC__, invocations.contents, 1) + }) + + test("Option.forEach does not shadow surrounding bindings", () => { + let result = ref(None) + + Option.forEach( + Some(1), + value => { + result.contents = Some(globalValue + value) + }, + ) + + eq(__LOC__, result.contents, Some(globalValue + 1)) + }) + + test("Option.map evaluates callback argument even when option is None", () => { + let invocations = ref(0) + let makeCallback = () => { + invocations.contents = invocations.contents + 1 + value => value + } + + Option.map(None, makeCallback())->ignore + + eq(__LOC__, invocations.contents, 1) + }) + + test("Option.map does not shadow surrounding bindings", () => { + let result = Option.map(Some(1), value => globalValue + value) + + eq(__LOC__, result, Some(globalValue + 1)) + }) + + test("Option.flatMap evaluates callback argument even when option is None", () => { + let invocations = ref(0) + let makeCallback = () => { + invocations.contents = invocations.contents + 1 + value => Some(value) + } + + Option.flatMap(None, makeCallback())->ignore + + eq(__LOC__, invocations.contents, 1) + }) + + test("Option.flatMap does not shadow surrounding bindings", () => { + let result = Option.flatMap(Some(1), value => Some(globalValue + value)) + + eq(__LOC__, result, Some(globalValue + 1)) + }) +}) diff --git a/tests/tests/src/reactTestUtils.mjs b/tests/tests/src/reactTestUtils.mjs index 287c48bf63..6c70cb9ce3 100644 --- a/tests/tests/src/reactTestUtils.mjs +++ b/tests/tests/src/reactTestUtils.mjs @@ -64,14 +64,18 @@ let DOM = { function prepareContainer(container, param) { let containerElement = document.createElement("div"); - Belt_Option.map(document.body, body => body.appendChild(containerElement)); + let body = document.body; + if (body !== undefined) { + Primitive_option.some(Primitive_option.valFromOption(body).appendChild(containerElement)); + } container.contents = Primitive_option.some(containerElement); } function cleanupContainer(container, param) { - Belt_Option.map(container.contents, prim => { - prim.remove(); - }); + let __res_option_value = container.contents; + if (__res_option_value !== undefined) { + Primitive_option.some((Primitive_option.valFromOption(__res_option_value).remove(), undefined)); + } container.contents = undefined; } From 592f244b99483327d7521417caa1b41bf4239543 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Mon, 22 Sep 2025 18:46:03 +0200 Subject: [PATCH 2/3] Add tests for pipe --- .../src/option_stdlib_optimization_test.mjs | 108 +++++++++++++++ .../src/option_stdlib_optimization_test.res | 126 ++++++++++++++++++ 2 files changed, 234 insertions(+) diff --git a/tests/tests/src/option_stdlib_optimization_test.mjs b/tests/tests/src/option_stdlib_optimization_test.mjs index 9d630dbd7b..92024dc9e9 100644 --- a/tests/tests/src/option_stdlib_optimization_test.mjs +++ b/tests/tests/src/option_stdlib_optimization_test.mjs @@ -9,6 +9,10 @@ function testPrimitive() { console.log(42); } +function testPrimitivePipe() { + console.log(42); +} + function testPrimitiveRef() { console.log(42); } @@ -17,6 +21,10 @@ function testNone() { } +function testNonePipe() { + +} + function testNoneRef() { } @@ -25,6 +33,10 @@ function testQualified() { console.log("hello"); } +function testQualifiedPipe() { + console.log("hello"); +} + function testQualifiedRef() { console.log("hello"); } @@ -33,6 +45,10 @@ function testComplexExpr() { console.log("true"); } +function testComplexExprPipe() { + console.log("true"); +} + function testComplexExprRef() { console.log("true"); } @@ -48,6 +64,17 @@ function testNestedCalls() { }); } +function testNestedCallsPipe() { + let opt = [ + 1, + 2, + 3 + ]; + opt.forEach(item => { + console.log(item); + }); +} + function testNestedCallsRef() { let opt = [ 1, @@ -63,6 +90,10 @@ function testNamedCallback() { console.log(42); } +function testNamedCallbackPipe() { + console.log(42); +} + function testNamedCallbackRef() { console.log(42); } @@ -72,6 +103,11 @@ function testMultiple() { console.log("second"); } +function testMultiplePipe() { + console.log("first"); + console.log("second"); +} + function testMultipleRef() { console.log("first"); console.log("second"); @@ -81,26 +117,38 @@ function testNonPrimitive() { console.log("hello"); } +function testNonPrimitivePipe() { + console.log("hello"); +} + function testNonPrimitiveRef() { console.log("hello"); } let ForEach = { testPrimitive: testPrimitive, + testPrimitivePipe: testPrimitivePipe, testPrimitiveRef: testPrimitiveRef, testNone: testNone, + testNonePipe: testNonePipe, testNoneRef: testNoneRef, testQualified: testQualified, + testQualifiedPipe: testQualifiedPipe, testQualifiedRef: testQualifiedRef, testComplexExpr: testComplexExpr, + testComplexExprPipe: testComplexExprPipe, testComplexExprRef: testComplexExprRef, testNestedCalls: testNestedCalls, + testNestedCallsPipe: testNestedCallsPipe, testNestedCallsRef: testNestedCallsRef, testNamedCallback: testNamedCallback, + testNamedCallbackPipe: testNamedCallbackPipe, testNamedCallbackRef: testNamedCallbackRef, testMultiple: testMultiple, + testMultiplePipe: testMultiplePipe, testMultipleRef: testMultipleRef, testNonPrimitive: testNonPrimitive, + testNonPrimitivePipe: testNonPrimitivePipe, testNonPrimitiveRef: testNonPrimitiveRef }; @@ -109,6 +157,11 @@ function testPrimitive$1() { console.log(result); } +function testPrimitivePipe$1() { + let result = 42 + 1 | 0; + console.log(result); +} + function testPrimitiveRef$1() { let result = 42 + 1 | 0; console.log(result); @@ -119,6 +172,11 @@ function testNone$1() { console.log(result); } +function testNonePipe$1() { + let result; + console.log(result); +} + function testNoneRef$1() { let result; console.log(result); @@ -129,6 +187,11 @@ function testQualified$1() { console.log(result); } +function testQualifiedPipe$1() { + let result = "hello" + " world"; + console.log(result); +} + function testQualifiedRef$1() { let result = "hello" + " world"; console.log(result); @@ -139,6 +202,11 @@ function testComplex() { console.log(result); } +function testComplexPipe() { + let result = "true"; + console.log(result); +} + function testComplexRef() { let result = "true"; console.log(result); @@ -149,6 +217,11 @@ function testNamedCallback$1() { console.log(result); } +function testNamedCallbackPipe$1() { + let result = 42 + 1 | 0; + console.log(result); +} + function testNamedCallbackRef$1() { let result = 42 + 1 | 0; console.log(result); @@ -156,14 +229,19 @@ function testNamedCallbackRef$1() { let $$Map = { testPrimitive: testPrimitive$1, + testPrimitivePipe: testPrimitivePipe$1, testPrimitiveRef: testPrimitiveRef$1, testNone: testNone$1, + testNonePipe: testNonePipe$1, testNoneRef: testNoneRef$1, testQualified: testQualified$1, + testQualifiedPipe: testQualifiedPipe$1, testQualifiedRef: testQualifiedRef$1, testComplex: testComplex, + testComplexPipe: testComplexPipe, testComplexRef: testComplexRef, testNamedCallback: testNamedCallback$1, + testNamedCallbackPipe: testNamedCallbackPipe$1, testNamedCallbackRef: testNamedCallbackRef$1 }; @@ -172,6 +250,11 @@ function testPrimitive$2() { console.log(result); } +function testPrimitivePipe$2() { + let result = 42 + 1 | 0; + console.log(result); +} + function testPrimitiveRef$2() { let result = 42 + 1 | 0; console.log(result); @@ -182,6 +265,11 @@ function testNone$2() { console.log(result); } +function testNonePipe$2() { + let result; + console.log(result); +} + function testNoneRef$2() { let result; console.log(result); @@ -192,6 +280,11 @@ function testQualified$2() { console.log(result); } +function testQualifiedPipe$2() { + let result = "hello" + " world"; + console.log(result); +} + function testQualifiedRef$2() { let result = "hello" + " world"; console.log(result); @@ -202,6 +295,11 @@ function testComplex$1() { console.log(result); } +function testComplexPipe$1() { + let result = "true"; + console.log(result); +} + function testComplexRef$1() { let result = "true"; console.log(result); @@ -212,6 +310,11 @@ function testNamedCallback$2() { console.log(result); } +function testNamedCallbackPipe$2() { + let result = 42 + 1 | 0; + console.log(result); +} + function testNamedCallbackRef$2() { let result = 42 + 1 | 0; console.log(result); @@ -219,14 +322,19 @@ function testNamedCallbackRef$2() { let FlatMap = { testPrimitive: testPrimitive$2, + testPrimitivePipe: testPrimitivePipe$2, testPrimitiveRef: testPrimitiveRef$2, testNone: testNone$2, + testNonePipe: testNonePipe$2, testNoneRef: testNoneRef$2, testQualified: testQualified$2, + testQualifiedPipe: testQualifiedPipe$2, testQualifiedRef: testQualifiedRef$2, testComplex: testComplex$1, + testComplexPipe: testComplexPipe$1, testComplexRef: testComplexRef$1, testNamedCallback: testNamedCallback$2, + testNamedCallbackPipe: testNamedCallbackPipe$2, testNamedCallbackRef: testNamedCallbackRef$2 }; diff --git a/tests/tests/src/option_stdlib_optimization_test.res b/tests/tests/src/option_stdlib_optimization_test.res index ab81a8b6d8..e7fb03b87b 100644 --- a/tests/tests/src/option_stdlib_optimization_test.res +++ b/tests/tests/src/option_stdlib_optimization_test.res @@ -13,6 +13,11 @@ module ForEach = { Option.forEach(opt, value => Console.log(value)) } + let testPrimitivePipe = () => { + let opt = Some(42) + opt->Option.forEach(value => Console.log(value)) + } + let testPrimitiveRef = () => { let opt = Some(42) switch opt { @@ -26,6 +31,11 @@ module ForEach = { Option.forEach(opt, value => Console.log(globalValue + value)) } + let testNonePipe = () => { + let opt = None + opt->Option.forEach(value => Console.log(globalValue + value)) + } + let testNoneRef = () => { let opt = None switch opt { @@ -39,6 +49,11 @@ module ForEach = { Stdlib_Option.forEach(opt, value => Console.log(value)) } + let testQualifiedPipe = () => { + let opt = Some("hello") + opt->Stdlib_Option.forEach(value => Console.log(value)) + } + let testQualifiedRef = () => { let opt = Some("hello") switch opt { @@ -58,6 +73,17 @@ module ForEach = { ) } + let testComplexExprPipe = () => { + let opt = Some(true) + opt->Option.forEach(value => + if value { + Console.log("true") + } else { + Console.log("false") + } + ) + } + let testComplexExprRef = () => { let opt = Some(true) switch opt { @@ -76,6 +102,11 @@ module ForEach = { Option.forEach(opt, value => Array.forEach(value, item => Console.log(item))) } + let testNestedCallsPipe = () => { + let opt = Some([1, 2, 3]) + opt->Option.forEach(value => Array.forEach(value, item => Console.log(item))) + } + let testNestedCallsRef = () => { let opt = Some([1, 2, 3]) switch opt { @@ -90,6 +121,12 @@ module ForEach = { Option.forEach(opt, log) } + let testNamedCallbackPipe = () => { + let opt = Some(42) + let log = value => Console.log(value) + opt->Option.forEach(log) + } + let testNamedCallbackRef = () => { let opt = Some(42) let log = value => Console.log(value) @@ -109,6 +146,16 @@ module ForEach = { Option.forEach(opt3, value => Console.log(value)) } + let testMultiplePipe = () => { + let opt1 = Some("first") + let opt2 = Some("second") + let opt3 = None + + opt1->Option.forEach(value => Console.log(value)) + opt2->Option.forEach(value => Console.log(value)) + opt3->Option.forEach(value => Console.log(value)) + } + let testMultipleRef = () => { let opt1 = Some("first") let opt2 = Some("second") @@ -133,6 +180,11 @@ module ForEach = { Option.forEach(opt, value => Console.log(value)) } + let testNonPrimitivePipe = () => { + let opt = Some("hello") + opt->Option.forEach(value => Console.log(value)) + } + let testNonPrimitiveRef = () => { let opt = Some("hello") switch opt { @@ -149,6 +201,12 @@ module Map = { Console.log(result) } + let testPrimitivePipe = () => { + let opt = Some(42) + let result = opt->Option.map(value => value + 1) + Console.log(result) + } + let testPrimitiveRef = () => { let opt = Some(42) let result = switch opt { @@ -164,6 +222,12 @@ module Map = { Console.log(result) } + let testNonePipe = () => { + let opt = None + let result = opt->Option.map(value => globalValue + value) + Console.log(result) + } + let testNoneRef = () => { let opt = None let result = switch opt { @@ -179,6 +243,12 @@ module Map = { Console.log(result) } + let testQualifiedPipe = () => { + let opt = Some("hello") + let result = opt->Stdlib_Option.map(value => value ++ " world") + Console.log(result) + } + let testQualifiedRef = () => { let opt = Some("hello") let result = switch opt { @@ -200,6 +270,18 @@ module Map = { Console.log(result) } + let testComplexPipe = () => { + let opt = Some(true) + let result = opt->Option.map(value => + if value { + "true" + } else { + "false" + } + ) + Console.log(result) + } + let testComplexRef = () => { let opt = Some(true) let result = switch opt { @@ -223,6 +305,13 @@ module Map = { Console.log(result) } + let testNamedCallbackPipe = () => { + let opt = Some(42) + let add = value => value + 1 + let result = opt->Option.map(add) + Console.log(result) + } + let testNamedCallbackRef = () => { let opt = Some(42) let add = value => value + 1 @@ -241,6 +330,12 @@ module FlatMap = { Console.log(result) } + let testPrimitivePipe = () => { + let opt = Some(42) + let result = opt->Option.flatMap(value => Some(value + 1)) + Console.log(result) + } + let testPrimitiveRef = () => { let opt = Some(42) let result = switch opt { @@ -256,6 +351,12 @@ module FlatMap = { Console.log(result) } + let testNonePipe = () => { + let opt = None + let result = opt->Option.flatMap(value => Some(globalValue + value)) + Console.log(result) + } + let testNoneRef = () => { let opt = None let result = switch opt { @@ -271,6 +372,12 @@ module FlatMap = { Console.log(result) } + let testQualifiedPipe = () => { + let opt = Some("hello") + let result = opt->Stdlib_Option.flatMap(value => Some(value ++ " world")) + Console.log(result) + } + let testQualifiedRef = () => { let opt = Some("hello") let result = switch opt { @@ -292,6 +399,18 @@ module FlatMap = { Console.log(result) } + let testComplexPipe = () => { + let opt = Some(true) + let result = opt->Option.flatMap(value => + if value { + Some("true") + } else { + None + } + ) + Console.log(result) + } + let testComplexRef = () => { let opt = Some(true) let result = switch opt { @@ -313,6 +432,13 @@ module FlatMap = { Console.log(result) } + let testNamedCallbackPipe = () => { + let opt = Some(42) + let add = value => Some(value + 1) + let result = opt->Option.flatMap(add) + Console.log(result) + } + let testNamedCallbackRef = () => { let opt = Some(42) let add = value => Some(value + 1) From 50dbc3c8100fc32ac89337eeb2a7aa23a417e6e5 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 23 Sep 2025 09:36:11 +0200 Subject: [PATCH 3/3] Add pipe chaining test --- .../src/option_stdlib_optimization_test.mjs | 27 ++++++++++++++----- .../src/option_stdlib_optimization_test.res | 18 +++++++++++++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/tests/tests/src/option_stdlib_optimization_test.mjs b/tests/tests/src/option_stdlib_optimization_test.mjs index 92024dc9e9..f32c392916 100644 --- a/tests/tests/src/option_stdlib_optimization_test.mjs +++ b/tests/tests/src/option_stdlib_optimization_test.mjs @@ -3,8 +3,22 @@ import * as Mocha from "mocha"; import * as Test_utils from "./test_utils.mjs"; import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js"; +import * as Belt_MapString from "@rescript/runtime/lib/es6/Belt_MapString.js"; import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js"; +function getIncidentCategoryName(incidents, categories, incidentId) { + let incident = incidentId !== undefined ? Belt_MapString.get(incidents, incidentId) : undefined; + let categoryId = incident !== undefined ? incident.categoryId : undefined; + let category = categoryId !== undefined ? Belt_MapString.get(categories, categoryId) : undefined; + if (category !== undefined) { + return category.name; + } +} + +let PipeChain = { + getIncidentCategoryName: getIncidentCategoryName +}; + function testPrimitive() { console.log(42); } @@ -348,12 +362,12 @@ Mocha.describe("Scope preservation in Option optimizations", () => { return _value => {}; }; Stdlib_Option.forEach(undefined, makeCallback()); - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 337, characters 7-14", invocations.contents, 1); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 481, characters 7-14", invocations.contents, 1); }); Mocha.test("Option.forEach does not shadow surrounding bindings", () => { let result; result = 89 + 1 | 0; - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 350, characters 7-14", result, 90); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 494, characters 7-14", result, 90); }); Mocha.test("Option.map evaluates callback argument even when option is None", () => { let invocations = { @@ -364,11 +378,11 @@ Mocha.describe("Scope preservation in Option optimizations", () => { return value => value; }; Stdlib_Option.map(undefined, makeCallback()); - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 362, characters 7-14", invocations.contents, 1); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 506, characters 7-14", invocations.contents, 1); }); Mocha.test("Option.map does not shadow surrounding bindings", () => { let result = 89 + 1 | 0; - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 368, characters 7-14", result, 90); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 512, characters 7-14", result, 90); }); Mocha.test("Option.flatMap evaluates callback argument even when option is None", () => { let invocations = { @@ -379,11 +393,11 @@ Mocha.describe("Scope preservation in Option optimizations", () => { return value => Primitive_option.some(value); }; Stdlib_Option.flatMap(undefined, makeCallback()); - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 380, characters 7-14", invocations.contents, 1); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 524, characters 7-14", invocations.contents, 1); }); Mocha.test("Option.flatMap does not shadow surrounding bindings", () => { let result = 89 + 1 | 0; - Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 386, characters 7-14", result, 90); + Test_utils.eq("File \"option_stdlib_optimization_test.res\", line 530, characters 7-14", result, 90); }); }); @@ -391,6 +405,7 @@ let globalValue = 89; export { globalValue, + PipeChain, ForEach, $$Map, FlatMap, diff --git a/tests/tests/src/option_stdlib_optimization_test.res b/tests/tests/src/option_stdlib_optimization_test.res index e7fb03b87b..ceedc38f4f 100644 --- a/tests/tests/src/option_stdlib_optimization_test.res +++ b/tests/tests/src/option_stdlib_optimization_test.res @@ -7,6 +7,24 @@ open Test_utils let globalValue = 89 +module PipeChain = { + // Modeled after some real world code that chains a lot of + // Option.flatMap/Option.map calls. + type incident = {incidentId: string, categoryId: option} + type category = {categoryId: string, name: string} + + let getIncidentCategoryName = ( + incidents: Belt.Map.String.t, + categories: Belt.Map.String.t, + ~incidentId, + ) => + incidentId + ->Option.flatMap(incidentId => incidents->Belt.Map.String.get(incidentId)) + ->Option.flatMap(incident => incident.categoryId) + ->Option.flatMap(categoryId => categories->Belt.Map.String.get(categoryId)) + ->Option.map(category => category.name) +} + module ForEach = { let testPrimitive = () => { let opt = Some(42)