Skip to content

Commit 06550da

Browse files
authored
Generate argument number checks for JS code (#237)
1 parent a06df41 commit 06550da

35 files changed

+568
-113
lines changed

hkmc2/jvm/src/test/scala/hkmc2/JSBackendDiffMaker.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import scala.collection.mutable
55
import mlscript.utils.*, shorthands.*
66
import utils.*
77

8+
import codegen.js.{JSBuilder, JSBuilderSanityChecks}
9+
import document.*
10+
import codegen.Block
11+
import codegen.js.Scope
12+
import hkmc2.syntax.Tree.Ident
13+
import hkmc2.codegen.Path
814

915
abstract class JSBackendDiffMaker extends MLsDiffMaker:
1016

@@ -13,6 +19,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker:
1319
val sjs = NullaryCommand("sjs")
1420
val showRepl = NullaryCommand("showRepl")
1521
val silent = NullaryCommand("silent")
22+
val noSanityCheck = NullaryCommand("noSanityCheck")
1623
val expect = Command("expect"): ln =>
1724
ln.trim
1825

@@ -42,7 +49,7 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker:
4249
if js.isSet then
4350
val low = ltl.givenIn:
4451
codegen.Lowering()
45-
val jsb = codegen.js.JSBuilder()
52+
val jsb = new JSBuilder with JSBuilderSanityChecks(noSanityCheck.isUnset)
4653
import semantics.*
4754
import codegen.*
4855
val le = low.program(blk)

hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ class JSBuilder extends CodeBuilder:
9696
case _ => result(fun)
9797
doc"${base}(${args.map(result).mkDocument(", ")})"
9898
case Value.Lam(ps, bod) => scope.nest givenIn:
99-
val vars = ps.map(p => scope.allocateName(p.sym)).mkDocument(", ")
100-
doc"($vars) => { #{ # ${
101-
body(bod)
99+
val (params, bodyDoc) = setupFunction(none, ps, bod)
100+
doc"($params) => { #{ # ${
101+
bodyDoc
102102
} #} # }"
103103
case Select(qual, id) =>
104104
val name = id.name
@@ -145,11 +145,11 @@ class JSBuilder extends CodeBuilder:
145145
case FunDefn(sym, Nil, body) =>
146146
TODO("getters")
147147
case FunDefn(sym, ParamList(_, ps) :: pss, bod) =>
148-
val paramList = ps.map(p => scope.allocateName(p.sym)).mkDocument(", ")
149148
val result = pss.foldRight(bod):
150149
case (ParamList(_, ps), block) =>
151150
Return(Lam(ps, block), false)
152-
doc"function ${sym.nme}(${paramList}) { #{ # ${body(result)} #} # }"
151+
val (params, bodyDoc) = setupFunction(some(sym.nme), ps, result)
152+
doc"function ${sym.nme}($params) { #{ # ${bodyDoc} #} # }"
153153
case ClsLikeDefn(sym, syntax.Cls, mtds, flds, ctor) =>
154154
val clsDefn = sym.defn.getOrElse(die)
155155
val clsParams = clsDefn.paramsOpt.getOrElse(Nil)
@@ -166,12 +166,12 @@ class JSBuilder extends CodeBuilder:
166166
} #} # }${
167167
mtds.map:
168168
case td @ FunDefn(_, ParamList(_, ps) :: pss, bod) =>
169-
val vars = ps.map(p => scope.allocateName(p.sym)).mkDocument(", ")
170169
val result = pss.foldRight(bod):
171170
case (ParamList(_, ps), block) =>
172171
Return(Lam(ps, block), false)
173-
doc" # ${td.sym.nme}($vars) { #{ # ${
174-
body(result)
172+
val (params, bodyDoc) = setupFunction(some(td.sym.nme), ps, result)
173+
doc" # ${td.sym.nme}($params) { #{ # ${
174+
bodyDoc
175175
} #} # }"
176176
.mkDocument(" ")
177177
}${
@@ -329,7 +329,12 @@ class JSBuilder extends CodeBuilder:
329329
def body(t: Block)(using Raise, Scope): Document = scope.nest givenIn:
330330
block(t)
331331

332-
332+
def setupFunction(name: Option[Str], params: List[semantics.Param], body: Block)(using Raise, Scope): (Document, Document) =
333+
val paramsList = params.map(p => scope.allocateName(p.sym)).mkDocument(", ")
334+
(paramsList, this.body(body))
335+
336+
337+
333338
object JSBuilder:
334339
import scala.util.matching.Regex
335340

@@ -400,6 +405,9 @@ object JSBuilder:
400405
)
401406

402407
def makeStringLiteral(s: Str): Str =
408+
s"\"${escapeStringCharacters(s)}\""
409+
410+
def escapeStringCharacters(s: Str): Str =
403411
s.map[Str] {
404412
case '"' => "\\\""
405413
case '\\' => "\\\\"
@@ -412,8 +420,24 @@ object JSBuilder:
412420
if 0 < c && c <= 255 && !c.isControl
413421
then c.toString
414422
else f"\\u${c.toInt}%04X"
415-
}.mkString("\"", "", "\"")
423+
}.mkString
416424

417425
end JSBuilder
418426

419427

428+
trait JSBuilderSanityChecks(instrument: Bool) extends JSBuilder:
429+
430+
val functionParamVarargSymbol = semantics.TempSymbol(0, N, "args")
431+
432+
override def setupFunction(name: Option[Str], params: List[semantics.Param], body: Block)(using Raise, Scope): (Document, Document) =
433+
if instrument then
434+
val paramsList = params.map(p => Scope.scope.allocateName(p.sym))
435+
val paramsStr = Scope.scope.allocateName(functionParamVarargSymbol)
436+
val functionName = JSBuilder.makeStringLiteral(name.fold("")(n => s"${JSBuilder.escapeStringCharacters(n)}"))
437+
val checkArgsNum = doc"globalThis.Predef.checkArgs($functionName, ${params.length}, $paramsStr.length);\n"
438+
val paramsAssign = paramsList.zipWithIndex.map{(nme, i) =>
439+
doc"let ${nme} = ${paramsStr}[$i];\n"}.mkDocument("")
440+
(doc"...$paramsStr", doc"$checkArgsNum$paramsAssign${this.body(body)}")
441+
else
442+
super.setupFunction(name, params, body)
443+

hkmc2/shared/src/test/mlscript-compile/Predef.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,28 @@ const Predef$class = class Predef {
3232
}
3333
tupleGet(xs1, i1) {
3434
return globalThis.Array.prototype.at.call(xs1, i1);
35+
}
36+
checkArgs(functionName, expected, got) {
37+
let scrut, name, scrut1, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6;
38+
scrut = got != expected;
39+
if (scrut) {
40+
scrut1 = functionName.length > 0;
41+
if (scrut1) {
42+
tmp = " '" + functionName;
43+
tmp1 = tmp + "'";
44+
} else {
45+
tmp1 = "";
46+
}
47+
name = tmp1;
48+
tmp2 = "Function" + name;
49+
tmp3 = tmp2 + " expected ";
50+
tmp4 = tmp3 + expected;
51+
tmp5 = tmp4 + " arguments but got ";
52+
tmp6 = tmp5 + got;
53+
throw globalThis.Error(tmp6);
54+
} else {
55+
return undefined;
56+
}
3557
}
3658
toString() { return "Predef"; }
3759
}; const Predef = new Predef$class;

hkmc2/shared/src/test/mlscript-compile/Predef.mls

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ fun tupleSlice(xs, i, j) =
2020

2121
fun tupleGet(xs, i) =
2222
globalThis.Array.prototype.at.call(xs, i)
23+
24+
fun checkArgs(functionName, expected, got) =
25+
if got != expected then
26+
let name = if functionName.length > 0 then " '" + functionName + "'" else ""
27+
throw globalThis.Error("Function" + name + " expected " + expected + " arguments but got " + got)
28+
else ()

hkmc2/shared/src/test/mlscript/basics/MultiParamLists.mls

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66

77
fun f(n1: Int): Int = n1
88
//│ JS:
9-
//│ function f(n1) { return n1; } undefined
9+
//│ function f(...args) {
10+
//│ globalThis.Predef.checkArgs("f", 1, args.length);
11+
//│ let n1 = args[0];
12+
//│ return n1;
13+
//│ }
14+
//│ undefined
1015

1116
f(42)
1217
//│ JS:
@@ -15,7 +20,18 @@ f(42)
1520

1621
fun f(n1: Int)(n2: Int): Int = (10 * n1 + n2)
1722
//│ JS:
18-
//│ function f(n1) { return (n2) => { let tmp; tmp = 10 * n1; return tmp + n2; }; } undefined
23+
//│ function f(...args) {
24+
//│ globalThis.Predef.checkArgs("f", 1, args.length);
25+
//│ let n1 = args[0];
26+
//│ return (...args1) => {
27+
//│ globalThis.Predef.checkArgs("", 1, args1.length);
28+
//│ let n2 = args1[0];
29+
//│ let tmp;
30+
//│ tmp = 10 * n1;
31+
//│ return tmp + n2;
32+
//│ };
33+
//│ }
34+
//│ undefined
1935

2036
f(4)(2)
2137
//│ JS:
@@ -24,9 +40,15 @@ f(4)(2)
2440

2541
fun f(n1: Int)(n2: Int)(n3: Int): Int = 10 * (10 * n1 + n2) + n3
2642
//│ JS:
27-
//│ function f(n1) {
28-
//│ return (n2) => {
29-
//│ return (n3) => {
43+
//│ function f(...args) {
44+
//│ globalThis.Predef.checkArgs("f", 1, args.length);
45+
//│ let n1 = args[0];
46+
//│ return (...args1) => {
47+
//│ globalThis.Predef.checkArgs("", 1, args1.length);
48+
//│ let n2 = args1[0];
49+
//│ return (...args2) => {
50+
//│ globalThis.Predef.checkArgs("", 1, args2.length);
51+
//│ let n3 = args2[0];
3052
//│ let tmp, tmp1, tmp2;
3153
//│ tmp = 10 * n1;
3254
//│ tmp1 = tmp + n2;
@@ -44,10 +66,18 @@ f(4)(2)(0)
4466

4567
fun f(n1: Int)(n2: Int)(n3: Int)(n4: Int): Int = 10 * (10 * (10 * n1 + n2) + n3) + n4
4668
//│ JS:
47-
//│ function f(n1) {
48-
//│ return (n2) => {
49-
//│ return (n3) => {
50-
//│ return (n4) => {
69+
//│ function f(...args) {
70+
//│ globalThis.Predef.checkArgs("f", 1, args.length);
71+
//│ let n1 = args[0];
72+
//│ return (...args1) => {
73+
//│ globalThis.Predef.checkArgs("", 1, args1.length);
74+
//│ let n2 = args1[0];
75+
//│ return (...args2) => {
76+
//│ globalThis.Predef.checkArgs("", 1, args2.length);
77+
//│ let n3 = args2[0];
78+
//│ return (...args3) => {
79+
//│ globalThis.Predef.checkArgs("", 1, args3.length);
80+
//│ let n4 = args3[0];
5181
//│ let tmp, tmp1, tmp2, tmp3, tmp4;
5282
//│ tmp = 10 * n1;
5383
//│ tmp1 = tmp + n2;

hkmc2/shared/src/test/mlscript/basics/OfLambdaArgs.mls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ foo of (id of x => 1), id of y => 2
1313

1414
:re
1515
foo(id of x => 1, id of y => 2)
16-
//│ ═══[RUNTIME ERROR] TypeError: g is not a function
16+
//│ ═══[RUNTIME ERROR] Error: Function 'id' expected 1 arguments but got 2
1717

1818

hkmc2/shared/src/test/mlscript/codegen/CaseOfCase.mls

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ fun test(x) =
1818
Some(v) then log(v)
1919
None then log("none")
2020
//│ JS:
21-
//│ function test(x) {
21+
//│ function test(...args) {
22+
//│ globalThis.Predef.checkArgs("test", 1, args.length);
23+
//│ let x = args[0];
2224
//│ let param0, v, scrut, param01, v1, tmp, tmp1;
2325
//│ if (x instanceof globalThis.Some.class) {
2426
//│ param0 = x.value;

hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,37 @@ case x then x
1111
:sjs
1212
case { x then x }
1313
//│ JS:
14-
//│ (caseScrut) => { let x; x = caseScrut; return x; }
14+
//│ (...args) => {
15+
//│ globalThis.Predef.checkArgs("", 1, args.length);
16+
//│ let caseScrut = args[0];
17+
//│ let x;
18+
//│ x = caseScrut;
19+
//│ return x;
20+
//│ }
1521
//│ = [Function (anonymous)]
1622

1723
:sjs
1824
x => if x is
1925
0 then true
2026
//│ JS:
21-
//│ (x) => { if (x === 0) { return true; } else { throw new this.Error("match error"); } }
27+
//│ (...args) => {
28+
//│ globalThis.Predef.checkArgs("", 1, args.length);
29+
//│ let x = args[0];
30+
//│ if (x === 0) {
31+
//│ return true;
32+
//│ } else {
33+
//│ throw new this.Error("match error");
34+
//│ }
35+
//│ }
2236
//│ = [Function (anonymous)]
2337

2438
:sjs
2539
case
2640
0 then true
2741
//│ JS:
28-
//│ (caseScrut) => {
42+
//│ (...args) => {
43+
//│ globalThis.Predef.checkArgs("", 1, args.length);
44+
//│ let caseScrut = args[0];
2945
//│ if (caseScrut === 0) {
3046
//│ return true;
3147
//│ } else {
@@ -39,7 +55,15 @@ case
3955
0 then true
4056
_ then false
4157
//│ JS:
42-
//│ (caseScrut) => { if (caseScrut === 0) { return true; } else { return false; } }
58+
//│ (...args) => {
59+
//│ globalThis.Predef.checkArgs("", 1, args.length);
60+
//│ let caseScrut = args[0];
61+
//│ if (caseScrut === 0) {
62+
//│ return true;
63+
//│ } else {
64+
//│ return false;
65+
//│ }
66+
//│ }
4367
//│ = [Function (anonymous)]
4468

4569
class Some(value)
@@ -50,7 +74,9 @@ val isDefined = case
5074
Some then true
5175
None then false
5276
//│ JS:
53-
//│ this.isDefined = (caseScrut) => {
77+
//│ this.isDefined = (...args) => {
78+
//│ globalThis.Predef.checkArgs("", 1, args.length);
79+
//│ let caseScrut = args[0];
5480
//│ if (caseScrut instanceof this.Some.class) {
5581
//│ return true;
5682
//│ } else {

hkmc2/shared/src/test/mlscript/codegen/ClassInClass.mls

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ class Outer(a, b) with
3535
//│ tmp5 = this.i1(this$Outer.a);
3636
//│ globalThis.log(tmp5)
3737
//│ }
38-
//│ i1(d) {
38+
//│ i1(...args) {
39+
//│ globalThis.Predef.checkArgs("i1", 1, args.length);
40+
//│ let d = args[0];
3941
//│ return [
4042
//│ this$Outer.b,
4143
//│ this.c,
@@ -50,10 +52,15 @@ class Outer(a, b) with
5052
//│ tmp2 = this.i.i1(1);
5153
//│ globalThis.log(tmp2)
5254
//│ }
53-
//│ o1(c) {
55+
//│ o1(...args) {
56+
//│ globalThis.Predef.checkArgs("o1", 1, args.length);
57+
//│ let c = args[0];
5458
//│ return this.Inner(c);
5559
//│ }
56-
//│ o2(c1, d) {
60+
//│ o2(...args1) {
61+
//│ globalThis.Predef.checkArgs("o2", 2, args1.length);
62+
//│ let c1 = args1[0];
63+
//│ let d = args1[1];
5764
//│ let tmp;
5865
//│ tmp = this.Inner(c1);
5966
//│ return tmp.i1(d);

hkmc2/shared/src/test/mlscript/codegen/ClassInFun.mls

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ fun test(a) =
99
class C with { val x = a }
1010
new C
1111
//│ JS:
12-
//│ function test(a) {
12+
//│ function test(...args) {
13+
//│ globalThis.Predef.checkArgs("test", 1, args.length);
14+
//│ let a = args[0];
1315
//│ class C {
1416
//│ constructor() {
1517
//│ this.x = a;
@@ -29,7 +31,9 @@ fun test(x) =
2931
class Foo(a, b)
3032
Foo(x, x + 1)
3133
//│ JS:
32-
//│ function test(x) {
34+
//│ function test(...args) {
35+
//│ globalThis.Predef.checkArgs("test", 1, args.length);
36+
//│ let x = args[0];
3337
//│ let tmp;
3438
//│ function Foo(a1, b1) { return new Foo.class(a1, b1); };
3539
//│ Foo.class = class Foo {
@@ -51,7 +55,8 @@ test(123)
5155

5256

5357
// * Forgot to pass the arg:
58+
:re
5459
test()
55-
//│ = Foo { a: undefined, b: NaN }
60+
//│ ═══[RUNTIME ERROR] Error: Function 'test' expected 1 arguments but got 0
5661

5762

0 commit comments

Comments
 (0)