jslike 1.4.4 → 1.5.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/dist/esm/index.js CHANGED
@@ -38,7 +38,8 @@ export function parse(code, options = {}) {
38
38
  ecmaVersion: 2022, // Support ES2022 features (including top-level await)
39
39
  sourceType: sourceType,
40
40
  locations: true, // Track source locations for better error messages
41
- allowReturnOutsideFunction: true // Allow top-level return statements
41
+ allowReturnOutsideFunction: true, // Allow top-level return statements
42
+ allowAwaitOutsideFunction: true // Allow top-level await
42
43
  });
43
44
  } catch (error) {
44
45
  // Reformat error message for consistency
@@ -97,34 +98,59 @@ function containsTopLevelAwait(node) {
97
98
  }
98
99
 
99
100
  export async function execute(code, env = null, options = {}) {
100
- // Parse the code
101
- const ast = parse(code, options);
101
+ // Get execution controller if provided
102
+ const controller = options.executionController;
102
103
 
103
- // Create global environment if not provided
104
- if (!env) {
105
- env = createGlobalEnvironment(new Environment());
104
+ // Mark execution as starting
105
+ if (controller) {
106
+ controller._start();
106
107
  }
107
108
 
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;
109
+ try {
110
+ // Parse the code
111
+ const ast = parse(code, options);
112
+
113
+ // Create global environment if not provided
114
+ if (!env) {
115
+ env = createGlobalEnvironment(new Environment());
116
+ }
117
+
118
+ // Create interpreter with module resolver, abort signal, and execution controller
119
+ const interpreter = new Interpreter(env, {
120
+ moduleResolver: options.moduleResolver,
121
+ abortSignal: options.abortSignal,
122
+ executionController: controller
123
+ });
124
+
125
+ // Use async evaluation if:
126
+ // 1. Explicitly requested module mode
127
+ // 2. AST contains import/export declarations
128
+ // 3. Code contains top-level await
129
+ // 4. Execution controller provided (needs async for pause/resume)
130
+ const needsAsync = options.sourceType === 'module' ||
131
+ containsModuleDeclarations(ast) ||
132
+ containsTopLevelAwait(ast) ||
133
+ controller != null;
134
+
135
+ if (needsAsync) {
136
+ const result = await interpreter.evaluateAsync(ast, env);
137
+ if (controller) {
138
+ controller._complete();
139
+ }
140
+ return result instanceof ReturnValue ? result.value : result;
141
+ } else {
142
+ const result = interpreter.evaluate(ast, env);
143
+ if (controller) {
144
+ controller._complete();
145
+ }
146
+ return result instanceof ReturnValue ? result.value : result;
147
+ }
148
+ } catch (e) {
149
+ // Mark as aborted if that's the error type
150
+ if (controller && e.name === 'AbortError') {
151
+ controller.state = 'aborted';
152
+ }
153
+ throw e;
128
154
  }
129
155
  }
130
156
 
@@ -138,6 +164,7 @@ export const isTopLevelAwait = containsModuleSyntax;
138
164
  export { Interpreter } from './interpreter/interpreter.js';
139
165
  export { Environment } from './runtime/environment.js';
140
166
  export { WangInterpreter, InMemoryModuleResolver } from './interpreter/index.js';
167
+ export { ExecutionController } from './runtime/execution-controller.js';
141
168
 
142
169
  /**
143
170
  * Abstract base class for module resolution
@@ -101,7 +101,8 @@ export class WangInterpreter {
101
101
 
102
102
  // Prepare execution options
103
103
  const options = {
104
- moduleResolver: this.moduleResolver
104
+ moduleResolver: this.moduleResolver,
105
+ executionController: userOptions.executionController
105
106
  // sourceType will be auto-detected from code
106
107
  };
107
108
 
@@ -9,10 +9,17 @@ export class Interpreter {
9
9
  this.moduleCache = new Map(); // Cache loaded modules
10
10
  this.moduleExports = {}; // Track exports in current module
11
11
  this.abortSignal = options.abortSignal;
12
+ this.executionController = options.executionController;
12
13
  }
13
14
 
14
- // Check if execution should be aborted
15
+ // Check if execution should be aborted (sync version)
15
16
  checkAbortSignal() {
17
+ // Check controller first if available
18
+ if (this.executionController) {
19
+ this.executionController._checkAbortSync();
20
+ return;
21
+ }
22
+ // Fall back to legacy abortSignal
16
23
  if (this.abortSignal && this.abortSignal.aborted) {
17
24
  const error = new Error('The operation was aborted');
18
25
  error.name = 'AbortError';
@@ -20,12 +27,26 @@ export class Interpreter {
20
27
  }
21
28
  }
22
29
 
30
+ // Checkpoint that returns a promise only when controller is present
31
+ // When no controller, returns null to signal no await needed
32
+ _getCheckpointPromise(node, env) {
33
+ if (this.executionController) {
34
+ this.executionController._setEnv(env);
35
+ return this.executionController._checkpoint(node);
36
+ } else {
37
+ this.checkAbortSignal();
38
+ return null; // Signal that no await is needed
39
+ }
40
+ }
41
+
23
42
  // Async evaluation for async functions - handles await expressions
24
43
  async evaluateAsync(node, env) {
25
44
  if (!node) return undefined;
26
45
 
27
- // Check for abort signal before evaluating
28
- this.checkAbortSignal();
46
+ // Checkpoint - yields if paused, throws if aborted
47
+ // Only await when there's actually a promise (controller present)
48
+ const checkpointPromise = this._getCheckpointPromise(node, env);
49
+ if (checkpointPromise) await checkpointPromise;
29
50
 
30
51
  // Handle await expressions by actually awaiting the promise
31
52
  if (node.type === 'AwaitExpression') {
@@ -262,6 +283,9 @@ export class Interpreter {
262
283
  await this.evaluateAsync(node.init, forEnv);
263
284
  }
264
285
  while (!node.test || await this.evaluateAsync(node.test, forEnv)) {
286
+ // Checkpoint at each loop iteration (only await if controller present)
287
+ const cp1 = this._getCheckpointPromise(node, forEnv);
288
+ if (cp1) await cp1;
265
289
  const result = await this.evaluateAsync(node.body, forEnv);
266
290
  if (result instanceof BreakSignal) {
267
291
  break;
@@ -290,6 +314,9 @@ export class Interpreter {
290
314
  const isConst = node.left.kind === 'const';
291
315
 
292
316
  for (const value of iterable) {
317
+ // Checkpoint at each loop iteration (only await if controller present)
318
+ const cp2 = this._getCheckpointPromise(node, forEnv);
319
+ if (cp2) await cp2;
293
320
  const iterEnv = forEnv.extend();
294
321
  if (declarator.id.type === 'Identifier') {
295
322
  iterEnv.define(declarator.id.name, value, isConst);
@@ -323,6 +350,9 @@ export class Interpreter {
323
350
  forEnv.define(varName, undefined);
324
351
 
325
352
  for (const key in obj) {
353
+ // Checkpoint at each loop iteration (only await if controller present)
354
+ const cp3 = this._getCheckpointPromise(node, forEnv);
355
+ if (cp3) await cp3;
326
356
  forEnv.set(varName, key);
327
357
  const result = await this.evaluateAsync(node.body, forEnv);
328
358
  if (result instanceof BreakSignal) {
@@ -341,6 +371,9 @@ export class Interpreter {
341
371
  // For WhileStatement with async body
342
372
  if (node.type === 'WhileStatement') {
343
373
  while (await this.evaluateAsync(node.test, env)) {
374
+ // Checkpoint at each loop iteration (only await if controller present)
375
+ const cp4 = this._getCheckpointPromise(node, env);
376
+ if (cp4) await cp4;
344
377
  const result = await this.evaluateAsync(node.body, env);
345
378
  if (result instanceof BreakSignal) {
346
379
  break;
@@ -358,6 +391,9 @@ export class Interpreter {
358
391
  // For DoWhileStatement with async body
359
392
  if (node.type === 'DoWhileStatement') {
360
393
  do {
394
+ // Checkpoint at each loop iteration (only await if controller present)
395
+ const cp5 = this._getCheckpointPromise(node, env);
396
+ if (cp5) await cp5;
361
397
  const result = await this.evaluateAsync(node.body, env);
362
398
  if (result instanceof BreakSignal) {
363
399
  break;
@@ -1161,6 +1197,9 @@ export class Interpreter {
1161
1197
  const metadata = func.__metadata || func;
1162
1198
  const funcEnv = new Environment(metadata.closure);
1163
1199
 
1200
+ // Get function name for call stack tracking
1201
+ const funcName = metadata.name || func.name || 'anonymous';
1202
+
1164
1203
  // Bind 'this' if provided (for method calls)
1165
1204
  if (thisContext !== undefined) {
1166
1205
  funcEnv.define('this', thisContext);
@@ -1196,18 +1235,54 @@ export class Interpreter {
1196
1235
  // Execute function body
1197
1236
  // If async, use async evaluation and return a promise
1198
1237
  if (metadata.async) {
1238
+ // Track call stack for async functions
1239
+ if (this.executionController) {
1240
+ this.executionController._pushCall(funcName);
1241
+ }
1199
1242
  return (async () => {
1243
+ try {
1244
+ if (metadata.expression) {
1245
+ // Arrow function with expression body
1246
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
1247
+ // If the result is a ThrowSignal, throw the error
1248
+ if (result instanceof ThrowSignal) {
1249
+ throw result.value;
1250
+ }
1251
+ return result;
1252
+ } else {
1253
+ // Block statement body
1254
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
1255
+ if (result instanceof ReturnValue) {
1256
+ return result.value;
1257
+ }
1258
+ // If the result is a ThrowSignal, throw the error
1259
+ if (result instanceof ThrowSignal) {
1260
+ throw result.value;
1261
+ }
1262
+ return undefined;
1263
+ }
1264
+ } finally {
1265
+ if (this.executionController) {
1266
+ this.executionController._popCall();
1267
+ }
1268
+ }
1269
+ })();
1270
+ } else {
1271
+ // Synchronous evaluation for non-async functions
1272
+ // Track call stack for sync functions
1273
+ if (this.executionController) {
1274
+ this.executionController._pushCall(funcName);
1275
+ }
1276
+ try {
1200
1277
  if (metadata.expression) {
1201
- // Arrow function with expression body
1202
- const result = await this.evaluateAsync(metadata.body, funcEnv);
1278
+ const result = this.evaluate(metadata.body, funcEnv);
1203
1279
  // If the result is a ThrowSignal, throw the error
1204
1280
  if (result instanceof ThrowSignal) {
1205
1281
  throw result.value;
1206
1282
  }
1207
1283
  return result;
1208
1284
  } else {
1209
- // Block statement body
1210
- const result = await this.evaluateAsync(metadata.body, funcEnv);
1285
+ const result = this.evaluate(metadata.body, funcEnv);
1211
1286
  if (result instanceof ReturnValue) {
1212
1287
  return result.value;
1213
1288
  }
@@ -1217,26 +1292,10 @@ export class Interpreter {
1217
1292
  }
1218
1293
  return undefined;
1219
1294
  }
1220
- })();
1221
- } else {
1222
- // Synchronous evaluation for non-async functions
1223
- if (metadata.expression) {
1224
- const result = this.evaluate(metadata.body, funcEnv);
1225
- // If the result is a ThrowSignal, throw the error
1226
- if (result instanceof ThrowSignal) {
1227
- throw result.value;
1228
- }
1229
- return result;
1230
- } else {
1231
- const result = this.evaluate(metadata.body, funcEnv);
1232
- if (result instanceof ReturnValue) {
1233
- return result.value;
1234
- }
1235
- // If the result is a ThrowSignal, throw the error
1236
- if (result instanceof ThrowSignal) {
1237
- throw result.value;
1295
+ } finally {
1296
+ if (this.executionController) {
1297
+ this.executionController._popCall();
1238
1298
  }
1239
- return undefined;
1240
1299
  }
1241
1300
  }
1242
1301
  }
@@ -2137,6 +2196,14 @@ export class Interpreter {
2137
2196
  if (Array.isArray(arg)) {
2138
2197
  return { __spread: true, __values: arg };
2139
2198
  }
2199
+ // Strings are iterable - spread into characters
2200
+ if (typeof arg === 'string') {
2201
+ return { __spread: true, __values: [...arg] };
2202
+ }
2203
+ // Handle other iterables (like Set, Map, etc.)
2204
+ if (arg !== null && arg !== undefined && typeof arg[Symbol.iterator] === 'function') {
2205
+ return { __spread: true, __values: [...arg] };
2206
+ }
2140
2207
  if (typeof arg === 'object' && arg !== null) {
2141
2208
  return { __spread: true, __values: Object.entries(arg) };
2142
2209
  }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * ExecutionController - Controls and monitors interpreter execution
3
+ *
4
+ * Provides pause/resume/abort capabilities and execution status introspection.
5
+ * Works with async evaluation only - sync evaluation can only abort.
6
+ */
7
+ export class ExecutionController {
8
+ constructor() {
9
+ this.state = 'idle'; // 'idle' | 'running' | 'paused' | 'aborted' | 'completed'
10
+ this.pauseRequested = false;
11
+ this.abortRequested = false;
12
+
13
+ // Debug info
14
+ this.stepCount = 0;
15
+ this.currentNode = null;
16
+ this.callStack = [];
17
+ this.currentEnv = null;
18
+
19
+ this._resolveResume = null;
20
+ }
21
+
22
+ // --- Control methods (called by user) ---
23
+
24
+ /**
25
+ * Request pause at next checkpoint. Only works during async evaluation.
26
+ */
27
+ pause() {
28
+ if (this.state === 'running') {
29
+ this.pauseRequested = true;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Resume execution after pause.
35
+ */
36
+ resume() {
37
+ if (this.state === 'paused' && this._resolveResume) {
38
+ this.pauseRequested = false;
39
+ this._resolveResume();
40
+ this._resolveResume = null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Abort execution. Works for both sync and async evaluation.
46
+ * If paused, will resume and then abort.
47
+ */
48
+ abort() {
49
+ this.abortRequested = true;
50
+ // Also resume if paused to allow abort to take effect
51
+ if (this._resolveResume) {
52
+ this._resolveResume();
53
+ this._resolveResume = null;
54
+ }
55
+ }
56
+
57
+ // --- Status (called by user) ---
58
+
59
+ /**
60
+ * Get current execution status with full debug information.
61
+ * @returns {Object} Status object with state, stepCount, currentNode, callStack, and variables
62
+ */
63
+ getStatus() {
64
+ return {
65
+ state: this.state,
66
+ stepCount: this.stepCount,
67
+ currentNode: this.currentNode?.type || null,
68
+ callStack: [...this.callStack],
69
+ variables: this._getEnvironmentVariables()
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Check if abort has been requested.
75
+ * Compatible with AbortSignal interface.
76
+ */
77
+ get aborted() {
78
+ return this.abortRequested;
79
+ }
80
+
81
+ // --- Internal methods (called by interpreter) ---
82
+
83
+ /**
84
+ * Mark execution as started. Called by execute().
85
+ * @internal
86
+ */
87
+ _start() {
88
+ this.state = 'running';
89
+ this.stepCount = 0;
90
+ this.currentNode = null;
91
+ this.callStack = [];
92
+ this.pauseRequested = false;
93
+ // Note: don't reset abortRequested - allow pre-abort
94
+ }
95
+
96
+ /**
97
+ * Mark execution as completed. Called by execute().
98
+ * @internal
99
+ */
100
+ _complete() {
101
+ this.state = 'completed';
102
+ }
103
+
104
+ /**
105
+ * Push a function call onto the call stack.
106
+ * @internal
107
+ */
108
+ _pushCall(name) {
109
+ this.callStack.push(name);
110
+ }
111
+
112
+ /**
113
+ * Pop a function call from the call stack.
114
+ * @internal
115
+ */
116
+ _popCall() {
117
+ this.callStack.pop();
118
+ }
119
+
120
+ /**
121
+ * Set the current environment for variable introspection.
122
+ * @internal
123
+ */
124
+ _setEnv(env) {
125
+ this.currentEnv = env;
126
+ }
127
+
128
+ /**
129
+ * Async checkpoint - yields if paused, throws if aborted.
130
+ * Called at coarse granularity points (loops, function calls).
131
+ * @internal
132
+ */
133
+ async _checkpoint(node) {
134
+ this.stepCount++;
135
+ this.currentNode = node;
136
+
137
+ if (this.abortRequested) {
138
+ this.state = 'aborted';
139
+ const error = new Error('The operation was aborted');
140
+ error.name = 'AbortError';
141
+ throw error;
142
+ }
143
+
144
+ if (this.pauseRequested && this.state === 'running') {
145
+ this.state = 'paused';
146
+ await new Promise(resolve => { this._resolveResume = resolve; });
147
+ this.state = 'running';
148
+
149
+ // Check abort after resume (user may have called abort while paused)
150
+ if (this.abortRequested) {
151
+ this.state = 'aborted';
152
+ const error = new Error('The operation was aborted');
153
+ error.name = 'AbortError';
154
+ throw error;
155
+ }
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Sync abort check - only throws if aborted, cannot pause.
161
+ * Used in sync evaluate() path.
162
+ * @internal
163
+ */
164
+ _checkAbortSync() {
165
+ if (this.abortRequested) {
166
+ this.state = 'aborted';
167
+ const error = new Error('The operation was aborted');
168
+ error.name = 'AbortError';
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Get all variables from current environment chain.
175
+ * @internal
176
+ */
177
+ _getEnvironmentVariables() {
178
+ if (!this.currentEnv) return {};
179
+ const vars = {};
180
+ let env = this.currentEnv;
181
+ while (env) {
182
+ if (env.vars) {
183
+ for (const [key, value] of env.vars) {
184
+ if (!(key in vars)) {
185
+ vars[key] = this._serializeValue(value);
186
+ }
187
+ }
188
+ }
189
+ env = env.parent;
190
+ }
191
+ return vars;
192
+ }
193
+
194
+ /**
195
+ * Serialize a value for status reporting.
196
+ * @internal
197
+ */
198
+ _serializeValue(value) {
199
+ if (value === undefined) return { type: 'undefined' };
200
+ if (value === null) return { type: 'null' };
201
+ if (typeof value === 'function' || (value && value.__isFunction)) {
202
+ return { type: 'function', name: value.name || 'anonymous' };
203
+ }
204
+ if (Array.isArray(value)) {
205
+ return { type: 'array', length: value.length };
206
+ }
207
+ if (typeof value === 'object') {
208
+ return { type: 'object', preview: Object.keys(value).slice(0, 5) };
209
+ }
210
+ return { type: typeof value, value };
211
+ }
212
+ }