lemmascript 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +1 -1
- package/tools/dist/dafny-emit.js +149 -36
- package/tools/dist/extract.js +480 -50
- package/tools/dist/lean-emit.js +6 -2
- package/tools/dist/lsc.js +21 -4
- package/tools/dist/resolve.js +225 -25
- package/tools/dist/specparser.js +5 -2
- package/tools/dist/transform.js +284 -19
- package/tools/dist/types.js +14 -1
package/README.md
CHANGED
|
@@ -16,8 +16,9 @@ See the external case studies:
|
|
|
16
16
|
- **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
|
|
17
17
|
- **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
|
|
18
18
|
- **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
|
|
19
|
-
- **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. Cookie verification done in-place. Dafny only.
|
|
20
|
-
- **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** —
|
|
19
|
+
- **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. [Cookie verification done **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79). Dafny only.
|
|
20
|
+
- **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination; 5 helper lemmas, 28 loop invariants). Dafny only.
|
|
21
|
+
- **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (replace — bounded length), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Dafny only.
|
|
21
22
|
|
|
22
23
|
## Setup
|
|
23
24
|
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ export function dafnyVerify(dfyPath, dir, timeLimit) {
|
|
|
35
35
|
console.log("Running dafny verify...");
|
|
36
36
|
try {
|
|
37
37
|
const content = readFileSync(dfyPath, "utf-8");
|
|
38
|
-
const stdLibFlag = content.includes("
|
|
38
|
+
const stdLibFlag = content.includes("Std.") ? " --standard-libraries" : "";
|
|
39
39
|
const timeLimitFlag = timeLimit ? ` --verification-time-limit ${timeLimit}` : "";
|
|
40
40
|
execSync(`dafny verify${stdLibFlag}${timeLimitFlag} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
|
|
41
41
|
return true;
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -51,12 +51,13 @@ function paramList(params) {
|
|
|
51
51
|
const OP_MAP = {
|
|
52
52
|
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
53
53
|
"∧": "&&", "∨": "||", "¬": "!",
|
|
54
|
+
"arrayConcat": "+",
|
|
54
55
|
};
|
|
55
56
|
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
56
57
|
// ── Expression emission ─────────────────────────────────────
|
|
57
58
|
function emitExpr(e) {
|
|
58
59
|
switch (e.kind) {
|
|
59
|
-
case "var": return escapeName(e.name);
|
|
60
|
+
case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
|
|
60
61
|
case "num": return `${e.value}`;
|
|
61
62
|
case "bool": return e.value ? "true" : "false";
|
|
62
63
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
@@ -79,20 +80,18 @@ function emitExpr(e) {
|
|
|
79
80
|
return `(${args[0]} in ${obj})`;
|
|
80
81
|
if (e.method === "push")
|
|
81
82
|
return `(${obj} + [${args[0]}])`;
|
|
82
|
-
if (e.method === "
|
|
83
|
+
if (e.method === "concat")
|
|
84
|
+
return `(${obj} + [${args[0]}])`;
|
|
85
|
+
if (e.method === "slice" && args.length === 1)
|
|
83
86
|
return `${obj}[${args[0]}..]`;
|
|
84
|
-
if (e.method === "
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (e.method === "filter")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (e.method === "every") {
|
|
93
|
-
needsStdCollections = true;
|
|
94
|
-
return `Seq.All(${obj}, ${args[0]})`;
|
|
95
|
-
}
|
|
87
|
+
if (e.method === "slice" && args.length === 2)
|
|
88
|
+
return `${obj}[${args[0]}..${args[1]}]`;
|
|
89
|
+
if (e.method === "map")
|
|
90
|
+
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
91
|
+
if (e.method === "filter")
|
|
92
|
+
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
93
|
+
if (e.method === "every")
|
|
94
|
+
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
96
95
|
if (e.method === "some" && e.args[0].kind === "lambda" &&
|
|
97
96
|
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
98
97
|
const lam = e.args[0];
|
|
@@ -143,6 +142,8 @@ function emitExpr(e) {
|
|
|
143
142
|
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
144
143
|
if (e.method === "has")
|
|
145
144
|
return `(${args[0]} in ${obj})`;
|
|
145
|
+
if (e.method === "delete")
|
|
146
|
+
return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
|
|
146
147
|
}
|
|
147
148
|
// Set methods
|
|
148
149
|
if (ty === "set") {
|
|
@@ -232,13 +233,24 @@ function emitExpr(e) {
|
|
|
232
233
|
}
|
|
233
234
|
if (e.fn === "BigInt" || e.fn === "Number")
|
|
234
235
|
return args[0]; // identity: both map to int
|
|
236
|
+
// Set literal: {a, b, c}
|
|
237
|
+
if (e.fn === "SetLiteral")
|
|
238
|
+
return `{${args.join(", ")}}`;
|
|
235
239
|
if (e.fn === "JSFloorDiv")
|
|
236
240
|
needsJSFloorDiv = true;
|
|
237
241
|
if (e.fn === "CeilReal")
|
|
238
242
|
needsCeilReal = true;
|
|
239
243
|
if (e.fn === "FloorReal")
|
|
240
244
|
needsFloorReal = true;
|
|
241
|
-
|
|
245
|
+
if (e.fn === "NatToString")
|
|
246
|
+
needsNatToString = true;
|
|
247
|
+
if (e.fn === "MathAbs")
|
|
248
|
+
needsMathAbs = true;
|
|
249
|
+
if (e.fn === "MathMin")
|
|
250
|
+
needsMathMin = true;
|
|
251
|
+
if (e.fn === "MathMax")
|
|
252
|
+
needsMathMax = true;
|
|
253
|
+
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
242
254
|
}
|
|
243
255
|
case "field": {
|
|
244
256
|
const obj = emitExpr(e.obj);
|
|
@@ -260,10 +272,40 @@ function emitExpr(e) {
|
|
|
260
272
|
const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
261
273
|
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
262
274
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
275
|
+
// Match constructor by field names — prefer exact match over first-field heuristic
|
|
276
|
+
let ctorName;
|
|
277
|
+
if (e.fields.length > 0) {
|
|
278
|
+
const fieldNames = new Set(e.fields.map(f => f.name));
|
|
279
|
+
for (const [name, fields] of _structureDecls) {
|
|
280
|
+
if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
|
|
281
|
+
ctorName = name;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!ctorName)
|
|
286
|
+
ctorName = _recordCtors.get(e.fields[0].name);
|
|
287
|
+
}
|
|
288
|
+
if (ctorName) {
|
|
289
|
+
const structFields = _structureDecls.get(ctorName);
|
|
290
|
+
if (structFields && e.fields.length < structFields.length) {
|
|
291
|
+
// Pad missing fields: match by name, fill None for optional
|
|
292
|
+
const provided = new Map(e.fields.map(f => [f.name, f]));
|
|
293
|
+
const vals = structFields.map(sf => {
|
|
294
|
+
const f = provided.get(sf.name);
|
|
295
|
+
if (f)
|
|
296
|
+
return emitExpr(f.value);
|
|
297
|
+
if (sf.type.kind === "optional") {
|
|
298
|
+
needsOptionType = true;
|
|
299
|
+
return "None";
|
|
300
|
+
}
|
|
301
|
+
return `/* missing: ${sf.name} */`;
|
|
302
|
+
});
|
|
303
|
+
return `${ctorName}(${vals.join(", ")})`;
|
|
304
|
+
}
|
|
305
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
266
306
|
return `${ctorName}(${vals.join(", ")})`;
|
|
307
|
+
}
|
|
308
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
267
309
|
return `(${vals.join(", ")})`;
|
|
268
310
|
}
|
|
269
311
|
case "if":
|
|
@@ -331,7 +373,13 @@ function emitStmt(s, indent) {
|
|
|
331
373
|
const pad = " ".repeat(indent);
|
|
332
374
|
switch (s.kind) {
|
|
333
375
|
case "let":
|
|
334
|
-
|
|
376
|
+
// Record literal assigned to map type → emit as map[k := v, ...]
|
|
377
|
+
if (s.type.kind === "map" && s.value.kind === "record" && !s.value.spread) {
|
|
378
|
+
const entries = s.value.fields.map(f => `${emitExpr({ kind: "str", value: f.name })} := ${emitExpr(f.value)}`);
|
|
379
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(resolveTy(s.type))} := map[${entries.join(", ")}];`;
|
|
380
|
+
}
|
|
381
|
+
if (s.value.kind === "havoc" || s.value.kind === "emptyMap" || s.value.kind === "emptySet" ||
|
|
382
|
+
(s.value.kind === "arrayLiteral" && s.value.elems.length === 0))
|
|
335
383
|
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
336
384
|
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
337
385
|
case "assign":
|
|
@@ -408,18 +456,23 @@ function emitStmt(s, indent) {
|
|
|
408
456
|
function emitDecl(d) {
|
|
409
457
|
switch (d.kind) {
|
|
410
458
|
case "inductive": {
|
|
459
|
+
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
411
460
|
const ctors = d.constructors.map(c => {
|
|
412
461
|
if (c.fields.length === 0)
|
|
413
462
|
return escapeName(c.name);
|
|
414
463
|
return `${escapeName(c.name)}(${paramList(c.fields)})`;
|
|
415
464
|
});
|
|
416
|
-
return `datatype ${d.name} = ${ctors.join(" | ")}`;
|
|
465
|
+
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
417
466
|
}
|
|
418
467
|
case "structure": {
|
|
419
468
|
return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
|
|
420
469
|
}
|
|
470
|
+
case "type-alias": {
|
|
471
|
+
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
472
|
+
}
|
|
421
473
|
case "def": {
|
|
422
|
-
const
|
|
474
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
475
|
+
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
423
476
|
for (const r of d.requires)
|
|
424
477
|
lines.push(` requires ${emitExpr(r)}`);
|
|
425
478
|
lines.push(`{`);
|
|
@@ -428,7 +481,7 @@ function emitDecl(d) {
|
|
|
428
481
|
// Companion lemma for ensures (proof target for LLM)
|
|
429
482
|
if (d.ensures.length > 0) {
|
|
430
483
|
lines.push("");
|
|
431
|
-
lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
|
|
484
|
+
lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
|
|
432
485
|
for (const r of d.requires)
|
|
433
486
|
lines.push(` requires ${emitExpr(r)}`);
|
|
434
487
|
for (const e of d.ensures)
|
|
@@ -439,7 +492,8 @@ function emitDecl(d) {
|
|
|
439
492
|
return lines.join("\n");
|
|
440
493
|
}
|
|
441
494
|
case "method": {
|
|
442
|
-
const
|
|
495
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
496
|
+
const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
|
|
443
497
|
for (const r of d.requires)
|
|
444
498
|
lines.push(` requires ${emitExpr(r)}`);
|
|
445
499
|
for (const e of d.ensures)
|
|
@@ -487,11 +541,14 @@ let needsStringToUpper = false;
|
|
|
487
541
|
let needsJSFloorDiv = false;
|
|
488
542
|
let needsCeilReal = false;
|
|
489
543
|
let needsFloorReal = false;
|
|
490
|
-
let needsStdCollections = false;
|
|
491
544
|
let needsOptionType = false;
|
|
492
545
|
let needsSetToSeq = false;
|
|
493
546
|
let needsBitAnd = false;
|
|
494
547
|
let needsPow2 = false;
|
|
548
|
+
let needsNatToString = false;
|
|
549
|
+
let needsMathAbs = false;
|
|
550
|
+
let needsMathMin = false;
|
|
551
|
+
let needsMathMax = false;
|
|
495
552
|
const POW2 = `function Pow2(n: int): int
|
|
496
553
|
requires n >= 0
|
|
497
554
|
decreases n
|
|
@@ -579,25 +636,64 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
|
579
636
|
var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
|
|
580
637
|
[upper] + StringToUpper(s[1..])
|
|
581
638
|
}`;
|
|
639
|
+
const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
|
|
640
|
+
const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
|
|
641
|
+
const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
642
|
+
decreases n
|
|
643
|
+
{
|
|
644
|
+
var digit := ('0' as int + n % 10) as char;
|
|
645
|
+
if n < 10 then [digit]
|
|
646
|
+
else NatToString(n / 10) + [digit]
|
|
647
|
+
}`;
|
|
648
|
+
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
582
649
|
// ── Constructor and record helpers ───────────────────────────
|
|
583
650
|
let _recordCtors = new Map();
|
|
651
|
+
let _structureDecls = new Map();
|
|
652
|
+
let _declaredTypes = new Set();
|
|
584
653
|
function buildRecordCtorMap(decls) {
|
|
585
654
|
_recordCtors = new Map();
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
655
|
+
_structureDecls = new Map();
|
|
656
|
+
_declaredTypes = new Set();
|
|
657
|
+
function collectDecl(d) {
|
|
658
|
+
if (d.kind === "structure") {
|
|
659
|
+
_declaredTypes.add(d.name);
|
|
660
|
+
_structureDecls.set(d.name, d.fields);
|
|
661
|
+
if (d.fields.length > 0)
|
|
662
|
+
_recordCtors.set(d.fields[0].name, d.name);
|
|
663
|
+
}
|
|
664
|
+
if (d.kind === "inductive")
|
|
665
|
+
_declaredTypes.add(d.name);
|
|
666
|
+
if (d.kind === "type-alias")
|
|
667
|
+
_declaredTypes.add(d.name);
|
|
668
|
+
if (d.kind === "def")
|
|
669
|
+
_declaredTypes.add(d.name);
|
|
589
670
|
if (d.kind === "namespace")
|
|
590
|
-
for (const inner of d.decls)
|
|
591
|
-
|
|
592
|
-
_recordCtors.set(inner.fields[0].name, inner.name);
|
|
593
|
-
}
|
|
671
|
+
for (const inner of d.decls)
|
|
672
|
+
collectDecl(inner);
|
|
594
673
|
}
|
|
674
|
+
for (const d of decls)
|
|
675
|
+
collectDecl(d);
|
|
676
|
+
}
|
|
677
|
+
/** Resolve a Ty to a Dafny-safe type, falling back to string for undeclared user types. */
|
|
678
|
+
function resolveTy(ty) {
|
|
679
|
+
if (ty.kind === "user" && !_declaredTypes.has(ty.name))
|
|
680
|
+
return { kind: "string" };
|
|
681
|
+
if (ty.kind === "optional")
|
|
682
|
+
return { kind: "optional", inner: resolveTy(ty.inner) };
|
|
683
|
+
if (ty.kind === "array")
|
|
684
|
+
return { kind: "array", elem: resolveTy(ty.elem) };
|
|
685
|
+
if (ty.kind === "map")
|
|
686
|
+
return { kind: "map", key: resolveTy(ty.key), value: resolveTy(ty.value) };
|
|
687
|
+
if (ty.kind === "set")
|
|
688
|
+
return { kind: "set", elem: resolveTy(ty.elem) };
|
|
689
|
+
return ty;
|
|
595
690
|
}
|
|
596
691
|
function qualifyCtor(name, type) {
|
|
597
692
|
const rawName = name.replace(/^\./, "");
|
|
693
|
+
const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
|
|
598
694
|
if (type)
|
|
599
|
-
return `${type}.${
|
|
600
|
-
return
|
|
695
|
+
return `${type}.${mapped}`;
|
|
696
|
+
return mapped;
|
|
601
697
|
}
|
|
602
698
|
/** Translate a Lean match pattern to Dafny syntax.
|
|
603
699
|
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
@@ -630,11 +726,14 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
630
726
|
needsJSFloorDiv = false;
|
|
631
727
|
needsCeilReal = false;
|
|
632
728
|
needsFloorReal = false;
|
|
633
|
-
needsStdCollections = false;
|
|
634
729
|
needsOptionType = false;
|
|
635
730
|
needsSetToSeq = false;
|
|
636
731
|
needsBitAnd = false;
|
|
637
732
|
needsPow2 = false;
|
|
733
|
+
needsNatToString = false;
|
|
734
|
+
needsMathAbs = false;
|
|
735
|
+
needsMathMin = false;
|
|
736
|
+
needsMathMax = false;
|
|
638
737
|
// Collect pure def names so we can skip their method wrappers
|
|
639
738
|
const pureDefs = new Set();
|
|
640
739
|
for (const d of file.decls) {
|
|
@@ -671,8 +770,6 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
671
770
|
const lines = [];
|
|
672
771
|
if (tsFileName)
|
|
673
772
|
lines.push(`// Generated by lsc from ${tsFileName}`);
|
|
674
|
-
if (needsStdCollections)
|
|
675
|
-
lines.push("import Std.Collections.Seq");
|
|
676
773
|
if (needsOptionType) {
|
|
677
774
|
lines.push("");
|
|
678
775
|
lines.push("datatype Option<T> = None | Some(value: T)");
|
|
@@ -733,6 +830,22 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
733
830
|
lines.push("");
|
|
734
831
|
lines.push(STRING_TO_UPPER);
|
|
735
832
|
}
|
|
833
|
+
if (needsNatToString) {
|
|
834
|
+
lines.push("");
|
|
835
|
+
lines.push(NAT_TO_STRING);
|
|
836
|
+
}
|
|
837
|
+
if (needsMathAbs) {
|
|
838
|
+
lines.push("");
|
|
839
|
+
lines.push(MATH_ABS);
|
|
840
|
+
}
|
|
841
|
+
if (needsMathMin) {
|
|
842
|
+
lines.push("");
|
|
843
|
+
lines.push(MATH_MIN);
|
|
844
|
+
}
|
|
845
|
+
if (needsMathMax) {
|
|
846
|
+
lines.push("");
|
|
847
|
+
lines.push(MATH_MAX);
|
|
848
|
+
}
|
|
736
849
|
lines.push(...declLines);
|
|
737
850
|
return lines.join("\n") + "\n";
|
|
738
851
|
}
|