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/tools/dist/transform.js
CHANGED
|
@@ -35,7 +35,10 @@ function mapExpr(e, f) {
|
|
|
35
35
|
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
36
36
|
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
37
37
|
case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
38
|
-
case "match":
|
|
38
|
+
case "match": {
|
|
39
|
+
const scr = typeof e.scrutinee === "string" ? e.scrutinee : r(e.scrutinee);
|
|
40
|
+
return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
|
|
41
|
+
}
|
|
39
42
|
case "forall": return { ...e, body: r(e.body) };
|
|
40
43
|
case "exists": return { ...e, body: r(e.body) };
|
|
41
44
|
case "let": return { ...e, value: r(e.value), body: r(e.body) };
|
|
@@ -66,6 +69,52 @@ function mapStmt(s, f) {
|
|
|
66
69
|
function mapStmts(stmts, f) {
|
|
67
70
|
return stmts.map(s => mapStmt(s, f));
|
|
68
71
|
}
|
|
72
|
+
/** Map over all sub-expressions in a TExpr (typed IR). */
|
|
73
|
+
function mapTExpr(e, f) {
|
|
74
|
+
const hit = f(e);
|
|
75
|
+
if (hit)
|
|
76
|
+
return hit;
|
|
77
|
+
const r = (x) => mapTExpr(x, f);
|
|
78
|
+
switch (e.kind) {
|
|
79
|
+
case "var":
|
|
80
|
+
case "num":
|
|
81
|
+
case "str":
|
|
82
|
+
case "bool":
|
|
83
|
+
case "result":
|
|
84
|
+
case "havoc": return e;
|
|
85
|
+
case "binop": return { ...e, left: r(e.left), right: r(e.right) };
|
|
86
|
+
case "unop": return { ...e, expr: r(e.expr) };
|
|
87
|
+
case "call": return { ...e, fn: r(e.fn), args: e.args.map(r) };
|
|
88
|
+
case "index": return { ...e, obj: r(e.obj), idx: r(e.idx) };
|
|
89
|
+
case "field": return { ...e, obj: r(e.obj) };
|
|
90
|
+
case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
|
|
91
|
+
case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
|
|
92
|
+
case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
|
|
93
|
+
case "forall": return { ...e, body: r(e.body) };
|
|
94
|
+
case "exists": return { ...e, body: r(e.body) };
|
|
95
|
+
case "lambda": return e;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Map over all expressions in a TStmt tree (typed IR). */
|
|
99
|
+
function mapTStmt(s, f) {
|
|
100
|
+
const r = (e) => mapTExpr(e, f);
|
|
101
|
+
switch (s.kind) {
|
|
102
|
+
case "let": return { ...s, init: r(s.init) };
|
|
103
|
+
case "assign": return { ...s, value: r(s.value) };
|
|
104
|
+
case "return": return { ...s, value: r(s.value) };
|
|
105
|
+
case "break":
|
|
106
|
+
case "continue":
|
|
107
|
+
case "throw": return s;
|
|
108
|
+
case "expr": return { ...s, expr: r(s.expr) };
|
|
109
|
+
case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapTStmt(t, f)), else: s.else.map(t => mapTStmt(t, f)) };
|
|
110
|
+
case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
|
|
111
|
+
case "switch": return { ...s, expr: r(s.expr), cases: s.cases.map(c => ({ ...c, body: c.body.map(t => mapTStmt(t, f)) })), defaultBody: s.defaultBody.map(t => mapTStmt(t, f)) };
|
|
112
|
+
case "forof": return { ...s, iterable: r(s.iterable), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
|
|
113
|
+
case "ghostLet": return { ...s, init: r(s.init) };
|
|
114
|
+
case "ghostAssign": return { ...s, value: r(s.value) };
|
|
115
|
+
case "assert": return { ...s, expr: r(s.expr) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
69
118
|
export const LEAN_OPTIONS = {
|
|
70
119
|
backend: "lean",
|
|
71
120
|
monadic: true,
|
|
@@ -75,10 +124,14 @@ export const DAFNY_OPTIONS = {
|
|
|
75
124
|
monadic: false,
|
|
76
125
|
};
|
|
77
126
|
/** Active options — set before each transform call. */
|
|
78
|
-
let _opts =
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
127
|
+
let _opts = DAFNY_OPTIONS;
|
|
128
|
+
/** Type declarations — set once per module transform for discriminated union handling. */
|
|
129
|
+
let _typeDecls = [];
|
|
130
|
+
/** Prefix match-bound field names to avoid capturing user variables.
|
|
131
|
+
* When prefix is given (the scrutinee name), include it to avoid
|
|
132
|
+
* collisions in nested matches on different variables. */
|
|
133
|
+
function matchBinder(fieldName, prefix) {
|
|
134
|
+
return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
|
|
82
135
|
}
|
|
83
136
|
const _forofCounters = new Map();
|
|
84
137
|
function isNat(ty) { return ty.kind === "nat"; }
|
|
@@ -215,6 +268,11 @@ function lowerExpr(e, binds) {
|
|
|
215
268
|
],
|
|
216
269
|
};
|
|
217
270
|
}
|
|
271
|
+
// || undefined on optional → identity (no-op: x || undefined = x)
|
|
272
|
+
if (e.op === "||" && e.left.ty.kind === "optional" &&
|
|
273
|
+
e.right.kind === "var" && e.right.name === "undefined") {
|
|
274
|
+
return lowerExpr(e.left, binds);
|
|
275
|
+
}
|
|
218
276
|
// || on optional → match Some/None with default
|
|
219
277
|
if (e.op === "||" && e.left.ty.kind === "optional") {
|
|
220
278
|
const optExpr = lowerExpr(e.left, binds);
|
|
@@ -228,6 +286,43 @@ function lowerExpr(e, binds) {
|
|
|
228
286
|
],
|
|
229
287
|
};
|
|
230
288
|
}
|
|
289
|
+
// || on map index → if key in map then map[key] else default
|
|
290
|
+
if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
|
|
291
|
+
const map = lowerExpr(e.left.obj, binds);
|
|
292
|
+
const key = lowerExpr(e.left.idx, binds);
|
|
293
|
+
const right = lowerExpr(e.right, binds);
|
|
294
|
+
return {
|
|
295
|
+
kind: "if",
|
|
296
|
+
cond: { kind: "binop", op: "in", left: key, right: map },
|
|
297
|
+
then: { kind: "index", arr: map, idx: key }, else: right,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// || on non-optional string/array/user → if non-empty then x else default
|
|
301
|
+
if (e.op === "||" && (e.left.ty.kind === "string" || e.left.ty.kind === "array" ||
|
|
302
|
+
(e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
|
|
303
|
+
const left = lowerExpr(e.left, binds);
|
|
304
|
+
const right = lowerExpr(e.right, binds);
|
|
305
|
+
return {
|
|
306
|
+
kind: "if",
|
|
307
|
+
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
|
|
308
|
+
then: left, else: right,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// int + string → NatToString(int) + string (string concatenation)
|
|
312
|
+
if (e.op === "+" && _opts.backend === "dafny") {
|
|
313
|
+
const isIntL = e.left.ty.kind === "int" || e.left.ty.kind === "nat";
|
|
314
|
+
const isIntR = e.right.ty.kind === "int" || e.right.ty.kind === "nat";
|
|
315
|
+
if (isIntL && e.right.ty.kind === "string") {
|
|
316
|
+
return { kind: "binop", op: "+",
|
|
317
|
+
left: { kind: "app", fn: "NatToString", args: [lowerExpr(e.left, binds)] },
|
|
318
|
+
right: lowerExpr(e.right, binds) };
|
|
319
|
+
}
|
|
320
|
+
if (e.left.ty.kind === "string" && isIntR) {
|
|
321
|
+
return { kind: "binop", op: "+",
|
|
322
|
+
left: lowerExpr(e.left, binds),
|
|
323
|
+
right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
231
326
|
return {
|
|
232
327
|
kind: "binop",
|
|
233
328
|
op: OP_MAP[e.op] ?? e.op,
|
|
@@ -249,6 +344,15 @@ function lowerExpr(e, binds) {
|
|
|
249
344
|
return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
|
|
250
345
|
}
|
|
251
346
|
case "call": {
|
|
347
|
+
// Math.abs/min/max → preamble functions
|
|
348
|
+
if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
|
|
349
|
+
if (e.fn.field === "abs" && e.args.length === 1)
|
|
350
|
+
return { kind: "app", fn: "MathAbs", args: [lowerExpr(e.args[0], binds)] };
|
|
351
|
+
if (e.fn.field === "min" && e.args.length === 2)
|
|
352
|
+
return { kind: "app", fn: "MathMin", args: e.args.map(a => lowerExpr(a, binds)) };
|
|
353
|
+
if (e.fn.field === "max" && e.args.length === 2)
|
|
354
|
+
return { kind: "app", fn: "MathMax", args: e.args.map(a => lowerExpr(a, binds)) };
|
|
355
|
+
}
|
|
252
356
|
// Math.ceil(x): CeilReal on real args, identity on int
|
|
253
357
|
if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
|
|
254
358
|
const arg = e.args[0];
|
|
@@ -296,13 +400,64 @@ function lowerExpr(e, binds) {
|
|
|
296
400
|
const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
|
|
297
401
|
return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
|
|
298
402
|
}
|
|
299
|
-
case "record":
|
|
300
|
-
|
|
403
|
+
case "record": {
|
|
404
|
+
// Discriminated union: { kind: 'NoOp' } → constructor NoOp
|
|
405
|
+
if (e.ty.kind === "user" && !e.spread) {
|
|
406
|
+
const tyName = e.ty.name;
|
|
407
|
+
// Match base type name (strip generic args: "Result<Model, Err>" → "Result")
|
|
408
|
+
const baseName = tyName.includes("<") ? tyName.slice(0, tyName.indexOf("<")) : tyName;
|
|
409
|
+
const decl = _typeDecls.find(d => d.name === baseName && (d.kind === "discriminated-union" || d.kind === "string-union"));
|
|
410
|
+
if (decl && decl.discriminant) {
|
|
411
|
+
const discField = e.fields.find(f => f.name === decl.discriminant);
|
|
412
|
+
if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
|
|
413
|
+
const variantName = String(discField.value.kind === "str" ? discField.value.value : discField.value.value);
|
|
414
|
+
const variant = decl.variants?.find(v => v.name === variantName);
|
|
415
|
+
if (variant) {
|
|
416
|
+
const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
|
|
417
|
+
if (nonDiscFields.length === 0) {
|
|
418
|
+
return { kind: "constructor", name: variantName, type: tyName };
|
|
419
|
+
}
|
|
420
|
+
// Constructor with args: match variant field order
|
|
421
|
+
const args = variant.fields.map(vf => {
|
|
422
|
+
const ef = nonDiscFields.find(f => f.name === vf.name);
|
|
423
|
+
return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
|
|
424
|
+
});
|
|
425
|
+
return { kind: "app", fn: variantName, args };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// For spread records, wrap non-optional values in Some for optional fields
|
|
431
|
+
if (e.spread) {
|
|
432
|
+
const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
|
|
433
|
+
const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
|
|
434
|
+
const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
|
|
435
|
+
const loweredFields = e.fields.map(f => {
|
|
436
|
+
let value = lowerExpr(f.value, binds);
|
|
437
|
+
if (structDecl?.fields) {
|
|
438
|
+
const fieldDecl = structDecl.fields.find(sf => sf.name === f.name);
|
|
439
|
+
if (fieldDecl) {
|
|
440
|
+
const fieldTy = parseTsType(fieldDecl.tsType);
|
|
441
|
+
const isUndef = f.value.kind === "var" && f.value.name === "undefined";
|
|
442
|
+
if (fieldTy.kind === "optional" && f.value.ty.kind !== "optional" && !isUndef) {
|
|
443
|
+
value = { kind: "app", fn: "Some", args: [value] };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { name: f.name, value };
|
|
448
|
+
});
|
|
449
|
+
return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
|
|
450
|
+
}
|
|
451
|
+
return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
452
|
+
}
|
|
301
453
|
case "arrayLiteral":
|
|
302
454
|
if (e.ty.kind === "map" && e.elems.length === 0)
|
|
303
455
|
return { kind: "emptyMap" };
|
|
304
456
|
if (e.ty.kind === "set" && e.elems.length === 0)
|
|
305
457
|
return { kind: "emptySet" };
|
|
458
|
+
// Set with initial elements: new Set([a, b]) → {a, b}
|
|
459
|
+
if (e.ty.kind === "set")
|
|
460
|
+
return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
|
|
306
461
|
return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
|
|
307
462
|
case "lambda":
|
|
308
463
|
return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
|
|
@@ -314,7 +469,49 @@ function lowerExpr(e, binds) {
|
|
|
314
469
|
const cond = lowerExpr(e.cond, binds);
|
|
315
470
|
let thenExpr = lowerExpr(e.then, binds);
|
|
316
471
|
let elseExpr = lowerExpr(e.else, binds);
|
|
317
|
-
//
|
|
472
|
+
// Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
|
|
473
|
+
if (e.narrowedVar && e.narrowedExpr) {
|
|
474
|
+
const scrutinee = lowerExpr(e.narrowedExpr, binds);
|
|
475
|
+
const bound = matchBinder(e.narrowedVar);
|
|
476
|
+
if (bound !== e.narrowedVar) {
|
|
477
|
+
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
478
|
+
}
|
|
479
|
+
const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
|
|
480
|
+
? { kind: "constructor", name: ".none" }
|
|
481
|
+
: { kind: "app", fn: "Some", args: [expr] };
|
|
482
|
+
thenExpr = wrapSomeNone(thenExpr, e.then);
|
|
483
|
+
elseExpr = wrapSomeNone(elseExpr, e.else);
|
|
484
|
+
return {
|
|
485
|
+
kind: "match", scrutinee,
|
|
486
|
+
arms: [
|
|
487
|
+
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
488
|
+
{ pattern: ".none", body: elseExpr },
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
// Optional cond with narrowedVar → match Some/None (truthiness)
|
|
493
|
+
if (e.narrowedVar && e.cond.ty.kind === "optional") {
|
|
494
|
+
const bound = matchBinder(e.narrowedVar);
|
|
495
|
+
// Replace the synthetic/narrowed var with the match-bound name
|
|
496
|
+
if (bound !== e.narrowedVar) {
|
|
497
|
+
thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
|
|
498
|
+
}
|
|
499
|
+
// The match produces an Optional: wrap branches in Some/None.
|
|
500
|
+
// Either branch being undefined signals None; otherwise wrap in Some.
|
|
501
|
+
const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
|
|
502
|
+
? { kind: "constructor", name: ".none" }
|
|
503
|
+
: { kind: "app", fn: "Some", args: [expr] };
|
|
504
|
+
thenExpr = wrapSomeNone(thenExpr, e.then);
|
|
505
|
+
elseExpr = wrapSomeNone(elseExpr, e.else);
|
|
506
|
+
return {
|
|
507
|
+
kind: "match", scrutinee: cond,
|
|
508
|
+
arms: [
|
|
509
|
+
{ pattern: `.some ${bound}`, body: thenExpr },
|
|
510
|
+
{ pattern: ".none", body: elseExpr },
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
// Non-optional: regular if with optional wrapping
|
|
318
515
|
if (e.ty.kind === "optional") {
|
|
319
516
|
if (e.then.kind === "var" && e.then.name === "undefined") {
|
|
320
517
|
thenExpr = { kind: "constructor", name: ".none" };
|
|
@@ -374,7 +571,7 @@ function ensuresToMatch(e, typeDecls) {
|
|
|
374
571
|
if (!variant)
|
|
375
572
|
return null;
|
|
376
573
|
const fields = variant.fields;
|
|
377
|
-
const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
|
|
574
|
+
const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name, obj.name)).join(" ")}` : `.${variantName}`;
|
|
378
575
|
let rhs = transformExpr(e.right);
|
|
379
576
|
rhs = replaceFieldAccess(rhs, obj.name, fields);
|
|
380
577
|
return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
|
|
@@ -384,7 +581,7 @@ function replaceFieldAccess(e, varName, fields) {
|
|
|
384
581
|
if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
|
|
385
582
|
const f = fields.find(f => f.name === x.field);
|
|
386
583
|
if (f)
|
|
387
|
-
return { kind: "var", name: matchBinder(f.name) };
|
|
584
|
+
return { kind: "var", name: matchBinder(f.name, varName) };
|
|
388
585
|
}
|
|
389
586
|
// If this let shadows the matched variable, stop replacing in the body
|
|
390
587
|
if (x.kind === "let" && x.name === varName)
|
|
@@ -610,6 +807,17 @@ function transformStmt(s, typeDecls) {
|
|
|
610
807
|
return [...binds, { kind: "assign", target: "_", value: expr }];
|
|
611
808
|
}
|
|
612
809
|
case "if": {
|
|
810
|
+
// Restructure && with optional check: extract the leftmost optional check
|
|
811
|
+
// from a && chain and nest the rest inside. Handles left-associative chains:
|
|
812
|
+
// if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
|
|
813
|
+
if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
|
|
814
|
+
const extracted = extractLeftmostOptional(s.cond);
|
|
815
|
+
if (extracted) {
|
|
816
|
+
const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
|
|
817
|
+
const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
|
|
818
|
+
return transformStmts([outerIf], typeDecls);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
613
821
|
// Lift from condition only (Lean rule: don't lift from branches)
|
|
614
822
|
const { binds, expr: cond } = liftMethodCalls(s.cond);
|
|
615
823
|
return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
|
|
@@ -708,6 +916,21 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
|
|
|
708
916
|
function mapStmtExprs(s, r) {
|
|
709
917
|
return mapStmt(s, e => r(e));
|
|
710
918
|
}
|
|
919
|
+
/** Extract the leftmost optional check from a && chain, returning the check and the rest.
|
|
920
|
+
* (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
|
|
921
|
+
function extractLeftmostOptional(cond) {
|
|
922
|
+
if (cond.kind !== "binop" || cond.op !== "&&")
|
|
923
|
+
return null;
|
|
924
|
+
const check = parseOptionalCheck(cond.left);
|
|
925
|
+
if (check && !check.negated)
|
|
926
|
+
return { optCond: cond.left, rest: cond.right };
|
|
927
|
+
if (cond.left.kind === "binop" && cond.left.op === "&&") {
|
|
928
|
+
const inner = extractLeftmostOptional(cond.left);
|
|
929
|
+
if (inner)
|
|
930
|
+
return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
711
934
|
/** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
|
|
712
935
|
function parseOptionalCheck(cond) {
|
|
713
936
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
|
|
@@ -726,9 +949,10 @@ function emitMatchStmt(chain, typeDecls) {
|
|
|
726
949
|
const arms = chain.cases.map(c => {
|
|
727
950
|
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
728
951
|
const fields = variant?.fields ?? [];
|
|
729
|
-
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
730
|
-
|
|
731
|
-
|
|
952
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
|
|
953
|
+
// Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
|
|
954
|
+
const replaced = replaceFieldAccessInTStmts(c.body, chain.varName, fields);
|
|
955
|
+
const body = transformStmts(replaced, typeDecls);
|
|
732
956
|
return { pattern, body };
|
|
733
957
|
});
|
|
734
958
|
if (chain.fallthrough.length > 0)
|
|
@@ -742,15 +966,33 @@ function emitSwitchStmt(s, typeDecls) {
|
|
|
742
966
|
const arms = s.cases.map(c => {
|
|
743
967
|
const variant = decl?.variants?.find(v => v.name === c.label);
|
|
744
968
|
const fields = variant?.fields ?? [];
|
|
745
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
746
|
-
|
|
747
|
-
|
|
969
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
970
|
+
// Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
|
|
971
|
+
const replaced = replaceFieldAccessInTStmts(c.body, varName, fields);
|
|
972
|
+
const body = transformStmts(replaced, typeDecls);
|
|
748
973
|
return { pattern, body };
|
|
749
974
|
});
|
|
750
975
|
if (s.defaultBody.length > 0)
|
|
751
976
|
arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
|
|
752
977
|
return { kind: "match", scrutinee: varName, arms };
|
|
753
978
|
}
|
|
979
|
+
/** Replace obj.field → binder var in typed IR (before transform).
|
|
980
|
+
* Uses the variant's declared field type since the resolve phase may not
|
|
981
|
+
* resolve field types on discriminated unions correctly. */
|
|
982
|
+
function replaceFieldAccessInTStmts(stmts, varName, fields) {
|
|
983
|
+
if (fields.length === 0)
|
|
984
|
+
return stmts;
|
|
985
|
+
return stmts.map(s => mapTStmt(s, e => {
|
|
986
|
+
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
|
|
987
|
+
const fi = fields.find(fi => fi.name === e.field);
|
|
988
|
+
if (fi) {
|
|
989
|
+
const ty = e.ty.kind !== "unknown" ? e.ty : parseTsType(fi.tsType);
|
|
990
|
+
return { kind: "var", name: matchBinder(fi.name, varName), ty };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
754
996
|
function replaceFieldAccessInStmts(stmts, varName, fields) {
|
|
755
997
|
if (fields.length === 0)
|
|
756
998
|
return stmts;
|
|
@@ -758,7 +1000,7 @@ function replaceFieldAccessInStmts(stmts, varName, fields) {
|
|
|
758
1000
|
if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
|
|
759
1001
|
const fi = fields.find(fi => fi.name === e.field);
|
|
760
1002
|
if (fi)
|
|
761
|
-
return { kind: "var", name: matchBinder(fi.name) };
|
|
1003
|
+
return { kind: "var", name: matchBinder(fi.name, varName) };
|
|
762
1004
|
}
|
|
763
1005
|
return null;
|
|
764
1006
|
};
|
|
@@ -836,11 +1078,12 @@ function transformPureSwitch(s, typeDecls) {
|
|
|
836
1078
|
const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
|
|
837
1079
|
if (!decl)
|
|
838
1080
|
return null;
|
|
1081
|
+
const varName = s.expr.kind === "var" ? s.expr.name : undefined;
|
|
839
1082
|
const arms = [];
|
|
840
1083
|
for (const c of s.cases) {
|
|
841
1084
|
const variant = decl.variants?.find(v => v.name === c.label);
|
|
842
1085
|
const fields = variant?.fields ?? [];
|
|
843
|
-
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
|
|
1086
|
+
const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
|
|
844
1087
|
let body = transformPureBody(c.body, typeDecls);
|
|
845
1088
|
if (!body)
|
|
846
1089
|
return null;
|
|
@@ -864,7 +1107,7 @@ function transformPureMatch(chain, typeDecls) {
|
|
|
864
1107
|
for (const c of chain.cases) {
|
|
865
1108
|
const variant = decl?.variants?.find(v => v.name === c.variant);
|
|
866
1109
|
const fields = variant?.fields ?? [];
|
|
867
|
-
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
|
|
1110
|
+
const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
|
|
868
1111
|
let body = transformPureBody(c.body, typeDecls);
|
|
869
1112
|
if (!body)
|
|
870
1113
|
return null;
|
|
@@ -896,6 +1139,7 @@ function transformTypeDecl(d) {
|
|
|
896
1139
|
else if (d.kind === "discriminated-union") {
|
|
897
1140
|
return {
|
|
898
1141
|
kind: "inductive", name: d.name,
|
|
1142
|
+
typeParams: d.typeParams,
|
|
899
1143
|
constructors: d.variants.map(v => ({
|
|
900
1144
|
name: v.name,
|
|
901
1145
|
fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
|
|
@@ -903,6 +1147,12 @@ function transformTypeDecl(d) {
|
|
|
903
1147
|
deriving: ["Repr", "Inhabited"],
|
|
904
1148
|
};
|
|
905
1149
|
}
|
|
1150
|
+
else if (d.kind === "alias") {
|
|
1151
|
+
return {
|
|
1152
|
+
kind: "type-alias", name: d.name,
|
|
1153
|
+
target: parseTsType(d.aliasOf),
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
906
1156
|
else {
|
|
907
1157
|
return {
|
|
908
1158
|
kind: "structure", name: d.name,
|
|
@@ -961,6 +1211,17 @@ function replaceVar(e, name, replacement) {
|
|
|
961
1211
|
});
|
|
962
1212
|
}
|
|
963
1213
|
// ── Top-level transform ──────────────────────────────────────
|
|
1214
|
+
/** Transform for Lean backend — same logic, Lean options. */
|
|
1215
|
+
export function transformModuleLean(mod, specImport) {
|
|
1216
|
+
const prev = _opts;
|
|
1217
|
+
_opts = LEAN_OPTIONS;
|
|
1218
|
+
try {
|
|
1219
|
+
return transformModule(mod, specImport);
|
|
1220
|
+
}
|
|
1221
|
+
finally {
|
|
1222
|
+
_opts = prev;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
964
1225
|
/** Transform for Dafny backend — same logic, Dafny options. */
|
|
965
1226
|
export function transformModuleDafny(mod) {
|
|
966
1227
|
const prev = _opts;
|
|
@@ -974,6 +1235,7 @@ export function transformModuleDafny(mod) {
|
|
|
974
1235
|
}
|
|
975
1236
|
export function transformModule(mod, specImport) {
|
|
976
1237
|
_forofCounters.clear();
|
|
1238
|
+
_typeDecls = mod.typeDecls;
|
|
977
1239
|
const typeDecls = mod.typeDecls.map(transformTypeDecl);
|
|
978
1240
|
// Module-level constants
|
|
979
1241
|
const constDecls = (mod.constants ?? []).map(c => ({
|
|
@@ -996,6 +1258,7 @@ export function transformModule(mod, specImport) {
|
|
|
996
1258
|
pureDefs.push({
|
|
997
1259
|
kind: "def",
|
|
998
1260
|
name: fn.name,
|
|
1261
|
+
typeParams: fn.typeParams,
|
|
999
1262
|
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1000
1263
|
returnType: fn.returnTy,
|
|
1001
1264
|
requires: fn.requires.map(transformExpr),
|
|
@@ -1046,6 +1309,7 @@ export function transformModule(mod, specImport) {
|
|
|
1046
1309
|
return {
|
|
1047
1310
|
kind: "method",
|
|
1048
1311
|
name: fn.name,
|
|
1312
|
+
typeParams: fn.typeParams,
|
|
1049
1313
|
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1050
1314
|
returnType: fn.returnTy,
|
|
1051
1315
|
requires: fn.requires.map(transformExpr),
|
|
@@ -1062,6 +1326,7 @@ export function transformModule(mod, specImport) {
|
|
|
1062
1326
|
return {
|
|
1063
1327
|
kind: "method",
|
|
1064
1328
|
name: fn.name,
|
|
1329
|
+
typeParams: fn.typeParams,
|
|
1065
1330
|
params: fn.params.map(p => ({ name: p.name, type: p.ty })),
|
|
1066
1331
|
returnType: fn.returnTy,
|
|
1067
1332
|
requires: fn.requires.map(transformExpr),
|
package/tools/dist/types.js
CHANGED
|
@@ -25,7 +25,14 @@ export function parseTsType(tsType) {
|
|
|
25
25
|
const t = tsType.trim();
|
|
26
26
|
// Union: T | undefined → optional<T>
|
|
27
27
|
if (t.includes(" | ")) {
|
|
28
|
-
|
|
28
|
+
let arms = t.split(" | ").map(a => a.trim());
|
|
29
|
+
// Normalize expanded boolean literals: true | false → boolean
|
|
30
|
+
const boolLits = new Set(["true", "false"]);
|
|
31
|
+
const hasBoth = arms.includes("true") && arms.includes("false");
|
|
32
|
+
if (hasBoth) {
|
|
33
|
+
arms = arms.filter(a => !boolLits.has(a));
|
|
34
|
+
arms.unshift("boolean");
|
|
35
|
+
}
|
|
29
36
|
const nonUndef = arms.filter(a => a !== "undefined");
|
|
30
37
|
if (nonUndef.length === 1 && arms.length === 2) {
|
|
31
38
|
return { kind: "optional", inner: parseTsType(nonUndef[0]) };
|
|
@@ -67,5 +74,11 @@ export function parseTsType(tsType) {
|
|
|
67
74
|
const setMatch = t.match(/^Set<(.+)>$/);
|
|
68
75
|
if (setMatch)
|
|
69
76
|
return { kind: "set", elem: parseTsType(setMatch[1]) };
|
|
77
|
+
// Tuple [T1, T2, ...] → array of the common element type
|
|
78
|
+
const tupleMatch = t.match(/^\[(.+)\]$/);
|
|
79
|
+
if (tupleMatch) {
|
|
80
|
+
const elems = splitTypeArgs(tupleMatch[1]);
|
|
81
|
+
return { kind: "array", elem: parseTsType(elems[0]) };
|
|
82
|
+
}
|
|
70
83
|
return { kind: "user", name: t };
|
|
71
84
|
}
|