@ball-lang/encoder 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.
package/src/encoder.ts ADDED
@@ -0,0 +1,969 @@
1
+ import ts from "typescript";
2
+ import type {
3
+ Program, Module, FunctionDef, Expression, Statement,
4
+ FieldValuePair, TypeDefinition, TypeAlias, EnumDef,
5
+ DescriptorProto, Struct,
6
+ } from "./types.ts";
7
+
8
+ export interface EncodeOptions {
9
+ moduleName?: string;
10
+ entryFunction?: string;
11
+ strict?: boolean;
12
+ }
13
+
14
+ type StdRef = { module: string; function: string };
15
+
16
+ const BINARY_OPS: Record<number, StdRef> = {
17
+ [ts.SyntaxKind.PlusToken]: { module: "std", function: "add" },
18
+ [ts.SyntaxKind.MinusToken]: { module: "std", function: "subtract" },
19
+ [ts.SyntaxKind.AsteriskToken]: { module: "std", function: "multiply" },
20
+ [ts.SyntaxKind.SlashToken]: { module: "std", function: "divide_double" },
21
+ [ts.SyntaxKind.PercentToken]: { module: "std", function: "modulo" },
22
+ [ts.SyntaxKind.AmpersandToken]: { module: "std", function: "bitwise_and" },
23
+ [ts.SyntaxKind.BarToken]: { module: "std", function: "bitwise_or" },
24
+ [ts.SyntaxKind.CaretToken]: { module: "std", function: "bitwise_xor" },
25
+ [ts.SyntaxKind.LessThanLessThanToken]: { module: "std", function: "shift_left" },
26
+ [ts.SyntaxKind.GreaterThanGreaterThanToken]:{ module: "std", function: "shift_right" },
27
+ [ts.SyntaxKind.EqualsEqualsEqualsToken]: { module: "std", function: "equals" },
28
+ [ts.SyntaxKind.ExclamationEqualsEqualsToken]:{ module: "std", function: "not_equals" },
29
+ [ts.SyntaxKind.EqualsEqualsToken]: { module: "std", function: "equals" },
30
+ [ts.SyntaxKind.ExclamationEqualsToken]: { module: "std", function: "not_equals" },
31
+ [ts.SyntaxKind.LessThanToken]: { module: "std", function: "less_than" },
32
+ [ts.SyntaxKind.GreaterThanToken]: { module: "std", function: "greater_than" },
33
+ [ts.SyntaxKind.LessThanEqualsToken]: { module: "std", function: "less_than_or_equal" },
34
+ [ts.SyntaxKind.GreaterThanEqualsToken]: { module: "std", function: "greater_than_or_equal" },
35
+ [ts.SyntaxKind.AmpersandAmpersandToken]: { module: "std", function: "and" },
36
+ [ts.SyntaxKind.BarBarToken]: { module: "std", function: "or" },
37
+ [ts.SyntaxKind.QuestionQuestionToken]: { module: "std", function: "null_coalesce" },
38
+ [ts.SyntaxKind.InstanceOfKeyword]: { module: "std", function: "is" },
39
+ };
40
+
41
+ const COMPOUND_OPS: Record<number, string> = {
42
+ [ts.SyntaxKind.PlusEqualsToken]: "+=",
43
+ [ts.SyntaxKind.MinusEqualsToken]: "-=",
44
+ [ts.SyntaxKind.AsteriskEqualsToken]: "*=",
45
+ [ts.SyntaxKind.SlashEqualsToken]: "/=",
46
+ [ts.SyntaxKind.PercentEqualsToken]: "%=",
47
+ [ts.SyntaxKind.AmpersandEqualsToken]: "&=",
48
+ [ts.SyntaxKind.BarEqualsToken]: "|=",
49
+ [ts.SyntaxKind.CaretEqualsToken]: "^=",
50
+ [ts.SyntaxKind.LessThanLessThanEqualsToken]: "<<=",
51
+ [ts.SyntaxKind.GreaterThanGreaterThanEqualsToken]: ">>=",
52
+ [ts.SyntaxKind.QuestionQuestionEqualsToken]: "??=",
53
+ };
54
+
55
+ export class TsEncoder {
56
+ private stdFunctions = new Set<string>();
57
+ private warnings: string[] = [];
58
+
59
+ encode(source: string, options: EncodeOptions = {}): Program {
60
+ const modName = options.moduleName ?? "main";
61
+ const entryFn = options.entryFunction ?? "main";
62
+
63
+ const sourceFile = ts.createSourceFile(
64
+ "input.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS
65
+ );
66
+
67
+ const functions: FunctionDef[] = [];
68
+ const typeDefs: TypeDefinition[] = [];
69
+ const typeAliases: TypeAlias[] = [];
70
+ const enums: EnumDef[] = [];
71
+
72
+ for (const stmt of sourceFile.statements) {
73
+ if (ts.isFunctionDeclaration(stmt) && stmt.name) {
74
+ functions.push(this.encodeFunction(stmt));
75
+ } else if (ts.isClassDeclaration(stmt) && stmt.name) {
76
+ this.encodeClass(stmt, functions, typeDefs);
77
+ } else if (ts.isInterfaceDeclaration(stmt)) {
78
+ typeDefs.push(this.encodeInterface(stmt));
79
+ } else if (ts.isTypeAliasDeclaration(stmt)) {
80
+ typeAliases.push(this.encodeTypeAlias(stmt));
81
+ } else if (ts.isEnumDeclaration(stmt)) {
82
+ enums.push(this.encodeEnum(stmt));
83
+ } else if (ts.isVariableStatement(stmt)) {
84
+ for (const decl of stmt.declarationList.declarations) {
85
+ if (ts.isIdentifier(decl.name)) {
86
+ functions.push({
87
+ name: decl.name.text,
88
+ body: decl.initializer ? this.encodeExpr(decl.initializer) : { literal: { stringValue: "" } },
89
+ metadata: { kind: "top_level_variable" },
90
+ });
91
+ } else if (ts.isObjectBindingPattern(decl.name) && decl.initializer) {
92
+ for (const element of decl.name.elements) {
93
+ const propName = element.propertyName
94
+ ? (ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.propertyName.getText())
95
+ : (ts.isIdentifier(element.name) ? element.name.text : element.name.getText());
96
+ const varName = ts.isIdentifier(element.name) ? element.name.text : element.name.getText();
97
+ functions.push({
98
+ name: varName,
99
+ body: { fieldAccess: { object: this.encodeExpr(decl.initializer!), field: propName } },
100
+ metadata: { kind: "top_level_variable", destructured: true },
101
+ });
102
+ }
103
+ } else if (ts.isArrayBindingPattern(decl.name) && decl.initializer) {
104
+ for (let i = 0; i < decl.name.elements.length; i++) {
105
+ const element = decl.name.elements[i];
106
+ if (ts.isOmittedExpression(element)) continue;
107
+ const varName = ts.isIdentifier(element.name) ? element.name.text : element.name.getText();
108
+ functions.push({
109
+ name: varName,
110
+ body: this.stdCall("index", [
111
+ { name: "target", value: this.encodeExpr(decl.initializer!) },
112
+ { name: "index", value: { literal: { intValue: `${i}` } } },
113
+ ]),
114
+ metadata: { kind: "top_level_variable", destructured: true },
115
+ });
116
+ }
117
+ }
118
+ }
119
+ } else if (ts.isExpressionStatement(stmt)) {
120
+ functions.push({
121
+ name: `__top_${functions.length}`,
122
+ body: this.encodeExpr(stmt.expression),
123
+ });
124
+ }
125
+ }
126
+
127
+ const stdModule = this.buildStdModule();
128
+ const modules: Module[] = [stdModule];
129
+
130
+ const userModule: Module = {
131
+ name: modName,
132
+ functions,
133
+ ...(typeDefs.length > 0 ? { typeDefs } : {}),
134
+ ...(typeAliases.length > 0 ? { typeAliases } : {}),
135
+ ...(enums.length > 0 ? { enums } : {}),
136
+ };
137
+ modules.push(userModule);
138
+
139
+ return {
140
+ name: modName,
141
+ version: "1.0.0",
142
+ modules,
143
+ entryModule: modName,
144
+ entryFunction: entryFn,
145
+ };
146
+ }
147
+
148
+ private encodeFunction(node: ts.FunctionDeclaration | ts.FunctionExpression | ts.MethodDeclaration): FunctionDef {
149
+ const name = node.name ? (ts.isIdentifier(node.name) ? node.name.text : node.name.getText()) : "";
150
+ const params = node.parameters.map(p =>
151
+ ts.isIdentifier(p.name) ? p.name.text : p.name.getText()
152
+ );
153
+
154
+ const metadata: Struct = {};
155
+ if (params.length > 0) metadata["params"] = params;
156
+ if (node.type) metadata["returnType"] = node.type.getText();
157
+
158
+ // Async functions
159
+ if (node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
160
+ metadata["is_async"] = true;
161
+ }
162
+
163
+ // Rest parameters
164
+ const lastParam = node.parameters[node.parameters.length - 1];
165
+ if (lastParam?.dotDotDotToken) {
166
+ metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
167
+ }
168
+
169
+ // Default parameter values
170
+ const defaults: Record<string, unknown> = {};
171
+ for (const p of node.parameters) {
172
+ if (p.initializer && ts.isIdentifier(p.name)) {
173
+ defaults[p.name.text] = p.initializer.getText();
174
+ }
175
+ }
176
+ if (Object.keys(defaults).length > 0) metadata["param_defaults"] = defaults;
177
+
178
+ const body = node.body ? this.encodeBody(node.body) : undefined;
179
+
180
+ const fn: FunctionDef = { name };
181
+ if (body) fn.body = body;
182
+ if (Object.keys(metadata).length > 0) fn.metadata = metadata;
183
+ if (node.type) fn.outputType = node.type.getText();
184
+
185
+ return fn;
186
+ }
187
+
188
+ private encodeBody(body: ts.Block | ts.Expression | ts.ConciseBody): Expression {
189
+ if (ts.isBlock(body)) {
190
+ return this.encodeBlock(body);
191
+ }
192
+ return this.encodeExpr(body as ts.Expression);
193
+ }
194
+
195
+ private encodeBlock(block: ts.Block): Expression {
196
+ const stmts: Statement[] = [];
197
+ for (const s of block.statements) {
198
+ stmts.push(...this.encodeStatement(s));
199
+ }
200
+ return { block: { statements: stmts } };
201
+ }
202
+
203
+ private encodeStatement(node: ts.Statement): Statement[] {
204
+ if (ts.isVariableStatement(node)) {
205
+ const results: Statement[] = [];
206
+ for (const decl of node.declarationList.declarations) {
207
+ if (ts.isObjectBindingPattern(decl.name) && decl.initializer) {
208
+ // Object destructuring: const { a, b } = obj
209
+ const source = this.encodeExpr(decl.initializer);
210
+ for (const element of decl.name.elements) {
211
+ const propName = element.propertyName
212
+ ? (ts.isIdentifier(element.propertyName) ? element.propertyName.text : element.propertyName.getText())
213
+ : (ts.isIdentifier(element.name) ? element.name.text : element.name.getText());
214
+ const varName = ts.isIdentifier(element.name) ? element.name.text : element.name.getText();
215
+ let value: Expression = { fieldAccess: { object: source, field: propName } };
216
+ if (element.initializer) {
217
+ value = this.stdCall("null_coalesce", [
218
+ { name: "left", value },
219
+ { name: "right", value: this.encodeExpr(element.initializer) },
220
+ ]);
221
+ }
222
+ results.push({ let: { name: varName, value } });
223
+ }
224
+ } else if (ts.isArrayBindingPattern(decl.name) && decl.initializer) {
225
+ // Array destructuring: const [x, y] = arr
226
+ const source = this.encodeExpr(decl.initializer);
227
+ for (let i = 0; i < decl.name.elements.length; i++) {
228
+ const element = decl.name.elements[i];
229
+ if (ts.isOmittedExpression(element)) continue;
230
+ const varName = ts.isIdentifier(element.name) ? element.name.text : element.name.getText();
231
+ let value: Expression = this.stdCall("index", [
232
+ { name: "target", value: source },
233
+ { name: "index", value: { literal: { intValue: `${i}` } } },
234
+ ]);
235
+ if (element.initializer) {
236
+ value = this.stdCall("null_coalesce", [
237
+ { name: "left", value },
238
+ { name: "right", value: this.encodeExpr(element.initializer) },
239
+ ]);
240
+ }
241
+ results.push({ let: { name: varName, value } });
242
+ }
243
+ } else {
244
+ results.push({
245
+ let: {
246
+ name: ts.isIdentifier(decl.name) ? decl.name.text : decl.name.getText(),
247
+ value: decl.initializer ? this.encodeExpr(decl.initializer) : undefined,
248
+ },
249
+ });
250
+ }
251
+ }
252
+ return results;
253
+ }
254
+ if (ts.isExpressionStatement(node)) {
255
+ return [{ expression: this.encodeExpr(node.expression) }];
256
+ }
257
+ if (ts.isReturnStatement(node)) {
258
+ return [{ expression: this.stdCall("return", node.expression
259
+ ? [{ name: "value", value: this.encodeExpr(node.expression) }]
260
+ : []) }];
261
+ }
262
+ if (ts.isIfStatement(node)) {
263
+ return [{ expression: this.encodeIf(node) }];
264
+ }
265
+ if (ts.isForStatement(node)) {
266
+ return [{ expression: this.encodeFor(node) }];
267
+ }
268
+ if (ts.isForOfStatement(node) || ts.isForInStatement(node)) {
269
+ return [{ expression: this.encodeForOf(node) }];
270
+ }
271
+ if (ts.isWhileStatement(node)) {
272
+ return [{ expression: this.encodeWhile(node) }];
273
+ }
274
+ if (ts.isDoStatement(node)) {
275
+ return [{ expression: this.encodeDoWhile(node) }];
276
+ }
277
+ if (ts.isTryStatement(node)) {
278
+ return [{ expression: this.encodeTry(node) }];
279
+ }
280
+ if (ts.isThrowStatement(node)) {
281
+ return [{ expression: this.stdCall("throw", [
282
+ { name: "value", value: this.encodeExpr(node.expression) },
283
+ ]) }];
284
+ }
285
+ if (ts.isBreakStatement(node)) {
286
+ const fields: FieldValuePair[] = [];
287
+ if (node.label) fields.push({ name: "label", value: { literal: { stringValue: node.label.text } } });
288
+ return [{ expression: this.stdCall("break", fields) }];
289
+ }
290
+ if (ts.isContinueStatement(node)) {
291
+ const fields: FieldValuePair[] = [];
292
+ if (node.label) fields.push({ name: "label", value: { literal: { stringValue: node.label.text } } });
293
+ return [{ expression: this.stdCall("continue", fields) }];
294
+ }
295
+ if (ts.isLabeledStatement(node)) {
296
+ return [{ expression: this.stdCall("labeled", [
297
+ { name: "label", value: { literal: { stringValue: node.label.text } } },
298
+ { name: "body", value: this.encodeBody(
299
+ ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
300
+ ) },
301
+ ]) }];
302
+ }
303
+ if (ts.isSwitchStatement(node)) {
304
+ return [{ expression: this.encodeSwitch(node) }];
305
+ }
306
+ if (ts.isBlock(node)) {
307
+ return [{ expression: this.encodeBlock(node) }];
308
+ }
309
+ this.warn(`Unhandled statement kind: ${ts.SyntaxKind[node.kind]}`);
310
+ return [{ expression: { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } } }];
311
+ }
312
+
313
+ private encodeExpr(node: ts.Expression): Expression {
314
+ if (ts.isNumericLiteral(node)) {
315
+ const text = node.text;
316
+ if (text.includes(".") || text.includes("e") || text.includes("E")) {
317
+ return { literal: { doubleValue: parseFloat(text) } };
318
+ }
319
+ return { literal: { intValue: text } };
320
+ }
321
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
322
+ return { literal: { stringValue: node.text } };
323
+ }
324
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
325
+ return { literal: { boolValue: true } };
326
+ }
327
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
328
+ return { literal: { boolValue: false } };
329
+ }
330
+ if (node.kind === ts.SyntaxKind.NullKeyword || node.kind === ts.SyntaxKind.UndefinedKeyword) {
331
+ return { literal: { stringValue: "" } };
332
+ }
333
+ if (ts.isIdentifier(node)) {
334
+ return { reference: { name: node.text } };
335
+ }
336
+ if (ts.isBinaryExpression(node)) {
337
+ return this.encodeBinary(node);
338
+ }
339
+ if (ts.isPrefixUnaryExpression(node)) {
340
+ return this.encodePrefixUnary(node);
341
+ }
342
+ if (ts.isPostfixUnaryExpression(node)) {
343
+ return this.encodePostfixUnary(node);
344
+ }
345
+ if (ts.isCallExpression(node)) {
346
+ return this.encodeCall(node);
347
+ }
348
+ if (ts.isPropertyAccessExpression(node)) {
349
+ if (node.questionDotToken) {
350
+ return this.stdCall("optional_access", [
351
+ { name: "object", value: this.encodeExpr(node.expression) },
352
+ { name: "field", value: { literal: { stringValue: node.name.text } } },
353
+ ], "ts_std");
354
+ }
355
+ return { fieldAccess: { object: this.encodeExpr(node.expression), field: node.name.text } };
356
+ }
357
+ if (ts.isElementAccessExpression(node)) {
358
+ if (node.questionDotToken) {
359
+ return this.stdCall("optional_access", [
360
+ { name: "object", value: this.encodeExpr(node.expression) },
361
+ { name: "field", value: this.encodeExpr(node.argumentExpression) },
362
+ ], "ts_std");
363
+ }
364
+ return this.stdCall("index", [
365
+ { name: "target", value: this.encodeExpr(node.expression) },
366
+ { name: "index", value: this.encodeExpr(node.argumentExpression) },
367
+ ]);
368
+ }
369
+ if (ts.isParenthesizedExpression(node)) {
370
+ return this.encodeExpr(node.expression);
371
+ }
372
+ if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
373
+ return this.encodeLambda(node);
374
+ }
375
+ if (ts.isArrayLiteralExpression(node)) {
376
+ return {
377
+ literal: {
378
+ listValue: { elements: node.elements.map(e => this.encodeExpr(e)) },
379
+ },
380
+ };
381
+ }
382
+ if (ts.isObjectLiteralExpression(node)) {
383
+ const fields: FieldValuePair[] = [];
384
+ for (const prop of node.properties) {
385
+ if (ts.isPropertyAssignment(prop)) {
386
+ if (ts.isComputedPropertyName(prop.name)) {
387
+ // Computed property: { [key]: value }
388
+ fields.push({
389
+ name: "__computed",
390
+ value: this.stdCall("computed_property", [
391
+ { name: "key", value: this.encodeExpr(prop.name.expression) },
392
+ { name: "value", value: this.encodeExpr(prop.initializer) },
393
+ ], "ts_std"),
394
+ });
395
+ } else {
396
+ const propName = ts.isIdentifier(prop.name)
397
+ ? prop.name.text
398
+ : ts.isStringLiteral(prop.name) ? prop.name.text : prop.name.getText();
399
+ fields.push({ name: propName, value: this.encodeExpr(prop.initializer) });
400
+ }
401
+ } else if (ts.isShorthandPropertyAssignment(prop)) {
402
+ fields.push({ name: prop.name.text, value: { reference: { name: prop.name.text } } });
403
+ } else if (ts.isSpreadAssignment(prop)) {
404
+ fields.push({
405
+ name: "__spread",
406
+ value: this.stdCall("spread", [
407
+ { name: "value", value: this.encodeExpr(prop.expression) },
408
+ ], "ts_std"),
409
+ });
410
+ }
411
+ }
412
+ return { messageCreation: { typeName: "", fields } };
413
+ }
414
+ if (ts.isConditionalExpression(node)) {
415
+ return this.stdCall("if", [
416
+ { name: "condition", value: this.encodeExpr(node.condition) },
417
+ { name: "then", value: { lambda: { name: "", body: this.encodeExpr(node.whenTrue) } } },
418
+ { name: "else", value: { lambda: { name: "", body: this.encodeExpr(node.whenFalse) } } },
419
+ ]);
420
+ }
421
+ if (ts.isTemplateExpression(node)) {
422
+ return this.encodeTemplate(node);
423
+ }
424
+ if (ts.isNewExpression(node)) {
425
+ return this.encodeNew(node);
426
+ }
427
+ if (ts.isTypeOfExpression(node)) {
428
+ return this.stdCall("type_of", [
429
+ { name: "value", value: this.encodeExpr(node.expression) },
430
+ ]);
431
+ }
432
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
433
+ return this.encodeExpr(node.expression);
434
+ }
435
+ if (ts.isNonNullExpression(node)) {
436
+ return this.stdCall("null_check", [
437
+ { name: "value", value: this.encodeExpr(node.expression) },
438
+ ]);
439
+ }
440
+ if (ts.isAwaitExpression(node)) {
441
+ return this.stdCall("await", [
442
+ { name: "value", value: this.encodeExpr(node.expression) },
443
+ ]);
444
+ }
445
+ if (ts.isSpreadElement(node)) {
446
+ return this.stdCall("spread", [
447
+ { name: "value", value: this.encodeExpr(node.expression) },
448
+ ], "ts_std");
449
+ }
450
+ if (ts.isVoidExpression(node)) {
451
+ return { literal: { stringValue: "" } };
452
+ }
453
+ if (ts.isTaggedTemplateExpression(node)) {
454
+ return this.encodeTaggedTemplate(node);
455
+ }
456
+ this.warn(`Unhandled expression kind: ${ts.SyntaxKind[node.kind]}`);
457
+ return { literal: { stringValue: `/* unhandled: ${ts.SyntaxKind[node.kind]} */` } };
458
+ }
459
+
460
+ private encodeBinary(node: ts.BinaryExpression): Expression {
461
+ const op = node.operatorToken.kind;
462
+
463
+ if (op === ts.SyntaxKind.EqualsToken) {
464
+ return this.stdCall("assign", [
465
+ { name: "target", value: this.encodeExpr(node.left) },
466
+ { name: "value", value: this.encodeExpr(node.right) },
467
+ ]);
468
+ }
469
+
470
+ const compound = COMPOUND_OPS[op];
471
+ if (compound) {
472
+ return this.stdCall("assign", [
473
+ { name: "target", value: this.encodeExpr(node.left) },
474
+ { name: "value", value: this.encodeExpr(node.right) },
475
+ { name: "op", value: { literal: { stringValue: compound } } },
476
+ ]);
477
+ }
478
+
479
+ if (op === ts.SyntaxKind.PlusToken && this.looksLikeStringConcat(node)) {
480
+ return this.stdCall("concat", [
481
+ { name: "left", value: this.encodeExpr(node.left) },
482
+ { name: "right", value: this.encodeExpr(node.right) },
483
+ ]);
484
+ }
485
+
486
+ const stdRef = BINARY_OPS[op];
487
+ if (stdRef) {
488
+ return this.stdCall(stdRef.function, [
489
+ { name: "left", value: this.encodeExpr(node.left) },
490
+ { name: "right", value: this.encodeExpr(node.right) },
491
+ ], stdRef.module);
492
+ }
493
+
494
+ if (op === ts.SyntaxKind.InKeyword) {
495
+ return this.stdCall("contains_key", [
496
+ { name: "map", value: this.encodeExpr(node.right) },
497
+ { name: "key", value: this.encodeExpr(node.left) },
498
+ ], "std_collections");
499
+ }
500
+
501
+ this.warn(`Unhandled binary operator: ${ts.SyntaxKind[op]}`);
502
+ return { literal: { stringValue: `/* binary: ${ts.SyntaxKind[op]} */` } };
503
+ }
504
+
505
+ private looksLikeStringConcat(node: ts.BinaryExpression): boolean {
506
+ return ts.isStringLiteral(node.left) || ts.isStringLiteral(node.right) ||
507
+ ts.isTemplateExpression(node.left) || ts.isNoSubstitutionTemplateLiteral(node.left);
508
+ }
509
+
510
+ private encodePrefixUnary(node: ts.PrefixUnaryExpression): Expression {
511
+ const op = node.operator;
512
+ if (op === ts.SyntaxKind.MinusToken) {
513
+ return this.stdCall("negate", [
514
+ { name: "value", value: this.encodeExpr(node.operand) },
515
+ ]);
516
+ }
517
+ if (op === ts.SyntaxKind.ExclamationToken) {
518
+ return this.stdCall("not", [
519
+ { name: "value", value: this.encodeExpr(node.operand) },
520
+ ]);
521
+ }
522
+ if (op === ts.SyntaxKind.TildeToken) {
523
+ return this.stdCall("bitwise_not", [
524
+ { name: "value", value: this.encodeExpr(node.operand) },
525
+ ]);
526
+ }
527
+ if (op === ts.SyntaxKind.PlusPlusToken) {
528
+ return this.stdCall("pre_increment", [
529
+ { name: "value", value: this.encodeExpr(node.operand) },
530
+ ]);
531
+ }
532
+ if (op === ts.SyntaxKind.MinusMinusToken) {
533
+ return this.stdCall("pre_decrement", [
534
+ { name: "value", value: this.encodeExpr(node.operand) },
535
+ ]);
536
+ }
537
+ this.warn(`Unhandled prefix operator: ${ts.SyntaxKind[op]}`);
538
+ return this.encodeExpr(node.operand);
539
+ }
540
+
541
+ private encodePostfixUnary(node: ts.PostfixUnaryExpression): Expression {
542
+ if (node.operator === ts.SyntaxKind.PlusPlusToken) {
543
+ return this.stdCall("post_increment", [
544
+ { name: "value", value: this.encodeExpr(node.operand) },
545
+ ]);
546
+ }
547
+ return this.stdCall("post_decrement", [
548
+ { name: "value", value: this.encodeExpr(node.operand) },
549
+ ]);
550
+ }
551
+
552
+ private encodeCall(node: ts.CallExpression): Expression {
553
+ const args = node.arguments.map((a, i) => ({
554
+ name: `arg${i}`,
555
+ value: this.encodeExpr(a),
556
+ }));
557
+
558
+ if (ts.isPropertyAccessExpression(node.expression)) {
559
+ const obj = this.encodeExpr(node.expression.expression);
560
+ const method = node.expression.name.text;
561
+
562
+ // Optional chaining call: obj?.method()
563
+ if (node.expression.questionDotToken || node.questionDotToken) {
564
+ return this.stdCall("optional_call", [
565
+ { name: "object", value: obj },
566
+ { name: "method", value: { literal: { stringValue: method } } },
567
+ ...args,
568
+ ], "ts_std");
569
+ }
570
+
571
+ return {
572
+ call: {
573
+ function: method,
574
+ input: {
575
+ messageCreation: {
576
+ typeName: "",
577
+ fields: [{ name: "self", value: obj }, ...args],
578
+ },
579
+ },
580
+ },
581
+ };
582
+ }
583
+
584
+ if (ts.isIdentifier(node.expression)) {
585
+ const fnName = node.expression.text;
586
+ return {
587
+ call: {
588
+ module: "",
589
+ function: fnName,
590
+ input: args.length > 0 ? {
591
+ messageCreation: { typeName: "", fields: args },
592
+ } : undefined,
593
+ },
594
+ };
595
+ }
596
+
597
+ return {
598
+ call: {
599
+ function: "__invoke",
600
+ input: {
601
+ messageCreation: {
602
+ typeName: "",
603
+ fields: [
604
+ { name: "callee", value: this.encodeExpr(node.expression) },
605
+ ...args,
606
+ ],
607
+ },
608
+ },
609
+ },
610
+ };
611
+ }
612
+
613
+ private encodeLambda(node: ts.ArrowFunction | ts.FunctionExpression): Expression {
614
+ const params = node.parameters.map(p =>
615
+ ts.isIdentifier(p.name) ? p.name.text : p.name.getText()
616
+ );
617
+ const body = node.body ? this.encodeBody(
618
+ ts.isBlock(node.body) ? node.body : node.body
619
+ ) : undefined;
620
+
621
+ const metadata: Struct = {};
622
+ if (params.length > 0) metadata["params"] = params;
623
+ if (node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
624
+ metadata["is_async"] = true;
625
+ }
626
+
627
+ // Rest parameters
628
+ const lastParam = node.parameters[node.parameters.length - 1];
629
+ if (lastParam?.dotDotDotToken) {
630
+ metadata["rest_param"] = ts.isIdentifier(lastParam.name) ? lastParam.name.text : lastParam.name.getText();
631
+ }
632
+
633
+ // Default parameter values
634
+ const defaults: Record<string, unknown> = {};
635
+ for (const p of node.parameters) {
636
+ if (p.initializer && ts.isIdentifier(p.name)) {
637
+ defaults[p.name.text] = p.initializer.getText();
638
+ }
639
+ }
640
+ if (Object.keys(defaults).length > 0) metadata["param_defaults"] = defaults;
641
+
642
+ // Destructured parameters
643
+ const destructured: Record<string, string> = {};
644
+ for (const p of node.parameters) {
645
+ if (ts.isObjectBindingPattern(p.name) || ts.isArrayBindingPattern(p.name)) {
646
+ const idx = node.parameters.indexOf(p);
647
+ destructured[`param${idx}`] = p.name.getText();
648
+ }
649
+ }
650
+ if (Object.keys(destructured).length > 0) metadata["destructured_params"] = destructured;
651
+
652
+ return {
653
+ lambda: {
654
+ name: "",
655
+ body,
656
+ ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
657
+ },
658
+ };
659
+ }
660
+
661
+ private encodeIf(node: ts.IfStatement): Expression {
662
+ const fields: FieldValuePair[] = [
663
+ { name: "condition", value: this.encodeExpr(node.expression) },
664
+ { name: "then", value: { lambda: { name: "", body: this.encodeBody(
665
+ ts.isBlock(node.thenStatement) ? node.thenStatement : ts.factory.createBlock([node.thenStatement])
666
+ ) } } },
667
+ ];
668
+ if (node.elseStatement) {
669
+ if (ts.isIfStatement(node.elseStatement)) {
670
+ fields.push({ name: "else", value: { lambda: { name: "", body: this.encodeIf(node.elseStatement) } } });
671
+ } else {
672
+ fields.push({ name: "else", value: { lambda: { name: "", body: this.encodeBody(
673
+ ts.isBlock(node.elseStatement) ? node.elseStatement : ts.factory.createBlock([node.elseStatement])
674
+ ) } } });
675
+ }
676
+ }
677
+ return this.stdCall("if", fields);
678
+ }
679
+
680
+ private encodeFor(node: ts.ForStatement): Expression {
681
+ const fields: FieldValuePair[] = [];
682
+ if (node.initializer) {
683
+ if (ts.isVariableDeclarationList(node.initializer)) {
684
+ const decls = node.initializer.declarations;
685
+ if (decls.length > 0) {
686
+ const d = decls[0];
687
+ fields.push({ name: "variable", value: { literal: { stringValue: ts.isIdentifier(d.name) ? d.name.text : d.name.getText() } } });
688
+ if (d.initializer) fields.push({ name: "start", value: this.encodeExpr(d.initializer) });
689
+ }
690
+ } else {
691
+ fields.push({ name: "init", value: { lambda: { name: "", body: this.encodeExpr(node.initializer) } } });
692
+ }
693
+ }
694
+ if (node.condition) {
695
+ fields.push({ name: "condition", value: { lambda: { name: "", body: this.encodeExpr(node.condition) } } });
696
+ }
697
+ if (node.incrementor) {
698
+ fields.push({ name: "update", value: { lambda: { name: "", body: this.encodeExpr(node.incrementor) } } });
699
+ }
700
+ fields.push({ name: "body", value: { lambda: { name: "", body: this.encodeBody(
701
+ ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
702
+ ) } } });
703
+ return this.stdCall("for", fields);
704
+ }
705
+
706
+ private encodeForOf(node: ts.ForOfStatement | ts.ForInStatement): Expression {
707
+ let varName = "";
708
+ if (ts.isVariableDeclarationList(node.initializer)) {
709
+ const d = node.initializer.declarations[0];
710
+ varName = ts.isIdentifier(d.name) ? d.name.text : d.name.getText();
711
+ }
712
+ const fnName = ts.isForInStatement(node) ? "for_in" : "for_each";
713
+ return this.stdCall(fnName, [
714
+ { name: "variable", value: { literal: { stringValue: varName } } },
715
+ { name: "iterable", value: this.encodeExpr(node.expression) },
716
+ { name: "body", value: { lambda: { name: "", body: this.encodeBody(
717
+ ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
718
+ ) } } },
719
+ ]);
720
+ }
721
+
722
+ private encodeWhile(node: ts.WhileStatement): Expression {
723
+ return this.stdCall("while", [
724
+ { name: "condition", value: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
725
+ { name: "body", value: { lambda: { name: "", body: this.encodeBody(
726
+ ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
727
+ ) } } },
728
+ ]);
729
+ }
730
+
731
+ private encodeDoWhile(node: ts.DoStatement): Expression {
732
+ return this.stdCall("do_while", [
733
+ { name: "condition", value: { lambda: { name: "", body: this.encodeExpr(node.expression) } } },
734
+ { name: "body", value: { lambda: { name: "", body: this.encodeBody(
735
+ ts.isBlock(node.statement) ? node.statement : ts.factory.createBlock([node.statement])
736
+ ) } } },
737
+ ]);
738
+ }
739
+
740
+ private encodeTry(node: ts.TryStatement): Expression {
741
+ const fields: FieldValuePair[] = [
742
+ { name: "body", value: { lambda: { name: "", body: this.encodeBlock(node.tryBlock) } } },
743
+ ];
744
+ if (node.catchClause) {
745
+ const cc = node.catchClause;
746
+ const catchFields: FieldValuePair[] = [];
747
+ if (cc.variableDeclaration && ts.isIdentifier(cc.variableDeclaration.name)) {
748
+ catchFields.push({ name: "variable", value: { literal: { stringValue: cc.variableDeclaration.name.text } } });
749
+ }
750
+ catchFields.push({ name: "body", value: { lambda: { name: "", body: this.encodeBlock(cc.block) } } });
751
+ fields.push({
752
+ name: "catch",
753
+ value: { messageCreation: { typeName: "", fields: catchFields } },
754
+ });
755
+ }
756
+ if (node.finallyBlock) {
757
+ fields.push({ name: "finally", value: { lambda: { name: "", body: this.encodeBlock(node.finallyBlock) } } });
758
+ }
759
+ return this.stdCall("try", fields);
760
+ }
761
+
762
+ private encodeSwitch(node: ts.SwitchStatement): Expression {
763
+ const cases: Expression[] = [];
764
+ for (const clause of node.caseBlock.clauses) {
765
+ const caseFields: FieldValuePair[] = [];
766
+ if (ts.isCaseClause(clause)) {
767
+ caseFields.push({ name: "value", value: this.encodeExpr(clause.expression) });
768
+ } else {
769
+ caseFields.push({ name: "isDefault", value: { literal: { boolValue: true } } });
770
+ }
771
+ const stmts: Statement[] = [];
772
+ for (const s of clause.statements) {
773
+ stmts.push(...this.encodeStatement(s));
774
+ }
775
+ caseFields.push({ name: "body", value: { lambda: { name: "", body: { block: { statements: stmts } } } } });
776
+ cases.push({ messageCreation: { typeName: "", fields: caseFields } });
777
+ }
778
+ return this.stdCall("switch", [
779
+ { name: "value", value: this.encodeExpr(node.expression) },
780
+ { name: "cases", value: { literal: { listValue: { elements: cases } } } },
781
+ ]);
782
+ }
783
+
784
+ private encodeTemplate(node: ts.TemplateExpression): Expression {
785
+ let result: Expression = { literal: { stringValue: node.head.text } };
786
+ for (const span of node.templateSpans) {
787
+ const part = this.stdCall("to_string", [
788
+ { name: "value", value: this.encodeExpr(span.expression) },
789
+ ]);
790
+ result = this.stdCall("concat", [
791
+ { name: "left", value: result },
792
+ { name: "right", value: part },
793
+ ]);
794
+ if (span.literal.text) {
795
+ result = this.stdCall("concat", [
796
+ { name: "left", value: result },
797
+ { name: "right", value: { literal: { stringValue: span.literal.text } } },
798
+ ]);
799
+ }
800
+ }
801
+ return result;
802
+ }
803
+
804
+ private encodeNew(node: ts.NewExpression): Expression {
805
+ const typeName = node.expression.getText();
806
+ const args = (node.arguments ?? []).map((a, i) => ({
807
+ name: `arg${i}`,
808
+ value: this.encodeExpr(a),
809
+ }));
810
+ return {
811
+ messageCreation: {
812
+ typeName,
813
+ fields: args,
814
+ },
815
+ };
816
+ }
817
+
818
+ private encodeClass(
819
+ node: ts.ClassDeclaration,
820
+ functions: FunctionDef[],
821
+ typeDefs: TypeDefinition[],
822
+ ): void {
823
+ const className = node.name!.text;
824
+
825
+ const descriptor: DescriptorProto = { name: className, field: [] };
826
+ const metadata: Struct = { kind: "class" };
827
+
828
+ if (node.heritageClauses) {
829
+ for (const hc of node.heritageClauses) {
830
+ if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types.length > 0) {
831
+ metadata["superclass"] = hc.types[0].expression.getText();
832
+ }
833
+ if (hc.token === ts.SyntaxKind.ImplementsKeyword) {
834
+ metadata["interfaces"] = hc.types.map(t => t.expression.getText());
835
+ }
836
+ }
837
+ }
838
+
839
+ const fieldInitializers: Record<string, string> = {};
840
+ let fieldNum = 1;
841
+ for (const member of node.members) {
842
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name)) {
843
+ const fieldMeta: Record<string, unknown> = {};
844
+ if (member.initializer) {
845
+ fieldInitializers[member.name.text] = member.initializer.getText();
846
+ fieldMeta["initializer"] = member.initializer.getText();
847
+ }
848
+ const isStatic = member.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword);
849
+ if (isStatic) fieldMeta["is_static"] = true;
850
+ descriptor.field!.push({
851
+ name: member.name.text,
852
+ number: fieldNum++,
853
+ type: member.type ? member.type.getText() : "any",
854
+ ...(Object.keys(fieldMeta).length > 0 ? { label: JSON.stringify(fieldMeta) } : {}),
855
+ });
856
+ }
857
+ if (ts.isMethodDeclaration(member) && ts.isIdentifier(member.name)) {
858
+ const fn = this.encodeFunction(member);
859
+ fn.name = `${className}.${fn.name}`;
860
+ const isStatic = member.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword);
861
+ if (isStatic) {
862
+ if (!fn.metadata) fn.metadata = {};
863
+ fn.metadata["is_static"] = true;
864
+ }
865
+ functions.push(fn);
866
+ }
867
+ if (ts.isConstructorDeclaration(member)) {
868
+ const fn = this.encodeFunction(member as any);
869
+ fn.name = `${className}.constructor`;
870
+ functions.push(fn);
871
+ }
872
+ }
873
+ if (Object.keys(fieldInitializers).length > 0) {
874
+ metadata["field_initializers"] = fieldInitializers;
875
+ }
876
+
877
+ typeDefs.push({ name: className, descriptor, metadata });
878
+ }
879
+
880
+ private encodeInterface(node: ts.InterfaceDeclaration): TypeDefinition {
881
+ const descriptor: DescriptorProto = { name: node.name.text, field: [] };
882
+ let fieldNum = 1;
883
+ for (const member of node.members) {
884
+ if (ts.isPropertySignature(member) && ts.isIdentifier(member.name)) {
885
+ descriptor.field!.push({
886
+ name: member.name.text,
887
+ number: fieldNum++,
888
+ type: member.type ? member.type.getText() : "any",
889
+ });
890
+ }
891
+ }
892
+ return { name: node.name.text, descriptor, metadata: { kind: "interface" } };
893
+ }
894
+
895
+ private encodeTypeAlias(node: ts.TypeAliasDeclaration): TypeAlias {
896
+ return {
897
+ name: node.name.text,
898
+ targetType: node.type.getText(),
899
+ };
900
+ }
901
+
902
+ private encodeEnum(node: ts.EnumDeclaration): EnumDef {
903
+ return {
904
+ name: node.name.text,
905
+ values: node.members.map((m, i) => ({
906
+ name: ts.isIdentifier(m.name) ? m.name.text : m.name.getText(),
907
+ intValue: m.initializer && ts.isNumericLiteral(m.initializer)
908
+ ? parseInt(m.initializer.text) : i,
909
+ })),
910
+ };
911
+ }
912
+
913
+ private encodeTaggedTemplate(node: ts.TaggedTemplateExpression): Expression {
914
+ const tag = this.encodeExpr(node.tag);
915
+ const parts: Expression[] = [];
916
+ const exprs: Expression[] = [];
917
+
918
+ if (ts.isNoSubstitutionTemplateLiteral(node.template)) {
919
+ parts.push({ literal: { stringValue: node.template.text } });
920
+ } else {
921
+ parts.push({ literal: { stringValue: node.template.head.text } });
922
+ for (const span of node.template.templateSpans) {
923
+ exprs.push(this.encodeExpr(span.expression));
924
+ parts.push({ literal: { stringValue: span.literal.text } });
925
+ }
926
+ }
927
+
928
+ return this.stdCall("tagged_template", [
929
+ { name: "tag", value: tag },
930
+ { name: "strings", value: { literal: { listValue: { elements: parts } } } },
931
+ { name: "expressions", value: { literal: { listValue: { elements: exprs } } } },
932
+ ], "ts_std");
933
+ }
934
+
935
+ private stdCall(fn: string, fields: FieldValuePair[], module = "std"): Expression {
936
+ this.stdFunctions.add(`${module}:${fn}`);
937
+ return {
938
+ call: {
939
+ module,
940
+ function: fn,
941
+ input: fields.length > 0 ? {
942
+ messageCreation: { typeName: "", fields },
943
+ } : undefined,
944
+ },
945
+ };
946
+ }
947
+
948
+ private buildStdModule(): Module {
949
+ const functions: FunctionDef[] = [];
950
+ for (const ref of this.stdFunctions) {
951
+ const [, fn] = ref.split(":");
952
+ functions.push({ name: fn, isBase: true });
953
+ }
954
+ functions.sort((a, b) => a.name.localeCompare(b.name));
955
+ return { name: "std", functions };
956
+ }
957
+
958
+ private warn(msg: string): void {
959
+ this.warnings.push(msg);
960
+ }
961
+
962
+ getWarnings(): string[] {
963
+ return [...this.warnings];
964
+ }
965
+ }
966
+
967
+ export function encode(source: string, options: EncodeOptions = {}): Program {
968
+ return new TsEncoder().encode(source, options);
969
+ }