jslike 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js ADDED
@@ -0,0 +1,337 @@
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 } 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
+ // Pre-process code to fix common patterns that fail in strict mode
27
+ function preprocessCode(code) {
28
+ // Pattern: object literal at statement level with commas
29
+ // Example: let arr = [1,2,3]\n{ first: first(arr), last: last(arr) }
30
+ // This fails to parse because labeled statements can't have commas
31
+ // We need to wrap it in parentheses to make it an expression: ({ ... })
32
+
33
+ const lines = code.split('\n');
34
+ const result = [];
35
+
36
+ for (let i = 0; i < lines.length; i++) {
37
+ let line = lines[i];
38
+ const trimmed = line.trim();
39
+
40
+ // Handle array literals and parenthesized expressions at statement level
41
+ // Add semicolon to previous line to prevent ASI issues
42
+ // Example: let x = 10\n[a, b] parses as: let x = 10[a, b] without semicolon
43
+ // Example: let x = 2\n({ y: 3 }) parses as: let x = 2({ y: 3 }) without semicolon
44
+ // But don't add if previous line ends with an operator (continuing expression)
45
+ // Note: We DON'T handle standalone `{` here because it's too ambiguous (could be a block)
46
+ if ((trimmed.startsWith('[') || trimmed.startsWith('(')) && result.length > 0) {
47
+ // Skip back over empty lines and comments to find the last real statement
48
+ let targetIdx = result.length - 1;
49
+ while (targetIdx >= 0) {
50
+ const checkLine = result[targetIdx].trim();
51
+ if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
52
+ break;
53
+ }
54
+ targetIdx--;
55
+ }
56
+
57
+ if (targetIdx >= 0) {
58
+ const prevLine = result[targetIdx];
59
+ const prevTrimmed = prevLine.trim();
60
+ // Don't add semicolon if previous line ends with an operator, block opener, or already has semicolon
61
+ // But / at the end might be a regex literal, not division operator
62
+ const endsWithOperator = /[+\-*%&|^<>=!?:]$/.test(prevTrimmed);
63
+ // Check for regex literal at end: /pattern/flags
64
+ const endsWithRegex = /\/[gims]*$/.test(prevTrimmed);
65
+ // Don't add if previous line looks like a control statement (if, while, etc.)
66
+ const isControlStatement = /^\s*(if|else|for|while|do|try|catch|finally|switch|function|class)\b/.test(prevTrimmed);
67
+ if (prevTrimmed && !prevTrimmed.endsWith(';') && !prevTrimmed.endsWith('{') &&
68
+ !prevTrimmed.endsWith('(') && !prevTrimmed.endsWith(',') && !endsWithOperator && !isControlStatement) {
69
+ result[targetIdx] = prevLine + ';';
70
+ }
71
+ }
72
+ }
73
+
74
+ // Handle inline object literals at statement level: { key: value, key2: value2 }
75
+ // Pattern: line has {...} with comma and/or colon inside (object-like)
76
+ // This needs to be wrapped in parens to avoid being parsed as a block
77
+ const hasInlineBlock = trimmed.match(/^(.*?)\s*\{([^{}]+)\}\s*$/);
78
+ if (hasInlineBlock && !trimmed.match(/^\s*(if|else|for|while|do|try|catch|finally|switch|function|class)\b/)) {
79
+ const before = hasInlineBlock[1].trim();
80
+ const inside = hasInlineBlock[2];
81
+
82
+ // Check if inside looks like object literal (has comma and/or colon)
83
+ const hasComma = inside.includes(',');
84
+ const hasColon = inside.includes(':');
85
+
86
+ // If nothing before or just await/return, and inside has object-like syntax, wrap it
87
+ if ((!before || before.match(/^(await|return)$/)) && (hasComma || hasColon)) {
88
+ // Check if previous line indicates we're inside an array/object (don't wrap in that case)
89
+ if (result.length > 0) {
90
+ const prevLine = result[result.length - 1];
91
+ const prevTrimmed = prevLine.trim();
92
+ // Skip wrapping if inside array or object literal
93
+ if (prevTrimmed.endsWith(',') || prevTrimmed.endsWith('[') || prevTrimmed.endsWith('{')) {
94
+ result.push(line);
95
+ continue;
96
+ }
97
+ }
98
+
99
+ // Add semicolon to previous line to avoid ASI issues
100
+ // Skip back over empty lines and comments
101
+ if (result.length > 0) {
102
+ let targetIdx = result.length - 1;
103
+ while (targetIdx >= 0) {
104
+ const checkLine = result[targetIdx].trim();
105
+ if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
106
+ break;
107
+ }
108
+ targetIdx--;
109
+ }
110
+
111
+ if (targetIdx >= 0) {
112
+ const prevLine = result[targetIdx];
113
+ const prevTrimmed = prevLine.trim();
114
+ if (prevTrimmed && !prevTrimmed.endsWith(';') && !prevTrimmed.endsWith('{')) {
115
+ result[targetIdx] = prevLine + ';';
116
+ }
117
+ }
118
+ }
119
+
120
+ // Wrap just the {...} part
121
+ const objPart = trimmed.substring(trimmed.indexOf('{'));
122
+ const prefix = trimmed.substring(0, trimmed.indexOf('{'));
123
+ line = prefix + '(' + objPart + ')';
124
+ result.push(line);
125
+ continue;
126
+ }
127
+ }
128
+
129
+ // Check if this line starts a block that looks like an object literal
130
+ // BUT: don't wrap if previous line is a function declaration
131
+ if (trimmed === '{' && i < lines.length - 1) {
132
+ // Check if previous line is a function declaration
133
+ if (result.length > 0) {
134
+ const prevTrimmed = result[result.length - 1].trim();
135
+ if (prevTrimmed.match(/^\s*(async\s+)?function\s*\w*\s*\([^)]*\)\s*$/) ||
136
+ prevTrimmed.match(/\)\s*=>\s*$/) ||
137
+ prevTrimmed.match(/\s+(if|else|for|while|do|try|catch|finally|switch)\s*\([^)]*\)\s*$/)) {
138
+ // This is a function/control structure body, not an object literal
139
+ result.push(line);
140
+ continue;
141
+ }
142
+ }
143
+
144
+ // Look ahead to see if block contains colons and commas (object literal pattern)
145
+ let blockLines = [line];
146
+ let j = i + 1;
147
+ let braceCount = 1;
148
+ let hasColon = false;
149
+ let hasComma = false;
150
+
151
+ // Collect the block
152
+ while (j < lines.length && braceCount > 0) {
153
+ const nextLine = lines[j];
154
+ blockLines.push(nextLine);
155
+
156
+ // Count braces (simple heuristic, not perfect but good enough)
157
+ for (const char of nextLine) {
158
+ if (char === '{') braceCount++;
159
+ if (char === '}') braceCount--;
160
+ }
161
+
162
+ // Check for object literal indicators
163
+ if (nextLine.includes(':')) hasColon = true;
164
+ if (nextLine.includes(',')) hasComma = true;
165
+
166
+ j++;
167
+ if (braceCount === 0) break;
168
+ }
169
+
170
+ // If block looks like object literal (has both : and ,), wrap in parens
171
+ if (hasColon && hasComma) {
172
+ // Check if previous line needs a semicolon to avoid ASI issues
173
+ // (wrapping in parens can cause previous expression to be treated as function call)
174
+ // Skip back over empty lines and comments to find the last real statement
175
+ if (result.length > 0) {
176
+ let targetIdx = result.length - 1;
177
+ while (targetIdx >= 0) {
178
+ const checkLine = result[targetIdx].trim();
179
+ if (checkLine && !checkLine.startsWith('//') && !checkLine.startsWith('/*')) {
180
+ // Found a non-empty, non-comment line
181
+ break;
182
+ }
183
+ targetIdx--;
184
+ }
185
+
186
+ if (targetIdx >= 0) {
187
+ const targetLine = result[targetIdx];
188
+ const targetTrimmed = targetLine.trim();
189
+ // Add semicolon if line doesn't end with one
190
+ // Note: Even if it ends with }, we need semicolon (could be object literal)
191
+ // Only skip if it ends with ; or { (block statement opener)
192
+ if (targetTrimmed && !targetTrimmed.endsWith(';') && !targetTrimmed.endsWith('{')) {
193
+ result[targetIdx] = targetLine + ';';
194
+ }
195
+ }
196
+ }
197
+
198
+ result.push('(' + blockLines[0]);
199
+ for (let k = 1; k < blockLines.length - 1; k++) {
200
+ result.push(blockLines[k]);
201
+ }
202
+ // Add closing paren after closing brace
203
+ const lastLine = blockLines[blockLines.length - 1];
204
+ result.push(lastLine.replace('}', '})'));
205
+
206
+ // Skip the lines we already processed
207
+ i = j - 1;
208
+ continue;
209
+ }
210
+ }
211
+
212
+ result.push(line);
213
+ }
214
+
215
+ return result.join('\n');
216
+ }
217
+
218
+ export function parse(code, options = {}) {
219
+ // Pre-process code to handle patterns that fail in strict mode
220
+ const processedCode = preprocessCode(code);
221
+
222
+ // Determine sourceType: use 'module' ONLY if explicitly requested or if code has imports/exports
223
+ // Default to 'script' for better compatibility with labeled statements
224
+ let sourceType = options.sourceType || 'script';
225
+ if (!options.sourceType && containsModuleSyntax(processedCode)) {
226
+ sourceType = 'module';
227
+ }
228
+
229
+ // Parse with Acorn
230
+ try {
231
+ return acornParse(processedCode, {
232
+ ecmaVersion: 2022, // Support ES2022 features (including top-level await)
233
+ sourceType: sourceType,
234
+ locations: false // Don't track source locations (faster)
235
+ });
236
+ } catch (error) {
237
+ // Reformat error message for consistency
238
+ throw new SyntaxError(
239
+ `Parse error at line ${error.loc?.line || '?'}: ${error.message}`
240
+ );
241
+ }
242
+ }
243
+
244
+ // Helper to detect if AST contains import/export declarations
245
+ function containsModuleDeclarations(node) {
246
+ if (!node || typeof node !== 'object') return false;
247
+
248
+ if (node.type === 'ImportDeclaration' ||
249
+ node.type === 'ExportNamedDeclaration' ||
250
+ node.type === 'ExportDefaultDeclaration' ||
251
+ node.type === 'ExportAllDeclaration') {
252
+ return true;
253
+ }
254
+
255
+ // Check Program body for module declarations
256
+ if (node.type === 'Program' && node.body) {
257
+ for (const statement of node.body) {
258
+ if (containsModuleDeclarations(statement)) return true;
259
+ }
260
+ }
261
+
262
+ return false;
263
+ }
264
+
265
+ // Helper to detect if AST contains top-level await expressions
266
+ function containsTopLevelAwait(node) {
267
+ if (!node || typeof node !== 'object') return false;
268
+
269
+ if (node.type === 'AwaitExpression') return true;
270
+
271
+ // Don't recurse into function bodies (they handle their own await)
272
+ if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
273
+ return false;
274
+ }
275
+
276
+ // Check all properties
277
+ for (const key in node) {
278
+ if (key === 'loc' || key === 'range') continue;
279
+ const value = node[key];
280
+ if (Array.isArray(value)) {
281
+ for (const item of value) {
282
+ if (containsTopLevelAwait(item)) return true;
283
+ }
284
+ } else if (typeof value === 'object') {
285
+ if (containsTopLevelAwait(value)) return true;
286
+ }
287
+ }
288
+
289
+ return false;
290
+ }
291
+
292
+ export async function execute(code, env = null, options = {}) {
293
+ // Parse the code
294
+ const ast = parse(code, options);
295
+
296
+ // Create global environment if not provided
297
+ if (!env) {
298
+ env = createGlobalEnvironment(new Environment());
299
+ }
300
+
301
+ // Create a child environment for user code to allow shadowing of built-ins
302
+ // This prevents conflicts when user code declares variables with same names as stdlib functions
303
+ const userEnv = env.extend();
304
+
305
+ // Create interpreter with module resolver and abort signal if provided
306
+ const interpreter = new Interpreter(env, {
307
+ moduleResolver: options.moduleResolver,
308
+ abortSignal: options.abortSignal
309
+ });
310
+
311
+ // Use async evaluation if:
312
+ // 1. Explicitly requested module mode
313
+ // 2. AST contains import/export declarations
314
+ // 3. Code contains top-level await
315
+ const needsAsync = options.sourceType === 'module' ||
316
+ containsModuleDeclarations(ast) ||
317
+ containsTopLevelAwait(ast);
318
+
319
+ if (needsAsync) {
320
+ const result = await interpreter.evaluateAsync(ast, userEnv);
321
+ return result;
322
+ } else {
323
+ const result = interpreter.evaluate(ast, userEnv);
324
+ return result;
325
+ }
326
+ }
327
+
328
+ export function createEnvironment() {
329
+ return createGlobalEnvironment(new Environment());
330
+ }
331
+
332
+ // Export utility functions for CLI tools
333
+ export { preprocessCode };
334
+ export const isTopLevelAwait = containsModuleSyntax;
335
+
336
+ export { Interpreter } from './interpreter/interpreter.js';
337
+ export { Environment } from './runtime/environment.js';
@@ -0,0 +1,174 @@
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
+ createExecutionEnvironment() {
46
+ // Create fresh environment for each execution
47
+ const env = createEnvironment();
48
+
49
+ // Add custom functions to environment, overriding built-ins if needed
50
+ for (const [name, func] of Object.entries(this.functions)) {
51
+ // Use set() to override existing variables instead of define()
52
+ if (env.has(name)) {
53
+ env.set(name, func);
54
+ } else {
55
+ env.define(name, func);
56
+ }
57
+ }
58
+
59
+ return env;
60
+ }
61
+
62
+ async execute(code, initialEnv = undefined, userOptions = {}) {
63
+ // Use provided environment, persistent environment, or create fresh one
64
+ const env = initialEnv || this.persistentEnv || this.createExecutionEnvironment();
65
+
66
+ // Setup console capture if withMetadata option is enabled
67
+ const withMetadata = userOptions.withMetadata || false;
68
+ const capturedLogs = [];
69
+
70
+ if (withMetadata) {
71
+ // Override log/warn/error functions to capture calls
72
+ const createCapture = (type) => {
73
+ return (...args) => {
74
+ capturedLogs.push({
75
+ type,
76
+ args,
77
+ timestamp: Date.now()
78
+ });
79
+ // Still output to console
80
+ console[type](...args);
81
+ return undefined;
82
+ };
83
+ };
84
+
85
+ env.set('log', createCapture('log'));
86
+ env.set('warn', createCapture('warn'));
87
+ env.set('error', createCapture('error'));
88
+ }
89
+
90
+ // Check if code contains top-level return statements
91
+ // Need to detect `return` at start of line, but NOT inside function bodies
92
+ // Simple heuristic: check if braces are balanced before the return statement
93
+ const hasTopLevelReturn = this.hasTopLevelReturn(code);
94
+
95
+ // Prepare execution options
96
+ const options = {
97
+ moduleResolver: this.moduleResolver
98
+ // sourceType will be auto-detected from code
99
+ };
100
+
101
+ let result;
102
+ if (hasTopLevelReturn) {
103
+ // Wrap code in async IIFE to support top-level return statements
104
+ const wrappedCode = `(async function() { ${code} })()`;
105
+ try {
106
+ result = await execute(wrappedCode, env, options);
107
+ } catch (error) {
108
+ throw error;
109
+ }
110
+ } else {
111
+ // Execute directly - JSLike returns last evaluated expression
112
+ try {
113
+ result = await execute(code, env, options);
114
+ } catch (error) {
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ // Return result with metadata if requested
120
+ if (withMetadata) {
121
+ return {
122
+ result,
123
+ metadata: {
124
+ logs: capturedLogs
125
+ }
126
+ };
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ hasTopLevelReturn(code) {
133
+ const lines = code.split('\n');
134
+ let braceDepth = 0;
135
+
136
+ for (const line of lines) {
137
+ // Check if this line has a return statement at the start (ignoring whitespace)
138
+ const trimmed = line.trim();
139
+ if (trimmed.startsWith('return ') || trimmed === 'return') {
140
+ // If we're at brace depth 0, it's a top-level return
141
+ if (braceDepth === 0) {
142
+ return true;
143
+ }
144
+ }
145
+
146
+ // Count braces on this line (after checking for return)
147
+ for (const char of line) {
148
+ if (char === '{') braceDepth++;
149
+ if (char === '}') braceDepth--;
150
+ }
151
+ }
152
+
153
+ return false;
154
+ }
155
+
156
+ async executeModule(code) {
157
+ // Same as execute for now - modules are handled by execute()
158
+ return await this.execute(code);
159
+ }
160
+ }
161
+
162
+ export class InMemoryModuleResolver {
163
+ constructor() {
164
+ this.modules = new Map();
165
+ }
166
+
167
+ addModule(name, code) {
168
+ this.modules.set(name, code);
169
+ }
170
+
171
+ async resolve(name) {
172
+ return this.modules.get(name);
173
+ }
174
+ }