jslike 1.4.5 → 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/index.d.cts CHANGED
@@ -6440,10 +6440,17 @@ class Interpreter {
6440
6440
  this.moduleCache = new Map(); // Cache loaded modules
6441
6441
  this.moduleExports = {}; // Track exports in current module
6442
6442
  this.abortSignal = options.abortSignal;
6443
+ this.executionController = options.executionController;
6443
6444
  }
6444
6445
 
6445
- // Check if execution should be aborted
6446
+ // Check if execution should be aborted (sync version)
6446
6447
  checkAbortSignal() {
6448
+ // Check controller first if available
6449
+ if (this.executionController) {
6450
+ this.executionController._checkAbortSync();
6451
+ return;
6452
+ }
6453
+ // Fall back to legacy abortSignal
6447
6454
  if (this.abortSignal && this.abortSignal.aborted) {
6448
6455
  const error = new Error('The operation was aborted');
6449
6456
  error.name = 'AbortError';
@@ -6451,12 +6458,26 @@ class Interpreter {
6451
6458
  }
6452
6459
  }
6453
6460
 
6461
+ // Checkpoint that returns a promise only when controller is present
6462
+ // When no controller, returns null to signal no await needed
6463
+ _getCheckpointPromise(node, env) {
6464
+ if (this.executionController) {
6465
+ this.executionController._setEnv(env);
6466
+ return this.executionController._checkpoint(node);
6467
+ } else {
6468
+ this.checkAbortSignal();
6469
+ return null; // Signal that no await is needed
6470
+ }
6471
+ }
6472
+
6454
6473
  // Async evaluation for async functions - handles await expressions
6455
6474
  async evaluateAsync(node, env) {
6456
6475
  if (!node) return undefined;
6457
6476
 
6458
- // Check for abort signal before evaluating
6459
- this.checkAbortSignal();
6477
+ // Checkpoint - yields if paused, throws if aborted
6478
+ // Only await when there's actually a promise (controller present)
6479
+ const checkpointPromise = this._getCheckpointPromise(node, env);
6480
+ if (checkpointPromise) await checkpointPromise;
6460
6481
 
6461
6482
  // Handle await expressions by actually awaiting the promise
6462
6483
  if (node.type === 'AwaitExpression') {
@@ -6693,6 +6714,9 @@ class Interpreter {
6693
6714
  await this.evaluateAsync(node.init, forEnv);
6694
6715
  }
6695
6716
  while (!node.test || await this.evaluateAsync(node.test, forEnv)) {
6717
+ // Checkpoint at each loop iteration (only await if controller present)
6718
+ const cp1 = this._getCheckpointPromise(node, forEnv);
6719
+ if (cp1) await cp1;
6696
6720
  const result = await this.evaluateAsync(node.body, forEnv);
6697
6721
  if (result instanceof BreakSignal) {
6698
6722
  break;
@@ -6721,6 +6745,9 @@ class Interpreter {
6721
6745
  const isConst = node.left.kind === 'const';
6722
6746
 
6723
6747
  for (const value of iterable) {
6748
+ // Checkpoint at each loop iteration (only await if controller present)
6749
+ const cp2 = this._getCheckpointPromise(node, forEnv);
6750
+ if (cp2) await cp2;
6724
6751
  const iterEnv = forEnv.extend();
6725
6752
  if (declarator.id.type === 'Identifier') {
6726
6753
  iterEnv.define(declarator.id.name, value, isConst);
@@ -6754,6 +6781,9 @@ class Interpreter {
6754
6781
  forEnv.define(varName, undefined);
6755
6782
 
6756
6783
  for (const key in obj) {
6784
+ // Checkpoint at each loop iteration (only await if controller present)
6785
+ const cp3 = this._getCheckpointPromise(node, forEnv);
6786
+ if (cp3) await cp3;
6757
6787
  forEnv.set(varName, key);
6758
6788
  const result = await this.evaluateAsync(node.body, forEnv);
6759
6789
  if (result instanceof BreakSignal) {
@@ -6772,6 +6802,9 @@ class Interpreter {
6772
6802
  // For WhileStatement with async body
6773
6803
  if (node.type === 'WhileStatement') {
6774
6804
  while (await this.evaluateAsync(node.test, env)) {
6805
+ // Checkpoint at each loop iteration (only await if controller present)
6806
+ const cp4 = this._getCheckpointPromise(node, env);
6807
+ if (cp4) await cp4;
6775
6808
  const result = await this.evaluateAsync(node.body, env);
6776
6809
  if (result instanceof BreakSignal) {
6777
6810
  break;
@@ -6789,6 +6822,9 @@ class Interpreter {
6789
6822
  // For DoWhileStatement with async body
6790
6823
  if (node.type === 'DoWhileStatement') {
6791
6824
  do {
6825
+ // Checkpoint at each loop iteration (only await if controller present)
6826
+ const cp5 = this._getCheckpointPromise(node, env);
6827
+ if (cp5) await cp5;
6792
6828
  const result = await this.evaluateAsync(node.body, env);
6793
6829
  if (result instanceof BreakSignal) {
6794
6830
  break;
@@ -7592,6 +7628,9 @@ class Interpreter {
7592
7628
  const metadata = func.__metadata || func;
7593
7629
  const funcEnv = new Environment(metadata.closure);
7594
7630
 
7631
+ // Get function name for call stack tracking
7632
+ const funcName = metadata.name || func.name || 'anonymous';
7633
+
7595
7634
  // Bind 'this' if provided (for method calls)
7596
7635
  if (thisContext !== undefined) {
7597
7636
  funcEnv.define('this', thisContext);
@@ -7627,18 +7666,54 @@ class Interpreter {
7627
7666
  // Execute function body
7628
7667
  // If async, use async evaluation and return a promise
7629
7668
  if (metadata.async) {
7669
+ // Track call stack for async functions
7670
+ if (this.executionController) {
7671
+ this.executionController._pushCall(funcName);
7672
+ }
7630
7673
  return (async () => {
7674
+ try {
7675
+ if (metadata.expression) {
7676
+ // Arrow function with expression body
7677
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
7678
+ // If the result is a ThrowSignal, throw the error
7679
+ if (result instanceof ThrowSignal) {
7680
+ throw result.value;
7681
+ }
7682
+ return result;
7683
+ } else {
7684
+ // Block statement body
7685
+ const result = await this.evaluateAsync(metadata.body, funcEnv);
7686
+ if (result instanceof ReturnValue) {
7687
+ return result.value;
7688
+ }
7689
+ // If the result is a ThrowSignal, throw the error
7690
+ if (result instanceof ThrowSignal) {
7691
+ throw result.value;
7692
+ }
7693
+ return undefined;
7694
+ }
7695
+ } finally {
7696
+ if (this.executionController) {
7697
+ this.executionController._popCall();
7698
+ }
7699
+ }
7700
+ })();
7701
+ } else {
7702
+ // Synchronous evaluation for non-async functions
7703
+ // Track call stack for sync functions
7704
+ if (this.executionController) {
7705
+ this.executionController._pushCall(funcName);
7706
+ }
7707
+ try {
7631
7708
  if (metadata.expression) {
7632
- // Arrow function with expression body
7633
- const result = await this.evaluateAsync(metadata.body, funcEnv);
7709
+ const result = this.evaluate(metadata.body, funcEnv);
7634
7710
  // If the result is a ThrowSignal, throw the error
7635
7711
  if (result instanceof ThrowSignal) {
7636
7712
  throw result.value;
7637
7713
  }
7638
7714
  return result;
7639
7715
  } else {
7640
- // Block statement body
7641
- const result = await this.evaluateAsync(metadata.body, funcEnv);
7716
+ const result = this.evaluate(metadata.body, funcEnv);
7642
7717
  if (result instanceof ReturnValue) {
7643
7718
  return result.value;
7644
7719
  }
@@ -7648,26 +7723,10 @@ class Interpreter {
7648
7723
  }
7649
7724
  return undefined;
7650
7725
  }
7651
- })();
7652
- } else {
7653
- // Synchronous evaluation for non-async functions
7654
- if (metadata.expression) {
7655
- const result = this.evaluate(metadata.body, funcEnv);
7656
- // If the result is a ThrowSignal, throw the error
7657
- if (result instanceof ThrowSignal) {
7658
- throw result.value;
7659
- }
7660
- return result;
7661
- } else {
7662
- const result = this.evaluate(metadata.body, funcEnv);
7663
- if (result instanceof ReturnValue) {
7664
- return result.value;
7665
- }
7666
- // If the result is a ThrowSignal, throw the error
7667
- if (result instanceof ThrowSignal) {
7668
- throw result.value;
7726
+ } finally {
7727
+ if (this.executionController) {
7728
+ this.executionController._popCall();
7669
7729
  }
7670
- return undefined;
7671
7730
  }
7672
7731
  }
7673
7732
  }
@@ -9228,7 +9287,8 @@ class WangInterpreter {
9228
9287
 
9229
9288
  // Prepare execution options
9230
9289
  const options = {
9231
- moduleResolver: this.moduleResolver
9290
+ moduleResolver: this.moduleResolver,
9291
+ executionController: userOptions.executionController
9232
9292
  // sourceType will be auto-detected from code
9233
9293
  };
9234
9294
 
@@ -9327,6 +9387,219 @@ class InMemoryModuleResolver {
9327
9387
  }
9328
9388
  }
9329
9389
 
9390
+ /**
9391
+ * ExecutionController - Controls and monitors interpreter execution
9392
+ *
9393
+ * Provides pause/resume/abort capabilities and execution status introspection.
9394
+ * Works with async evaluation only - sync evaluation can only abort.
9395
+ */
9396
+ class ExecutionController {
9397
+ constructor() {
9398
+ this.state = 'idle'; // 'idle' | 'running' | 'paused' | 'aborted' | 'completed'
9399
+ this.pauseRequested = false;
9400
+ this.abortRequested = false;
9401
+
9402
+ // Debug info
9403
+ this.stepCount = 0;
9404
+ this.currentNode = null;
9405
+ this.callStack = [];
9406
+ this.currentEnv = null;
9407
+
9408
+ this._resolveResume = null;
9409
+ }
9410
+
9411
+ // --- Control methods (called by user) ---
9412
+
9413
+ /**
9414
+ * Request pause at next checkpoint. Only works during async evaluation.
9415
+ */
9416
+ pause() {
9417
+ if (this.state === 'running') {
9418
+ this.pauseRequested = true;
9419
+ }
9420
+ }
9421
+
9422
+ /**
9423
+ * Resume execution after pause.
9424
+ */
9425
+ resume() {
9426
+ if (this.state === 'paused' && this._resolveResume) {
9427
+ this.pauseRequested = false;
9428
+ this._resolveResume();
9429
+ this._resolveResume = null;
9430
+ }
9431
+ }
9432
+
9433
+ /**
9434
+ * Abort execution. Works for both sync and async evaluation.
9435
+ * If paused, will resume and then abort.
9436
+ */
9437
+ abort() {
9438
+ this.abortRequested = true;
9439
+ // Also resume if paused to allow abort to take effect
9440
+ if (this._resolveResume) {
9441
+ this._resolveResume();
9442
+ this._resolveResume = null;
9443
+ }
9444
+ }
9445
+
9446
+ // --- Status (called by user) ---
9447
+
9448
+ /**
9449
+ * Get current execution status with full debug information.
9450
+ * @returns {Object} Status object with state, stepCount, currentNode, callStack, and variables
9451
+ */
9452
+ getStatus() {
9453
+ return {
9454
+ state: this.state,
9455
+ stepCount: this.stepCount,
9456
+ currentNode: this.currentNode?.type || null,
9457
+ callStack: [...this.callStack],
9458
+ variables: this._getEnvironmentVariables()
9459
+ };
9460
+ }
9461
+
9462
+ /**
9463
+ * Check if abort has been requested.
9464
+ * Compatible with AbortSignal interface.
9465
+ */
9466
+ get aborted() {
9467
+ return this.abortRequested;
9468
+ }
9469
+
9470
+ // --- Internal methods (called by interpreter) ---
9471
+
9472
+ /**
9473
+ * Mark execution as started. Called by execute().
9474
+ * @internal
9475
+ */
9476
+ _start() {
9477
+ this.state = 'running';
9478
+ this.stepCount = 0;
9479
+ this.currentNode = null;
9480
+ this.callStack = [];
9481
+ this.pauseRequested = false;
9482
+ // Note: don't reset abortRequested - allow pre-abort
9483
+ }
9484
+
9485
+ /**
9486
+ * Mark execution as completed. Called by execute().
9487
+ * @internal
9488
+ */
9489
+ _complete() {
9490
+ this.state = 'completed';
9491
+ }
9492
+
9493
+ /**
9494
+ * Push a function call onto the call stack.
9495
+ * @internal
9496
+ */
9497
+ _pushCall(name) {
9498
+ this.callStack.push(name);
9499
+ }
9500
+
9501
+ /**
9502
+ * Pop a function call from the call stack.
9503
+ * @internal
9504
+ */
9505
+ _popCall() {
9506
+ this.callStack.pop();
9507
+ }
9508
+
9509
+ /**
9510
+ * Set the current environment for variable introspection.
9511
+ * @internal
9512
+ */
9513
+ _setEnv(env) {
9514
+ this.currentEnv = env;
9515
+ }
9516
+
9517
+ /**
9518
+ * Async checkpoint - yields if paused, throws if aborted.
9519
+ * Called at coarse granularity points (loops, function calls).
9520
+ * @internal
9521
+ */
9522
+ async _checkpoint(node) {
9523
+ this.stepCount++;
9524
+ this.currentNode = node;
9525
+
9526
+ if (this.abortRequested) {
9527
+ this.state = 'aborted';
9528
+ const error = new Error('The operation was aborted');
9529
+ error.name = 'AbortError';
9530
+ throw error;
9531
+ }
9532
+
9533
+ if (this.pauseRequested && this.state === 'running') {
9534
+ this.state = 'paused';
9535
+ await new Promise(resolve => { this._resolveResume = resolve; });
9536
+ this.state = 'running';
9537
+
9538
+ // Check abort after resume (user may have called abort while paused)
9539
+ if (this.abortRequested) {
9540
+ this.state = 'aborted';
9541
+ const error = new Error('The operation was aborted');
9542
+ error.name = 'AbortError';
9543
+ throw error;
9544
+ }
9545
+ }
9546
+ }
9547
+
9548
+ /**
9549
+ * Sync abort check - only throws if aborted, cannot pause.
9550
+ * Used in sync evaluate() path.
9551
+ * @internal
9552
+ */
9553
+ _checkAbortSync() {
9554
+ if (this.abortRequested) {
9555
+ this.state = 'aborted';
9556
+ const error = new Error('The operation was aborted');
9557
+ error.name = 'AbortError';
9558
+ throw error;
9559
+ }
9560
+ }
9561
+
9562
+ /**
9563
+ * Get all variables from current environment chain.
9564
+ * @internal
9565
+ */
9566
+ _getEnvironmentVariables() {
9567
+ if (!this.currentEnv) return {};
9568
+ const vars = {};
9569
+ let env = this.currentEnv;
9570
+ while (env) {
9571
+ if (env.vars) {
9572
+ for (const [key, value] of env.vars) {
9573
+ if (!(key in vars)) {
9574
+ vars[key] = this._serializeValue(value);
9575
+ }
9576
+ }
9577
+ }
9578
+ env = env.parent;
9579
+ }
9580
+ return vars;
9581
+ }
9582
+
9583
+ /**
9584
+ * Serialize a value for status reporting.
9585
+ * @internal
9586
+ */
9587
+ _serializeValue(value) {
9588
+ if (value === undefined) return { type: 'undefined' };
9589
+ if (value === null) return { type: 'null' };
9590
+ if (typeof value === 'function' || (value && value.__isFunction)) {
9591
+ return { type: 'function', name: value.name || 'anonymous' };
9592
+ }
9593
+ if (Array.isArray(value)) {
9594
+ return { type: 'array', length: value.length };
9595
+ }
9596
+ if (typeof value === 'object') {
9597
+ return { type: 'object', preview: Object.keys(value).slice(0, 5) };
9598
+ }
9599
+ return { type: typeof value, value };
9600
+ }
9601
+ }
9602
+
9330
9603
  // Use bundled Acorn parser for zero runtime dependencies
9331
9604
 
9332
9605
  // Helper to detect if code contains module syntax or top-level await
@@ -9423,34 +9696,59 @@ function containsTopLevelAwait(node) {
9423
9696
  }
9424
9697
 
9425
9698
  async function execute(code, env = null, options = {}) {
9426
- // Parse the code
9427
- const ast = parse(code, options);
9699
+ // Get execution controller if provided
9700
+ const controller = options.executionController;
9428
9701
 
9429
- // Create global environment if not provided
9430
- if (!env) {
9431
- env = createGlobalEnvironment(new Environment());
9702
+ // Mark execution as starting
9703
+ if (controller) {
9704
+ controller._start();
9432
9705
  }
9433
9706
 
9434
- // Create interpreter with module resolver and abort signal if provided
9435
- const interpreter = new Interpreter(env, {
9436
- moduleResolver: options.moduleResolver,
9437
- abortSignal: options.abortSignal
9438
- });
9707
+ try {
9708
+ // Parse the code
9709
+ const ast = parse(code, options);
9439
9710
 
9440
- // Use async evaluation if:
9441
- // 1. Explicitly requested module mode
9442
- // 2. AST contains import/export declarations
9443
- // 3. Code contains top-level await
9444
- const needsAsync = options.sourceType === 'module' ||
9445
- containsModuleDeclarations(ast) ||
9446
- containsTopLevelAwait(ast);
9447
-
9448
- if (needsAsync) {
9449
- const result = await interpreter.evaluateAsync(ast, env);
9450
- return result instanceof ReturnValue ? result.value : result;
9451
- } else {
9452
- const result = interpreter.evaluate(ast, env);
9453
- return result instanceof ReturnValue ? result.value : result;
9711
+ // Create global environment if not provided
9712
+ if (!env) {
9713
+ env = createGlobalEnvironment(new Environment());
9714
+ }
9715
+
9716
+ // Create interpreter with module resolver, abort signal, and execution controller
9717
+ const interpreter = new Interpreter(env, {
9718
+ moduleResolver: options.moduleResolver,
9719
+ abortSignal: options.abortSignal,
9720
+ executionController: controller
9721
+ });
9722
+
9723
+ // Use async evaluation if:
9724
+ // 1. Explicitly requested module mode
9725
+ // 2. AST contains import/export declarations
9726
+ // 3. Code contains top-level await
9727
+ // 4. Execution controller provided (needs async for pause/resume)
9728
+ const needsAsync = options.sourceType === 'module' ||
9729
+ containsModuleDeclarations(ast) ||
9730
+ containsTopLevelAwait(ast) ||
9731
+ controller != null;
9732
+
9733
+ if (needsAsync) {
9734
+ const result = await interpreter.evaluateAsync(ast, env);
9735
+ if (controller) {
9736
+ controller._complete();
9737
+ }
9738
+ return result instanceof ReturnValue ? result.value : result;
9739
+ } else {
9740
+ const result = interpreter.evaluate(ast, env);
9741
+ if (controller) {
9742
+ controller._complete();
9743
+ }
9744
+ return result instanceof ReturnValue ? result.value : result;
9745
+ }
9746
+ } catch (e) {
9747
+ // Mark as aborted if that's the error type
9748
+ if (controller && e.name === 'AbortError') {
9749
+ controller.state = 'aborted';
9750
+ }
9751
+ throw e;
9454
9752
  }
9455
9753
  }
9456
9754
 
@@ -9503,4 +9801,4 @@ class ModuleResolver {
9503
9801
  * @property {any} [metadata] - Optional metadata about the module
9504
9802
  */
9505
9803
 
9506
- export { Environment, InMemoryModuleResolver, Interpreter, ModuleResolver, WangInterpreter, createEnvironment, execute, isTopLevelAwait, parse };
9804
+ export { Environment, ExecutionController, InMemoryModuleResolver, Interpreter, ModuleResolver, WangInterpreter, createEnvironment, execute, isTopLevelAwait, parse };