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.
- package/.github/workflows/ci.yml +66 -0
- package/README.md +1547 -0
- package/create-kimchi-app/README.md +44 -0
- package/create-kimchi-app/index.js +214 -0
- package/create-kimchi-app/package.json +22 -0
- package/editors/README.md +121 -0
- package/editors/sublime/KimchiLang.sublime-syntax +138 -0
- package/editors/vscode/README.md +90 -0
- package/editors/vscode/kimchilang-1.1.0.vsix +0 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +55 -0
- package/editors/vscode/src/extension.js +354 -0
- package/editors/vscode/syntaxes/kimchi.tmLanguage.json +215 -0
- package/examples/api/client.km +36 -0
- package/examples/async_pipe.km +58 -0
- package/examples/basic.kimchi +109 -0
- package/examples/cli_framework/README.md +92 -0
- package/examples/cli_framework/calculator.km +61 -0
- package/examples/cli_framework/deploy.km +126 -0
- package/examples/cli_framework/greeter.km +26 -0
- package/examples/config.static +27 -0
- package/examples/config.static.js +10 -0
- package/examples/env_test.km +37 -0
- package/examples/fibonacci.kimchi +17 -0
- package/examples/greeter.km +15 -0
- package/examples/hello.js +1 -0
- package/examples/hello.kimchi +3 -0
- package/examples/js_interop.km +42 -0
- package/examples/logger_example.km +34 -0
- package/examples/memo_fibonacci.km +17 -0
- package/examples/myapp/lib/http.js +14 -0
- package/examples/myapp/lib/http.km +16 -0
- package/examples/myapp/main.km +16 -0
- package/examples/myapp/main_with_mock.km +42 -0
- package/examples/myapp/services/api.js +18 -0
- package/examples/myapp/services/api.km +18 -0
- package/examples/new_features.kimchi +52 -0
- package/examples/project_example.static +20 -0
- package/examples/readme_examples.km +240 -0
- package/examples/reduce_pattern_match.km +85 -0
- package/examples/regex_match.km +46 -0
- package/examples/sample.js +45 -0
- package/examples/sample.km +39 -0
- package/examples/secrets.static +35 -0
- package/examples/secrets.static.js +30 -0
- package/examples/shell-example.mjs +144 -0
- package/examples/shell_example.km +19 -0
- package/examples/stdlib_test.km +22 -0
- package/examples/test_example.km +69 -0
- package/examples/testing/README.md +88 -0
- package/examples/testing/http_client.km +18 -0
- package/examples/testing/math.km +48 -0
- package/examples/testing/math.test.km +93 -0
- package/examples/testing/user_service.km +29 -0
- package/examples/testing/user_service.test.km +72 -0
- package/examples/use-config.mjs +141 -0
- package/examples/use_config.km +13 -0
- package/install.sh +59 -0
- package/package.json +29 -0
- package/pantry/acorn/index.km +1 -0
- package/pantry/is_number/index.km +1 -0
- package/pantry/is_odd/index.km +2 -0
- package/project.static +6 -0
- package/src/cli.js +1245 -0
- package/src/generator.js +1241 -0
- package/src/index.js +141 -0
- package/src/js2km.js +568 -0
- package/src/lexer.js +822 -0
- package/src/linter.js +810 -0
- package/src/package-manager.js +307 -0
- package/src/parser.js +1876 -0
- package/src/static-parser.js +500 -0
- package/src/typechecker.js +950 -0
- package/stdlib/array.km +0 -0
- package/stdlib/bitwise.km +38 -0
- package/stdlib/console.km +49 -0
- package/stdlib/date.km +97 -0
- package/stdlib/function.km +44 -0
- package/stdlib/http.km +197 -0
- package/stdlib/http.md +333 -0
- package/stdlib/index.km +26 -0
- package/stdlib/json.km +17 -0
- package/stdlib/logger.js +114 -0
- package/stdlib/logger.km +104 -0
- package/stdlib/math.km +120 -0
- package/stdlib/object.km +41 -0
- package/stdlib/promise.km +33 -0
- package/stdlib/string.km +93 -0
- package/stdlib/testing.md +265 -0
- package/test/test.js +599 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// KimchiLang - A modern programming language that transpiles to JavaScript
|
|
2
|
+
|
|
3
|
+
import { Lexer, tokenize } from './lexer.js';
|
|
4
|
+
import { Parser, parse } from './parser.js';
|
|
5
|
+
import { CodeGenerator, generate } from './generator.js';
|
|
6
|
+
import { TypeChecker } from './typechecker.js';
|
|
7
|
+
import { Linter, Severity } from './linter.js';
|
|
8
|
+
|
|
9
|
+
// Module registry for tracking required args across modules
|
|
10
|
+
const moduleRegistry = new Map();
|
|
11
|
+
|
|
12
|
+
export class KimchiCompiler {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Register a module's required args for cross-module validation
|
|
18
|
+
static registerModule(modulePath, requiredArgs) {
|
|
19
|
+
moduleRegistry.set(modulePath, requiredArgs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get required args for a module
|
|
23
|
+
static getModuleRequiredArgs(modulePath) {
|
|
24
|
+
return moduleRegistry.get(modulePath) || [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
compile(source, modulePath = null) {
|
|
28
|
+
// Step 1: Tokenize
|
|
29
|
+
const tokens = tokenize(source);
|
|
30
|
+
|
|
31
|
+
if (this.options.debug) {
|
|
32
|
+
console.log('Tokens:', tokens);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Step 2: Parse
|
|
36
|
+
const ast = parse(tokens);
|
|
37
|
+
|
|
38
|
+
if (this.options.debug) {
|
|
39
|
+
console.log('AST:', JSON.stringify(ast, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Step 2.5: Extract and register required args for this module
|
|
43
|
+
const argDeclarations = ast.body.filter(stmt => stmt.type === 'ArgDeclaration');
|
|
44
|
+
const requiredArgs = argDeclarations.filter(a => a.required).map(a => a.name);
|
|
45
|
+
if (modulePath) {
|
|
46
|
+
KimchiCompiler.registerModule(modulePath, requiredArgs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 2.6: Validate dep calls against registered modules and detect static files
|
|
50
|
+
const depStatements = ast.body.filter(stmt => stmt.type === 'DepStatement');
|
|
51
|
+
for (const dep of depStatements) {
|
|
52
|
+
// Check if this dependency is a static file
|
|
53
|
+
// Static files have .static extension and are detected by the staticFileResolver option
|
|
54
|
+
if (this.options.staticFileResolver) {
|
|
55
|
+
dep.isStatic = this.options.staticFileResolver(dep.path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Skip validation for static files (they don't have factory functions)
|
|
59
|
+
if (dep.isStatic) continue;
|
|
60
|
+
|
|
61
|
+
const depRequiredArgs = KimchiCompiler.getModuleRequiredArgs(dep.path);
|
|
62
|
+
if (depRequiredArgs.length > 0) {
|
|
63
|
+
// Check if all required args are provided in the overrides
|
|
64
|
+
const providedArgs = dep.overrides ? this.extractProvidedArgs(dep.overrides) : [];
|
|
65
|
+
for (const requiredArg of depRequiredArgs) {
|
|
66
|
+
if (!providedArgs.includes(requiredArg)) {
|
|
67
|
+
throw new Error(`Compile Error: Required argument '${requiredArg}' not provided for dependency '${dep.path}'`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Step 2.7: Type checking
|
|
74
|
+
if (!this.options.skipTypeCheck) {
|
|
75
|
+
const typeChecker = new TypeChecker({ modulePath });
|
|
76
|
+
const typeErrors = typeChecker.check(ast);
|
|
77
|
+
if (typeErrors.length > 0) {
|
|
78
|
+
const errorMessages = typeErrors.map(e => `Type Error: ${e.message}`).join('\n');
|
|
79
|
+
throw new Error(errorMessages);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Step 2.8: Linting
|
|
84
|
+
if (!this.options.skipLint) {
|
|
85
|
+
const linter = new Linter(this.options.lintOptions || {});
|
|
86
|
+
const lintMessages = linter.lint(ast, source);
|
|
87
|
+
|
|
88
|
+
// Collect lint errors (not warnings/info)
|
|
89
|
+
const lintErrors = lintMessages.filter(m => m.severity === Severity.Error);
|
|
90
|
+
if (lintErrors.length > 0) {
|
|
91
|
+
const errorMessages = lintErrors.map(m => `Lint Error [${m.rule}]: ${m.message}`).join('\n');
|
|
92
|
+
throw new Error(errorMessages);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show warnings only if explicitly requested
|
|
96
|
+
if (this.options.showLintWarnings && lintMessages.length > 0) {
|
|
97
|
+
const warnings = lintMessages.filter(m => m.severity !== Severity.Error);
|
|
98
|
+
if (warnings.length > 0) {
|
|
99
|
+
console.error(linter.formatMessages());
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Step 3: Generate JavaScript
|
|
105
|
+
const javascript = generate(ast, this.options);
|
|
106
|
+
|
|
107
|
+
if (this.options.debug) {
|
|
108
|
+
console.log('Generated JavaScript:', javascript);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return javascript;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
extractProvidedArgs(overrides) {
|
|
115
|
+
if (overrides.type !== 'ObjectExpression') return [];
|
|
116
|
+
return overrides.properties.map(p => {
|
|
117
|
+
// Key is stored as a string directly in the parser
|
|
118
|
+
if (typeof p.key === 'string') return p.key;
|
|
119
|
+
if (p.key.type === 'Identifier') return p.key.name;
|
|
120
|
+
if (p.key.type === 'Literal') return p.key.value;
|
|
121
|
+
return null;
|
|
122
|
+
}).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
run(source) {
|
|
126
|
+
const javascript = this.compile(source);
|
|
127
|
+
return eval(javascript);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { tokenize, parse, generate };
|
|
132
|
+
|
|
133
|
+
export function compile(source, options = {}) {
|
|
134
|
+
const compiler = new KimchiCompiler(options);
|
|
135
|
+
return compiler.compile(source);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function run(source, options = {}) {
|
|
139
|
+
const compiler = new KimchiCompiler(options);
|
|
140
|
+
return compiler.run(source);
|
|
141
|
+
}
|
package/src/js2km.js
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
// JavaScript to KimchiLang Converter
|
|
2
|
+
// Transforms JavaScript source code into KimchiLang syntax
|
|
3
|
+
|
|
4
|
+
import * as acorn from 'acorn';
|
|
5
|
+
|
|
6
|
+
export class JS2KM {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.indent = 0;
|
|
9
|
+
this.output = [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
convert(jsSource) {
|
|
13
|
+
const ast = acorn.parse(jsSource, {
|
|
14
|
+
ecmaVersion: 'latest',
|
|
15
|
+
sourceType: 'module',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
this.output = [];
|
|
19
|
+
this.indent = 0;
|
|
20
|
+
|
|
21
|
+
for (const node of ast.body) {
|
|
22
|
+
this.visitNode(node);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return this.output.join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
emit(line) {
|
|
29
|
+
const indentation = ' '.repeat(this.indent);
|
|
30
|
+
this.output.push(indentation + line);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
emitEmpty() {
|
|
34
|
+
this.output.push('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
visitNode(node) {
|
|
38
|
+
switch (node.type) {
|
|
39
|
+
case 'VariableDeclaration':
|
|
40
|
+
this.visitVariableDeclaration(node);
|
|
41
|
+
break;
|
|
42
|
+
case 'FunctionDeclaration':
|
|
43
|
+
this.visitFunctionDeclaration(node);
|
|
44
|
+
break;
|
|
45
|
+
case 'ClassDeclaration':
|
|
46
|
+
this.visitClassDeclaration(node);
|
|
47
|
+
break;
|
|
48
|
+
case 'ExpressionStatement':
|
|
49
|
+
this.visitExpressionStatement(node);
|
|
50
|
+
break;
|
|
51
|
+
case 'IfStatement':
|
|
52
|
+
this.visitIfStatement(node);
|
|
53
|
+
break;
|
|
54
|
+
case 'ForStatement':
|
|
55
|
+
this.visitForStatement(node);
|
|
56
|
+
break;
|
|
57
|
+
case 'ForOfStatement':
|
|
58
|
+
this.visitForOfStatement(node);
|
|
59
|
+
break;
|
|
60
|
+
case 'ForInStatement':
|
|
61
|
+
this.visitForInStatement(node);
|
|
62
|
+
break;
|
|
63
|
+
case 'WhileStatement':
|
|
64
|
+
this.visitWhileStatement(node);
|
|
65
|
+
break;
|
|
66
|
+
case 'ReturnStatement':
|
|
67
|
+
this.visitReturnStatement(node);
|
|
68
|
+
break;
|
|
69
|
+
case 'ThrowStatement':
|
|
70
|
+
this.visitThrowStatement(node);
|
|
71
|
+
break;
|
|
72
|
+
case 'TryStatement':
|
|
73
|
+
this.visitTryStatement(node);
|
|
74
|
+
break;
|
|
75
|
+
case 'BlockStatement':
|
|
76
|
+
this.visitBlockStatement(node);
|
|
77
|
+
break;
|
|
78
|
+
case 'ExportNamedDeclaration':
|
|
79
|
+
this.visitExportNamedDeclaration(node);
|
|
80
|
+
break;
|
|
81
|
+
case 'ExportDefaultDeclaration':
|
|
82
|
+
this.visitExportDefaultDeclaration(node);
|
|
83
|
+
break;
|
|
84
|
+
case 'ImportDeclaration':
|
|
85
|
+
this.visitImportDeclaration(node);
|
|
86
|
+
break;
|
|
87
|
+
default:
|
|
88
|
+
this.emit(`// Unsupported: ${node.type}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
visitVariableDeclaration(node, expose = false) {
|
|
93
|
+
for (const decl of node.declarations) {
|
|
94
|
+
const name = decl.id.name;
|
|
95
|
+
const init = decl.init ? this.visitExpression(decl.init) : 'null';
|
|
96
|
+
const prefix = expose ? 'expose ' : '';
|
|
97
|
+
|
|
98
|
+
// Handle require() calls - convert to dep statement
|
|
99
|
+
if (decl.init && decl.init.type === 'CallExpression' &&
|
|
100
|
+
decl.init.callee.name === 'require' &&
|
|
101
|
+
decl.init.arguments.length > 0) {
|
|
102
|
+
const modulePath = decl.init.arguments[0].value;
|
|
103
|
+
const depPath = modulePath.replace(/[\/\.]/g, '.').replace(/^\.+/, '');
|
|
104
|
+
this.emit(`as ${name} dep ${depPath}`);
|
|
105
|
+
} else {
|
|
106
|
+
this.emit(`${prefix}dec ${name} = ${init}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
visitFunctionDeclaration(node, expose = false) {
|
|
112
|
+
const name = node.id.name;
|
|
113
|
+
const params = node.params.map(p => this.visitPattern(p)).join(', ');
|
|
114
|
+
const prefix = expose ? 'expose ' : '';
|
|
115
|
+
|
|
116
|
+
this.emit(`${prefix}fn ${name}(${params}) {`);
|
|
117
|
+
this.indent++;
|
|
118
|
+
|
|
119
|
+
for (const stmt of node.body.body) {
|
|
120
|
+
this.visitNode(stmt);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.indent--;
|
|
124
|
+
this.emit('}');
|
|
125
|
+
this.emitEmpty();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
visitClassDeclaration(node, expose = false) {
|
|
129
|
+
const name = node.id.name;
|
|
130
|
+
const prefix = expose ? 'expose ' : '';
|
|
131
|
+
|
|
132
|
+
// Transform class to factory function
|
|
133
|
+
this.emit(`// Converted from class ${name}`);
|
|
134
|
+
this.emit(`${prefix}fn create${name}(${this.getConstructorParams(node)}) {`);
|
|
135
|
+
this.indent++;
|
|
136
|
+
|
|
137
|
+
// Create the object with methods
|
|
138
|
+
this.emit('return {');
|
|
139
|
+
this.indent++;
|
|
140
|
+
|
|
141
|
+
const methods = node.body.body.filter(m => m.type === 'MethodDefinition' && m.kind === 'method');
|
|
142
|
+
for (let i = 0; i < methods.length; i++) {
|
|
143
|
+
const method = methods[i];
|
|
144
|
+
const methodName = method.key.name;
|
|
145
|
+
const params = method.value.params.map(p => this.visitPattern(p)).join(', ');
|
|
146
|
+
|
|
147
|
+
this.emit(`${methodName}: fn(${params}) {`);
|
|
148
|
+
this.indent++;
|
|
149
|
+
|
|
150
|
+
for (const stmt of method.value.body.body) {
|
|
151
|
+
this.visitNode(this.transformThisReferences(stmt));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.indent--;
|
|
155
|
+
const comma = i < methods.length - 1 ? ',' : '';
|
|
156
|
+
this.emit(`}${comma}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.indent--;
|
|
160
|
+
this.emit('}');
|
|
161
|
+
|
|
162
|
+
this.indent--;
|
|
163
|
+
this.emit('}');
|
|
164
|
+
this.emitEmpty();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
getConstructorParams(classNode) {
|
|
168
|
+
const constructor = classNode.body.body.find(
|
|
169
|
+
m => m.type === 'MethodDefinition' && m.kind === 'constructor'
|
|
170
|
+
);
|
|
171
|
+
if (constructor) {
|
|
172
|
+
return constructor.value.params.map(p => this.visitPattern(p)).join(', ');
|
|
173
|
+
}
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
transformThisReferences(node) {
|
|
178
|
+
// Deep clone and transform this.x to x (captured from closure)
|
|
179
|
+
// For simplicity, we'll handle this in expression generation
|
|
180
|
+
return node;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
visitExpressionStatement(node) {
|
|
184
|
+
// Skip "use strict" directive
|
|
185
|
+
if (node.expression.type === 'Literal' && node.expression.value === 'use strict') {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check for module.exports = ... -> expose dec
|
|
190
|
+
if (node.expression.type === 'AssignmentExpression' &&
|
|
191
|
+
node.expression.left.type === 'MemberExpression' &&
|
|
192
|
+
node.expression.left.object.name === 'module' &&
|
|
193
|
+
node.expression.left.property.name === 'exports') {
|
|
194
|
+
const value = this.visitExpression(node.expression.right);
|
|
195
|
+
this.emit(`expose dec exports = ${value}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const expr = this.visitExpression(node.expression);
|
|
200
|
+
|
|
201
|
+
// Check for console.log -> print
|
|
202
|
+
if (node.expression.type === 'CallExpression' &&
|
|
203
|
+
node.expression.callee.type === 'MemberExpression' &&
|
|
204
|
+
node.expression.callee.object.name === 'console' &&
|
|
205
|
+
node.expression.callee.property.name === 'log') {
|
|
206
|
+
const args = node.expression.arguments.map(a => this.visitExpression(a)).join(' + ');
|
|
207
|
+
this.emit(`print ${args}`);
|
|
208
|
+
} else {
|
|
209
|
+
this.emit(expr);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
visitIfStatement(node) {
|
|
214
|
+
// Convert to pattern matching syntax
|
|
215
|
+
const test = this.visitExpression(node.test);
|
|
216
|
+
this.emit(`|${test}| => {`);
|
|
217
|
+
this.indent++;
|
|
218
|
+
|
|
219
|
+
if (node.consequent.type === 'BlockStatement') {
|
|
220
|
+
for (const stmt of node.consequent.body) {
|
|
221
|
+
this.visitNode(stmt);
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
this.visitNode(node.consequent);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.indent--;
|
|
228
|
+
this.emit('}');
|
|
229
|
+
|
|
230
|
+
if (node.alternate) {
|
|
231
|
+
if (node.alternate.type === 'IfStatement') {
|
|
232
|
+
this.visitIfStatement(node.alternate);
|
|
233
|
+
} else {
|
|
234
|
+
// else block - use a catch-all pattern
|
|
235
|
+
this.emit('|true| => {');
|
|
236
|
+
this.indent++;
|
|
237
|
+
if (node.alternate.type === 'BlockStatement') {
|
|
238
|
+
for (const stmt of node.alternate.body) {
|
|
239
|
+
this.visitNode(stmt);
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
this.visitNode(node.alternate);
|
|
243
|
+
}
|
|
244
|
+
this.indent--;
|
|
245
|
+
this.emit('}');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
visitForStatement(node) {
|
|
251
|
+
// Traditional for loop - convert to while
|
|
252
|
+
if (node.init) {
|
|
253
|
+
this.visitNode(node.init);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const test = node.test ? this.visitExpression(node.test) : 'true';
|
|
257
|
+
this.emit(`while ${test} {`);
|
|
258
|
+
this.indent++;
|
|
259
|
+
|
|
260
|
+
if (node.body.type === 'BlockStatement') {
|
|
261
|
+
for (const stmt of node.body.body) {
|
|
262
|
+
this.visitNode(stmt);
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
this.visitNode(node.body);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (node.update) {
|
|
269
|
+
this.emit(this.visitExpression(node.update));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
this.indent--;
|
|
273
|
+
this.emit('}');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
visitForOfStatement(node) {
|
|
277
|
+
const left = this.visitPattern(node.left.declarations ? node.left.declarations[0].id : node.left);
|
|
278
|
+
const right = this.visitExpression(node.right);
|
|
279
|
+
|
|
280
|
+
this.emit(`for ${left} in ${right} {`);
|
|
281
|
+
this.indent++;
|
|
282
|
+
|
|
283
|
+
if (node.body.type === 'BlockStatement') {
|
|
284
|
+
for (const stmt of node.body.body) {
|
|
285
|
+
this.visitNode(stmt);
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
this.visitNode(node.body);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
this.indent--;
|
|
292
|
+
this.emit('}');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
visitForInStatement(node) {
|
|
296
|
+
// for...in iterates over keys
|
|
297
|
+
const left = this.visitPattern(node.left.declarations ? node.left.declarations[0].id : node.left);
|
|
298
|
+
const right = this.visitExpression(node.right);
|
|
299
|
+
|
|
300
|
+
this.emit(`// Note: for...in iterates over keys`);
|
|
301
|
+
this.emit(`for ${left} in Object.keys(${right}) {`);
|
|
302
|
+
this.indent++;
|
|
303
|
+
|
|
304
|
+
if (node.body.type === 'BlockStatement') {
|
|
305
|
+
for (const stmt of node.body.body) {
|
|
306
|
+
this.visitNode(stmt);
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
this.visitNode(node.body);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.indent--;
|
|
313
|
+
this.emit('}');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
visitWhileStatement(node) {
|
|
317
|
+
const test = this.visitExpression(node.test);
|
|
318
|
+
this.emit(`while ${test} {`);
|
|
319
|
+
this.indent++;
|
|
320
|
+
|
|
321
|
+
if (node.body.type === 'BlockStatement') {
|
|
322
|
+
for (const stmt of node.body.body) {
|
|
323
|
+
this.visitNode(stmt);
|
|
324
|
+
}
|
|
325
|
+
} else {
|
|
326
|
+
this.visitNode(node.body);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.indent--;
|
|
330
|
+
this.emit('}');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
visitReturnStatement(node) {
|
|
334
|
+
if (node.argument) {
|
|
335
|
+
this.emit(`return ${this.visitExpression(node.argument)}`);
|
|
336
|
+
} else {
|
|
337
|
+
this.emit('return');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
visitThrowStatement(node) {
|
|
342
|
+
this.emit(`throw ${this.visitExpression(node.argument)}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
visitTryStatement(node) {
|
|
346
|
+
this.emit('try {');
|
|
347
|
+
this.indent++;
|
|
348
|
+
|
|
349
|
+
for (const stmt of node.block.body) {
|
|
350
|
+
this.visitNode(stmt);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
this.indent--;
|
|
354
|
+
|
|
355
|
+
if (node.handler) {
|
|
356
|
+
const param = node.handler.param ? node.handler.param.name : 'e';
|
|
357
|
+
this.emit(`} catch(${param}) {`);
|
|
358
|
+
this.indent++;
|
|
359
|
+
|
|
360
|
+
for (const stmt of node.handler.body.body) {
|
|
361
|
+
this.visitNode(stmt);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.indent--;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (node.finalizer) {
|
|
368
|
+
this.emit('} finally {');
|
|
369
|
+
this.indent++;
|
|
370
|
+
|
|
371
|
+
for (const stmt of node.finalizer.body) {
|
|
372
|
+
this.visitNode(stmt);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.indent--;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
this.emit('}');
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
visitBlockStatement(node) {
|
|
382
|
+
this.emit('{');
|
|
383
|
+
this.indent++;
|
|
384
|
+
|
|
385
|
+
for (const stmt of node.body) {
|
|
386
|
+
this.visitNode(stmt);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.indent--;
|
|
390
|
+
this.emit('}');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
visitExportNamedDeclaration(node) {
|
|
394
|
+
if (node.declaration) {
|
|
395
|
+
if (node.declaration.type === 'FunctionDeclaration') {
|
|
396
|
+
this.visitFunctionDeclaration(node.declaration, true);
|
|
397
|
+
} else if (node.declaration.type === 'VariableDeclaration') {
|
|
398
|
+
this.visitVariableDeclaration(node.declaration, true);
|
|
399
|
+
} else if (node.declaration.type === 'ClassDeclaration') {
|
|
400
|
+
this.visitClassDeclaration(node.declaration, true);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
visitExportDefaultDeclaration(node) {
|
|
406
|
+
this.emit('// Default export');
|
|
407
|
+
if (node.declaration.type === 'FunctionDeclaration') {
|
|
408
|
+
this.visitFunctionDeclaration(node.declaration, true);
|
|
409
|
+
} else if (node.declaration.type === 'ClassDeclaration') {
|
|
410
|
+
this.visitClassDeclaration(node.declaration, true);
|
|
411
|
+
} else {
|
|
412
|
+
this.emit(`expose dec default = ${this.visitExpression(node.declaration)}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
visitImportDeclaration(node) {
|
|
417
|
+
const source = node.source.value;
|
|
418
|
+
|
|
419
|
+
for (const spec of node.specifiers) {
|
|
420
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
421
|
+
this.emit(`// Import: ${spec.local.name} from "${source}"`);
|
|
422
|
+
this.emit(`as ${spec.local.name} dep ${source.replace(/[\/\.]/g, '.')}`);
|
|
423
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
424
|
+
this.emit(`// Import: { ${spec.imported.name} } from "${source}"`);
|
|
425
|
+
this.emit(`as ${spec.local.name} dep ${source.replace(/[\/\.]/g, '.')}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
visitExpression(node) {
|
|
431
|
+
if (!node) return '';
|
|
432
|
+
|
|
433
|
+
switch (node.type) {
|
|
434
|
+
case 'Identifier':
|
|
435
|
+
return node.name;
|
|
436
|
+
|
|
437
|
+
case 'Literal':
|
|
438
|
+
if (typeof node.value === 'string') {
|
|
439
|
+
return `"${node.value}"`;
|
|
440
|
+
}
|
|
441
|
+
return String(node.value);
|
|
442
|
+
|
|
443
|
+
case 'TemplateLiteral':
|
|
444
|
+
return this.visitTemplateLiteral(node);
|
|
445
|
+
|
|
446
|
+
case 'BinaryExpression':
|
|
447
|
+
case 'LogicalExpression':
|
|
448
|
+
return `${this.visitExpression(node.left)} ${node.operator} ${this.visitExpression(node.right)}`;
|
|
449
|
+
|
|
450
|
+
case 'UnaryExpression':
|
|
451
|
+
return `${node.operator}${this.visitExpression(node.argument)}`;
|
|
452
|
+
|
|
453
|
+
case 'UpdateExpression':
|
|
454
|
+
if (node.prefix) {
|
|
455
|
+
return `${node.operator}${this.visitExpression(node.argument)}`;
|
|
456
|
+
}
|
|
457
|
+
return `${this.visitExpression(node.argument)}${node.operator}`;
|
|
458
|
+
|
|
459
|
+
case 'AssignmentExpression':
|
|
460
|
+
return `${this.visitExpression(node.left)} ${node.operator} ${this.visitExpression(node.right)}`;
|
|
461
|
+
|
|
462
|
+
case 'MemberExpression':
|
|
463
|
+
if (node.object.type === 'ThisExpression') {
|
|
464
|
+
// Transform this.x to just x (closure capture)
|
|
465
|
+
return node.property.name;
|
|
466
|
+
}
|
|
467
|
+
if (node.computed) {
|
|
468
|
+
return `${this.visitExpression(node.object)}[${this.visitExpression(node.property)}]`;
|
|
469
|
+
}
|
|
470
|
+
return `${this.visitExpression(node.object)}.${node.property.name}`;
|
|
471
|
+
|
|
472
|
+
case 'CallExpression':
|
|
473
|
+
const callee = this.visitExpression(node.callee);
|
|
474
|
+
const args = node.arguments.map(a => this.visitExpression(a)).join(', ');
|
|
475
|
+
return `${callee}(${args})`;
|
|
476
|
+
|
|
477
|
+
case 'NewExpression':
|
|
478
|
+
// Transform new X() to createX() or X()
|
|
479
|
+
const className = this.visitExpression(node.callee);
|
|
480
|
+
const newArgs = node.arguments.map(a => this.visitExpression(a)).join(', ');
|
|
481
|
+
return `create${className}(${newArgs})`;
|
|
482
|
+
|
|
483
|
+
case 'ArrayExpression':
|
|
484
|
+
const elements = node.elements.map(e => {
|
|
485
|
+
if (e && e.type === 'SpreadElement') {
|
|
486
|
+
return `...${this.visitExpression(e.argument)}`;
|
|
487
|
+
}
|
|
488
|
+
return e ? this.visitExpression(e) : '';
|
|
489
|
+
}).join(', ');
|
|
490
|
+
return `[${elements}]`;
|
|
491
|
+
|
|
492
|
+
case 'ObjectExpression':
|
|
493
|
+
const props = node.properties.map(p => {
|
|
494
|
+
if (p.type === 'SpreadElement') {
|
|
495
|
+
return `...${this.visitExpression(p.argument)}`;
|
|
496
|
+
}
|
|
497
|
+
const key = p.key.type === 'Identifier' ? p.key.name : this.visitExpression(p.key);
|
|
498
|
+
const value = this.visitExpression(p.value);
|
|
499
|
+
if (p.shorthand) {
|
|
500
|
+
return key;
|
|
501
|
+
}
|
|
502
|
+
return `${key}: ${value}`;
|
|
503
|
+
}).join(', ');
|
|
504
|
+
return `{ ${props} }`;
|
|
505
|
+
|
|
506
|
+
case 'ArrowFunctionExpression':
|
|
507
|
+
case 'FunctionExpression':
|
|
508
|
+
const fnParams = node.params.map(p => this.visitPattern(p)).join(', ');
|
|
509
|
+
if (node.body.type === 'BlockStatement') {
|
|
510
|
+
// Multi-line arrow function
|
|
511
|
+
let body = [];
|
|
512
|
+
for (const stmt of node.body.body) {
|
|
513
|
+
// Simplified - just get the expression
|
|
514
|
+
if (stmt.type === 'ReturnStatement' && stmt.argument) {
|
|
515
|
+
body.push(`return ${this.visitExpression(stmt.argument)}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return `fn(${fnParams}) { ${body.join('; ')} }`;
|
|
519
|
+
}
|
|
520
|
+
return `${fnParams} => ${this.visitExpression(node.body)}`;
|
|
521
|
+
|
|
522
|
+
case 'ConditionalExpression':
|
|
523
|
+
return `${this.visitExpression(node.test)} ? ${this.visitExpression(node.consequent)} : ${this.visitExpression(node.alternate)}`;
|
|
524
|
+
|
|
525
|
+
case 'ThisExpression':
|
|
526
|
+
return '/* this */';
|
|
527
|
+
|
|
528
|
+
case 'SpreadElement':
|
|
529
|
+
return `...${this.visitExpression(node.argument)}`;
|
|
530
|
+
|
|
531
|
+
case 'AwaitExpression':
|
|
532
|
+
return `await ${this.visitExpression(node.argument)}`;
|
|
533
|
+
|
|
534
|
+
default:
|
|
535
|
+
return `/* ${node.type} */`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
visitTemplateLiteral(node) {
|
|
540
|
+
let result = '"';
|
|
541
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
542
|
+
result += node.quasis[i].value.raw;
|
|
543
|
+
if (i < node.expressions.length) {
|
|
544
|
+
result += '" + ' + this.visitExpression(node.expressions[i]) + ' + "';
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
result += '"';
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
visitPattern(node) {
|
|
552
|
+
if (node.type === 'Identifier') {
|
|
553
|
+
return node.name;
|
|
554
|
+
}
|
|
555
|
+
if (node.type === 'AssignmentPattern') {
|
|
556
|
+
return `${node.left.name} = ${this.visitExpression(node.right)}`;
|
|
557
|
+
}
|
|
558
|
+
if (node.type === 'RestElement') {
|
|
559
|
+
return `...${node.argument.name}`;
|
|
560
|
+
}
|
|
561
|
+
return '/* pattern */';
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function convertJS(jsSource) {
|
|
566
|
+
const converter = new JS2KM();
|
|
567
|
+
return converter.convert(jsSource);
|
|
568
|
+
}
|