JavaScript code generation backlog
When working on #53, I met a lot of things that we could improved in the future. Here are them.
-
[ ] Find a neat way to store query results. Requirements:
- If the initialization fails, the variable should not be declared.
- Declaration should not pollute global context.
-
[x] ~~Implement
Class.Member instancecalling convention. Source~~ This syntax might be changed in the new parser. -
[x] Add timeout to
stream.read()otherwise we will occasionally suffer from infinite loops. Source (Fixed in #126) -
[x] Automatically check references to unimplemented variables. Source
-
[x] ~~Support merging arrays in
withConstruct. Source TBD.~~ The semantics ofwithmight be changed. -
[ ] Some
// TODOintroduced in commits from #53. Source TBD.As for 1 Aug, 2022. The
// TODOs are as follows. They are not urgent at this moment.- Translation of
Blk. https://github.com/hkust-taco/mlscript/blob/ba99965401408470e4072d95081199c504c004a8/shared/src/main/scala/mlscript/JSBackend.scala#L177-L178 - Consider traits when sorting classes. https://github.com/hkust-taco/mlscript/blob/ba99965401408470e4072d95081199c504c004a8/shared/src/main/scala/mlscript/JSBackend.scala#L482-L485
- Improve parentheses in code generation. https://github.com/hkust-taco/mlscript/blob/ba99965401408470e4072d95081199c504c004a8/shared/src/main/scala/mlscript/codegen/Codegen.scala#L415-L416
- Load built-in symbols from
Typer. https://github.com/hkust-taco/mlscript/blob/ba99965401408470e4072d95081199c504c004a8/shared/src/main/scala/mlscript/codegen/Scope.scala#L23-L26 https://github.com/hkust-taco/mlscript/blob/ba99965401408470e4072d95081199c504c004a8/shared/src/main/scala/mlscript/codegen/Scope.scala#L45-L51
- Translation of
-
[x] Trait test in pattern matching has not been implemented. This can be done via
Symbol.
- [ ] Trait test in pattern matching has not been implemented. This can be done via
Symbol.
Traits can also introduce method definitions that can be inherited by classes, so they're not just for pattern matching!
- [X] Implement tuple field accessors, as in
(1,2,3)._1– they currently returnundefined
[EDIT: actually we'll do it the JS way in the new parser so this shouldn't be necessary]
-
[X]
console.logdoes not seem to print anything in DiffTests (kind of fixed by @fo5for in https://github.com/hkust-taco/mlscript/commit/ecca9d8d5072723b575496dac151067292d5129e, using stderr instead) -
[x] fields defined in traits are not used to initialize class fields in constructors
:js trait X: { x: int } class Y: X method MX = this.x //│ Defined trait X //│ Defined class Y //│ Defined Y.MX: (Y & x) -> int //│ // Prelude //│ class Y { //│ constructor(fields) { //│ } //│ get MX() { //│ return this.x; //│ } //│ } //│ // End of generated code (Y {x = 1}).x //│ res: 1 //│ = undefined -
[x]
thisis captured by enclosing functions, giving it wrong semantics (#123)::js class A: { x: int } method MA = let _ = 1 in this.x //│ Defined class A //│ Defined A.MA: A -> int //│ // Prelude //│ class A { //│ constructor(fields) { //│ this.x = fields.x; //│ } //│ get MA() { //│ return ((function (_) { //│ return this.x; //│ })(1)); //│ } //│ } //│ // End of generated code (A { x = 1 }).MA //│ res: int //│ Runtime error: //│ TypeError: Cannot read properties of undefined (reading 'x')
-
[x] Handling of the
resrunning variable is not consistent with DiffTests:42 //│ res: 42 //│ = 42 :js def foo = "oops" //│ // Query 1 //│ globalThis.foo = "oops"; //│ res = foo; //│ // End of generated code //│ foo: "oops" //│ = 'oops' res //│ res: 42 //│ = 'oops'
- [x] Fix syntax error of objects wrapped by lambdas, should be
((x) => ({t1: gt(1)(2), t2: 1}))(#124).
fun x -> {t1 = gt 1 2; t2 = 1}
//│ // Prelude
//│ function gt(x, y) {
//│ if (arguments.length === 2) {
//│ return x > y;
//│ }else {
//│ return (y) => x > y;
//│ }
//│ }
//│ let res;
//│ // Query 1
//│ res = ((x) => {
//│ t1: gt(1)(2),
//│ t2: 1
//│ });
//| // ((x) => {t1: gt(1)(2), t2: 1}) is illegal
//│ // End of generated code
//│ res: anything -> {t1: bool, t2: 1}
//│ = > undefined
@andongfan would you like to try and tackle some of this issues, including the new one?
@chengluyu Is the list of issues on this ticket up to date? I feel like a few have been solved already.
@andongfan would you like to try and tackle some of this issues, including the new one?
I think I can have a try :)
- [x] Code execution for variables with primes is broken -- they return the result of the previous query (Fixed in #122)
x = 0
//│ x: 0
//│ = 0
y' = true
//│ y': true
//│ = 0
- [x] Code execution for variables with primes is broken -- they return the result of the previous query
x = 0 //│ x: 0 //│ = 0 y' = true //│ y': true //│ = 0
:js
y' = true
//│ // Query 1
//│ globalThis["y'"] = true;
//│ res = y';
//│ // End of generated code
//│ y': true
//│ = 0
We didn't handle the case.
Since when can we use primes in the identifiers?
They've always been supported in ML-family languages like OCaml and Haskell 😛
- [X] Division of integer values yields a non-integer value. (fixed in https://github.com/hkust-taco/mlscript/commit/f0a16428e12899781274540ede4c1e5df439565c)
def foo = 1 / 2
//│ foo: int
//│ = 0.5
def foo = 1 / 0
//│ foo: int
//│ = Infinity
def foo = 0 / 0
//│ foo: int
//│ = NaN
// Javascript code generated from
// def foo = 1 / 2
const foo = 1 / 2;
// Expecting a truncation operation here. like:
const foo = Math.trunc(1 / 2);
@zhao-huang Good catch! The initial context in Typer.scala should have "/" -> numberBinOpTy instead of "/" -> intBinOpTy, where numberBinOpTy = fun(singleTup(DecType), fun(singleTup(DecType), DecType)(noProv))(noProv).
Note that int <: number so 1 / 2 should be typeable as number.
- [ ] Fix code generation
undefinedfor blocks inlet/funbindings.
:p
let rec f = 1
//│ |#let| |#rec| |f| |#=| |1|
//│ Parsed: let rec f = 1;
//│ Desugared: rec def f: 1
//│ f: 1
//│ = 1
// TODO FIX undefined: should `return 1`;
:p
:js
let rec f =
1
//│ |#let| |#rec| |f| |#=|→|1|←|
//│ Parsed: let rec f = {1};
//│ Desugared: rec def f: {1}
//│ // Query 1
//│ globalThis.f5 = (() => {
//│ 1;
//│ })();
//│ // End of generated code
//│ f: 1
//│ = undefined
-
[ ] Fix:
def update2 i arr = let a = arr[i] in arr[i] <- case a of undefined -> error, _ -> a //│ update2: int -> MutArray[in 'a out 'a & ~undefined] -> unit //│ = [Function: update2] update2 0 ((mut 1,)) //│ Runtime error: //│ TypeError: ((intermediate value) , []) is not a function
- [ ] Remove the
selfkludge introduced in https://github.com/hkust-taco/mlscript/pull/123. It seems the simpler solution was to simply never generatefunction(){...}lambdas, preferring=>lambdas instead, which do not have the stupidthiscapture semantics.
- [ ] Cc @NeilKleistGao
:js let x' = 1 //│ let x': 1 //│ // Prelude //│ let res; //│ class TypingUnit {} //│ const typing_unit = new TypingUnit; //│ // Query 1 //│ globalThis.x$ = 1; //│ // End of generated code //│ x' //│ = 1 :js class Foo(x': Int) //│ class Foo(x': Int) //│ // Prelude //│ class TypingUnit1 { //│ #Foo; //│ constructor() { //│ } //│ get Foo() { //│ const qualifier = this; //│ if (this.#Foo === undefined) { //│ class Foo { //│ #x'; //│ constructor(x$) { //│ this.#x' = x$; //│ } //│ static //│ unapply(x) { //│ return [x.#x']; //│ } //│ }; //│ this.#Foo = ((x') => Object.freeze(new Foo(x'))); //│ this.#Foo.class = Foo; //│ this.#Foo.unapply = Foo.unapply; //│ } //│ return this.#Foo; //│ } //│ } //│ const typing_unit1 = new TypingUnit1; //│ globalThis.Foo = typing_unit1.Foo; //│ // End of generated code //│ Syntax error: //│ Unexpected string
-
[x] It seems there's a diff-test bug where it fails to show/execute the last result here:
// * FIXME This should crash due to `error`, // * but the crash is somehow swallowed and we get the result of the previous statement instead! // * The same happens with any other side effect, like `log(...)` // * Note: this doesn't happen if the last line is in a spearate diff-test block :showRepl fun foo(x) = error let r = foo(1) //│ fun foo: anything -> nothing //│ let r: nothing //│ ┌ Block at NuScratch.mls:21 //│ ├─┬ Prelude //│ │ ├── Code //│ │ │ function error() { //│ │ │ throw new Error("an error was thrown"); //│ │ │ } //│ │ │ let res; //│ │ │ class TypingUnit {} //│ │ │ const typing_unit = new TypingUnit; //│ │ └── Reply //│ │ undefined //│ ├─┬ Query 1/2 //│ │ ├── Prelude: <empty> //│ │ ├── Code: //│ │ ├── globalThis.foo = function foo(x) { //│ │ ├── return error(); //│ │ ├── }; //│ │ ├── Intermediate: [Function: foo] //│ │ └── Reply: [success] [Function: foo] //│ └─┬ Query 2/2 //│ ├── Prelude: <empty> //│ ├── Code: //│ ├── globalThis.r = foo(1); //│ └── Reply: [runtime error] Error: an error was thrown //│ r //│ = [Function: foo]
CC @chengluyu
- It seems there's a diff-test bug where it fails to show/execute the last result here: [...]
After a quick check, I found that the code for outputting type inference results and execution results in DiffTests is a bit different. I remember that the previous behavior was to output the execution result after each type result. When did this change? I'm still debugging.
It probably changed because :NewDefs works quite differently from before. The entire typing unit is typed and run as a whole instead of statement by statement.
Removed bad solution.
Thanks for the pointer! We can temporarily fix this by replacing this line (https://github.com/hkust-taco/mlscript/blob/febbc0ba04c1b6b1821c86ddf9d5696d5fb05a30/shared/src/test/scala/mlscript/DiffTests.scala#L605) with case NuFunDef(_, nme, snme, tparams, bod) =>
Then,
// * FIXME This should crash due to `error`,
// * but the crash is somehow swallowed and we get the result of the previous statement instead!
// * The same happens with any other side effect, like `log(...)`
// * Note: this doesn't happen if the last line is in a spearate diff-test block
fun foo(x) = error
let v = foo(1)
//│ fun foo: anything -> nothing
//│ let v: nothing
//│ foo
//│ = [Function: foo]
//│ v
//│ Runtime error:
//│ Error: an error was thrown
I propose the PR to fix this.
-
[x] Code like this doesn't work (Cc @NeilKleistGao):
// FIXME fun y = 1 module Foo { fun x = y } //│ fun y: 1 //│ module Foo { //│ fun x: 1 //│ } //│ Code generation encountered an error: //│ unresolved symbol y // FIXME module Foo { fun x = y } fun y = 1 //│ module Foo { //│ fun x: 1 //│ } //│ fun y: 1 //│ Code generation encountered an error: //│ unresolved symbol y
I couldn't for the life of me quickly figure out what was going on here because the JS backend infrastructure is so damn opaque and undebuggable. For example there's no way to visualize what's going wrong in the scope graph. I really don't understand why you haven't integrated it with the tracing infrastructure @chengluyu @NeilKleistGao – it shouldn't be hard at all.
-
[ ] Somehow the renaming behavior between declared classes and declared functions is inconsistent when the name is reserved in JS. We rename the latter and not the former.
declare fun throw: anything -> nothing //│ fun throw: anything -> nothing throw(0); 0 //│ 0 //│ res //│ Runtime error: //│ ReferenceError: throw1 is not defined declare class throw(arg: anything): nothing //│ declare class throw(arg: anything): nothing throw(0); 0 //│ 0 //│ res //│ Runtime error: //│ 0
- [ ] Somehow the renaming behavior between declared classes and declared functions is inconsistent when the name is reserved in JS. We rename the latter and not the former.
declare fun throw: anything -> nothing //│ fun throw: anything -> nothing throw(0); 0 //│ 0 //│ res //│ Runtime error: //│ ReferenceError: throw1 is not defined declare class throw(arg: anything): nothing //│ declare class throw(arg: anything): nothing throw(0); 0 //│ 0 //│ res //│ Runtime error: //│ 0
I guess this is another problem caused by the code in #186. We really need to refactor the code in JSTestBackend.
I will do this after the paper submission.
-
[ ] Class names are not escaped, unlike function names
:js fun f' = 0 //│ fun f': 0 //│ // Prelude //│ let res; //│ class TypingUnit {} //│ const typing_unit = new TypingUnit; //│ // Query 1 //│ globalThis.f$ = function f$() { //│ return 0; //│ }; //│ // End of generated code :js module F' //│ module F' //│ // Prelude //│ class TypingUnit1 { //│ #F'; //│ constructor() { //│ } //│ get "F'"() { //│ const qualifier = this; //│ if (this.#F' === undefined) { //│ class F' {} //│ this.#F' = new F'(); //│ this.#F'.class = F'; //│ } //│ return this.#F'; //│ } //│ } //│ const typing_unit1 = new TypingUnit1; //│ globalThis["F'"] = typing_unit1["F'"]; //│ // End of generated code //│ Syntax error: //│ Unexpected string
- [x] Brackets in expressions are not handled properly Likely cause: JSBackend.scala:280
:js
1 - (2 - 3)
//│ Int
//│ // Prelude
//│ let res;
//│ class TypingUnit {}
//│ const typing_unit = new TypingUnit;
//│ // Query 1
//│ res = 1 - 2 - 3;
//│ // End of generated code
//│ res
//│ = -4```
- [ ] Avoid shadowing previous results if the current code has codegen errors (see #186 )
r with { x: 1 }
//│ {x: 1}
//│ res
//│ = { x: 1 }
:e // TODO support
let r = new {}
//│ ╔══[ERROR] Unexpected type `anything` after `new` keyword
//│ ║ l.80: let r = new {}
//│ ╙── ^^
//│ let r: error
//│ Code generation encountered an error:
//│ Unsupported `new` class term: Bra(rcd = true, Rcd())
:ge
r : Object
//│ Object
//│ Code generation encountered an error:
//│ unguarded recursive use of by-value binding r
:ge
r with { x: 1 }
//│ error & {x: 1}
//│ Code generation encountered an error:
//│ unguarded recursive use of by-value binding r
- [ ] In the web demo (but NOT in diff-tests!!), expression
2 + 2generates JS code+(2, 2), which returns2. This might be since https://github.com/hkust-taco/mlscript/pull/204
-
[ ] Error in generated code for:
:js module Ref { mut val value = 123; set value = 1 } //│ module Ref extends B { //│ fun get: 'A //│ mut val value: 123 | 1 //│ } //│ where //│ 'A :> 123 | 1 //│ // Prelude //│ class TypingUnit47 { //│ #Ref; //│ constructor() { //│ } //│ get Ref() { //│ const qualifier = this; //│ if (this.#Ref === undefined) { //│ class Ref extends B.class { //│ #value; //│ get value() { return this.#value; } //│ set value(value) { return this.#value = value; } //│ constructor() { //│ super(); //│ this.#value = 123; //│ const value = this.#value; //│ void(value = 1); //│ } //│ } //│ this.#Ref = new Ref(); //│ this.#Ref.class = Ref; //│ } //│ return this.#Ref; //│ } //│ } //│ const typing_unit47 = new TypingUnit47; //│ globalThis.Ref = typing_unit47.Ref; //│ // End of generated code //│ TEST CASE FAILURE: There was an unexpected runtime error //│ Runtime error: //│ TypeError: Assignment to constant variable.
- [ ] While class instances are frozen on construction via
Object.freeze, it seems modules are not.