jslike 1.4.0 → 1.4.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/dist/esm/ast/nodes.js +236 -0
- package/dist/esm/cli/wang-run.js +89 -0
- package/dist/esm/cli/wang-validate.js +87 -0
- package/dist/esm/editor/monaco/index.js +234 -0
- package/dist/esm/editor/wang-prism.js +136 -0
- package/dist/esm/errors/enhanced-error.js +124 -0
- package/dist/esm/index.js +182 -0
- package/dist/esm/interpreter/index.js +201 -0
- package/dist/esm/interpreter/interpreter.js +2155 -0
- package/dist/esm/metadata/index.js +531 -0
- package/dist/esm/parser.js +6241 -0
- package/dist/esm/resolvers/memory.js +17 -0
- package/dist/esm/runtime/builtins.js +517 -0
- package/dist/esm/runtime/environment.js +85 -0
- package/dist/esm/validator/index.js +100 -0
- package/dist/index.cjs +10 -4
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +10 -4
- package/package.json +3 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Wang Language Definition for Prism.js
|
|
2
|
+
/* global Prism */
|
|
3
|
+
(function (Prism) {
|
|
4
|
+
Prism.languages.wang = {
|
|
5
|
+
// Comments
|
|
6
|
+
comment: [
|
|
7
|
+
{
|
|
8
|
+
pattern: /\/\*[\s\S]*?\*\//,
|
|
9
|
+
greedy: true,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
pattern: /\/\/.*/,
|
|
13
|
+
greedy: true,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
// Template literals with interpolation
|
|
18
|
+
'template-string': {
|
|
19
|
+
pattern: /`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,
|
|
20
|
+
greedy: true,
|
|
21
|
+
inside: {
|
|
22
|
+
'template-punctuation': {
|
|
23
|
+
pattern: /^`|`$/,
|
|
24
|
+
alias: 'string',
|
|
25
|
+
},
|
|
26
|
+
interpolation: {
|
|
27
|
+
pattern: /((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,
|
|
28
|
+
lookbehind: true,
|
|
29
|
+
inside: {
|
|
30
|
+
'interpolation-punctuation': {
|
|
31
|
+
pattern: /^\$\{|\}$/,
|
|
32
|
+
alias: 'punctuation',
|
|
33
|
+
},
|
|
34
|
+
rest: Prism.languages.wang,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
string: /[\s\S]+/,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Strings
|
|
42
|
+
string: [
|
|
43
|
+
{
|
|
44
|
+
pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
|
|
45
|
+
greedy: true,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
|
|
49
|
+
// Regular expressions
|
|
50
|
+
regex: {
|
|
51
|
+
pattern: /\/(?:\\.|[^\/\\\r\n])+\/[gimsuy]*/,
|
|
52
|
+
greedy: true,
|
|
53
|
+
inside: {
|
|
54
|
+
'regex-delimiter': {
|
|
55
|
+
pattern: /^\/|\/[gimsuy]*$/,
|
|
56
|
+
alias: 'punctuation',
|
|
57
|
+
},
|
|
58
|
+
'regex-flags': {
|
|
59
|
+
pattern: /[gimsuy]+$/,
|
|
60
|
+
alias: 'keyword',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Keywords
|
|
66
|
+
keyword:
|
|
67
|
+
/\b(?:let|const|var|if|else|for|while|do|break|continue|return|function|class|extends|constructor|async|await|import|export|from|as|try|catch|finally|throw|new|this|super|typeof|instanceof|in|of)\b/,
|
|
68
|
+
|
|
69
|
+
// Boolean and null values
|
|
70
|
+
boolean: /\b(?:true|false)\b/,
|
|
71
|
+
null: /\b(?:null|undefined)\b/,
|
|
72
|
+
|
|
73
|
+
// Numbers
|
|
74
|
+
number: /\b(?:0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/,
|
|
75
|
+
|
|
76
|
+
// Arrow function operator
|
|
77
|
+
'arrow-operator': {
|
|
78
|
+
pattern: /=>/,
|
|
79
|
+
alias: 'operator',
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Operators (order matters for matching)
|
|
83
|
+
operator:
|
|
84
|
+
/\+\+|--|===|!==|==|!=|<=|>=|<<|>>>|>>|&&|\|\||[?]{2}|\?\.|\.{3}|\*\*|\+=|-=|\*=|\/=|[+\-*/%<>&|^~!?:]=?/,
|
|
85
|
+
|
|
86
|
+
// Punctuation
|
|
87
|
+
punctuation: /[{}[\];(),.:]/,
|
|
88
|
+
|
|
89
|
+
// Functions (pattern for function calls)
|
|
90
|
+
function: /\b[a-zA-Z_$][\w$]*(?=\s*\()/,
|
|
91
|
+
|
|
92
|
+
// Class names (after class or extends)
|
|
93
|
+
'class-name': {
|
|
94
|
+
pattern: /(\b(?:class|extends)\s+)[a-zA-Z_$][\w$]*/,
|
|
95
|
+
lookbehind: true,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// Property names in object literals
|
|
99
|
+
property: {
|
|
100
|
+
pattern:
|
|
101
|
+
/((?:^|[,{])[ \t]*)(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*|"(?:\\.|[^\\"\r\n])*"|'(?:\\.|[^\\'\r\n])*')(?=\s*:)/m,
|
|
102
|
+
lookbehind: true,
|
|
103
|
+
greedy: true,
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// Variables and identifiers (must come last)
|
|
107
|
+
variable: /\b[a-zA-Z_$][\w$]*/,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Set wang as an alias for javascript-like highlighting
|
|
111
|
+
Prism.languages.wang['class-name'].pattern = /(\b(?:class|extends|new)\s+)[a-zA-Z_$][\w$]*/;
|
|
112
|
+
|
|
113
|
+
// Hook to handle special Wang syntax
|
|
114
|
+
Prism.hooks.add('before-tokenize', function (env) {
|
|
115
|
+
if (env.language !== 'wang') {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Hook to add special classes for Wang-specific features
|
|
121
|
+
Prism.hooks.add('wrap', function (env) {
|
|
122
|
+
if (env.language !== 'wang') {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add special class for Wang keywords
|
|
127
|
+
if (env.type === 'keyword' && ['import', 'export', 'from', 'as'].includes(env.content)) {
|
|
128
|
+
env.classes.push('wang-module');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
})(Prism);
|
|
132
|
+
|
|
133
|
+
// Export for module systems
|
|
134
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
135
|
+
module.exports = Prism.languages.wang;
|
|
136
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced error classes for better developer experience
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Simple Levenshtein distance for "did you mean" suggestions
|
|
6
|
+
function levenshteinDistance(a, b) {
|
|
7
|
+
const matrix = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i <= b.length; i++) {
|
|
10
|
+
matrix[i] = [i];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (let j = 0; j <= a.length; j++) {
|
|
14
|
+
matrix[0][j] = j;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let i = 1; i <= b.length; i++) {
|
|
18
|
+
for (let j = 1; j <= a.length; j++) {
|
|
19
|
+
if (b.charAt(i - 1) === a.charAt(j - 1)) {
|
|
20
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
21
|
+
} else {
|
|
22
|
+
matrix[i][j] = Math.min(
|
|
23
|
+
matrix[i - 1][j - 1] + 1,
|
|
24
|
+
matrix[i][j - 1] + 1,
|
|
25
|
+
matrix[i - 1][j] + 1
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return matrix[b.length][a.length];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Find similar strings for suggestions
|
|
35
|
+
function findSimilar(target, candidates, maxDistance = 3) {
|
|
36
|
+
const suggestions = [];
|
|
37
|
+
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
const distance = levenshteinDistance(target.toLowerCase(), candidate.toLowerCase());
|
|
40
|
+
if (distance <= maxDistance && distance > 0) {
|
|
41
|
+
suggestions.push({ name: candidate, distance });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return suggestions
|
|
46
|
+
.sort((a, b) => a.distance - b.distance)
|
|
47
|
+
.slice(0, 3)
|
|
48
|
+
.map(s => s.name);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Get available methods/properties on an object
|
|
52
|
+
function getAvailableMethods(obj) {
|
|
53
|
+
if (obj === null || obj === undefined) return [];
|
|
54
|
+
|
|
55
|
+
const methods = new Set();
|
|
56
|
+
|
|
57
|
+
// Get own properties
|
|
58
|
+
Object.getOwnPropertyNames(obj).forEach(name => {
|
|
59
|
+
if (typeof obj[name] === 'function') {
|
|
60
|
+
methods.add(name);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Get prototype methods
|
|
65
|
+
let proto = Object.getPrototypeOf(obj);
|
|
66
|
+
while (proto && proto !== Object.prototype) {
|
|
67
|
+
Object.getOwnPropertyNames(proto).forEach(name => {
|
|
68
|
+
if (typeof proto[name] === 'function' && name !== 'constructor') {
|
|
69
|
+
methods.add(name);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
proto = Object.getPrototypeOf(proto);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Array.from(methods).sort();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class EnhancedTypeError extends TypeError {
|
|
79
|
+
constructor(message, context = {}) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = 'TypeError';
|
|
82
|
+
this.objectName = context.objectName || 'object';
|
|
83
|
+
this.methodName = context.methodName || 'method';
|
|
84
|
+
this.objectValue = context.objectValue;
|
|
85
|
+
this.availableMethods = context.availableMethods || [];
|
|
86
|
+
this.suggestions = context.suggestions || [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getFormattedMessage() {
|
|
90
|
+
let formatted = this.message;
|
|
91
|
+
|
|
92
|
+
// Add available methods
|
|
93
|
+
if (this.availableMethods.length > 0) {
|
|
94
|
+
const methodList = this.availableMethods.slice(0, 10).join(', ');
|
|
95
|
+
const more = this.availableMethods.length > 10
|
|
96
|
+
? ` (and ${this.availableMethods.length - 10} more)`
|
|
97
|
+
: '';
|
|
98
|
+
formatted += `\n\nAvailable methods on '${this.objectName}': ${methodList}${more}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add suggestions
|
|
102
|
+
if (this.suggestions.length > 0) {
|
|
103
|
+
formatted += `\n\nDid you mean: ${this.suggestions.join(', ')}?`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return formatted;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createMethodNotFoundError(objectName, methodName, objectValue) {
|
|
111
|
+
const availableMethods = getAvailableMethods(objectValue);
|
|
112
|
+
const suggestions = findSimilar(methodName, availableMethods);
|
|
113
|
+
|
|
114
|
+
const message = `TypeError when calling method '${methodName}' on object '${objectName}': ` +
|
|
115
|
+
`Expected: function, Received: undefined`;
|
|
116
|
+
|
|
117
|
+
return new EnhancedTypeError(message, {
|
|
118
|
+
objectName,
|
|
119
|
+
methodName,
|
|
120
|
+
objectValue,
|
|
121
|
+
availableMethods,
|
|
122
|
+
suggestions
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Use bundled Acorn parser for zero runtime dependencies
|
|
2
|
+
import { parse as acornParse } from './parser.js';
|
|
3
|
+
import { Interpreter } from './interpreter/interpreter.js';
|
|
4
|
+
import { Environment, ReturnValue } from './runtime/environment.js';
|
|
5
|
+
import { createGlobalEnvironment } from './runtime/builtins.js';
|
|
6
|
+
|
|
7
|
+
// Helper to detect if code contains module syntax or top-level await
|
|
8
|
+
function containsModuleSyntax(code) {
|
|
9
|
+
// Trigger module mode for:
|
|
10
|
+
// 1. import/export statements
|
|
11
|
+
// 2. Top-level await (await not inside a function)
|
|
12
|
+
if (/^\s*(import|export)\s+/m.test(code)) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check for top-level await (simple heuristic: await at start of line or after statement)
|
|
17
|
+
// This isn't perfect but handles common cases, including await in template literals
|
|
18
|
+
// Also matches await after operators like || or && or = or inside parentheses
|
|
19
|
+
if (/^await\s+/m.test(code) || /[;\n{(|&=]\s*await\s+/.test(code)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
export function parse(code, options = {}) {
|
|
28
|
+
// Determine sourceType: use 'module' ONLY if explicitly requested or if code has imports/exports
|
|
29
|
+
// Default to 'script' for better compatibility with labeled statements
|
|
30
|
+
let sourceType = options.sourceType || 'script';
|
|
31
|
+
if (!options.sourceType && containsModuleSyntax(code)) {
|
|
32
|
+
sourceType = 'module';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Parse with Acorn
|
|
36
|
+
try {
|
|
37
|
+
return acornParse(code, {
|
|
38
|
+
ecmaVersion: 2022, // Support ES2022 features (including top-level await)
|
|
39
|
+
sourceType: sourceType,
|
|
40
|
+
locations: true, // Track source locations for better error messages
|
|
41
|
+
allowReturnOutsideFunction: true // Allow top-level return statements
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Reformat error message for consistency
|
|
45
|
+
throw new SyntaxError(
|
|
46
|
+
`Parse error at line ${error.loc?.line || '?'}: ${error.message}`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper to detect if AST contains import/export declarations
|
|
52
|
+
function containsModuleDeclarations(node) {
|
|
53
|
+
if (!node || typeof node !== 'object') return false;
|
|
54
|
+
|
|
55
|
+
if (node.type === 'ImportDeclaration' ||
|
|
56
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
57
|
+
node.type === 'ExportDefaultDeclaration' ||
|
|
58
|
+
node.type === 'ExportAllDeclaration') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check Program body for module declarations
|
|
63
|
+
if (node.type === 'Program' && node.body) {
|
|
64
|
+
for (const statement of node.body) {
|
|
65
|
+
if (containsModuleDeclarations(statement)) return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Helper to detect if AST contains top-level await expressions
|
|
73
|
+
function containsTopLevelAwait(node) {
|
|
74
|
+
if (!node || typeof node !== 'object') return false;
|
|
75
|
+
|
|
76
|
+
if (node.type === 'AwaitExpression') return true;
|
|
77
|
+
|
|
78
|
+
// Don't recurse into function bodies (they handle their own await)
|
|
79
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check all properties
|
|
84
|
+
for (const key in node) {
|
|
85
|
+
if (key === 'loc' || key === 'range') continue;
|
|
86
|
+
const value = node[key];
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const item of value) {
|
|
89
|
+
if (containsTopLevelAwait(item)) return true;
|
|
90
|
+
}
|
|
91
|
+
} else if (typeof value === 'object') {
|
|
92
|
+
if (containsTopLevelAwait(value)) return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function execute(code, env = null, options = {}) {
|
|
100
|
+
// Parse the code
|
|
101
|
+
const ast = parse(code, options);
|
|
102
|
+
|
|
103
|
+
// Create global environment if not provided
|
|
104
|
+
if (!env) {
|
|
105
|
+
env = createGlobalEnvironment(new Environment());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create interpreter with module resolver and abort signal if provided
|
|
109
|
+
const interpreter = new Interpreter(env, {
|
|
110
|
+
moduleResolver: options.moduleResolver,
|
|
111
|
+
abortSignal: options.abortSignal
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Use async evaluation if:
|
|
115
|
+
// 1. Explicitly requested module mode
|
|
116
|
+
// 2. AST contains import/export declarations
|
|
117
|
+
// 3. Code contains top-level await
|
|
118
|
+
const needsAsync = options.sourceType === 'module' ||
|
|
119
|
+
containsModuleDeclarations(ast) ||
|
|
120
|
+
containsTopLevelAwait(ast);
|
|
121
|
+
|
|
122
|
+
if (needsAsync) {
|
|
123
|
+
const result = await interpreter.evaluateAsync(ast, env);
|
|
124
|
+
return result instanceof ReturnValue ? result.value : result;
|
|
125
|
+
} else {
|
|
126
|
+
const result = interpreter.evaluate(ast, env);
|
|
127
|
+
return result instanceof ReturnValue ? result.value : result;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createEnvironment() {
|
|
132
|
+
return createGlobalEnvironment(new Environment());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Export utility functions for CLI tools
|
|
136
|
+
export const isTopLevelAwait = containsModuleSyntax;
|
|
137
|
+
|
|
138
|
+
export { Interpreter } from './interpreter/interpreter.js';
|
|
139
|
+
export { Environment } from './runtime/environment.js';
|
|
140
|
+
export { WangInterpreter, InMemoryModuleResolver } from './interpreter/index.js';
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Abstract base class for module resolution
|
|
144
|
+
* Extend this class to implement custom module loading strategies
|
|
145
|
+
*/
|
|
146
|
+
export class ModuleResolver {
|
|
147
|
+
/**
|
|
148
|
+
* Resolve a module and return its code
|
|
149
|
+
* @param {string} modulePath - The module path to resolve
|
|
150
|
+
* @param {string} [fromPath] - The path of the importing module
|
|
151
|
+
* @returns {Promise<ModuleResolution>} The resolved module
|
|
152
|
+
*/
|
|
153
|
+
async resolve(modulePath, fromPath) {
|
|
154
|
+
throw new Error('ModuleResolver.resolve() must be implemented by subclass');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if a module exists
|
|
159
|
+
* @param {string} modulePath - The module path to check
|
|
160
|
+
* @param {string} [fromPath] - The path of the importing module
|
|
161
|
+
* @returns {Promise<boolean>} Whether the module exists
|
|
162
|
+
*/
|
|
163
|
+
async exists(modulePath, fromPath) {
|
|
164
|
+
throw new Error('ModuleResolver.exists() must be implemented by subclass');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* List available modules
|
|
169
|
+
* @param {string} [prefix] - Optional prefix to filter modules
|
|
170
|
+
* @returns {Promise<string[]>} List of module paths
|
|
171
|
+
*/
|
|
172
|
+
async list(prefix) {
|
|
173
|
+
throw new Error('ModuleResolver.list() must be implemented by subclass');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @typedef {Object} ModuleResolution
|
|
179
|
+
* @property {string} code - The module source code
|
|
180
|
+
* @property {string} path - The resolved module path
|
|
181
|
+
* @property {any} [metadata] - Optional metadata about the module
|
|
182
|
+
*/
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WangInterpreter compatibility adapter for JSLike
|
|
3
|
+
* Maps Wang's interpreter API to JSLike's API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execute, createEnvironment } from '../index.js';
|
|
7
|
+
|
|
8
|
+
export class WangInterpreter {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.moduleResolver = options.moduleResolver;
|
|
12
|
+
this.functions = options.functions || {};
|
|
13
|
+
// Create a persistent environment for setVariable support
|
|
14
|
+
this.persistentEnv = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setVariable(name, value) {
|
|
18
|
+
// If persistent environment doesn't exist, create it
|
|
19
|
+
if (!this.persistentEnv) {
|
|
20
|
+
this.persistentEnv = createEnvironment();
|
|
21
|
+
|
|
22
|
+
// Add custom functions
|
|
23
|
+
for (const [funcName, func] of Object.entries(this.functions)) {
|
|
24
|
+
if (this.persistentEnv.has(funcName)) {
|
|
25
|
+
this.persistentEnv.set(funcName, func);
|
|
26
|
+
} else {
|
|
27
|
+
this.persistentEnv.define(funcName, func);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Set the variable in persistent environment
|
|
33
|
+
if (this.persistentEnv.has(name)) {
|
|
34
|
+
this.persistentEnv.set(name, value);
|
|
35
|
+
} else {
|
|
36
|
+
this.persistentEnv.define(name, value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Alias for setVariable - specifically for binding functions
|
|
41
|
+
bindFunction(name, func) {
|
|
42
|
+
return this.setVariable(name, func);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Expose global context for accessing variables after execution
|
|
46
|
+
get globalContext() {
|
|
47
|
+
return {
|
|
48
|
+
variables: this.persistentEnv?.vars || new Map()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
createExecutionEnvironment() {
|
|
53
|
+
// Create fresh environment for each execution
|
|
54
|
+
const env = createEnvironment();
|
|
55
|
+
|
|
56
|
+
// Add custom functions to environment, overriding built-ins if needed
|
|
57
|
+
for (const [name, func] of Object.entries(this.functions)) {
|
|
58
|
+
// Use set() to override existing variables instead of define()
|
|
59
|
+
if (env.has(name)) {
|
|
60
|
+
env.set(name, func);
|
|
61
|
+
} else {
|
|
62
|
+
env.define(name, func);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return env;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async execute(code, initialEnv = undefined, userOptions = {}) {
|
|
70
|
+
// Use provided environment, persistent environment, or create fresh one
|
|
71
|
+
const env = initialEnv || this.persistentEnv || this.createExecutionEnvironment();
|
|
72
|
+
|
|
73
|
+
// Setup console capture if withMetadata option is enabled
|
|
74
|
+
const withMetadata = userOptions.withMetadata || false;
|
|
75
|
+
const capturedLogs = [];
|
|
76
|
+
|
|
77
|
+
if (withMetadata) {
|
|
78
|
+
// Override log/warn/error functions to capture calls
|
|
79
|
+
const createCapture = (type) => {
|
|
80
|
+
return (...args) => {
|
|
81
|
+
capturedLogs.push({
|
|
82
|
+
type,
|
|
83
|
+
args,
|
|
84
|
+
timestamp: Date.now()
|
|
85
|
+
});
|
|
86
|
+
// Still output to console
|
|
87
|
+
console[type](...args);
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
env.set('log', createCapture('log'));
|
|
93
|
+
env.set('warn', createCapture('warn'));
|
|
94
|
+
env.set('error', createCapture('error'));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if code contains top-level return statements
|
|
98
|
+
// Need to detect `return` at start of line, but NOT inside function bodies
|
|
99
|
+
// Simple heuristic: check if braces are balanced before the return statement
|
|
100
|
+
const hasTopLevelReturn = this.hasTopLevelReturn(code);
|
|
101
|
+
|
|
102
|
+
// Prepare execution options
|
|
103
|
+
const options = {
|
|
104
|
+
moduleResolver: this.moduleResolver
|
|
105
|
+
// sourceType will be auto-detected from code
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let result;
|
|
109
|
+
if (hasTopLevelReturn) {
|
|
110
|
+
// Wrap code in async IIFE to support top-level return statements
|
|
111
|
+
const wrappedCode = `(async function() { ${code} })()`;
|
|
112
|
+
try {
|
|
113
|
+
result = await execute(wrappedCode, env, options);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Execute directly - JSLike returns last evaluated expression
|
|
119
|
+
try {
|
|
120
|
+
result = await execute(code, env, options);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Return result with metadata if requested
|
|
127
|
+
if (withMetadata) {
|
|
128
|
+
return {
|
|
129
|
+
result,
|
|
130
|
+
metadata: {
|
|
131
|
+
logs: capturedLogs
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
hasTopLevelReturn(code) {
|
|
140
|
+
const lines = code.split('\n');
|
|
141
|
+
let braceDepth = 0;
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
// Check if this line has a return statement at the start (ignoring whitespace)
|
|
145
|
+
const trimmed = line.trim();
|
|
146
|
+
if (trimmed.startsWith('return ') || trimmed === 'return') {
|
|
147
|
+
// If we're at brace depth 0, it's a top-level return
|
|
148
|
+
if (braceDepth === 0) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Count braces on this line (after checking for return)
|
|
154
|
+
for (const char of line) {
|
|
155
|
+
if (char === '{') braceDepth++;
|
|
156
|
+
if (char === '}') braceDepth--;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async executeModule(code) {
|
|
164
|
+
// Same as execute for now - modules are handled by execute()
|
|
165
|
+
return await this.execute(code);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export class InMemoryModuleResolver {
|
|
170
|
+
constructor() {
|
|
171
|
+
this.modules = new Map();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
addModule(name, code, metadata) {
|
|
175
|
+
this.modules.set(name, { code, metadata });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async resolve(modulePath, fromPath) {
|
|
179
|
+
const module = this.modules.get(modulePath);
|
|
180
|
+
if (!module) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
code: typeof module === 'string' ? module : module.code,
|
|
185
|
+
path: modulePath,
|
|
186
|
+
metadata: module.metadata
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async exists(modulePath, fromPath) {
|
|
191
|
+
return this.modules.has(modulePath);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async list(prefix) {
|
|
195
|
+
const paths = Array.from(this.modules.keys());
|
|
196
|
+
if (prefix) {
|
|
197
|
+
return paths.filter(p => p.startsWith(prefix));
|
|
198
|
+
}
|
|
199
|
+
return paths;
|
|
200
|
+
}
|
|
201
|
+
}
|