Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
- Preserve trailing comments between the type and `=` in locally abstract value constraints (`let f: type a. t /* comment */ = value`). https://github.com/rescript-lang/rescript/pull/8575
- Enforce function arity in interface/module inclusion and type coercion. Previously a curried implementation (e.g. `int => int => int`) could satisfy an uncurried interface (`(int, int) => int`) or be coerced to it, which could miscompile calls made through the interface type. Such mismatches are now compile errors with an explanatory hint. https://github.com/rescript-lang/rescript/pull/8559
- Fix termination-analysis false positives for functions whose progress flows through un-annotated helpers: collecting the callees of a function binding was accidentally disabled in 2024 (the collection guard required a node shape that uncurried code never produces), so helpers calling `@progress` functions were no longer added to the function table. https://github.com/rescript-lang/rescript/pull/8568
- Fix default values of optional parameters being computed at the wrong time for curried functions: in `(~x=default, y) => (~z=default, w) => ...`, `x`'s default was only computed when the *inner* function was applied. Each default is now computed when its own parameter group is applied. https://github.com/rescript-lang/rescript/pull/8568
- Fix default values of optional parameters being computed at the wrong time for curried functions: in `(~x=default, y) => (~z=default, w) => ...`, `x`'s default was only computed when the _inner_ function was applied. Each default is now computed when its own parameter group is applied. https://github.com/rescript-lang/rescript/pull/8568
- Fix bare labeled arrow types (`~x: int => string`) getting no arity: they printed identically to their parenthesized form (`(~x: int) => string`) but did not unify with it. https://github.com/rescript-lang/rescript/pull/8563
- Fix losses of fidelity when code passes through an external PPX: the internal `@res.async` marker no longer leaks into the program, attributes on an arrow type or on an `await` expression are no longer dropped or relocated (previously this could crash the formatter), JSX elements keep their closing tag, and PPX-emitted OCaml-style `function` is desugared instead of crashing the compiler. https://github.com/rescript-lang/rescript/pull/8561
- Preserve multibyte characters when wrapping long source lines in compiler code frames. https://github.com/rescript-lang/rescript/pull/8520
Expand All @@ -79,6 +79,7 @@

#### :house: Internal

- Add before/after optimization views and inline diffs to the developer playground's Lambda tab, with optimized Lambda output in playground API v9. https://github.com/rescript-lang/rescript/pull/8636
- Developer playground: Make panes resizable with wrapping text. https://github.com/rescript-lang/rescript/pull/8628
- Normalize Lambda terms where they are built: a match guard stays structured data until its fallthrough is known, and `apply` and `mk_builtin` go through the folding constructors. https://github.com/rescript-lang/rescript/pull/8615
- Replace non-escaping local mutable blocks with scalar bindings when all uses are direct field accesses, generalizing reference unboxing to multi-field records and references captured by JavaScript closures. https://github.com/rescript-lang/rescript/pull/8617
Expand Down
6 changes: 5 additions & 1 deletion compiler/core/lam_compile_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ let required_modules (lam : Lambda.t) : Lam_module_ident.Hash_set.t =
collect lam;
required

let compile (output_prefix : string) export_idents hoisted (lam : Lambda.t) =
let compile ?on_optimized_lambda (output_prefix : string) export_idents hoisted

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems I cannot extract optimized lambda for the playground without this addition

(lam : Lambda.t) =
let debug_ir = !Js_config.debug_ir in
let diagnostics =
if debug_ir then Some (Ir_diagnostics.create ~output_prefix) else None
Expand Down Expand Up @@ -334,6 +335,9 @@ let compile (output_prefix : string) export_idents hoisted (lam : Lambda.t) =
lam
in

(* Capture the final whole-term optimization result before export grouping
and JavaScript lowering. The callback is absent in normal compilation. *)
Option.iter (fun capture -> capture lam) on_optimized_lambda;
let ({Lam_coercion.groups} as coerced_input), meta =
Lam_coercion.coerce_and_group_big_lambda meta lam
in
Expand Down
4 changes: 4 additions & 0 deletions compiler/core/lam_compile_main.mli
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,17 @@
*)

val compile :
?on_optimized_lambda:(Lambda.t -> unit) ->
string ->
Ident.t list ->
Lambda.hoisted_function list ->
Lambda.t ->
J.deps_program
(** For toplevel, [filename] is [""] which is the same as
{!Env.get_unit_name ()}

[on_optimized_lambda], when supplied, observes the whole-term Lambda after
optimization and before export grouping and JavaScript lowering.
*)

val lambda_as_module : J.deps_program -> string -> unit
25 changes: 22 additions & 3 deletions compiler/jsoo/jsoo_playground_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@
* v6: Added `config.experimental_features` and `config.jsx_preserve_mode` to the BundleConfig.
* v7: Added debug dump output APIs for developer playground tooling.
* v8: Added genType and source map configuration and compilation outputs.
* v9: Added optimized Lambda output to debug compilation.
* *)
let api_version = "8"
let api_version = "9"

module Js = Js_of_ocaml.Js

Expand Down Expand Up @@ -667,8 +668,18 @@ module Compile = struct
let {Translmod.lambda; exports; hoisted_functions} =
Translmod.transl_implementation modulename typed_tree
in
let optimized_lambda = ref None in
let on_optimized_lambda =
if include_debug_outputs then
Some
(fun lambda ->
optimized_lambda :=
Some (Printer.to_string Printlambda.lambda lambda))
else None
in
let lambda_output =
Lam_compile_main.compile "" exports hoisted_functions lambda
Lam_compile_main.compile ?on_optimized_lambda "" exports
hoisted_functions lambda
in
let js_code, source_map =
render_javascript ~module_system ~filename ~source:str ~source_map_mode
Expand Down Expand Up @@ -725,7 +736,15 @@ module Compile = struct
("lam", inject @@ Js.string lam);
|]
in
Js.Unsafe.obj (Array.concat [attrs; debug_attrs; gentype_attrs])
let optimized_lambda_attrs =
match !optimized_lambda with
| None -> [||]
| Some output ->
Js.Unsafe.[|("lambda_optimized", inject @@ Js.string output)|]
in
Js.Unsafe.obj
(Array.concat
[attrs; debug_attrs; gentype_attrs; optimized_lambda_attrs])
else Js.Unsafe.obj attrs
with e -> (
match e with
Expand Down
2 changes: 1 addition & 1 deletion packages/dev-playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"prepare-pages-site": "node scripts/prepare-pages-site.mjs",
"res:build": "rescript",
"res:watch": "rescript -w",
"test": "rescript && node --test scripts/source-map-navigation.test.mjs scripts/pane-layout.test.mjs",
"test": "rescript && node --test scripts/source-map-navigation.test.mjs scripts/pane-layout.test.mjs scripts/lambda-diff.test.mjs",
"dev": "vite --host 127.0.0.1",
"build": "rescript && vite build",
"preview": "vite preview --host 127.0.0.1"
Expand Down
241 changes: 241 additions & 0 deletions packages/dev-playground/scripts/lambda-diff.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
compare,
maxCharacters,
maxLines,
sections,
} from "../src/LambdaDiff.res.mjs";
import * as Inline from "../src/LambdaInlineDiff.res.mjs";

function changes(before, after) {
const result = compare(before, after);
assert.equal(result.TAG, "Changes");
const lines = result._0;
for (const [side, text] of [
["before", before],
["after", after],
]) {
const original = lines.filter(line => line[side] !== undefined);
assert.equal(original.map(line => line.text).join("\n"), text);
assert.deepEqual(
original.map(line => line[side]),
original.map((_, index) => index + 1),
);
}
return lines;
}

test("identical dumps, including empty dumps, need no diff", () => {
assert.equal(compare("", ""), "Identical");
assert.equal(compare("(makeblock 42)\n", "(makeblock 42)\n"), "Identical");
});

test("replacement retains before and after line numbers", () => {
assert.deepEqual(
changes("(let\n unused/1\n result)\n", "(let\n result)\n"),
[
{ kind: "Same", text: "(let", before: 1, after: 1 },
{ kind: "Removed", text: " unused/1", before: 2, after: undefined },
{ kind: "Same", text: " result)", before: 3, after: 2 },
{ kind: "Same", text: "", before: 4, after: 3 },
],
);
assert.deepEqual(
changes("before", "after").map(line => line.kind),
["Removed", "Added"],
);
});

test("insertions, deletions, empty inputs and final newlines are lossless", () => {
for (const [before, after] of [
["", "(block 42)"],
["(block 42)", ""],
["a\nb", "a\ninsert\nb"],
["a\nb", "insert\na\nb\nend"],
["a\n", "a"],
["a", "a\n"],
["\n\n", "\n"],
])
changes(before, after);
});

test("repeated Lambda lines align deterministically without dropping text", () => {
const before = "(let\n x/1\n (let\n x/1\n ))";
const after = "(let\n (let\n x/1\n ))";
assert.equal(
changes(before, after).filter(line => line.kind !== "Same").length,
1,
);
assert.deepEqual(compare(before, after), compare(before, after));
changes("x/1", "x/2"); // Identifier changes must not be normalized away.
});

test("exhaustive small dumps reconstruct both inputs", () => {
const dumps = [""];
for (let length = 1; length <= 4; length++) {
for (let bits = 0; bits < 2 ** length; bits++) {
dumps.push(
Array.from({ length }, (_, i) =>
(bits >> i) & 1 ? "(x)" : "(y)",
).join("\n"),
);
}
}
for (const before of dumps) {
for (const after of dumps) {
if (before !== after) changes(before, after);
}
}
});

test("bounds comparison work and output size", () => {
assert.equal(compare("a\n".repeat(1100), "b\n".repeat(1100)), "TooLarge");
assert.equal(compare("a".repeat(maxCharacters), "b"), "TooLarge");
assert.equal(compare("\n".repeat(maxLines), ""), "TooLarge");
});

test("trims common edges before applying the comparison budget", () => {
const edge = "(unchanged)\n".repeat(2000);
const lines = changes(`${edge}before\n${edge}`, `${edge}after\n${edge}`);
assert.equal(lines.filter(line => line.kind !== "Same").length, 2);
});

test("collapsed regions preserve every line and keep context around changes", () => {
const context = Array.from({ length: 20 }, (_, i) => `line ${i}`).join("\n");
const lines = changes(
`${context}\nold\n${context}\nold\n${context}`,
`${context}\nnew\n${context}\nnew\n${context}`,
);
const grouped = sections(lines);
assert.deepEqual(
grouped.flatMap(section => section._0),
lines,
);
const hidden = grouped.filter(section => section.TAG === "Collapsed");
assert.deepEqual(
hidden.map(section => section._0.length),
[17, 14, 17],
);
for (const section of hidden) {
assert.ok(section._0.every(line => line.kind === "Same"));
}
assert.ok(
sections(changes("a\nb\nc", "a\nB\nc")).every(
section => section.TAG === "Visible",
),
);
});

function inlineChanges(before, after) {
const lines = changes(before, after);
const highlights = Inline.highlight(lines);
for (const line of lines) {
const parts = Inline.forLine(highlights, line);
if (parts) assert.equal(parts.map(part => part.text).join(""), line.text);
if (line.kind === "Same") assert.equal(parts, undefined);
}
return highlights;
}

const changedText = parts =>
parts.filter(part => part.changed).map(part => part.text);

test("inline diff highlights changed Lambda tokens, not the shared expression", () => {
const result = inlineChanges(" (+ value/1 10)", " (+ value/1 20)");
assert.deepEqual(changedText(result.before[1]), ["10"]);
assert.deepEqual(changedText(result.after[1]), ["20"]);
const renamed = inlineChanges("(apply value/123 42)", "(apply value/124 42)");
assert.deepEqual(changedText(renamed.before[1]), ["value/123"]);
assert.deepEqual(changedText(renamed.after[1]), ["value/124"]);
});

test("inline diff preserves whitespace, Unicode, and escaped quoted constants", () => {
const result = inlineChanges(
' (apply print/1 "a \\"quoted\\" 🙂")',
'\t(apply print/1 "b \\"quoted\\" 🙂")',
);
assert.deepEqual(changedText(result.before[1]), ['"a \\"quoted\\" 🙂"']);
assert.deepEqual(changedText(result.after[1]), ['"b \\"quoted\\" 🙂"']);
const whitespace = inlineChanges(
" (apply café/1 42)",
"\t(apply café/1 42)",
);
assert.deepEqual(changedText(whitespace.before[1]), []);
assert.deepEqual(changedText(whitespace.after[1]), []);
});

test("inline diff handles token insertion, deletion and repeated tokens", () => {
const result = inlineChanges("(apply f/1 x/2 x/2)", "(apply f/1 x/2)");
assert.deepEqual(changedText(result.before[1]), ["x/2"]);
assert.deepEqual(changedText(result.after[1]), []);
const inserted = inlineChanges("(apply f/1 x/2)", "(apply f/1 x/2 42)");
assert.deepEqual(changedText(inserted.before[1]), []);
assert.deepEqual(changedText(inserted.after[1]), ["42"]);
});

test("inline pairing skips unmatched lines in an uneven changed block", () => {
const result = inlineChanges(
"(unused z/9)\n (+ x/1 10)\n (- y/2 30)",
" (+ x/1 20)\n (- y/2 40)",
);
assert.equal(result.before[1], undefined);
assert.deepEqual(changedText(result.before[2]), ["10"]);
assert.deepEqual(changedText(result.after[1]), ["20"]);
assert.deepEqual(changedText(result.before[3]), ["30"]);
assert.deepEqual(changedText(result.after[2]), ["40"]);
});

test("inline pairing prefers a better later match over a weaker first match", () => {
const result = inlineChanges(
"(apply other/1 10)\n(apply f/2 20)",
"(apply f/2 30)",
);
assert.equal(result.before[1], undefined);
assert.deepEqual(changedText(result.before[2]), ["20"]);
assert.deepEqual(changedText(result.after[1]), ["30"]);
});

test("unrelated expressions and one-sided changes retain only line highlighting", () => {
for (const [before, after] of [
[" (foo/1 10)", " (bar/2 20)"],
["((", "))"],
["(a)\n(b)", "(a)"],
["(a)", "(a)\n(b)"],
]) {
assert.deepEqual(inlineChanges(before, after), { before: {}, after: {} });
}
});

test("inline highlighting falls back for oversized blocks, lines and token counts", () => {
const largeBlock = Array.from(
{ length: Inline.maxBlockLines },
(_, i) => `(apply f/${i} 10)`,
).join("\n");
assert.deepEqual(
inlineChanges(largeBlock, largeBlock.replaceAll(" 10)", " 20)")),
{ before: {}, after: {} },
);
for (const body of [
"x".repeat(Inline.maxLineCharacters),
"x ".repeat(Inline.maxTokens),
]) {
assert.deepEqual(
inlineChanges(`(apply ${body} 10)`, `(apply ${body} 20)`),
{ before: {}, after: {} },
);
}
});

test("inline token work is budgeted across blocks and resets for each dump", () => {
const body = "x ".repeat(110);
const before = Array.from(
{ length: 30 },
(_, i) => `(apply ${body} 10)\nseparator/${i}`,
).join("\n");
const after = before.replaceAll(" 10)", " 20)");
const result = inlineChanges(before, after);
assert.ok(Object.keys(result.before).length > 0);
assert.ok(Object.keys(result.before).length < 30);
assert.deepEqual(inlineChanges(before, after), result);
});
1 change: 1 addition & 0 deletions packages/dev-playground/src/Bindings.res
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ module CompileResult = {
@get external parsetree: compileResult => option<string> = "parsetree"
@get external typedtree: compileResult => option<string> = "typedtree"
@get external lambda: compileResult => option<string> = "lambda"
@get external lambdaOptimized: compileResult => option<string> = "lambda_optimized"
@get external gentype: compileResult => option<string> = "gentype"
@get external sourceMap: compileResult => option<string> = "source_map"
@get external errors: compileResult => option<array<diagnostic>> = "errors"
Expand Down
14 changes: 13 additions & 1 deletion packages/dev-playground/src/CompilerApi.res
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type success = {
parsetree: string,
typedtree: string,
lambda: string,
lambdaOptimized: option<string>,
gentype: option<string>,
sourceMap: option<string>,
warnings: array<string>,
Expand Down Expand Up @@ -415,9 +416,20 @@ let normalize = (compileOutput, elapsedMs): compileResult => {
}

let gentype = compileOutput->CompileResult.gentype
let lambdaOptimized = compileOutput->CompileResult.lambdaOptimized
let sourceMap = compileOutput->CompileResult.sourceMap

Ok({jsCode, parsetree, typedtree, lambda, gentype, sourceMap, warnings, time: elapsedMs})
Ok({
jsCode,
parsetree,
typedtree,
lambda,
lambdaOptimized,
gentype,
sourceMap,
warnings,
time: elapsedMs,
})

| _ => Error(failureFromCompileOutput(compileOutput, elapsedMs))
}
Expand Down
1 change: 1 addition & 0 deletions packages/dev-playground/src/CompilerApi.resi
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type success = {
parsetree: string,
typedtree: string,
lambda: string,
lambdaOptimized: option<string>,
gentype: option<string>,
sourceMap: option<string>,
warnings: array<string>,
Expand Down
Loading
Loading