kimchilang 1.0.1

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.
Files changed (90) hide show
  1. package/.github/workflows/ci.yml +66 -0
  2. package/README.md +1547 -0
  3. package/create-kimchi-app/README.md +44 -0
  4. package/create-kimchi-app/index.js +214 -0
  5. package/create-kimchi-app/package.json +22 -0
  6. package/editors/README.md +121 -0
  7. package/editors/sublime/KimchiLang.sublime-syntax +138 -0
  8. package/editors/vscode/README.md +90 -0
  9. package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
  10. package/editors/vscode/language-configuration.json +37 -0
  11. package/editors/vscode/package.json +55 -0
  12. package/editors/vscode/src/extension.js +354 -0
  13. package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
  14. package/examples/api/client.km +36 -0
  15. package/examples/async_pipe.km +58 -0
  16. package/examples/basic.kimchi +109 -0
  17. package/examples/cli_framework/README.md +92 -0
  18. package/examples/cli_framework/calculator.km +61 -0
  19. package/examples/cli_framework/deploy.km +126 -0
  20. package/examples/cli_framework/greeter.km +26 -0
  21. package/examples/config.static +27 -0
  22. package/examples/config.static.js +10 -0
  23. package/examples/env_test.km +37 -0
  24. package/examples/fibonacci.kimchi +17 -0
  25. package/examples/greeter.km +15 -0
  26. package/examples/hello.js +1 -0
  27. package/examples/hello.kimchi +3 -0
  28. package/examples/js_interop.km +42 -0
  29. package/examples/logger_example.km +34 -0
  30. package/examples/memo_fibonacci.km +17 -0
  31. package/examples/myapp/lib/http.js +14 -0
  32. package/examples/myapp/lib/http.km +16 -0
  33. package/examples/myapp/main.km +16 -0
  34. package/examples/myapp/main_with_mock.km +42 -0
  35. package/examples/myapp/services/api.js +18 -0
  36. package/examples/myapp/services/api.km +18 -0
  37. package/examples/new_features.kimchi +52 -0
  38. package/examples/project_example.static +20 -0
  39. package/examples/readme_examples.km +240 -0
  40. package/examples/reduce_pattern_match.km +85 -0
  41. package/examples/regex_match.km +46 -0
  42. package/examples/sample.js +45 -0
  43. package/examples/sample.km +39 -0
  44. package/examples/secrets.static +35 -0
  45. package/examples/secrets.static.js +30 -0
  46. package/examples/shell-example.mjs +144 -0
  47. package/examples/shell_example.km +19 -0
  48. package/examples/stdlib_test.km +22 -0
  49. package/examples/test_example.km +69 -0
  50. package/examples/testing/README.md +88 -0
  51. package/examples/testing/http_client.km +18 -0
  52. package/examples/testing/math.km +48 -0
  53. package/examples/testing/math.test.km +93 -0
  54. package/examples/testing/user_service.km +29 -0
  55. package/examples/testing/user_service.test.km +72 -0
  56. package/examples/use-config.mjs +141 -0
  57. package/examples/use_config.km +13 -0
  58. package/install.sh +59 -0
  59. package/package.json +29 -0
  60. package/pantry/acorn/index.km +1 -0
  61. package/pantry/is_number/index.km +1 -0
  62. package/pantry/is_odd/index.km +2 -0
  63. package/project.static +6 -0
  64. package/src/cli.js +1245 -0
  65. package/src/generator.js +1241 -0
  66. package/src/index.js +141 -0
  67. package/src/js2km.js +568 -0
  68. package/src/lexer.js +822 -0
  69. package/src/linter.js +810 -0
  70. package/src/package-manager.js +307 -0
  71. package/src/parser.js +1876 -0
  72. package/src/static-parser.js +500 -0
  73. package/src/typechecker.js +950 -0
  74. package/stdlib/array.km +0 -0
  75. package/stdlib/bitwise.km +38 -0
  76. package/stdlib/console.km +49 -0
  77. package/stdlib/date.km +97 -0
  78. package/stdlib/function.km +44 -0
  79. package/stdlib/http.km +197 -0
  80. package/stdlib/http.md +333 -0
  81. package/stdlib/index.km +26 -0
  82. package/stdlib/json.km +17 -0
  83. package/stdlib/logger.js +114 -0
  84. package/stdlib/logger.km +104 -0
  85. package/stdlib/math.km +120 -0
  86. package/stdlib/object.km +41 -0
  87. package/stdlib/promise.km +33 -0
  88. package/stdlib/string.km +93 -0
  89. package/stdlib/testing.md +265 -0
  90. package/test/test.js +599 -0
@@ -0,0 +1,1241 @@
1
+ // KimchiLang Code Generator - Converts AST to JavaScript
2
+
3
+ import { NodeType } from './parser.js';
4
+
5
+ // Helper to check if a node or its children contain shell blocks
6
+ function containsShellBlock(node) {
7
+ if (!node) return false;
8
+ if (node.type === NodeType.ShellBlock) return true;
9
+
10
+ // Check all properties that could contain child nodes
11
+ for (const key of Object.keys(node)) {
12
+ const value = node[key];
13
+ if (Array.isArray(value)) {
14
+ for (const item of value) {
15
+ if (item && typeof item === 'object' && containsShellBlock(item)) {
16
+ return true;
17
+ }
18
+ }
19
+ } else if (value && typeof value === 'object' && value.type) {
20
+ if (containsShellBlock(value)) return true;
21
+ }
22
+ }
23
+ return false;
24
+ }
25
+
26
+ export class CodeGenerator {
27
+ constructor(options = {}) {
28
+ this.indent = 0;
29
+ this.indentStr = options.indentStr || ' ';
30
+ this.output = '';
31
+ this.options = options;
32
+ }
33
+
34
+ generate(ast) {
35
+ this.output = '';
36
+ this.visitProgram(ast);
37
+ return this.output;
38
+ }
39
+
40
+ emit(code) {
41
+ this.output += code;
42
+ }
43
+
44
+ emitLine(code = '') {
45
+ this.output += this.getIndent() + code + '\n';
46
+ }
47
+
48
+ getIndent() {
49
+ return this.indentStr.repeat(this.indent);
50
+ }
51
+
52
+ pushIndent() {
53
+ this.indent++;
54
+ }
55
+
56
+ popIndent() {
57
+ this.indent--;
58
+ }
59
+
60
+ emitRuntimeExtensions() {
61
+ // Extend Array prototype with stdlib methods
62
+ this.emitLine('// KimchiLang stdlib extensions');
63
+ this.emitLine('if (!Array.prototype._kmExtended) {');
64
+ this.pushIndent();
65
+ this.emitLine('Array.prototype._kmExtended = true;');
66
+ this.emitLine('Array.prototype.first = function() { return this[0]; };');
67
+ this.emitLine('Array.prototype.last = function() { return this[this.length - 1]; };');
68
+ this.emitLine('Array.prototype.isEmpty = function() { return this.length === 0; };');
69
+ this.emitLine('Array.prototype.sum = function() { return this.reduce((a, b) => a + b, 0); };');
70
+ this.emitLine('Array.prototype.product = function() { return this.reduce((a, b) => a * b, 1); };');
71
+ this.emitLine('Array.prototype.average = function() { return this.reduce((a, b) => a + b, 0) / this.length; };');
72
+ this.emitLine('Array.prototype.max = function() { return Math.max(...this); };');
73
+ this.emitLine('Array.prototype.min = function() { return Math.min(...this); };');
74
+ this.emitLine('Array.prototype.take = function(n) { return this.slice(0, n); };');
75
+ this.emitLine('Array.prototype.drop = function(n) { return this.slice(n); };');
76
+ this.emitLine('Array.prototype.flatten = function() { return this.flat(Infinity); };');
77
+ this.emitLine('Array.prototype.unique = function() { return [...new Set(this)]; };');
78
+ this.popIndent();
79
+ this.emitLine('}');
80
+
81
+ // Extend String prototype with stdlib methods
82
+ this.emitLine('if (!String.prototype._kmExtended) {');
83
+ this.pushIndent();
84
+ this.emitLine('String.prototype._kmExtended = true;');
85
+ this.emitLine('String.prototype.isEmpty = function() { return this.length === 0; };');
86
+ this.emitLine('String.prototype.isBlank = function() { return this.trim().length === 0; };');
87
+ this.emitLine('String.prototype.toChars = function() { return this.split(""); };');
88
+ this.emitLine('String.prototype.toLines = function() { return this.split("\\n"); };');
89
+ this.emitLine('String.prototype.capitalize = function() { return this.length === 0 ? this : this[0].toUpperCase() + this.slice(1); };');
90
+ this.popIndent();
91
+ this.emitLine('}');
92
+
93
+ // Add Object utility functions (can't extend Object.prototype safely)
94
+ this.emitLine('const _obj = {');
95
+ this.pushIndent();
96
+ this.emitLine('keys: (o) => Object.keys(o),');
97
+ this.emitLine('values: (o) => Object.values(o),');
98
+ this.emitLine('entries: (o) => Object.entries(o),');
99
+ this.emitLine('fromEntries: (arr) => Object.fromEntries(arr),');
100
+ this.emitLine('has: (o, k) => Object.hasOwn(o, k),');
101
+ this.emitLine('freeze: (o) => Object.freeze(o),');
102
+ this.emitLine('isEmpty: (o) => Object.keys(o).length === 0,');
103
+ this.emitLine('size: (o) => Object.keys(o).length,');
104
+ this.popIndent();
105
+ this.emitLine('};');
106
+ this.emitLine();
107
+
108
+ // Add error helper function for typed errors
109
+ this.emitLine('function error(message, _id = "Error") {');
110
+ this.pushIndent();
111
+ this.emitLine('const e = new Error(message);');
112
+ this.emitLine('e._id = _id;');
113
+ this.emitLine('return e;');
114
+ this.popIndent();
115
+ this.emitLine('}');
116
+ this.emitLine('error.create = (_id) => {');
117
+ this.pushIndent();
118
+ this.emitLine('const fn = (message) => error(message, _id);');
119
+ this.emitLine('fn._id = _id;');
120
+ this.emitLine('return fn;');
121
+ this.popIndent();
122
+ this.emitLine('};');
123
+ this.emitLine();
124
+
125
+ // Secret wrapper class - masks value when converted to string
126
+ this.emitLine('class _Secret {');
127
+ this.pushIndent();
128
+ this.emitLine('constructor(value) { this._value = value; }');
129
+ this.emitLine('toString() { return "********"; }');
130
+ this.emitLine('valueOf() { return this._value; }');
131
+ this.emitLine('get value() { return this._value; }');
132
+ this.emitLine('[Symbol.toPrimitive](hint) { return hint === "string" ? "********" : this._value; }');
133
+ this.popIndent();
134
+ this.emitLine('}');
135
+ this.emitLine('function _secret(value) { return new _Secret(value); }');
136
+ this.emitLine();
137
+
138
+ // Deep freeze helper for dec declarations
139
+ this.emitLine('function _deepFreeze(obj) {');
140
+ this.pushIndent();
141
+ this.emitLine('if (obj === null || typeof obj !== "object") return obj;');
142
+ this.emitLine('Object.keys(obj).forEach(key => _deepFreeze(obj[key]));');
143
+ this.emitLine('return Object.freeze(obj);');
144
+ this.popIndent();
145
+ this.emitLine('}');
146
+ this.emitLine();
147
+
148
+ // Async-aware pipe helper - awaits each step in the chain
149
+ this.emitLine('async function _pipe(value, ...fns) {');
150
+ this.pushIndent();
151
+ this.emitLine('let result = value;');
152
+ this.emitLine('for (const fn of fns) {');
153
+ this.pushIndent();
154
+ this.emitLine('result = await fn(result);');
155
+ this.popIndent();
156
+ this.emitLine('}');
157
+ this.emitLine('return result;');
158
+ this.popIndent();
159
+ this.emitLine('}');
160
+ this.emitLine();
161
+
162
+ // Async-aware flow helper - creates an async composed function
163
+ this.emitLine('function _flow(...fns) {');
164
+ this.pushIndent();
165
+ this.emitLine('return async (...args) => {');
166
+ this.pushIndent();
167
+ this.emitLine('let result = await fns[0](...args);');
168
+ this.emitLine('for (let i = 1; i < fns.length; i++) {');
169
+ this.pushIndent();
170
+ this.emitLine('result = await fns[i](result);');
171
+ this.popIndent();
172
+ this.emitLine('}');
173
+ this.emitLine('return result;');
174
+ this.popIndent();
175
+ this.emitLine('};');
176
+ this.popIndent();
177
+ this.emitLine('}');
178
+ this.emitLine();
179
+
180
+ // Shell execution helper (async)
181
+ this.emitLine('async function _shell(command, inputs = {}) {');
182
+ this.pushIndent();
183
+ this.emitLine('const { exec } = await import("child_process");');
184
+ this.emitLine('const { promisify } = await import("util");');
185
+ this.emitLine('const execAsync = promisify(exec);');
186
+ this.emitLine('// Interpolate inputs into command');
187
+ this.emitLine('let cmd = command;');
188
+ this.emitLine('for (const [key, value] of Object.entries(inputs)) {');
189
+ this.pushIndent();
190
+ this.emitLine('cmd = cmd.replace(new RegExp("\\\\$" + key + "\\\\b", "g"), String(value));');
191
+ this.popIndent();
192
+ this.emitLine('}');
193
+ this.emitLine('try {');
194
+ this.pushIndent();
195
+ this.emitLine('const { stdout, stderr } = await execAsync(cmd);');
196
+ this.emitLine('return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 };');
197
+ this.popIndent();
198
+ this.emitLine('} catch (error) {');
199
+ this.pushIndent();
200
+ this.emitLine('return { stdout: error.stdout?.trim() || "", stderr: error.stderr?.trim() || error.message, exitCode: error.code || 1 };');
201
+ this.popIndent();
202
+ this.emitLine('}');
203
+ this.popIndent();
204
+ this.emitLine('}');
205
+ this.emitLine();
206
+
207
+ // Testing framework runtime
208
+ this.emitLine('// Testing framework');
209
+ this.emitLine('const _tests = [];');
210
+ this.emitLine('let _currentDescribe = null;');
211
+ this.emitLine('function _describe(name, fn) {');
212
+ this.pushIndent();
213
+ this.emitLine('const prev = _currentDescribe;');
214
+ this.emitLine('_currentDescribe = { name, tests: [], parent: prev };');
215
+ this.emitLine('fn();');
216
+ this.emitLine('if (prev) { prev.tests.push(_currentDescribe); }');
217
+ this.emitLine('else { _tests.push(_currentDescribe); }');
218
+ this.emitLine('_currentDescribe = prev;');
219
+ this.popIndent();
220
+ this.emitLine('}');
221
+ this.emitLine('function _test(name, fn) {');
222
+ this.pushIndent();
223
+ this.emitLine('const test = { name, fn, describe: _currentDescribe };');
224
+ this.emitLine('if (_currentDescribe) { _currentDescribe.tests.push(test); }');
225
+ this.emitLine('else { _tests.push(test); }');
226
+ this.popIndent();
227
+ this.emitLine('}');
228
+ this.emitLine('function _expect(actual) {');
229
+ this.pushIndent();
230
+ this.emitLine('return {');
231
+ this.pushIndent();
232
+ this.emitLine('toBe(expected) { if (actual !== expected) throw new Error(`Expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); },');
233
+ this.emitLine('toEqual(expected) { if (JSON.stringify(actual) !== JSON.stringify(expected)) throw new Error(`Expected ${JSON.stringify(expected)} to equal ${JSON.stringify(actual)}`); },');
234
+ this.emitLine('toContain(item) { if (!actual.includes(item)) throw new Error(`Expected ${JSON.stringify(actual)} to contain ${JSON.stringify(item)}`); },');
235
+ this.emitLine('toBeNull() { if (actual !== null) throw new Error(`Expected null but got ${JSON.stringify(actual)}`); },');
236
+ this.emitLine('toBeTruthy() { if (!actual) throw new Error(`Expected truthy but got ${JSON.stringify(actual)}`); },');
237
+ this.emitLine('toBeFalsy() { if (actual) throw new Error(`Expected falsy but got ${JSON.stringify(actual)}`); },');
238
+ this.emitLine('toBeGreaterThan(n) { if (actual <= n) throw new Error(`Expected ${actual} > ${n}`); },');
239
+ this.emitLine('toBeLessThan(n) { if (actual >= n) throw new Error(`Expected ${actual} < ${n}`); },');
240
+ this.emitLine('toHaveLength(n) { if (actual.length !== n) throw new Error(`Expected length ${n} but got ${actual.length}`); },');
241
+ this.emitLine('toMatch(pattern) { if (!pattern.test(actual)) throw new Error(`Expected ${JSON.stringify(actual)} to match ${pattern}`); },');
242
+ this.emitLine('toThrow(msg) { try { actual(); throw new Error("Expected to throw"); } catch(e) { if (msg && !e.message.includes(msg)) throw new Error(`Expected error containing "${msg}" but got "${e.message}"`); } },');
243
+ this.popIndent();
244
+ this.emitLine('};');
245
+ this.popIndent();
246
+ this.emitLine('}');
247
+ this.emitLine('function _assert(condition, message) { if (!condition) throw new Error(message); }');
248
+ this.emitLine('async function _runTests() {');
249
+ this.pushIndent();
250
+ this.emitLine('let passed = 0, failed = 0;');
251
+ this.emitLine('async function runItem(item, indent = "") {');
252
+ this.pushIndent();
253
+ this.emitLine('if (item.fn) {');
254
+ this.pushIndent();
255
+ this.emitLine('try { await item.fn(); console.log(indent + "✓ " + item.name); passed++; }');
256
+ this.emitLine('catch (e) { console.log(indent + "✗ " + item.name); console.log(indent + " " + e.message); failed++; }');
257
+ this.popIndent();
258
+ this.emitLine('} else {');
259
+ this.pushIndent();
260
+ this.emitLine('console.log(indent + item.name);');
261
+ this.emitLine('for (const t of item.tests) await runItem(t, indent + " ");');
262
+ this.popIndent();
263
+ this.emitLine('}');
264
+ this.popIndent();
265
+ this.emitLine('}');
266
+ this.emitLine('for (const item of _tests) await runItem(item);');
267
+ this.emitLine('console.log(`\\n${passed + failed} tests, ${passed} passed, ${failed} failed`);');
268
+ this.emitLine('return { passed, failed };');
269
+ this.popIndent();
270
+ this.emitLine('}');
271
+ this.emitLine();
272
+ }
273
+
274
+ visit(node) {
275
+ if (!node) return '';
276
+
277
+ const methodName = `visit${node.type}`;
278
+ if (this[methodName]) {
279
+ return this[methodName](node);
280
+ }
281
+
282
+ throw new Error(`Unknown node type: ${node.type}`);
283
+ }
284
+
285
+ visitProgram(node) {
286
+ // Separate deps, args, env, dec declarations, and other statements
287
+ const depStatements = node.body.filter(stmt => stmt.type === NodeType.DepStatement);
288
+ const argDeclarations = node.body.filter(stmt => stmt.type === NodeType.ArgDeclaration);
289
+ const envDeclarations = node.body.filter(stmt => stmt.type === NodeType.EnvDeclaration);
290
+ const decDeclarations = node.body.filter(stmt => stmt.type === NodeType.DecDeclaration);
291
+ const otherStatements = node.body.filter(stmt =>
292
+ stmt.type !== NodeType.DepStatement && stmt.type !== NodeType.ArgDeclaration && stmt.type !== NodeType.EnvDeclaration
293
+ );
294
+
295
+ // Build list of dep paths for distinguishing deps from args in _opts
296
+ const depPaths = depStatements.map(dep => dep.path);
297
+ const argNames = argDeclarations.map(arg => arg.name);
298
+
299
+ // First, emit the raw imports for all dependencies
300
+ // Static files (.static) are imported directly, regular modules use factory pattern
301
+ // External modules (@ prefix) are resolved from .km_modules directory
302
+ for (const dep of depStatements) {
303
+ const moduleVar = `_dep_${dep.alias}`;
304
+ if (dep.isStatic) {
305
+ // Static files export all declarations directly, no factory function
306
+ // Use absolute path if basePath is provided (for run command)
307
+ const relativePath = './' + dep.pathParts.join('/') + '.static.js';
308
+ // For absolute paths, go up from basePath to project root, then use full path
309
+ const filePath = this.options.basePath
310
+ ? `file://${this.options.basePath}/../${dep.pathParts.join('/')}.static.js`
311
+ : relativePath;
312
+ this.emitLine(`import * as ${moduleVar} from '${filePath}';`);
313
+ } else if (dep.isExternal) {
314
+ // External module from .km_modules: @foo.bar -> .km_modules/foo/bar.km
315
+ const filePath = './.km_modules/' + dep.pathParts.join('/') + '.km';
316
+ this.emitLine(`import ${moduleVar} from '${filePath}';`);
317
+ } else {
318
+ const filePath = './' + dep.pathParts.join('/') + '.km';
319
+ this.emitLine(`import ${moduleVar} from '${filePath}';`);
320
+ }
321
+ }
322
+ if (depStatements.length > 0) {
323
+ this.emitLine();
324
+ }
325
+
326
+ // Emit stdlib prototype extensions
327
+ this.emitRuntimeExtensions();
328
+
329
+ // Export default factory function
330
+ this.emitLine('export default function(_opts = {}) {');
331
+ this.pushIndent();
332
+
333
+ // Validate required args
334
+ for (const arg of argDeclarations) {
335
+ if (arg.required) {
336
+ this.emitLine(`if (_opts["${arg.name}"] === undefined) throw new Error("Required argument '${arg.name}' not provided");`);
337
+ }
338
+ }
339
+ if (argDeclarations.some(a => a.required)) {
340
+ this.emitLine();
341
+ }
342
+
343
+ // Extract args from _opts with defaults
344
+ for (const arg of argDeclarations) {
345
+ const secretWrap = arg.secret ? '_secret(' : '';
346
+ const secretClose = arg.secret ? ')' : '';
347
+ if (arg.defaultValue) {
348
+ const defaultCode = this.visitExpression(arg.defaultValue);
349
+ this.emitLine(`const ${arg.name} = ${secretWrap}_opts["${arg.name}"] !== undefined ? _opts["${arg.name}"] : ${defaultCode}${secretClose};`);
350
+ } else {
351
+ this.emitLine(`const ${arg.name} = ${secretWrap}_opts["${arg.name}"]${secretClose};`);
352
+ }
353
+ }
354
+ if (argDeclarations.length > 0) {
355
+ this.emitLine();
356
+ }
357
+
358
+ // Extract env vars from process.env
359
+ for (const env of envDeclarations) {
360
+ if (env.required) {
361
+ this.emitLine(`if (process.env["${env.name}"] === undefined) throw new Error("Required environment variable '${env.name}' not set");`);
362
+ }
363
+ }
364
+ if (envDeclarations.some(e => e.required)) {
365
+ this.emitLine();
366
+ }
367
+
368
+ for (const env of envDeclarations) {
369
+ const secretWrap = env.secret ? '_secret(' : '';
370
+ const secretClose = env.secret ? ')' : '';
371
+ if (env.defaultValue) {
372
+ const defaultCode = this.visitExpression(env.defaultValue);
373
+ this.emitLine(`const ${env.name} = ${secretWrap}process.env["${env.name}"] !== undefined ? process.env["${env.name}"] : ${defaultCode}${secretClose};`);
374
+ } else {
375
+ this.emitLine(`const ${env.name} = ${secretWrap}process.env["${env.name}"]${secretClose};`);
376
+ }
377
+ }
378
+ if (envDeclarations.length > 0) {
379
+ this.emitLine();
380
+ }
381
+
382
+ // Resolve each dependency, checking _opts first (for injection)
383
+ for (const dep of depStatements) {
384
+ const moduleVar = `_dep_${dep.alias}`;
385
+ if (dep.isStatic) {
386
+ // Static files are imported directly, no factory function, no overrides
387
+ this.emitLine(`const ${dep.alias} = ${moduleVar};`);
388
+ } else if (dep.overrides) {
389
+ const overridesCode = this.visitExpression(dep.overrides);
390
+ this.emitLine(`const ${dep.alias} = _opts["${dep.path}"] || ${moduleVar}(${overridesCode});`);
391
+ } else {
392
+ this.emitLine(`const ${dep.alias} = _opts["${dep.path}"] || ${moduleVar}();`);
393
+ }
394
+ }
395
+ if (depStatements.length > 0) {
396
+ this.emitLine();
397
+ }
398
+
399
+ // Emit the rest of the module body (top-level statements)
400
+ for (const stmt of otherStatements) {
401
+ this.visitStatement(stmt, true);
402
+ }
403
+
404
+ // Return an object with all exported values (collect exports)
405
+ const exports = this.collectExports(otherStatements);
406
+ if (exports.length > 0) {
407
+ this.emitLine();
408
+ this.emitLine(`return { ${exports.join(', ')} };`);
409
+ }
410
+
411
+ this.popIndent();
412
+ this.emitLine('}');
413
+ }
414
+
415
+ collectExports(statements) {
416
+ const exports = [];
417
+ for (const stmt of statements) {
418
+ // Only export items marked with 'expose'
419
+ if (!stmt.exposed) continue;
420
+
421
+ if (stmt.type === NodeType.FunctionDeclaration) {
422
+ exports.push(stmt.name);
423
+ } else if (stmt.type === NodeType.DecDeclaration) {
424
+ exports.push(stmt.name);
425
+ }
426
+ }
427
+ return exports;
428
+ }
429
+
430
+ visitStatement(node, isTopLevel = false) {
431
+ switch (node.type) {
432
+ case NodeType.DecDeclaration:
433
+ this.visitDecDeclaration(node);
434
+ break;
435
+ case NodeType.FunctionDeclaration:
436
+ this.visitFunctionDeclaration(node);
437
+ break;
438
+ case NodeType.IfStatement:
439
+ this.visitIfStatement(node);
440
+ break;
441
+ case NodeType.WhileStatement:
442
+ this.visitWhileStatement(node);
443
+ break;
444
+ case NodeType.ForInStatement:
445
+ this.visitForInStatement(node);
446
+ break;
447
+ case NodeType.ReturnStatement:
448
+ this.visitReturnStatement(node);
449
+ break;
450
+ case NodeType.BreakStatement:
451
+ this.emitLine('break;');
452
+ break;
453
+ case NodeType.ContinueStatement:
454
+ this.emitLine('continue;');
455
+ break;
456
+ case NodeType.TryStatement:
457
+ this.visitTryStatement(node);
458
+ break;
459
+ case NodeType.ThrowStatement:
460
+ this.visitThrowStatement(node);
461
+ break;
462
+ case NodeType.PatternMatch:
463
+ this.visitPatternMatch(node, isTopLevel);
464
+ break;
465
+ case NodeType.PrintStatement:
466
+ this.visitPrintStatement(node);
467
+ break;
468
+ case NodeType.DepStatement:
469
+ this.visitDepStatement(node);
470
+ break;
471
+ case NodeType.EnumDeclaration:
472
+ this.visitEnumDeclaration(node);
473
+ break;
474
+ case NodeType.ArgDeclaration:
475
+ // Args are handled in visitProgram, not individually
476
+ break;
477
+ case NodeType.EnvDeclaration:
478
+ // Env vars are handled in visitProgram, not individually
479
+ break;
480
+ case NodeType.JSBlock:
481
+ this.visitJSBlock(node);
482
+ break;
483
+ case NodeType.ShellBlock:
484
+ this.visitShellBlock(node);
485
+ break;
486
+ case NodeType.TestBlock:
487
+ this.visitTestBlock(node);
488
+ break;
489
+ case NodeType.DescribeBlock:
490
+ this.visitDescribeBlock(node);
491
+ break;
492
+ case NodeType.ExpectStatement:
493
+ this.visitExpectStatement(node);
494
+ break;
495
+ case NodeType.AssertStatement:
496
+ this.visitAssertStatement(node);
497
+ break;
498
+ case NodeType.ExpressionStatement:
499
+ this.emitLine(this.visitExpression(node.expression) + ';');
500
+ break;
501
+ case NodeType.BlockStatement:
502
+ this.visitBlockStatement(node);
503
+ break;
504
+ default:
505
+ throw new Error(`Unknown statement type: ${node.type}`);
506
+ }
507
+ }
508
+
509
+ visitDecDeclaration(node) {
510
+ // dec creates deeply immutable variables using Object.freeze recursively
511
+ let init = this.visitExpression(node.init);
512
+
513
+ // Wrap with _secret() if marked as secret
514
+ if (node.secret) {
515
+ init = `_secret(${init})`;
516
+ }
517
+
518
+ if (node.destructuring) {
519
+ // Handle destructuring patterns
520
+ if (node.pattern.type === NodeType.ObjectPattern) {
521
+ // Object destructuring: const { a, b } = _deepFreeze(obj);
522
+ const props = node.pattern.properties.map(p => {
523
+ if (p.key === p.value) {
524
+ return p.key;
525
+ }
526
+ return `${p.key}: ${p.value}`;
527
+ }).join(', ');
528
+ this.emitLine(`const { ${props} } = _deepFreeze(${init});`);
529
+ } else if (node.pattern.type === NodeType.ArrayPattern) {
530
+ // Array destructuring: const [x, y] = _deepFreeze(arr);
531
+ const elems = node.pattern.elements.map(e => {
532
+ if (e === null) return '';
533
+ return e.name;
534
+ }).join(', ');
535
+ this.emitLine(`const [${elems}] = _deepFreeze(${init});`);
536
+ }
537
+ } else {
538
+ this.emitLine(`const ${node.name} = _deepFreeze(${init});`);
539
+ }
540
+ }
541
+
542
+ visitFunctionDeclaration(node) {
543
+ // Auto-make functions async if they contain shell blocks
544
+ const hasShellBlock = containsShellBlock(node.body);
545
+ const async = (node.async || hasShellBlock) ? 'async ' : '';
546
+ const params = this.generateParams(node.params);
547
+
548
+ if (node.memoized) {
549
+ // Generate memoized function
550
+ this.emitLine(`const ${node.name} = (() => {`);
551
+ this.pushIndent();
552
+ this.emitLine('const _cache = new Map();');
553
+ this.emitLine(`return ${async}function(${params}) {`);
554
+ this.pushIndent();
555
+ this.emitLine('const _key = JSON.stringify([...arguments]);');
556
+ this.emitLine('if (_cache.has(_key)) return _cache.get(_key);');
557
+ // Generate function body, capturing the result
558
+ // For async functions, the inner IIFE must also be async and awaited
559
+ if (node.async) {
560
+ this.emitLine('const _result = await (async () => {');
561
+ } else {
562
+ this.emitLine('const _result = (() => {');
563
+ }
564
+ this.pushIndent();
565
+ for (const stmt of node.body.body) {
566
+ this.visitStatement(stmt);
567
+ }
568
+ this.popIndent();
569
+ this.emitLine('})();');
570
+ this.emitLine('_cache.set(_key, _result);');
571
+ this.emitLine('return _result;');
572
+ this.popIndent();
573
+ this.emitLine('};');
574
+ this.popIndent();
575
+ this.emitLine('})();');
576
+ } else {
577
+ this.emitLine(`${async}function ${node.name}(${params}) {`);
578
+ this.pushIndent();
579
+ for (const stmt of node.body.body) {
580
+ this.visitStatement(stmt);
581
+ }
582
+ this.popIndent();
583
+ this.emitLine('}');
584
+ }
585
+ this.emitLine();
586
+ }
587
+
588
+ visitEnumDeclaration(node) {
589
+ // Generate enum as a frozen object with auto-incrementing values
590
+ this.emitLine(`const ${node.name} = Object.freeze({`);
591
+ this.pushIndent();
592
+
593
+ let autoValue = 0;
594
+ for (let i = 0; i < node.members.length; i++) {
595
+ const member = node.members[i];
596
+ let value;
597
+
598
+ if (member.value !== null) {
599
+ value = this.visitExpression(member.value);
600
+ // If it's a number literal, update autoValue for next member
601
+ if (member.value.type === NodeType.Literal && typeof member.value.value === 'number') {
602
+ autoValue = member.value.value + 1;
603
+ }
604
+ } else {
605
+ value = autoValue;
606
+ autoValue++;
607
+ }
608
+
609
+ const comma = i < node.members.length - 1 ? ',' : '';
610
+ this.emitLine(`${member.name}: ${value}${comma}`);
611
+ }
612
+
613
+ this.popIndent();
614
+ this.emitLine('});');
615
+ this.emitLine();
616
+ }
617
+
618
+ visitJSBlock(node) {
619
+ // JS interop block - wraps raw JavaScript in an IIFE with optional inputs
620
+ // Syntax: js { code } -> (() => { code })();
621
+ // js(a, b) { code } -> ((a, b) => { code })(a, b);
622
+
623
+ if (node.inputs.length === 0) {
624
+ // No inputs - simple IIFE
625
+ this.emitLine('(() => {');
626
+ this.pushIndent();
627
+ // Emit the raw JS code, preserving formatting
628
+ const lines = node.code.split('\n');
629
+ for (const line of lines) {
630
+ if (line.trim()) {
631
+ this.emitLine(line.trim());
632
+ }
633
+ }
634
+ this.popIndent();
635
+ this.emitLine('})();');
636
+ } else {
637
+ // With inputs - IIFE that receives kimchi variables
638
+ const params = node.inputs.join(', ');
639
+ this.emitLine(`((${params}) => {`);
640
+ this.pushIndent();
641
+ const lines = node.code.split('\n');
642
+ for (const line of lines) {
643
+ if (line.trim()) {
644
+ this.emitLine(line.trim());
645
+ }
646
+ }
647
+ this.popIndent();
648
+ this.emitLine(`})(${params});`);
649
+ }
650
+ this.emitLine();
651
+ }
652
+
653
+ visitJSBlockExpression(node) {
654
+ // JS block as expression - returns an IIFE that can be assigned
655
+ // Syntax: dec result = js(a, b) { return a + b; }
656
+ // Generates: ((a, b) => { return a + b; })(a, b)
657
+
658
+ const lines = node.code.split('\n').filter(l => l.trim()).map(l => l.trim()).join(' ');
659
+
660
+ if (node.inputs.length === 0) {
661
+ return `(() => { ${lines} })()`;
662
+ } else {
663
+ const params = node.inputs.join(', ');
664
+ return `((${params}) => { ${lines} })(${params})`;
665
+ }
666
+ }
667
+
668
+ visitShellBlock(node) {
669
+ // Shell interop block - executes shell command asynchronously
670
+ // Syntax: shell { command } -> await _shell("command");
671
+ // shell(a, b) { command } -> await _shell("command", { a, b });
672
+
673
+ const command = JSON.stringify(node.command);
674
+
675
+ if (node.inputs.length === 0) {
676
+ this.emitLine(`await _shell(${command});`);
677
+ } else {
678
+ const inputsObj = `{ ${node.inputs.join(', ')} }`;
679
+ this.emitLine(`await _shell(${command}, ${inputsObj});`);
680
+ }
681
+ }
682
+
683
+ visitShellBlockExpression(node) {
684
+ // Shell block as expression - returns the shell result
685
+ // Syntax: dec result = shell { ls -la }
686
+ // Generates: await _shell("ls -la")
687
+
688
+ const command = JSON.stringify(node.command);
689
+
690
+ if (node.inputs.length === 0) {
691
+ return `await _shell(${command})`;
692
+ } else {
693
+ const inputsObj = `{ ${node.inputs.join(', ')} }`;
694
+ return `await _shell(${command}, ${inputsObj})`;
695
+ }
696
+ }
697
+
698
+ generateParams(params) {
699
+ return params.map(p => {
700
+ if (p.type === 'RestElement') {
701
+ return `...${p.argument}`;
702
+ }
703
+ if (p.defaultValue) {
704
+ return `${p.name} = ${this.visitExpression(p.defaultValue)}`;
705
+ }
706
+ return p.name;
707
+ }).join(', ');
708
+ }
709
+
710
+ visitBlockStatement(node) {
711
+ this.emitLine('{');
712
+ this.pushIndent();
713
+ for (const stmt of node.body) {
714
+ this.visitStatement(stmt);
715
+ }
716
+ this.popIndent();
717
+ this.emitLine('}');
718
+ }
719
+
720
+ visitIfStatement(node, isElseIf = false) {
721
+ const prefix = isElseIf ? '' : this.getIndent();
722
+ this.emit(prefix + `if (${this.visitExpression(node.test)}) {\n`);
723
+ this.pushIndent();
724
+ for (const stmt of node.consequent.body) {
725
+ this.visitStatement(stmt);
726
+ }
727
+ this.popIndent();
728
+
729
+ if (node.alternate) {
730
+ if (node.alternate.type === NodeType.IfStatement) {
731
+ this.emit(this.getIndent() + '} else ');
732
+ this.visitIfStatement(node.alternate, true);
733
+ } else {
734
+ this.emitLine('} else {');
735
+ this.pushIndent();
736
+ for (const stmt of node.alternate.body) {
737
+ this.visitStatement(stmt);
738
+ }
739
+ this.popIndent();
740
+ this.emitLine('}');
741
+ }
742
+ } else {
743
+ this.emitLine('}');
744
+ }
745
+ }
746
+
747
+ visitWhileStatement(node) {
748
+ this.emitLine(`while (${this.visitExpression(node.test)}) {`);
749
+ this.pushIndent();
750
+ for (const stmt of node.body.body) {
751
+ this.visitStatement(stmt);
752
+ }
753
+ this.popIndent();
754
+ this.emitLine('}');
755
+ }
756
+
757
+ visitForInStatement(node) {
758
+ const iterable = this.visitExpression(node.iterable);
759
+ this.emitLine(`for (const ${node.variable} of ${iterable}) {`);
760
+ this.pushIndent();
761
+ for (const stmt of node.body.body) {
762
+ this.visitStatement(stmt);
763
+ }
764
+ this.popIndent();
765
+ this.emitLine('}');
766
+ }
767
+
768
+ visitReturnStatement(node) {
769
+ if (node.argument) {
770
+ this.emitLine(`return ${this.visitExpression(node.argument)};`);
771
+ } else {
772
+ this.emitLine('return;');
773
+ }
774
+ }
775
+
776
+ visitTryStatement(node) {
777
+ this.emitLine('try {');
778
+ this.pushIndent();
779
+ for (const stmt of node.block.body) {
780
+ this.visitStatement(stmt);
781
+ }
782
+ this.popIndent();
783
+
784
+ if (node.handler) {
785
+ const param = node.handler.param ? `(${node.handler.param})` : '';
786
+ this.emitLine(`} catch ${param} {`);
787
+ this.pushIndent();
788
+ for (const stmt of node.handler.body.body) {
789
+ this.visitStatement(stmt);
790
+ }
791
+ this.popIndent();
792
+ }
793
+
794
+ if (node.finalizer) {
795
+ this.emitLine('} finally {');
796
+ this.pushIndent();
797
+ for (const stmt of node.finalizer.body) {
798
+ this.visitStatement(stmt);
799
+ }
800
+ this.popIndent();
801
+ }
802
+
803
+ this.emitLine('}');
804
+ }
805
+
806
+ visitThrowStatement(node) {
807
+ this.emitLine(`throw ${this.visitExpression(node.argument)};`);
808
+ }
809
+
810
+ visitPatternMatch(node, isTopLevel = false) {
811
+ // Standalone pattern matching: |condition| => code
812
+ // Regex pattern matching: subject ~ /regex/ => code
813
+ // At top level: use if/else if chain (no return)
814
+ // Inside function: each case returns from the function when matched
815
+
816
+ // For regex pattern matching with subject, evaluate subject once
817
+ let subjectVar = null;
818
+ if (node.subject && node.isRegex) {
819
+ subjectVar = '_subject';
820
+ const subjectExpr = this.visitExpression(node.subject);
821
+ this.emitLine(`const ${subjectVar} = ${subjectExpr};`);
822
+ }
823
+
824
+ for (let i = 0; i < node.cases.length; i++) {
825
+ const matchCase = node.cases[i];
826
+ let condition;
827
+
828
+ if (matchCase.isRegex || matchCase.test.type === NodeType.RegexLiteral) {
829
+ // Regex pattern: test against subject
830
+ // Store match result in $match for access in the body
831
+ const regex = this.visitExpression(matchCase.test);
832
+ const target = subjectVar || '_subject';
833
+ condition = `($match = ${regex}.exec(${target}))`;
834
+ } else {
835
+ condition = this.visitExpression(matchCase.test);
836
+ }
837
+
838
+ // Use else if for subsequent cases
839
+ const prefix = i === 0 ? 'if' : '} else if';
840
+ if (i > 0) {
841
+ this.popIndent();
842
+ }
843
+ this.emitLine(`${prefix} (${condition}) {`);
844
+ this.pushIndent();
845
+
846
+ if (matchCase.consequent.type === NodeType.BlockStatement) {
847
+ for (const stmt of matchCase.consequent.body) {
848
+ this.visitStatement(stmt);
849
+ }
850
+ } else {
851
+ this.visitStatement(matchCase.consequent);
852
+ }
853
+
854
+ // Only add return inside functions, not at top level
855
+ if (!isTopLevel) {
856
+ this.emitLine('return;');
857
+ }
858
+ }
859
+
860
+ // Close the final if block
861
+ if (node.cases.length > 0) {
862
+ this.popIndent();
863
+ this.emitLine('}');
864
+ }
865
+ }
866
+
867
+ visitPrintStatement(node) {
868
+ this.emitLine(`console.log(${this.visitExpression(node.argument)});`);
869
+ }
870
+
871
+ visitDepStatement(node) {
872
+ // Convert dotted path to file path: project.salesforce.client -> ./project/salesforce/client.km
873
+ const filePath = './' + node.pathParts.join('/') + '.km';
874
+
875
+ // Import the module (which exports a factory function)
876
+ const moduleVar = `_dep_${node.alias}`;
877
+ this.emitLine(`import ${moduleVar} from '${filePath}';`);
878
+
879
+ // Call the factory function with optional overrides to get the actual module
880
+ if (node.overrides) {
881
+ const overridesCode = this.visitExpression(node.overrides);
882
+ this.emitLine(`const ${node.alias} = ${moduleVar}(${overridesCode});`);
883
+ } else {
884
+ this.emitLine(`const ${node.alias} = ${moduleVar}();`);
885
+ }
886
+ }
887
+
888
+ visitExpression(node) {
889
+ switch (node.type) {
890
+ case NodeType.Literal:
891
+ return this.visitLiteral(node);
892
+ case NodeType.Identifier:
893
+ return node.name;
894
+ case NodeType.BinaryExpression:
895
+ return this.visitBinaryExpression(node);
896
+ case NodeType.UnaryExpression:
897
+ return this.visitUnaryExpression(node);
898
+ case NodeType.AssignmentExpression:
899
+ return this.visitAssignmentExpression(node);
900
+ case NodeType.CallExpression:
901
+ return this.visitCallExpression(node);
902
+ case NodeType.MemberExpression:
903
+ return this.visitMemberExpression(node);
904
+ case NodeType.ArrayExpression:
905
+ return this.visitArrayExpression(node);
906
+ case NodeType.ObjectExpression:
907
+ return this.visitObjectExpression(node);
908
+ case NodeType.ArrowFunctionExpression:
909
+ return this.visitArrowFunctionExpression(node);
910
+ case NodeType.ConditionalExpression:
911
+ return this.visitConditionalExpression(node);
912
+ case NodeType.AwaitExpression:
913
+ return `await ${this.visitExpression(node.argument)}`;
914
+ case NodeType.SpreadElement:
915
+ return `...${this.visitExpression(node.argument)}`;
916
+ case NodeType.RangeExpression:
917
+ return this.visitRangeExpression(node);
918
+ case NodeType.FlowExpression:
919
+ return this.visitFlowExpression(node);
920
+ case NodeType.PipeExpression:
921
+ return this.visitPipeExpression(node);
922
+ case NodeType.TemplateLiteral:
923
+ return this.visitTemplateLiteral(node);
924
+ case NodeType.JSBlock:
925
+ return this.visitJSBlockExpression(node);
926
+ case NodeType.ShellBlock:
927
+ return this.visitShellBlockExpression(node);
928
+ case NodeType.RegexLiteral:
929
+ return this.visitRegexLiteral(node);
930
+ case NodeType.MatchExpression:
931
+ return this.visitMatchExpression(node);
932
+ default:
933
+ throw new Error(`Unknown expression type: ${node.type}`);
934
+ }
935
+ }
936
+
937
+ visitLiteral(node) {
938
+ // Numbers - use raw value to preserve format (hex, binary, etc)
939
+ if (node.isNumber) {
940
+ return node.raw;
941
+ }
942
+ // Strings
943
+ if (node.isString || typeof node.value === 'string') {
944
+ // Check if it's a template string
945
+ if (node.raw && node.raw.startsWith('`')) {
946
+ return node.raw;
947
+ }
948
+ return JSON.stringify(node.value);
949
+ }
950
+ if (node.value === null) {
951
+ return 'null';
952
+ }
953
+ if (typeof node.value === 'boolean') {
954
+ return node.value ? 'true' : 'false';
955
+ }
956
+ return String(node.raw || node.value);
957
+ }
958
+
959
+ visitRegexLiteral(node) {
960
+ return `/${node.pattern}/${node.flags}`;
961
+ }
962
+
963
+ visitMatchExpression(node) {
964
+ // Match expression: subject ~ /regex/ or subject ~ /regex/ => { body }
965
+ const subject = this.visitExpression(node.subject);
966
+ const regex = this.visitExpression(node.pattern);
967
+
968
+ if (node.body) {
969
+ // With body: execute body with $match available, return result
970
+ // Wrap in IIFE to create scope for $match
971
+ if (node.body.type === NodeType.BlockStatement) {
972
+ // Block body - generate IIFE with statements
973
+ let bodyCode = '';
974
+ for (const stmt of node.body.body) {
975
+ if (stmt.type === NodeType.ReturnStatement) {
976
+ bodyCode += `return ${this.visitExpression(stmt.argument)};`;
977
+ } else {
978
+ // For other statements, we'd need to handle them
979
+ // For now, assume return statements
980
+ bodyCode += this.visitExpression(stmt.expression) + ';';
981
+ }
982
+ }
983
+ return `(($match) => { ${bodyCode} })(${regex}.exec(${subject}))`;
984
+ } else {
985
+ // Expression body
986
+ const bodyExpr = this.visitExpression(node.body);
987
+ return `(($match) => ${bodyExpr})(${regex}.exec(${subject}))`;
988
+ }
989
+ } else {
990
+ // Without body: return first match (match[0]) or null
991
+ return `(${regex}.exec(${subject}) || [])[0]`;
992
+ }
993
+ }
994
+
995
+ visitBinaryExpression(node) {
996
+ const left = this.visitExpression(node.left);
997
+ const right = this.visitExpression(node.right);
998
+
999
+ // Handle 'is' operator - compares ._id properties
1000
+ if (node.operator === 'is') {
1001
+ return `(${left}?._id === ${right}?._id)`;
1002
+ }
1003
+
1004
+ // Handle 'is not' operator - negated ._id comparison
1005
+ if (node.operator === 'is not') {
1006
+ return `(${left}?._id !== ${right}?._id)`;
1007
+ }
1008
+
1009
+ return `(${left} ${node.operator} ${right})`;
1010
+ }
1011
+
1012
+ visitUnaryExpression(node) {
1013
+ const argument = this.visitExpression(node.argument);
1014
+ if (node.operator === '!' || node.operator === '~' || node.operator === '-') {
1015
+ return `${node.operator}${argument}`;
1016
+ }
1017
+ return `${node.operator} ${argument}`;
1018
+ }
1019
+
1020
+ visitAssignmentExpression(node) {
1021
+ const left = this.visitExpression(node.left);
1022
+ const right = this.visitExpression(node.right);
1023
+ return `${left} ${node.operator} ${right}`;
1024
+ }
1025
+
1026
+ visitCallExpression(node) {
1027
+ const callee = this.visitExpression(node.callee);
1028
+ const args = node.arguments.map(a => this.visitExpression(a)).join(', ');
1029
+ return `${callee}(${args})`;
1030
+ }
1031
+
1032
+ visitMemberExpression(node) {
1033
+ const object = this.visitExpression(node.object);
1034
+ if (node.computed) {
1035
+ const property = this.visitExpression(node.property);
1036
+ // Use optional chaining for safe access
1037
+ return `${object}?.[${property}]`;
1038
+ }
1039
+ // Use optional chaining for safe access
1040
+ return `${object}?.${node.property}`;
1041
+ }
1042
+
1043
+ visitArrayExpression(node) {
1044
+ const elements = node.elements.map(e => this.visitExpression(e)).join(', ');
1045
+ return `[${elements}]`;
1046
+ }
1047
+
1048
+ visitObjectExpression(node) {
1049
+ if (node.properties.length === 0) {
1050
+ return '{}';
1051
+ }
1052
+
1053
+ const props = node.properties.map(p => {
1054
+ // Handle spread element
1055
+ if (p.type === NodeType.SpreadElement) {
1056
+ return `...${this.visitExpression(p.argument)}`;
1057
+ }
1058
+
1059
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(p.key) ? p.key : JSON.stringify(p.key);
1060
+ const value = this.visitExpression(p.value);
1061
+
1062
+ if (p.shorthand && p.value.type === NodeType.Identifier && p.value.name === p.key) {
1063
+ return key;
1064
+ }
1065
+ return `${key}: ${value}`;
1066
+ }).join(', ');
1067
+
1068
+ return `{ ${props} }`;
1069
+ }
1070
+
1071
+ visitArrowFunctionExpression(node) {
1072
+ const params = this.generateParams(node.params);
1073
+ const paramsStr = node.params.length === 1 && !node.params[0].defaultValue
1074
+ ? node.params[0].name
1075
+ : `(${params})`;
1076
+
1077
+ // Auto-make arrow functions async if they contain shell blocks
1078
+ const hasShellBlock = containsShellBlock(node.body);
1079
+ const asyncPrefix = hasShellBlock ? 'async ' : '';
1080
+
1081
+ if (node.body.type === NodeType.BlockStatement) {
1082
+ const bodyLines = [];
1083
+ const savedOutput = this.output;
1084
+ this.output = '';
1085
+ this.pushIndent();
1086
+ for (const stmt of node.body.body) {
1087
+ this.visitStatement(stmt);
1088
+ }
1089
+ this.popIndent();
1090
+ const bodyContent = this.output;
1091
+ this.output = savedOutput;
1092
+ return `${asyncPrefix}${paramsStr} => {\n${bodyContent}${this.getIndent()}}`;
1093
+ }
1094
+
1095
+ const body = this.visitExpression(node.body);
1096
+ return `${asyncPrefix}${paramsStr} => ${body}`;
1097
+ }
1098
+
1099
+ visitConditionalExpression(node) {
1100
+ const test = this.visitExpression(node.test);
1101
+ const consequent = this.visitExpression(node.consequent);
1102
+ const alternate = this.visitExpression(node.alternate);
1103
+ return `(${test} ? ${consequent} : ${alternate})`;
1104
+ }
1105
+
1106
+ visitRangeExpression(node) {
1107
+ const start = this.visitExpression(node.start);
1108
+ const end = this.visitExpression(node.end);
1109
+ return `Array.from({ length: ${end} - ${start} }, (_, i) => ${start} + i)`;
1110
+ }
1111
+
1112
+ visitFlowExpression(node) {
1113
+ // Flow: composedFn >> fn1 fn2 fn3
1114
+ // Generates: const composedFn = _flow(fn1, fn2, fn3);
1115
+ // This creates an async composed function that awaits each step
1116
+ const { name, functions } = node;
1117
+
1118
+ if (functions.length === 0) {
1119
+ return `const ${name} = (x) => x`;
1120
+ }
1121
+
1122
+ // Use the _flow helper which handles async functions
1123
+ return `const ${name} = _flow(${functions.join(', ')})`;
1124
+ }
1125
+
1126
+ visitPipeExpression(node) {
1127
+ // Pipe: value ~> fn1 ~> fn2
1128
+ // Collect all pipe steps and use _pipe helper for async support
1129
+ const steps = [];
1130
+ let current = node;
1131
+
1132
+ // Walk the pipe chain to collect all steps
1133
+ while (current.type === NodeType.PipeExpression) {
1134
+ steps.unshift(this.visitExpression(current.right));
1135
+ current = current.left;
1136
+ }
1137
+
1138
+ // current is now the initial value
1139
+ const initial = this.visitExpression(current);
1140
+
1141
+ // Use _pipe helper which awaits each step
1142
+ return `_pipe(${initial}, ${steps.join(', ')})`;
1143
+ }
1144
+
1145
+ visitTemplateLiteral(node) {
1146
+ // Convert KimchiLang string interpolation to JavaScript template literal
1147
+ // node.parts contains string segments, node.expressions contains parsed AST nodes
1148
+ let result = '`';
1149
+
1150
+ for (let i = 0; i < node.parts.length; i++) {
1151
+ // Escape backticks in the string parts
1152
+ result += node.parts[i].replace(/`/g, '\\`');
1153
+
1154
+ // Add expression if there is one after this part
1155
+ if (i < node.expressions.length) {
1156
+ result += '${' + this.visitExpression(node.expressions[i]) + '}';
1157
+ }
1158
+ }
1159
+
1160
+ result += '`';
1161
+ return result;
1162
+ }
1163
+
1164
+ // Testing framework code generation
1165
+ visitTestBlock(node) {
1166
+ this.emitLine(`_test(${JSON.stringify(node.name)}, async () => {`);
1167
+ this.pushIndent();
1168
+ for (const stmt of node.body.body) {
1169
+ this.visitStatement(stmt);
1170
+ }
1171
+ this.popIndent();
1172
+ this.emitLine('});');
1173
+ }
1174
+
1175
+ visitDescribeBlock(node) {
1176
+ this.emitLine(`_describe(${JSON.stringify(node.name)}, () => {`);
1177
+ this.pushIndent();
1178
+ for (const stmt of node.body.body) {
1179
+ this.visitStatement(stmt);
1180
+ }
1181
+ this.popIndent();
1182
+ this.emitLine('});');
1183
+ }
1184
+
1185
+ visitExpectStatement(node) {
1186
+ const actual = this.visitExpression(node.actual);
1187
+ const matcher = node.matcher;
1188
+ const expected = node.expected ? this.visitExpression(node.expected) : '';
1189
+
1190
+ // Generate appropriate assertion based on matcher
1191
+ switch (matcher) {
1192
+ case 'toBe':
1193
+ this.emitLine(`_expect(${actual}).toBe(${expected});`);
1194
+ break;
1195
+ case 'toEqual':
1196
+ this.emitLine(`_expect(${actual}).toEqual(${expected});`);
1197
+ break;
1198
+ case 'toContain':
1199
+ this.emitLine(`_expect(${actual}).toContain(${expected});`);
1200
+ break;
1201
+ case 'toBeNull':
1202
+ this.emitLine(`_expect(${actual}).toBeNull();`);
1203
+ break;
1204
+ case 'toBeTruthy':
1205
+ this.emitLine(`_expect(${actual}).toBeTruthy();`);
1206
+ break;
1207
+ case 'toBeFalsy':
1208
+ this.emitLine(`_expect(${actual}).toBeFalsy();`);
1209
+ break;
1210
+ case 'toBeGreaterThan':
1211
+ this.emitLine(`_expect(${actual}).toBeGreaterThan(${expected});`);
1212
+ break;
1213
+ case 'toBeLessThan':
1214
+ this.emitLine(`_expect(${actual}).toBeLessThan(${expected});`);
1215
+ break;
1216
+ case 'toThrow':
1217
+ this.emitLine(`_expect(${actual}).toThrow(${expected});`);
1218
+ break;
1219
+ case 'toMatch':
1220
+ this.emitLine(`_expect(${actual}).toMatch(${expected});`);
1221
+ break;
1222
+ case 'toHaveLength':
1223
+ this.emitLine(`_expect(${actual}).toHaveLength(${expected});`);
1224
+ break;
1225
+ default:
1226
+ // Generic matcher
1227
+ this.emitLine(`_expect(${actual}).${matcher}(${expected});`);
1228
+ }
1229
+ }
1230
+
1231
+ visitAssertStatement(node) {
1232
+ const condition = this.visitExpression(node.condition);
1233
+ const message = node.message ? this.visitExpression(node.message) : JSON.stringify('Assertion failed');
1234
+ this.emitLine(`_assert(${condition}, ${message});`);
1235
+ }
1236
+ }
1237
+
1238
+ export function generate(ast, options = {}) {
1239
+ const generator = new CodeGenerator(options);
1240
+ return generator.generate(ast);
1241
+ }