lemmascript 0.0.1 → 0.1.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.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Lean IR → text. Trivial pretty-printer.
3
+ * No logic, no type decisions — just serialization.
4
+ */
5
+ // ── Lean keyword escaping ────────────────────────────────────
6
+ const LEAN_KEYWORDS = new Set([
7
+ "def", "theorem", "lemma", "example", "structure", "class", "instance",
8
+ "inductive", "where", "match", "with", "if", "then", "else", "do",
9
+ "let", "mut", "return", "for", "in", "while", "break", "continue",
10
+ "import", "open", "section", "namespace", "end", "set_option",
11
+ "variable", "axiom", "constant", "private", "protected", "noncomputable",
12
+ "partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
13
+ "at", "from", "to", "deriving", "extends", "true", "false",
14
+ ]);
15
+ function escapeName(name) {
16
+ return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
17
+ }
18
+ // ── Operator precedence (for parenthesization) ──────────────
19
+ const PREC = {
20
+ "→": 1, "∨": 2, "∧": 3,
21
+ "=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
22
+ "+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
23
+ };
24
+ function prec(op) { return PREC[op] ?? 10; }
25
+ // ── Expression emission ─────────────────────────────────────
26
+ function emitExpr(e, parentPrec) {
27
+ switch (e.kind) {
28
+ case "var": return escapeName(e.name);
29
+ case "num": return `${e.value}`;
30
+ case "bool": return e.value ? "true" : "false";
31
+ case "str": return `"${e.value}"`;
32
+ case "constructor": return `.${e.name}`;
33
+ case "arrayLiteral":
34
+ if (e.elems.length === 0)
35
+ return `#[]`;
36
+ return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
37
+ case "dotCall": {
38
+ const obj = emitExpr(e.obj);
39
+ const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
40
+ const receiver = wrap ? `(${obj})` : obj;
41
+ const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
42
+ return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
43
+ }
44
+ case "lambda": {
45
+ const params = e.params.map(p => p.name).join(" ");
46
+ // Single return statement → expression lambda
47
+ if (e.body.length === 1 && e.body[0].kind === "return") {
48
+ return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
49
+ }
50
+ // Multi-statement → do block
51
+ return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
52
+ }
53
+ case "unop":
54
+ if (e.op === "¬")
55
+ return `¬(${emitExpr(e.expr)})`;
56
+ if (e.op === "-" && e.expr.kind === "num")
57
+ return `-${e.expr.value}`;
58
+ return `(-${emitExpr(e.expr)})`;
59
+ case "binop": {
60
+ const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
61
+ return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
62
+ }
63
+ case "implies": {
64
+ const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
65
+ const s = parts.join(" → ");
66
+ return parentPrec !== undefined ? `(${s})` : s;
67
+ }
68
+ case "app": {
69
+ const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
70
+ return `${e.fn} ${args.join(" ")}`;
71
+ }
72
+ case "field": {
73
+ const obj = emitExpr(e.obj);
74
+ const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
75
+ return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
76
+ }
77
+ case "toNat": {
78
+ const inner = emitExpr(e.expr);
79
+ const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
80
+ return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
81
+ }
82
+ case "index":
83
+ return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
84
+ case "record": {
85
+ const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
86
+ if (e.spread)
87
+ return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
88
+ return `{ ${fields.join(", ")} }`;
89
+ }
90
+ case "if":
91
+ return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
92
+ case "match": {
93
+ const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
94
+ return `match ${e.scrutinee} with ${arms.join(" ")}`;
95
+ }
96
+ case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
97
+ case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
98
+ case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
99
+ }
100
+ }
101
+ // ── Statement emission ──────────────────────────────────────
102
+ function emitStmts(stmts, indent) {
103
+ const pad = " ".repeat(indent);
104
+ return stmts.map(s => emitStmt(s, indent)).join("\n");
105
+ }
106
+ function emitStmt(s, indent) {
107
+ const pad = " ".repeat(indent);
108
+ switch (s.kind) {
109
+ case "let":
110
+ return s.mutable
111
+ ? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
112
+ : `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
113
+ case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
114
+ case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
115
+ case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
116
+ case "return": return `${pad}return ${emitExpr(s.value)}`;
117
+ case "break": return `${pad}break`;
118
+ case "continue": return `${pad}continue`;
119
+ case "if": {
120
+ let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
121
+ if (s.else.length > 0) {
122
+ if (s.else.length === 1 && s.else[0].kind === "if") {
123
+ const ei = s.else[0];
124
+ out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
125
+ if (ei.else.length > 0)
126
+ out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
127
+ }
128
+ else {
129
+ out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
130
+ }
131
+ }
132
+ return out;
133
+ }
134
+ case "match": {
135
+ const lines = [`${pad}match ${s.scrutinee} with`];
136
+ for (const arm of s.arms) {
137
+ lines.push(`${pad}| ${arm.pattern} =>`);
138
+ lines.push(emitStmts(arm.body, indent + 1));
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ case "while": {
143
+ const lines = [`${pad}while ${emitExpr(s.cond)}`];
144
+ for (const inv of s.invariants)
145
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
146
+ if (s.doneWith)
147
+ lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
148
+ if (s.decreasing)
149
+ lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
150
+ lines.push(`${pad}do`);
151
+ lines.push(emitStmts(s.body, indent + 1));
152
+ return lines.join("\n");
153
+ }
154
+ case "forin": {
155
+ const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
156
+ for (const inv of s.invariants)
157
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
158
+ lines.push(`${pad}do`);
159
+ lines.push(emitStmts(s.body, indent + 1));
160
+ return lines.join("\n");
161
+ }
162
+ }
163
+ }
164
+ // ── Declaration emission ─────────────────────────────────────
165
+ function emitDecl(d) {
166
+ switch (d.kind) {
167
+ case "inductive": {
168
+ const lines = [`inductive ${d.name} where`];
169
+ for (const c of d.constructors) {
170
+ if (c.fields.length === 0) {
171
+ lines.push(` | ${c.name} : ${d.name}`);
172
+ }
173
+ else {
174
+ const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
175
+ lines.push(` | ${c.name} ${params} : ${d.name}`);
176
+ }
177
+ }
178
+ if (d.deriving.length > 0)
179
+ lines.push(`deriving ${d.deriving.join(", ")}`);
180
+ return lines.join("\n");
181
+ }
182
+ case "structure": {
183
+ const lines = [`structure ${d.name} where`];
184
+ for (const f of d.fields)
185
+ lines.push(` ${escapeName(f.name)} : ${f.type}`);
186
+ if (d.deriving.length > 0)
187
+ lines.push(`deriving ${d.deriving.join(", ")}`);
188
+ return lines.join("\n");
189
+ }
190
+ case "def": {
191
+ const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
192
+ return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
193
+ }
194
+ case "method": {
195
+ const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
196
+ const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
197
+ for (const r of d.requires)
198
+ lines.push(` require ${emitExpr(r)}`);
199
+ for (const e of d.ensures)
200
+ lines.push(` ensures ${emitExpr(e)}`);
201
+ lines.push(" do");
202
+ lines.push(emitStmts(d.body, 2));
203
+ return lines.join("\n");
204
+ }
205
+ case "namespace": {
206
+ const lines = [`namespace ${d.name}`];
207
+ for (const inner of d.decls)
208
+ lines.push("", emitDecl(inner));
209
+ lines.push("", `end ${d.name}`);
210
+ return lines.join("\n");
211
+ }
212
+ }
213
+ }
214
+ /** Emit a pure expression with indented if/match blocks. */
215
+ function emitPureExpr(e, indent) {
216
+ const pad = " ".repeat(indent);
217
+ switch (e.kind) {
218
+ case "if":
219
+ return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
220
+ case "match": {
221
+ const lines = [`${pad}match ${e.scrutinee} with`];
222
+ for (const arm of e.arms) {
223
+ lines.push(`${pad}| ${arm.pattern} =>`);
224
+ lines.push(emitPureExpr(arm.body, indent + 1));
225
+ }
226
+ return lines.join("\n");
227
+ }
228
+ case "let":
229
+ return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
230
+ default:
231
+ return `${pad}${emitExpr(e)}`;
232
+ }
233
+ }
234
+ // ── File emission ────────────────────────────────────────────
235
+ export function emitFile(file) {
236
+ const lines = [];
237
+ if (file.comment) {
238
+ lines.push("/-");
239
+ lines.push(file.comment);
240
+ lines.push("-/");
241
+ }
242
+ for (const imp of file.imports)
243
+ lines.push(`import ${imp}`);
244
+ if (file.options.length > 0)
245
+ lines.push("");
246
+ for (const opt of file.options)
247
+ lines.push(`set_option ${opt.key} ${opt.value}`);
248
+ for (const decl of file.decls) {
249
+ lines.push("");
250
+ lines.push(emitDecl(decl));
251
+ }
252
+ return lines.join("\n") + "\n";
253
+ }
@@ -0,0 +1,435 @@
1
+ /**
2
+ * Extract — ts-morph → Raw IR.
3
+ *
4
+ * Produces structured AST nodes, not strings.
5
+ * The only strings are //@ annotation expressions (parsed later by specparser).
6
+ */
7
+ import { Project, Node, SyntaxKind } from "ts-morph";
8
+ // ── Expression extraction ────────────────────────────────────
9
+ function extractExpr(node) {
10
+ // Numeric literal
11
+ if (Node.isNumericLiteral(node)) {
12
+ return { kind: "num", value: Number(node.getLiteralValue()) };
13
+ }
14
+ // String literal
15
+ if (Node.isStringLiteral(node)) {
16
+ return { kind: "str", value: node.getLiteralValue() };
17
+ }
18
+ // Boolean literals: true, false
19
+ if (Node.isTrueLiteral(node))
20
+ return { kind: "bool", value: true };
21
+ if (Node.isFalseLiteral(node))
22
+ return { kind: "bool", value: false };
23
+ // Identifier
24
+ if (Node.isIdentifier(node)) {
25
+ return { kind: "var", name: node.getText() };
26
+ }
27
+ // Property access: x.foo
28
+ if (Node.isPropertyAccessExpression(node)) {
29
+ return { kind: "field", obj: extractExpr(node.getExpression()), field: node.getName() };
30
+ }
31
+ // Element access: arr[i]
32
+ if (Node.isElementAccessExpression(node)) {
33
+ const arg = node.getArgumentExpression();
34
+ if (!arg)
35
+ throw new Error(`Missing index in element access: ${node.getText()}`);
36
+ return { kind: "index", obj: extractExpr(node.getExpression()), idx: extractExpr(arg) };
37
+ }
38
+ // Call expression: f(a, b)
39
+ if (Node.isCallExpression(node)) {
40
+ return {
41
+ kind: "call",
42
+ fn: extractExpr(node.getExpression()),
43
+ args: node.getArguments().map(a => extractExpr(a)),
44
+ };
45
+ }
46
+ // Binary expression: a + b, a === b, etc.
47
+ if (Node.isBinaryExpression(node)) {
48
+ const op = node.getOperatorToken().getText();
49
+ // Assignment: a = b → handled at statement level, but can appear in expressions
50
+ if (op === "=") {
51
+ // This is an assignment expression; extract as binop for now
52
+ return { kind: "binop", op: "=", left: extractExpr(node.getLeft()), right: extractExpr(node.getRight()) };
53
+ }
54
+ return { kind: "binop", op, left: extractExpr(node.getLeft()), right: extractExpr(node.getRight()) };
55
+ }
56
+ // Prefix unary: !x, -x
57
+ if (Node.isPrefixUnaryExpression(node)) {
58
+ const opToken = node.getOperatorToken();
59
+ let op;
60
+ switch (opToken) {
61
+ case SyntaxKind.ExclamationToken:
62
+ op = "!";
63
+ break;
64
+ case SyntaxKind.MinusToken:
65
+ op = "-";
66
+ break;
67
+ case SyntaxKind.PlusToken:
68
+ op = "+";
69
+ break;
70
+ default: op = String(opToken);
71
+ }
72
+ return { kind: "unop", op, expr: extractExpr(node.getOperand()) };
73
+ }
74
+ // Parenthesized: (x)
75
+ if (Node.isParenthesizedExpression(node)) {
76
+ return extractExpr(node.getExpression());
77
+ }
78
+ // Arrow function: (x) => expr or (x) => { stmts }
79
+ if (Node.isArrowFunction(node)) {
80
+ const params = node.getParameters().map(p => {
81
+ const typeNode = p.getTypeNode();
82
+ return { name: p.getName(), tsType: typeNode ? typeNode.getText() : undefined };
83
+ });
84
+ const body = node.getBody();
85
+ if (Node.isExpression(body)) {
86
+ return { kind: "lambda", params, body: extractExpr(body) };
87
+ }
88
+ if (Node.isBlock(body)) {
89
+ return { kind: "lambda", params, body: extractStmts(body.getStatements()) };
90
+ }
91
+ throw new Error(`Unsupported arrow function body: ${node.getText().slice(0, 80)}`);
92
+ }
93
+ // Array literal: [a, b, c] → arrayLiteral, [...arr, elem] → push(arr, elem)
94
+ if (Node.isArrayLiteralExpression(node)) {
95
+ const elems = node.getElements();
96
+ // [...arr, elem] → push(arr, elem)
97
+ if (elems.length === 2 && Node.isSpreadElement(elems[0])) {
98
+ return { kind: "call", fn: { kind: "field", obj: extractExpr(elems[0].getExpression()), field: "push" }, args: [extractExpr(elems[1])] };
99
+ }
100
+ // [a, b, c] or [] → arrayLiteral
101
+ return { kind: "arrayLiteral", elems: elems.map(e => extractExpr(e)) };
102
+ }
103
+ // Object literal: { res: true, done: false } or { ...obj, res: true }
104
+ if (Node.isObjectLiteralExpression(node)) {
105
+ let spread = null;
106
+ const fields = [];
107
+ for (const prop of node.getProperties()) {
108
+ if (Node.isSpreadAssignment(prop)) {
109
+ spread = extractExpr(prop.getExpression());
110
+ }
111
+ else if (Node.isPropertyAssignment(prop)) {
112
+ const init = prop.getInitializer();
113
+ if (init)
114
+ fields.push({ name: prop.getName(), value: extractExpr(init) });
115
+ }
116
+ }
117
+ return { kind: "record", spread, fields };
118
+ }
119
+ // Ternary: cond ? then : else
120
+ if (Node.isConditionalExpression(node)) {
121
+ return { kind: "conditional", cond: extractExpr(node.getCondition()), then: extractExpr(node.getWhenTrue()), else: extractExpr(node.getWhenFalse()) };
122
+ }
123
+ throw new Error(`Unsupported expression: ${node.getText()}`);
124
+ }
125
+ // ── Annotation parsing ───────────────────────────────────────
126
+ const PREFIX = "//@ ";
127
+ const KEYWORDS = ["requires", "ensures", "invariant", "decreases", "done_with", "type"];
128
+ function parseAnnotations(node) {
129
+ const result = [];
130
+ for (const range of node.getLeadingCommentRanges()) {
131
+ const text = range.getText().trim();
132
+ if (!text.startsWith(PREFIX))
133
+ continue;
134
+ const content = text.slice(PREFIX.length);
135
+ const sp = content.indexOf(" ");
136
+ if (sp === -1)
137
+ continue;
138
+ const kw = content.slice(0, sp);
139
+ if (!KEYWORDS.includes(kw))
140
+ continue;
141
+ result.push({ kind: kw, expr: content.slice(sp + 1).trim() });
142
+ }
143
+ return result;
144
+ }
145
+ function collectAnnotations(node, body) {
146
+ const own = parseAnnotations(node);
147
+ if (body && body.length > 0)
148
+ return [...own, ...parseAnnotations(body[0])];
149
+ return own;
150
+ }
151
+ // ── Type declaration extraction ──────────────────────────────
152
+ function extractTypeDecl(decl) {
153
+ const name = decl.getName();
154
+ const type = decl.getType();
155
+ if (type.isUnion()) {
156
+ const members = type.getUnionTypes();
157
+ if (members.every(m => m.isStringLiteral())) {
158
+ return { name, kind: "string-union", values: members.map(m => m.getLiteralValue()) };
159
+ }
160
+ if (members.every(m => m.isObject())) {
161
+ const discriminant = findDiscriminant(members);
162
+ if (discriminant) {
163
+ const variants = members.map(m => {
164
+ const tagProp = m.getProperty(discriminant);
165
+ const tagType = tagProp?.getTypeAtLocation(decl);
166
+ const tag = tagType?.getLiteralValue();
167
+ const fields = [];
168
+ for (const prop of m.getProperties()) {
169
+ if (prop.getName() === discriminant)
170
+ continue;
171
+ fields.push({ name: prop.getName(), tsType: typeToString(prop.getTypeAtLocation(decl)) });
172
+ }
173
+ return { name: tag, fields };
174
+ });
175
+ return { name, kind: "discriminated-union", discriminant, variants };
176
+ }
177
+ }
178
+ }
179
+ if (type.isObject())
180
+ return extractRecord(name, type, decl);
181
+ return null;
182
+ }
183
+ function extractInterface(decl) {
184
+ // Collect field type overrides from trailing //@ type annotations
185
+ const overrides = new Map();
186
+ for (const member of decl.getMembers()) {
187
+ if (Node.isPropertySignature(member)) {
188
+ const text = member.getTrailingCommentRanges().map(r => r.getText()).join(" ");
189
+ const match = text.match(/\/\/@ type (\w+)/);
190
+ if (match)
191
+ overrides.set(member.getName(), match[1]);
192
+ }
193
+ }
194
+ return extractRecord(decl.getName(), decl.getType(), decl, overrides);
195
+ }
196
+ function extractRecord(name, type, locationNode, overrides) {
197
+ const props = type.getProperties();
198
+ if (props.length === 0)
199
+ return null;
200
+ const fields = [];
201
+ for (const prop of props) {
202
+ const override = overrides?.get(prop.getName());
203
+ const tsType = override ?? typeToString(prop.getTypeAtLocation(locationNode));
204
+ fields.push({ name: prop.getName(), tsType });
205
+ }
206
+ return { name, kind: "record", fields };
207
+ }
208
+ function findDiscriminant(members) {
209
+ if (members.length === 0)
210
+ return null;
211
+ const firstProps = members[0].getProperties();
212
+ for (const prop of firstProps) {
213
+ const name = prop.getName();
214
+ const allHave = members.every(m => {
215
+ const p = m.getProperty(name);
216
+ if (!p)
217
+ return false;
218
+ const t = p.getDeclarations()[0] ? p.getTypeAtLocation(p.getDeclarations()[0]) : null;
219
+ return t?.isStringLiteral() ?? false;
220
+ });
221
+ if (allHave)
222
+ return name;
223
+ }
224
+ return null;
225
+ }
226
+ function typeToString(type) {
227
+ if (type.isNumber())
228
+ return "number";
229
+ if (type.isString())
230
+ return "string";
231
+ if (type.isBoolean())
232
+ return "boolean";
233
+ if (type.isArray()) {
234
+ const elem = type.getArrayElementTypeOrThrow();
235
+ return `${typeToString(elem)}[]`;
236
+ }
237
+ const symbol = type.getSymbol() ?? type.getAliasSymbol();
238
+ if (symbol)
239
+ return symbol.getName();
240
+ return type.getText();
241
+ }
242
+ const COMPOUND_OPS = {
243
+ "+=": "+", "-=": "-", "*=": "*", "/=": "/", "%=": "%",
244
+ };
245
+ // ── Statement extraction ─────────────────────────────────────
246
+ function extractStmts(stmts) {
247
+ const result = [];
248
+ for (const s of stmts) {
249
+ const line = s.getStartLineNumber();
250
+ if (Node.isVariableStatement(s)) {
251
+ for (const d of s.getDeclarations()) {
252
+ const declType = d.getType();
253
+ const init = d.getInitializer();
254
+ result.push({
255
+ kind: "let",
256
+ name: d.getName(),
257
+ mutable: s.getDeclarationKind() === "let",
258
+ tsType: typeToString(declType),
259
+ init: init ? extractExpr(init) : { kind: "var", name: "default" },
260
+ line,
261
+ });
262
+ }
263
+ continue;
264
+ }
265
+ if (Node.isWhileStatement(s)) {
266
+ const bodyNode = s.getStatement();
267
+ const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [];
268
+ const annots = collectAnnotations(s, bodyStmts);
269
+ result.push({
270
+ kind: "while",
271
+ cond: extractExpr(s.getExpression()),
272
+ invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
273
+ decreases: annots.find(a => a.kind === "decreases")?.expr ?? null,
274
+ doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
275
+ body: extractStmts(bodyStmts),
276
+ line,
277
+ });
278
+ continue;
279
+ }
280
+ if (Node.isForOfStatement(s)) {
281
+ const init = s.getInitializer();
282
+ const varName = Node.isVariableDeclarationList(init) ? init.getDeclarations()[0]?.getName() ?? "_" : "_";
283
+ const bodyNode = s.getStatement();
284
+ const bodyStmts = Node.isBlock(bodyNode) ? bodyNode.getStatements() : [bodyNode];
285
+ const annots = collectAnnotations(s, bodyStmts);
286
+ result.push({
287
+ kind: "forof",
288
+ varName,
289
+ iterable: extractExpr(s.getExpression()),
290
+ invariants: annots.filter(a => a.kind === "invariant").map(a => a.expr),
291
+ doneWith: annots.find(a => a.kind === "done_with")?.expr ?? null,
292
+ body: extractStmts(bodyStmts),
293
+ line,
294
+ });
295
+ continue;
296
+ }
297
+ if (Node.isIfStatement(s)) {
298
+ const thenNode = s.getThenStatement();
299
+ const elseNode = s.getElseStatement();
300
+ result.push({
301
+ kind: "if",
302
+ cond: extractExpr(s.getExpression()),
303
+ then: Node.isBlock(thenNode) ? extractStmts(thenNode.getStatements()) : extractStmts([thenNode]),
304
+ else: elseNode
305
+ ? Node.isBlock(elseNode) ? extractStmts(elseNode.getStatements()) : extractStmts([elseNode])
306
+ : [],
307
+ line,
308
+ });
309
+ continue;
310
+ }
311
+ if (Node.isSwitchStatement(s)) {
312
+ const exprNode = s.getExpression();
313
+ const exprAst = extractExpr(exprNode);
314
+ const discriminant = exprAst.kind === "field" ? exprAst.field : "";
315
+ const switchExpr = exprAst.kind === "field" ? exprAst.obj : exprAst;
316
+ const cases = [];
317
+ let defaultBody = [];
318
+ for (const clause of s.getClauses()) {
319
+ if (Node.isCaseClause(clause)) {
320
+ const label = clause.getExpression().getText().replace(/^["']|["']$/g, "");
321
+ const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
322
+ cases.push({ label, body: extractStmts(bodyStmts) });
323
+ }
324
+ else {
325
+ const bodyStmts = clause.getStatements().filter(st => !Node.isBreakStatement(st));
326
+ defaultBody = extractStmts(bodyStmts);
327
+ }
328
+ }
329
+ result.push({ kind: "switch", expr: switchExpr, discriminant, cases, defaultBody, line });
330
+ continue;
331
+ }
332
+ if (Node.isReturnStatement(s)) {
333
+ const expr = s.getExpression();
334
+ result.push({ kind: "return", value: expr ? extractExpr(expr) : { kind: "var", name: "()" }, line });
335
+ continue;
336
+ }
337
+ if (Node.isBreakStatement(s)) {
338
+ result.push({ kind: "break", line });
339
+ continue;
340
+ }
341
+ if (Node.isContinueStatement(s)) {
342
+ result.push({ kind: "continue", line });
343
+ continue;
344
+ }
345
+ if (Node.isExpressionStatement(s)) {
346
+ const expr = s.getExpression();
347
+ // x = e
348
+ if (Node.isBinaryExpression(expr) && expr.getOperatorToken().getText() === "=") {
349
+ result.push({ kind: "assign", target: expr.getLeft().getText(), value: extractExpr(expr.getRight()), line });
350
+ // x += e, x -= e, etc.
351
+ }
352
+ else if (Node.isBinaryExpression(expr) && COMPOUND_OPS[expr.getOperatorToken().getText()]) {
353
+ const op = COMPOUND_OPS[expr.getOperatorToken().getText()];
354
+ const target = expr.getLeft().getText();
355
+ result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: extractExpr(expr.getRight()) }, line });
356
+ // i++, i--
357
+ }
358
+ else if (Node.isPostfixUnaryExpression(expr)) {
359
+ const target = expr.getOperand().getText();
360
+ const op = expr.getOperatorToken() === SyntaxKind.PlusPlusToken ? "+" : "-";
361
+ result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: { kind: "num", value: 1 } }, line });
362
+ // ++i, --i
363
+ }
364
+ else if (Node.isPrefixUnaryExpression(expr) && (expr.getOperatorToken() === SyntaxKind.PlusPlusToken || expr.getOperatorToken() === SyntaxKind.MinusMinusToken)) {
365
+ const target = expr.getOperand().getText();
366
+ const op = expr.getOperatorToken() === SyntaxKind.PlusPlusToken ? "+" : "-";
367
+ result.push({ kind: "assign", target, value: { kind: "binop", op, left: { kind: "var", name: target }, right: { kind: "num", value: 1 } }, line });
368
+ }
369
+ else {
370
+ result.push({ kind: "expr", expr: extractExpr(expr), line });
371
+ }
372
+ continue;
373
+ }
374
+ throw new Error(`Unsupported statement at line ${line}: ${s.getText().slice(0, 80)}`);
375
+ }
376
+ return result;
377
+ }
378
+ // ── Function extraction ──────────────────────────────────────
379
+ function extractFunction(fn) {
380
+ const body = fn.getBody();
381
+ if (!body || !Node.isBlock(body))
382
+ throw new Error(`${fn.getName()}: function body is not a block`);
383
+ const bodyStmts = body.getStatements();
384
+ const annots = collectAnnotations(fn, bodyStmts);
385
+ const typeAnnotations = [];
386
+ for (const a of annots) {
387
+ if (a.kind === "type") {
388
+ const parts = a.expr.split(/\s+/);
389
+ if (parts.length === 2)
390
+ typeAnnotations.push({ name: parts[0], type: parts[1] });
391
+ }
392
+ }
393
+ return {
394
+ name: fn.getName() ?? "<anonymous>",
395
+ params: fn.getParameters().map(p => ({ name: p.getName(), tsType: p.getTypeNode()?.getText() ?? "unknown" })),
396
+ returnType: fn.getReturnTypeNode()?.getText() ?? "unknown",
397
+ requires: annots.filter(a => a.kind === "requires").map(a => a.expr),
398
+ ensures: annots.filter(a => a.kind === "ensures").map(a => a.expr),
399
+ typeAnnotations,
400
+ body: extractStmts(bodyStmts),
401
+ line: fn.getStartLineNumber(),
402
+ };
403
+ }
404
+ // ── Module extraction ────────────────────────────────────────
405
+ export function extractModule(sourceFile) {
406
+ const typeDecls = [];
407
+ // Extract type declarations in source order to respect dependencies
408
+ for (const stmt of sourceFile.getStatements()) {
409
+ if (Node.isTypeAliasDeclaration(stmt)) {
410
+ const info = extractTypeDecl(stmt);
411
+ if (info)
412
+ typeDecls.push(info);
413
+ }
414
+ else if (Node.isInterfaceDeclaration(stmt)) {
415
+ const info = extractInterface(stmt);
416
+ if (info)
417
+ typeDecls.push(info);
418
+ }
419
+ }
420
+ return {
421
+ file: sourceFile.getFilePath(),
422
+ typeDecls,
423
+ functions: sourceFile.getFunctions().map(extractFunction),
424
+ };
425
+ }
426
+ // ── Main ─────────────────────────────────────────────────────
427
+ if (process.argv[1]?.match(/extract\.(ts|js)$/)) {
428
+ const file = process.argv[2];
429
+ if (!file) {
430
+ console.error("Usage: extract <file.ts>");
431
+ process.exit(1);
432
+ }
433
+ const proj = new Project({ compilerOptions: { strict: true } });
434
+ console.log(JSON.stringify(extractModule(proj.addSourceFileAtPath(file)), null, 2));
435
+ }