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..f32c392916 --- /dev/null +++ b/tests/tests/src/option_stdlib_optimization_test.mjs @@ -0,0 +1,413 @@ +// 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 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); +} + +function testPrimitivePipe() { + console.log(42); +} + +function testPrimitiveRef() { + console.log(42); +} + +function testNone() { + +} + +function testNonePipe() { + +} + +function testNoneRef() { + +} + +function testQualified() { + console.log("hello"); +} + +function testQualifiedPipe() { + console.log("hello"); +} + +function testQualifiedRef() { + console.log("hello"); +} + +function testComplexExpr() { + console.log("true"); +} + +function testComplexExprPipe() { + console.log("true"); +} + +function testComplexExprRef() { + console.log("true"); +} + +function testNestedCalls() { + let opt = [ + 1, + 2, + 3 + ]; + opt.forEach(item => { + console.log(item); + }); +} + +function testNestedCallsPipe() { + 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 testNamedCallbackPipe() { + console.log(42); +} + +function testNamedCallbackRef() { + console.log(42); +} + +function testMultiple() { + console.log("first"); + console.log("second"); +} + +function testMultiplePipe() { + console.log("first"); + console.log("second"); +} + +function testMultipleRef() { + console.log("first"); + console.log("second"); +} + +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 +}; + +function testPrimitive$1() { + let result = 42 + 1 | 0; + 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); +} + +function testNone$1() { + let result; + console.log(result); +} + +function testNonePipe$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 testQualifiedPipe$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 testComplexPipe() { + 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 testNamedCallbackPipe$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, + 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 +}; + +function testPrimitive$2() { + let result = 42 + 1 | 0; + 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); +} + +function testNone$2() { + let result; + console.log(result); +} + +function testNonePipe$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 testQualifiedPipe$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 testComplexPipe$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 testNamedCallbackPipe$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, + 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 +}; + +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 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 494, 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 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 512, 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 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 530, characters 7-14", result, 90); + }); +}); + +let globalValue = 89; + +export { + globalValue, + PipeChain, + 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..ceedc38f4f --- /dev/null +++ b/tests/tests/src/option_stdlib_optimization_test.res @@ -0,0 +1,532 @@ +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 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) + 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 { + | Some(value) => Console.log(value) + | None => () + } + } + + let testNone = () => { + let opt = None + 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 { + | Some(value) => Console.log(globalValue + value) + | None => () + } + } + + let testQualified = () => { + let opt = Some("hello") + 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 { + | 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 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 { + | 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 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 { + | 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 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) + 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 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") + 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 testNonPrimitivePipe = () => { + let opt = Some("hello") + opt->Option.forEach(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 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 { + | 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 testNonePipe = () => { + let opt = None + let result = opt->Option.map(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 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 { + | 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 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 { + | 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 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 + 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 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 { + | 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 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 { + | 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 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 { + | 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 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 { + | 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 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) + 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; }