jslike 1.4.5 → 1.6.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
@@ -98,34 +98,59 @@ function containsTopLevelAwait(node) {
98
98
  }
99
99
 
100
100
  export async function execute(code, env = null, options = {}) {
101
- // Parse the code
102
- const ast = parse(code, options);
101
+ // Get execution controller if provided
102
+ const controller = options.executionController;
103
103
 
104
- // Create global environment if not provided
105
- if (!env) {
106
- env = createGlobalEnvironment(new Environment());
104
+ // Mark execution as starting
105
+ if (controller) {
106
+ controller._start();
107
107
  }
108
108
 
109
- // Create interpreter with module resolver and abort signal if provided
110
- const interpreter = new Interpreter(env, {
111
- moduleResolver: options.moduleResolver,
112
- abortSignal: options.abortSignal
113
- });
114
-
115
- // Use async evaluation if:
116
- // 1. Explicitly requested module mode
117
- // 2. AST contains import/export declarations
118
- // 3. Code contains top-level await
119
- const needsAsync = options.sourceType === 'module' ||
120
- containsModuleDeclarations(ast) ||
121
- containsTopLevelAwait(ast);
122
-
123
- if (needsAsync) {
124
- const result = await interpreter.evaluateAsync(ast, env);
125
- return result instanceof ReturnValue ? result.value : result;
126
- } else {
127
- const result = interpreter.evaluate(ast, env);
128
- 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;
129
154
  }
130
155
  }
131
156
 
@@ -139,6 +164,7 @@ export const isTopLevelAwait = containsModuleSyntax;
139
164
  export { Interpreter } from './interpreter/interpreter.js';
140
165
  export { Environment } from './runtime/environment.js';
141
166
  export { WangInterpreter, InMemoryModuleResolver } from './interpreter/index.js';
167
+ export { ExecutionController } from './runtime/execution-controller.js';
142
168
 
143
169
  /**
144
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;
@@ -600,6 +636,26 @@ export class Interpreter {
600
636
  return this.evaluateClassExpression(node, env);
601
637
  }
602
638
 
639
+ // JSX Support (async)
640
+ if (node.type === 'JSXElement') {
641
+ return await this.evaluateJSXElementAsync(node, env);
642
+ }
643
+
644
+ if (node.type === 'JSXFragment') {
645
+ return await this.evaluateJSXFragmentAsync(node, env);
646
+ }
647
+
648
+ if (node.type === 'JSXExpressionContainer') {
649
+ if (node.expression.type === 'JSXEmptyExpression') {
650
+ return undefined;
651
+ }
652
+ return await this.evaluateAsync(node.expression, env);
653
+ }
654
+
655
+ if (node.type === 'JSXText') {
656
+ return this.normalizeJSXText(node.value);
657
+ }
658
+
603
659
  // Only leaf nodes should fall through to sync evaluate
604
660
  // These have no sub-expressions that could contain await
605
661
  if (['Literal', 'Identifier', 'BreakStatement', 'ContinueStatement',
@@ -774,6 +830,22 @@ export class Interpreter {
774
830
  case 'Property':
775
831
  return this.evaluateProperty(node, env);
776
832
 
833
+ // JSX Support
834
+ case 'JSXElement':
835
+ return this.evaluateJSXElement(node, env);
836
+
837
+ case 'JSXFragment':
838
+ return this.evaluateJSXFragment(node, env);
839
+
840
+ case 'JSXExpressionContainer':
841
+ if (node.expression.type === 'JSXEmptyExpression') {
842
+ return undefined;
843
+ }
844
+ return this.evaluate(node.expression, env);
845
+
846
+ case 'JSXText':
847
+ return this.normalizeJSXText(node.value);
848
+
777
849
  default:
778
850
  throw new Error(`Unknown node type: ${node.type}`);
779
851
  }
@@ -1161,6 +1233,9 @@ export class Interpreter {
1161
1233
  const metadata = func.__metadata || func;
1162
1234
  const funcEnv = new Environment(metadata.closure);
1163
1235
 
1236
+ // Get function name for call stack tracking
1237
+ const funcName = metadata.name || func.name || 'anonymous';
1238
+
1164
1239
  // Bind 'this' if provided (for method calls)
1165
1240
  if (thisContext !== undefined) {
1166
1241
  funcEnv.define('this', thisContext);
@@ -1196,18 +1271,54 @@ export class Interpreter {
1196
1271
  // Execute function body
1197
1272
  // If async, use async evaluation and return a promise
1198
1273
  if (metadata.async) {
1274
+ // Track call stack for async functions
1275
+ if (this.executionController) {
1276
+ this.executionController._pushCall(funcName);
1277
+ }
1199
1278
  return (async () => {
1279
+ try {
1280
+ if (metadata.expression) {
1281
+ // Arrow function with expression body
1282
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
1283
+ // If the result is a ThrowSignal, throw the error
1284
+ if (result instanceof ThrowSignal) {
1285
+ throw result.value;
1286
+ }
1287
+ return result;
1288
+ } else {
1289
+ // Block statement body
1290
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
1291
+ if (result instanceof ReturnValue) {
1292
+ return result.value;
1293
+ }
1294
+ // If the result is a ThrowSignal, throw the error
1295
+ if (result instanceof ThrowSignal) {
1296
+ throw result.value;
1297
+ }
1298
+ return undefined;
1299
+ }
1300
+ } finally {
1301
+ if (this.executionController) {
1302
+ this.executionController._popCall();
1303
+ }
1304
+ }
1305
+ })();
1306
+ } else {
1307
+ // Synchronous evaluation for non-async functions
1308
+ // Track call stack for sync functions
1309
+ if (this.executionController) {
1310
+ this.executionController._pushCall(funcName);
1311
+ }
1312
+ try {
1200
1313
  if (metadata.expression) {
1201
- // Arrow function with expression body
1202
- const result = await this.evaluateAsync(metadata.body, funcEnv);
1314
+ const result = this.evaluate(metadata.body, funcEnv);
1203
1315
  // If the result is a ThrowSignal, throw the error
1204
1316
  if (result instanceof ThrowSignal) {
1205
1317
  throw result.value;
1206
1318
  }
1207
1319
  return result;
1208
1320
  } else {
1209
- // Block statement body
1210
- const result = await this.evaluateAsync(metadata.body, funcEnv);
1321
+ const result = this.evaluate(metadata.body, funcEnv);
1211
1322
  if (result instanceof ReturnValue) {
1212
1323
  return result.value;
1213
1324
  }
@@ -1217,26 +1328,10 @@ export class Interpreter {
1217
1328
  }
1218
1329
  return undefined;
1219
1330
  }
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;
1331
+ } finally {
1332
+ if (this.executionController) {
1333
+ this.executionController._popCall();
1238
1334
  }
1239
- return undefined;
1240
1335
  }
1241
1336
  }
1242
1337
  }
@@ -2175,4 +2270,253 @@ export class Interpreter {
2175
2270
  // Already handled in evaluateObjectExpression
2176
2271
  return undefined;
2177
2272
  }
2273
+
2274
+ // ===== JSX Support =====
2275
+
2276
+ evaluateJSXElement(node, env) {
2277
+ const createElement = this.getCreateElement(env);
2278
+ const { type, props } = this.evaluateJSXOpeningElement(node.openingElement, env);
2279
+ const children = this.evaluateJSXChildren(node.children, env);
2280
+
2281
+ if (children.length === 0) {
2282
+ return createElement(type, props);
2283
+ } else if (children.length === 1) {
2284
+ return createElement(type, props, children[0]);
2285
+ }
2286
+ return createElement(type, props, ...children);
2287
+ }
2288
+
2289
+ evaluateJSXFragment(node, env) {
2290
+ const createElement = this.getCreateElement(env);
2291
+ const Fragment = this.getFragment(env);
2292
+ const children = this.evaluateJSXChildren(node.children, env);
2293
+
2294
+ if (children.length === 0) {
2295
+ return createElement(Fragment, null);
2296
+ } else if (children.length === 1) {
2297
+ return createElement(Fragment, null, children[0]);
2298
+ }
2299
+ return createElement(Fragment, null, ...children);
2300
+ }
2301
+
2302
+ evaluateJSXOpeningElement(node, env) {
2303
+ const type = this.evaluateJSXElementName(node.name, env);
2304
+ const props = {};
2305
+
2306
+ for (const attr of node.attributes) {
2307
+ if (attr.type === 'JSXAttribute') {
2308
+ const name = attr.name.type === 'JSXIdentifier'
2309
+ ? attr.name.name
2310
+ : `${attr.name.namespace.name}:${attr.name.name.name}`;
2311
+ const value = attr.value
2312
+ ? this.evaluateJSXAttributeValue(attr.value, env)
2313
+ : true;
2314
+ props[name] = value;
2315
+ } else if (attr.type === 'JSXSpreadAttribute') {
2316
+ Object.assign(props, this.evaluate(attr.argument, env));
2317
+ }
2318
+ }
2319
+
2320
+ return { type, props: Object.keys(props).length > 0 ? props : null };
2321
+ }
2322
+
2323
+ evaluateJSXElementName(node, env) {
2324
+ if (node.type === 'JSXIdentifier') {
2325
+ const name = node.name;
2326
+ // Lowercase = intrinsic ('div'), Uppercase = component
2327
+ if (name[0] === name[0].toLowerCase()) {
2328
+ return name;
2329
+ }
2330
+ return env.get(name);
2331
+ } else if (node.type === 'JSXMemberExpression') {
2332
+ const object = this.evaluateJSXElementName(node.object, env);
2333
+ return object[node.property.name];
2334
+ } else if (node.type === 'JSXNamespacedName') {
2335
+ return `${node.namespace.name}:${node.name.name}`;
2336
+ }
2337
+ throw new Error(`Unknown JSX element name type: ${node.type}`);
2338
+ }
2339
+
2340
+ evaluateJSXAttributeValue(node, env) {
2341
+ if (node.type === 'Literal') return node.value;
2342
+ if (node.type === 'JSXExpressionContainer') {
2343
+ return this.evaluate(node.expression, env);
2344
+ }
2345
+ if (node.type === 'JSXElement') return this.evaluateJSXElement(node, env);
2346
+ if (node.type === 'JSXFragment') return this.evaluateJSXFragment(node, env);
2347
+ throw new Error(`Unknown JSX attribute value type: ${node.type}`);
2348
+ }
2349
+
2350
+ evaluateJSXChildren(children, env) {
2351
+ const result = [];
2352
+ for (const child of children) {
2353
+ if (child.type === 'JSXText') {
2354
+ const text = this.normalizeJSXText(child.value);
2355
+ if (text) result.push(text);
2356
+ } else if (child.type === 'JSXExpressionContainer') {
2357
+ if (child.expression.type !== 'JSXEmptyExpression') {
2358
+ const value = this.evaluate(child.expression, env);
2359
+ if (Array.isArray(value)) {
2360
+ result.push(...value);
2361
+ } else if (value !== null && value !== undefined && value !== false) {
2362
+ result.push(value);
2363
+ }
2364
+ }
2365
+ } else if (child.type === 'JSXElement') {
2366
+ result.push(this.evaluateJSXElement(child, env));
2367
+ } else if (child.type === 'JSXFragment') {
2368
+ result.push(this.evaluateJSXFragment(child, env));
2369
+ }
2370
+ }
2371
+ return result;
2372
+ }
2373
+
2374
+ normalizeJSXText(text) {
2375
+ // React's JSX whitespace normalization
2376
+ const lines = text.split('\n');
2377
+ const normalized = lines
2378
+ .map((line, i) => {
2379
+ let result = line;
2380
+ if (i === 0) result = result.trimStart();
2381
+ if (i === lines.length - 1) result = result.trimEnd();
2382
+ return result;
2383
+ })
2384
+ .filter(line => line.length > 0)
2385
+ .join(' ');
2386
+ return normalized || null;
2387
+ }
2388
+
2389
+ getCreateElement(env) {
2390
+ // Try React.createElement first
2391
+ try {
2392
+ const React = env.get('React');
2393
+ if (React && React.createElement) {
2394
+ return React.createElement.bind(React);
2395
+ }
2396
+ } catch (e) { /* not defined */ }
2397
+
2398
+ // Try standalone createElement
2399
+ try {
2400
+ return env.get('createElement');
2401
+ } catch (e) { /* not defined */ }
2402
+
2403
+ // Fallback: simple element factory for non-React usage
2404
+ return (type, props, ...children) => ({
2405
+ $$typeof: Symbol.for('react.element'),
2406
+ type,
2407
+ props: {
2408
+ ...props,
2409
+ children: children.length === 0 ? undefined : children.length === 1 ? children[0] : children
2410
+ },
2411
+ key: props?.key ?? null,
2412
+ ref: props?.ref ?? null
2413
+ });
2414
+ }
2415
+
2416
+ getFragment(env) {
2417
+ // Try React.Fragment
2418
+ try {
2419
+ const React = env.get('React');
2420
+ if (React && React.Fragment) {
2421
+ return React.Fragment;
2422
+ }
2423
+ } catch (e) { /* not defined */ }
2424
+
2425
+ // Try standalone Fragment
2426
+ try {
2427
+ return env.get('Fragment');
2428
+ } catch (e) { /* not defined */ }
2429
+
2430
+ // Fallback: Symbol for fragments
2431
+ return Symbol.for('react.fragment');
2432
+ }
2433
+
2434
+ // ===== Async JSX Support =====
2435
+
2436
+ async evaluateJSXElementAsync(node, env) {
2437
+ const checkpointPromise = this._getCheckpointPromise(node, env);
2438
+ if (checkpointPromise) await checkpointPromise;
2439
+
2440
+ const createElement = this.getCreateElement(env);
2441
+ const { type, props } = await this.evaluateJSXOpeningElementAsync(node.openingElement, env);
2442
+ const children = await this.evaluateJSXChildrenAsync(node.children, env);
2443
+
2444
+ if (children.length === 0) {
2445
+ return createElement(type, props);
2446
+ } else if (children.length === 1) {
2447
+ return createElement(type, props, children[0]);
2448
+ }
2449
+ return createElement(type, props, ...children);
2450
+ }
2451
+
2452
+ async evaluateJSXFragmentAsync(node, env) {
2453
+ const checkpointPromise = this._getCheckpointPromise(node, env);
2454
+ if (checkpointPromise) await checkpointPromise;
2455
+
2456
+ const createElement = this.getCreateElement(env);
2457
+ const Fragment = this.getFragment(env);
2458
+ const children = await this.evaluateJSXChildrenAsync(node.children, env);
2459
+
2460
+ if (children.length === 0) {
2461
+ return createElement(Fragment, null);
2462
+ } else if (children.length === 1) {
2463
+ return createElement(Fragment, null, children[0]);
2464
+ }
2465
+ return createElement(Fragment, null, ...children);
2466
+ }
2467
+
2468
+ async evaluateJSXOpeningElementAsync(node, env) {
2469
+ const type = this.evaluateJSXElementName(node.name, env);
2470
+ const props = {};
2471
+
2472
+ for (const attr of node.attributes) {
2473
+ if (attr.type === 'JSXAttribute') {
2474
+ const name = attr.name.type === 'JSXIdentifier'
2475
+ ? attr.name.name
2476
+ : `${attr.name.namespace.name}:${attr.name.name.name}`;
2477
+ const value = attr.value
2478
+ ? await this.evaluateJSXAttributeValueAsync(attr.value, env)
2479
+ : true;
2480
+ props[name] = value;
2481
+ } else if (attr.type === 'JSXSpreadAttribute') {
2482
+ Object.assign(props, await this.evaluateAsync(attr.argument, env));
2483
+ }
2484
+ }
2485
+
2486
+ return { type, props: Object.keys(props).length > 0 ? props : null };
2487
+ }
2488
+
2489
+ async evaluateJSXAttributeValueAsync(node, env) {
2490
+ if (node.type === 'Literal') return node.value;
2491
+ if (node.type === 'JSXExpressionContainer') {
2492
+ return await this.evaluateAsync(node.expression, env);
2493
+ }
2494
+ if (node.type === 'JSXElement') return await this.evaluateJSXElementAsync(node, env);
2495
+ if (node.type === 'JSXFragment') return await this.evaluateJSXFragmentAsync(node, env);
2496
+ throw new Error(`Unknown JSX attribute value type: ${node.type}`);
2497
+ }
2498
+
2499
+ async evaluateJSXChildrenAsync(children, env) {
2500
+ const result = [];
2501
+ for (const child of children) {
2502
+ if (child.type === 'JSXText') {
2503
+ const text = this.normalizeJSXText(child.value);
2504
+ if (text) result.push(text);
2505
+ } else if (child.type === 'JSXExpressionContainer') {
2506
+ if (child.expression.type !== 'JSXEmptyExpression') {
2507
+ const value = await this.evaluateAsync(child.expression, env);
2508
+ if (Array.isArray(value)) {
2509
+ result.push(...value);
2510
+ } else if (value !== null && value !== undefined && value !== false) {
2511
+ result.push(value);
2512
+ }
2513
+ }
2514
+ } else if (child.type === 'JSXElement') {
2515
+ result.push(await this.evaluateJSXElementAsync(child, env));
2516
+ } else if (child.type === 'JSXFragment') {
2517
+ result.push(await this.evaluateJSXFragmentAsync(child, env));
2518
+ }
2519
+ }
2520
+ return result;
2521
+ }
2178
2522
  }