sommark 4.1.0 → 4.2.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/core/evaluator.js CHANGED
@@ -1,12 +1,45 @@
1
- import { quickJS } from "@sebastianwessel/quickjs";
2
- import fs from "node:fs";
3
- import path from "node:path";
1
+ import { getQuickJS } from "quickjs-emscripten";
2
+ import path from "pathe";
4
3
  import * as acorn from "acorn";
5
4
  import SomMark, { registerHostCompile, registerHostSettings } from "./helpers/lib.js";
5
+ import { formatMessage } from "./errors.js";
6
6
 
7
7
  // Global tracker to ensure deep recursive Smark compilation never exceeds safe boundaries
8
8
  let globalCompilationDepth = 0;
9
9
 
10
+ async function prefetchImports(code, baseDir, fsImpl) {
11
+ if (!fsImpl?.readFile) return;
12
+ let ast;
13
+ try { ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "module" }); }
14
+ catch { return; }
15
+
16
+ for (const node of ast.body) {
17
+ if (node.type !== "ImportDeclaration") continue;
18
+ const importPath = node.source.value;
19
+ const resolved = /^https?:\/\//.test(baseDir)
20
+ ? new URL(importPath, baseDir.endsWith("/") ? baseDir : baseDir + "/").href
21
+ : path.resolve(baseDir, importPath);
22
+
23
+ if (fsImpl.existsSync(resolved)) continue; // already cached
24
+
25
+ try {
26
+ const content = await fsImpl.readFile(resolved);
27
+ if (resolved.endsWith(".js")) {
28
+ const nextBase = /^https?:\/\//.test(resolved)
29
+ ? resolved.slice(0, resolved.lastIndexOf("/") + 1)
30
+ : path.dirname(resolved);
31
+ await prefetchImports(content, nextBase, fsImpl);
32
+ }
33
+ } catch { /* let QuickJS surface the error */ }
34
+ }
35
+ }
36
+
37
+ let compilerClass = null;
38
+
39
+ export function setCompilerClass(cls) {
40
+ compilerClass = cls;
41
+ }
42
+
10
43
  // Pure, top-level stateless adapters to avoid circular references and closures over EvaluatorState
11
44
  const customFetchAdapter = async (input, init, security = {}) => {
12
45
  const allowFetch = security?.allowFetch !== false;
@@ -104,17 +137,17 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
104
137
 
105
138
  globalCompilationDepth++;
106
139
  try {
107
- // Securely isolate and deep-clone options to strip parent VM proxies
108
140
  const cleanOptions = JSON.parse(JSON.stringify(options || {}));
109
- const { default: SomMarkCompiler } = await import("../index.js");
141
+ if (!compilerClass) {
142
+ throw new Error("Compiler class is not registered in the evaluator.");
143
+ }
110
144
  const compilerOptions = {
145
+ ...cleanOptions,
111
146
  src,
112
147
  format: cleanOptions.format || "html",
113
- variables: cleanOptions.variables || {},
114
- formatOption: cleanOptions.formatOption || {},
115
148
  security: parentSecurity
116
149
  };
117
- const sm = new SomMarkCompiler(compilerOptions);
150
+ const sm = new compilerClass(compilerOptions);
118
151
  return await sm.transpile();
119
152
  } finally {
120
153
  globalCompilationDepth--;
@@ -124,25 +157,124 @@ const customCompileAdapter = async (src, options, parentSecurity = {}) => {
124
157
  // Register statically once at module loading
125
158
  registerHostCompile(customCompileAdapter);
126
159
 
127
- /**
128
- * EvaluatorState
129
- *
130
- * Houses the actual state, scopes, and QuickJS VM instance for a single transpilation lifecycle.
131
- */
160
+ let defaultFs = null;
161
+ let quickJSInstance = null;
162
+ async function getQuickJSModule() {
163
+ if (!quickJSInstance) {
164
+ quickJSInstance = await getQuickJS();
165
+ }
166
+ return quickJSInstance;
167
+ }
168
+
169
+ function objectToHandle(context, obj) {
170
+ if (obj === undefined) {
171
+ return context.undefined;
172
+ }
173
+ const jsonStr = JSON.stringify(obj);
174
+ const stringHandle = context.newString(jsonStr);
175
+ const jsonHandle = context.getProp(context.global, "JSON");
176
+ const parseHandle = context.getProp(jsonHandle, "parse");
177
+ const result = context.callFunction(parseHandle, jsonHandle, stringHandle);
178
+ stringHandle.dispose();
179
+ parseHandle.dispose();
180
+ jsonHandle.dispose();
181
+ return result.unwrap();
182
+ }
183
+
184
+ function expose(context, vars, pendingDeferreds) {
185
+ for (const [key, value] of Object.entries(vars)) {
186
+ let handle;
187
+ if (typeof value === "function") {
188
+ handle = context.newFunction(key, (...args) => {
189
+ try {
190
+ const jsArgs = args.map(arg => context.dump(arg));
191
+ const res = value(...jsArgs);
192
+ if (res instanceof Promise || (res && typeof res === "object" && typeof res.then === "function")) {
193
+ const deferred = context.newPromise();
194
+ if (pendingDeferreds) {
195
+ pendingDeferreds.add(deferred);
196
+ }
197
+ res.then(
198
+ (resolvedVal) => {
199
+ try {
200
+ if (!context.alive) return;
201
+ if (resolvedVal === undefined) {
202
+ deferred.resolve();
203
+ } else {
204
+ const valHandle = objectToHandle(context, resolvedVal);
205
+ deferred.resolve(valHandle);
206
+ valHandle.dispose();
207
+ }
208
+ } catch (e) {
209
+ if (context.alive) {
210
+ const errHandle = context.newError(e.message || String(e));
211
+ deferred.reject(errHandle);
212
+ errHandle.dispose();
213
+ }
214
+ } finally {
215
+ if (pendingDeferreds) {
216
+ pendingDeferreds.delete(deferred);
217
+ }
218
+ if (context.alive) {
219
+ deferred.dispose();
220
+ }
221
+ }
222
+ },
223
+ (rejectedErr) => {
224
+ try {
225
+ if (!context.alive) return;
226
+ const errHandle = context.newError(rejectedErr.message || String(rejectedErr));
227
+ deferred.reject(errHandle);
228
+ errHandle.dispose();
229
+ } catch (e) {
230
+ // ignore
231
+ } finally {
232
+ if (pendingDeferreds) {
233
+ pendingDeferreds.delete(deferred);
234
+ }
235
+ if (context.alive) {
236
+ deferred.dispose();
237
+ }
238
+ }
239
+ }
240
+ );
241
+ return deferred.handle.dup();
242
+ } else if (res === undefined) {
243
+ return;
244
+ } else {
245
+ return objectToHandle(context, res);
246
+ }
247
+ } catch (err) {
248
+ throw context.newError(err.message || String(err));
249
+ }
250
+ });
251
+ } else {
252
+ handle = objectToHandle(context, value);
253
+ }
254
+ context.setProp(context.global, key, handle);
255
+ handle.dispose();
256
+ }
257
+ }
258
+
132
259
  class EvaluatorState {
133
260
  constructor() {
134
261
  this.runtime = null;
135
- this.baseDir = process.cwd();
262
+ this.context = null;
263
+ this.baseDir = "/";
136
264
  this.scopes = [{}];
137
265
  this.dynamicTagsStack = [new Map()];
138
266
  this.deadline = 0;
267
+ this.pendingDeferreds = new Set();
139
268
  }
140
269
 
141
- /**
142
- * Initializes the QuickJS VM.
143
- */
144
270
  async init(baseDir = null, security = {}, settings = {}, mapperFile = null) {
145
- if (baseDir) this.baseDir = baseDir;
271
+ if (baseDir) {
272
+ this.baseDir = baseDir;
273
+ } else if (settings?.instance?.cwd) {
274
+ this.baseDir = settings.instance.cwd;
275
+ } else {
276
+ this.baseDir = "/";
277
+ }
146
278
  this.scopes = [{}];
147
279
  this.dynamicTagsStack = [new Map()];
148
280
  this.security = security;
@@ -150,38 +282,39 @@ class EvaluatorState {
150
282
  this.mapperFile = mapperFile;
151
283
  registerHostSettings(settings);
152
284
 
153
- if (this.runtime) {
154
- this.runtime.vm.expose({
285
+ this.nodeFs = defaultFs;
286
+
287
+ if (this.context) {
288
+ this.expose({
155
289
  __allowRaw: this.security.allowRaw !== false
156
290
  });
157
291
  return;
158
292
  }
159
293
 
160
- const { createRuntime } = await quickJS();
161
-
162
- this.runtime = await createRuntime({
163
- allowFetch: true,
164
- fetchAdapter: async (input, init) => {
165
- return await customFetchAdapter(input, init, this.security);
166
- },
167
- allowFs: true,
168
- env: {}
169
- });
294
+ const QuickJS = await getQuickJSModule();
295
+ this.runtime = QuickJS.newRuntime();
296
+ this.context = this.runtime.newContext();
170
297
 
171
298
  this.deadline = 0;
172
- if (this.runtime?.vm?.context?.runtime?.setInterruptHandler) {
173
- this.runtime.vm.context.runtime.setInterruptHandler(() => {
174
- return this.deadline > 0 && Date.now() > this.deadline;
175
- });
176
- }
299
+ this.runtime.setInterruptHandler(() => {
300
+ return this.deadline > 0 && Date.now() > this.deadline;
301
+ });
177
302
 
178
- // Expose standard library version & compile adapter, then construct the frozen global namespace inside the VM
179
- this.runtime.vm.expose({
303
+ this.expose({
180
304
  __hostSomMarkVersion: SomMark.version,
181
- __hostSomMarkSettings: () => JSON.stringify(SomMark.settings),
305
+ __hostSomMarkSettings: () => {
306
+ const clean = { ...SomMark.settings };
307
+ delete clean.instance;
308
+ delete clean.fs;
309
+ return JSON.stringify(clean);
310
+ },
182
311
  __hostCompile: async (src, options) => {
183
312
  return await customCompileAdapter(src, options, this.security);
184
313
  },
314
+ __hostFetch: async (input, initStr) => {
315
+ const init = initStr ? JSON.parse(initStr) : undefined;
316
+ return await customFetchAdapter(input, init, this.security);
317
+ },
185
318
  __hostRegisterDynamicTag: (id, options) => {
186
319
  this.registerDynamicTag(id, options);
187
320
  },
@@ -207,7 +340,8 @@ class EvaluatorState {
207
340
  __allowRaw: this.security.allowRaw !== false
208
341
  });
209
342
 
210
- await this.runtime.vm.evalCode(`
343
+ // Setup standard library and namespace
344
+ const setupRes = this.context.evalCode(`
211
345
  const __nativeFetch = globalThis.fetch;
212
346
  class TagBuilder {
213
347
  constructor(tagName) {
@@ -321,7 +455,7 @@ class EvaluatorState {
321
455
  return Object.freeze(parsed);
322
456
  },
323
457
  fetch: async (input, init) => {
324
- const plainRes = await __nativeFetch(input, init);
458
+ const plainRes = await __hostFetch(input.toString(), init ? JSON.stringify(init) : "");
325
459
  return {
326
460
  status: plainRes.status,
327
461
  ok: plainRes.ok,
@@ -381,41 +515,51 @@ class EvaluatorState {
381
515
  }
382
516
  };
383
517
 
384
- // Deep freeze the SomMark standard library to make it completely immutable
385
518
  Object.freeze(SomMark);
386
519
 
387
- // Establish the global SomMark constant (non-writable, non-configurable)
388
520
  Object.defineProperty(globalThis, "SomMark", {
389
521
  value: SomMark,
390
522
  writable: false,
391
523
  configurable: false
392
524
  });
393
525
 
394
- // Prevent direct/un-namespaced global fetch usage to enforce standard library architecture
395
526
  delete globalThis.fetch;
396
527
  delete globalThis.process;
397
528
  `);
398
529
 
399
- // Configure host-based module loader to support local imports perfectly
400
- this.runtime.vm.context.runtime.setModuleLoader((moduleName) => {
530
+ if (setupRes.error) {
531
+ const err = this.context.dump(setupRes.error);
532
+ setupRes.error.dispose();
533
+ throw new Error("VM initialization failed: " + JSON.stringify(err));
534
+ }
535
+ setupRes.value.dispose();
536
+
537
+ // Configure module loader using virtual FS implementation
538
+ this.runtime.setModuleLoader((moduleName) => {
401
539
  try {
402
540
  const isRaw = moduleName.endsWith("?raw");
403
541
  const cleanModuleName = isRaw ? moduleName.slice(0, -4) : moduleName;
404
- const resolvedPath = path.resolve(this.baseDir, cleanModuleName);
405
- if (fs.existsSync(resolvedPath)) {
406
- let source = fs.readFileSync(resolvedPath, "utf8");
542
+ const resolvedPath = /^https?:\/\//.test(this.baseDir)
543
+ ? new URL(cleanModuleName, this.baseDir.endsWith("/") ? this.baseDir : this.baseDir + "/").href
544
+ : path.resolve(this.baseDir, cleanModuleName);
545
+
546
+ const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
547
+ if (!fsImpl) {
548
+ throw new Error("No filesystem implementation available.");
549
+ }
550
+
551
+ if (fsImpl.existsSync(resolvedPath)) {
552
+ let source = fsImpl.readFileSync(resolvedPath, "utf8");
407
553
 
408
554
  if (isRaw) {
409
555
  const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
410
556
  return `export default \`${escapedSource}\`;`;
411
557
  }
412
558
 
413
- // Support JSON files
414
559
  if (resolvedPath.endsWith(".json")) {
415
560
  source = `export default ${source};`;
416
561
  }
417
562
 
418
- // Support Smark files
419
563
  if (resolvedPath.endsWith(".smark")) {
420
564
  const escapedSource = source.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\${/g, "\\${");
421
565
  source = `
@@ -425,7 +569,7 @@ class EvaluatorState {
425
569
  `;
426
570
  }
427
571
 
428
- return source; // MUST BE A STRING
572
+ return source;
429
573
  }
430
574
  throw new Error(`Module not found: ${moduleName}`);
431
575
  } catch (err) {
@@ -434,37 +578,37 @@ class EvaluatorState {
434
578
  });
435
579
  }
436
580
 
437
- /**
438
- * Pushes a new block scope level.
439
- */
581
+ expose(vars) {
582
+ if (!this.context) return;
583
+ expose(this.context, vars, this.pendingDeferreds);
584
+ }
585
+
440
586
  pushScope() {
441
587
  this.scopes.push({});
442
588
  this.dynamicTagsStack.push(new Map());
443
589
  }
444
590
 
445
- /**
446
- * Pops the current block scope level, cleaning up VM globals and restoring parent scope variables.
447
- */
448
591
  async popScope() {
449
592
  if (this.scopes.length > 1) {
450
593
  const popped = this.scopes.pop();
451
594
  this.dynamicTagsStack.pop();
452
595
  const keysToDelete = Object.keys(popped);
453
- if (keysToDelete.length > 0 && this.runtime) {
596
+ if (keysToDelete.length > 0 && this.context) {
454
597
  try {
455
598
  const deleteCode = keysToDelete.map(k => `delete globalThis['${k}'];`).join(" ");
456
- await this.runtime.vm.evalCode(deleteCode, "cleanup.js");
599
+ const deleteRes = this.context.evalCode(deleteCode, "cleanup.js");
600
+ if (deleteRes.value) deleteRes.value.dispose();
601
+ if (deleteRes.error) deleteRes.error.dispose();
457
602
  } catch (e) {
458
603
  // ignore
459
604
  }
460
605
  }
461
- // Restore parent scopes
462
- if (this.runtime) {
606
+ if (this.context) {
463
607
  const merged = {};
464
608
  for (const scope of this.scopes) {
465
609
  Object.assign(merged, scope);
466
610
  }
467
- this.runtime.vm.expose(merged);
611
+ this.expose(merged);
468
612
  }
469
613
  }
470
614
  }
@@ -490,8 +634,8 @@ class EvaluatorState {
490
634
  }
491
635
 
492
636
  async executeDynamicTag(id, payload) {
493
- if (!this.runtime) throw new Error("EvaluatorState not initialized");
494
- this.runtime.vm.expose({
637
+ if (!this.context) throw new Error("EvaluatorState not initialized");
638
+ this.expose({
495
639
  __activeTagPayload: () => JSON.stringify(payload)
496
640
  });
497
641
  const code = `
@@ -509,18 +653,43 @@ class EvaluatorState {
509
653
  return res;
510
654
  })()
511
655
  `;
512
- let result = await this.runtime.vm.evalCode(code, "render_tag.js");
513
- if (result instanceof Promise || (result && typeof result === "object" && typeof result.then === "function")) {
514
- result = await result;
656
+ const evalRes = this.context.evalCode(code, "render_tag.js");
657
+ if (evalRes.error) {
658
+ const err = this.context.dump(evalRes.error);
659
+ evalRes.error.dispose();
660
+ throw err;
661
+ }
662
+
663
+ let resultHandle = evalRes.unwrap();
664
+ const state = this.context.getPromiseState(resultHandle);
665
+ if (state && state.type === "pending") {
666
+ while (true) {
667
+ this.runtime.executePendingJobs();
668
+ const curState = this.context.getPromiseState(resultHandle);
669
+ if (curState.type !== "pending") {
670
+ if (curState.type === "fulfilled") {
671
+ resultHandle.dispose();
672
+ resultHandle = curState.value;
673
+ } else {
674
+ const errHandle = curState.error;
675
+ const err = this.context.dump(errHandle);
676
+ errHandle.dispose();
677
+ resultHandle.dispose();
678
+ throw err;
679
+ }
680
+ break;
681
+ }
682
+ await new Promise(resolve => setTimeout(resolve, 1));
683
+ }
515
684
  }
685
+
686
+ const result = this.context.dump(resultHandle);
687
+ resultHandle.dispose();
516
688
  return result;
517
689
  }
518
690
 
519
- /**
520
- * Synchronizes changed VM global variables back to the scope stack.
521
- */
522
- async _syncScopes() {
523
- if (!this.runtime) return;
691
+ _syncScopes() {
692
+ if (!this.context) return;
524
693
  const allKeysSet = new Set();
525
694
  for (const scope of this.scopes) {
526
695
  for (const key of Object.keys(scope)) {
@@ -531,17 +700,23 @@ class EvaluatorState {
531
700
  if (allKeys.length > 0) {
532
701
  try {
533
702
  const getValuesCode = `export default { ${allKeys.map(k => `${JSON.stringify(k)}: globalThis['${k}']`).join(", ")} };`;
534
- const valuesRes = await this.runtime.vm.evalCode(getValuesCode, "sync.js", { type: 'module' });
535
- if (valuesRes && typeof valuesRes === 'object' && 'default' in valuesRes) {
536
- const syncedValues = valuesRes.default;
537
- for (const [key, val] of Object.entries(syncedValues)) {
538
- for (let s = this.scopes.length - 1; s >= 0; s--) {
539
- if (key in this.scopes[s]) {
540
- this.scopes[s][key] = val;
541
- break;
703
+ const valuesRes = this.context.evalCode(getValuesCode, "sync.js", { type: 'module' });
704
+ if (valuesRes.value) {
705
+ const syncedValuesObj = this.context.dump(valuesRes.value);
706
+ valuesRes.value.dispose();
707
+ if (syncedValuesObj && typeof syncedValuesObj === 'object' && 'default' in syncedValuesObj) {
708
+ const syncedValues = syncedValuesObj.default;
709
+ for (const [key, val] of Object.entries(syncedValues)) {
710
+ for (let s = this.scopes.length - 1; s >= 0; s--) {
711
+ if (key in this.scopes[s]) {
712
+ this.scopes[s][key] = val;
713
+ break;
714
+ }
542
715
  }
543
716
  }
544
717
  }
718
+ } else if (valuesRes.error) {
719
+ valuesRes.error.dispose();
545
720
  }
546
721
  } catch (err) {
547
722
  // ignore
@@ -549,36 +724,28 @@ class EvaluatorState {
549
724
  }
550
725
  }
551
726
 
552
- /**
553
- * Injects variables safely into the sandbox.
554
- */
555
727
  inject(vars) {
556
- if (!this.runtime) return;
728
+ if (!this.context) return;
557
729
  const currentScope = this.scopes[this.scopes.length - 1];
558
730
  Object.assign(currentScope, vars);
559
- this.runtime.vm.expose(vars);
731
+ this.expose(vars);
560
732
  }
561
733
 
562
- /**
563
- * Executes code asynchronously and returns resolved result.
564
- */
565
734
  async execute(code) {
566
- if (!this.runtime) throw new Error("Evaluator not initialized");
735
+ if (!this.context) throw new Error("Evaluator not initialized");
567
736
 
568
737
  const timeout = this.security?.timeout ?? 5000;
569
- this.deadline = Date.now() + timeout; // Dynamic timeout safety safeguard
738
+ this.deadline = Date.now() + timeout;
570
739
 
571
- // Keep QuickJS event loop alive in the background during execution
572
740
  const interval = setInterval(() => {
573
741
  try {
574
- this.runtime.vm.context.runtime.executePendingJobs();
742
+ this.runtime.executePendingJobs();
575
743
  } catch (err) {
576
744
  // ignore
577
745
  }
578
746
  }, 1);
579
747
 
580
748
  try {
581
- // Detect top-level declarations for Auto-Export
582
749
  let autoExportedNames = [];
583
750
  let hasExplicitExports = false;
584
751
  try {
@@ -605,7 +772,7 @@ class EvaluatorState {
605
772
  }
606
773
  }
607
774
  } catch (e) {
608
- // If it fails to parse as module, it might be a simple expression, ignore
775
+ // Ignore parsing errors for simple expression fragments
609
776
  }
610
777
 
611
778
  const hasImportExport = hasExplicitExports || /\bimport\b/.test(code);
@@ -613,7 +780,6 @@ class EvaluatorState {
613
780
 
614
781
  let finalCode = code;
615
782
 
616
- // Rewrite the last expression statement to be export default so we automatically return its value
617
783
  try {
618
784
  const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', allowReturnOutsideFunction: true });
619
785
  const lastNode = ast.body[ast.body.length - 1];
@@ -639,29 +805,113 @@ class EvaluatorState {
639
805
 
640
806
  const isModule = hasImportExport || hasAwait || autoExportedNames.length > 0 || finalCode.includes("export default");
641
807
 
808
+ const fsImpl = this.settings?.fs || this.settings?.instance?.fs || this.nodeFs;
809
+ if (isModule) await prefetchImports(finalCode, this.baseDir, fsImpl);
810
+
642
811
  let result;
643
812
  if (isModule) {
644
- // Evaluate as module using Arena
645
- const evalPromise = this.runtime.vm.evalCode(finalCode, "main.js", {
646
- strict: true,
647
- strip: true,
648
- backtraceBarrier: true,
649
- type: 'module'
650
- });
813
+ const evalRes = this.context.evalCode(finalCode, "main.js", { type: 'module' });
814
+ if (evalRes.error) {
815
+ const err = this.context.dump(evalRes.error);
816
+ evalRes.error.dispose();
817
+ throw err;
818
+ }
819
+
820
+ let resultHandle = evalRes.unwrap();
821
+ const state = this.context.getPromiseState(resultHandle);
822
+ if (state && state.type === "pending") {
823
+ while (true) {
824
+ this.runtime.executePendingJobs();
825
+ const curState = this.context.getPromiseState(resultHandle);
826
+ if (curState.type !== "pending") {
827
+ if (curState.type === "fulfilled") {
828
+ resultHandle.dispose();
829
+ resultHandle = curState.value;
830
+ } else {
831
+ const errHandle = curState.error;
832
+ const err = this.context.dump(errHandle);
833
+ errHandle.dispose();
834
+ resultHandle.dispose();
835
+ throw err;
836
+ }
837
+ break;
838
+ }
839
+ await new Promise(resolve => setTimeout(resolve, 1));
840
+ }
841
+ }
842
+
843
+ let defaultHandle = this.context.getProp(resultHandle, "default");
844
+ let resolvedDefaultHandle = defaultHandle;
845
+ let isPromise = false;
846
+
847
+ const defaultState = this.context.getPromiseState(defaultHandle);
848
+ if (defaultState && !defaultState.notAPromise) {
849
+ isPromise = true;
850
+ if (defaultState.type === "pending") {
851
+ while (true) {
852
+ this.runtime.executePendingJobs();
853
+ const curState = this.context.getPromiseState(defaultHandle);
854
+ if (curState.type !== "pending") {
855
+ if (curState.type === "fulfilled") {
856
+ resolvedDefaultHandle = curState.value;
857
+ } else {
858
+ const errHandle = curState.error;
859
+ const err = this.context.dump(errHandle);
860
+ errHandle.dispose();
861
+ defaultHandle.dispose();
862
+ resultHandle.dispose();
863
+ throw err;
864
+ }
865
+ break;
866
+ }
867
+ await new Promise(resolve => setTimeout(resolve, 1));
868
+ }
869
+ } else if (defaultState.type === "fulfilled") {
870
+ resolvedDefaultHandle = defaultState.value;
871
+ } else if (defaultState.type === "rejected") {
872
+ const errHandle = defaultState.error;
873
+ const err = this.context.dump(errHandle);
874
+ errHandle.dispose();
875
+ defaultHandle.dispose();
876
+ resultHandle.dispose();
877
+ throw err;
878
+ }
879
+ }
880
+
881
+ const defaultValue = this.context.dump(resolvedDefaultHandle);
882
+
883
+ if (isPromise) {
884
+ resolvedDefaultHandle.dispose();
885
+ }
886
+ defaultHandle.dispose();
887
+
888
+ const res = this.context.dump(resultHandle);
651
889
 
652
- const res = await evalPromise;
890
+ this.context.setProp(this.context.global, "__tempModule", resultHandle);
891
+ const copyRes = this.context.evalCode(`
892
+ for (const key of Object.keys(__tempModule)) {
893
+ if (key !== "default") {
894
+ globalThis[key] = __tempModule[key];
895
+ }
896
+ }
897
+ delete globalThis.__tempModule;
898
+ `);
899
+ if (copyRes.error) {
900
+ copyRes.error.dispose();
901
+ } else {
902
+ copyRes.value.dispose();
903
+ }
904
+ resultHandle.dispose();
653
905
 
654
- // Move exports directly to global scope in the VM
655
906
  if (res && typeof res === 'object') {
656
907
  const currentScope = this.scopes[this.scopes.length - 1];
657
908
  for (const [key, val] of Object.entries(res)) {
658
909
  if (key !== 'default') {
659
910
  currentScope[key] = val;
660
- this.runtime.vm.expose({ [key]: val });
661
911
  }
662
912
  }
663
913
  if ('default' in res) {
664
- result = res.default;
914
+ result = defaultValue;
665
915
  } else {
666
916
  result = undefined;
667
917
  }
@@ -669,17 +919,41 @@ class EvaluatorState {
669
919
  result = res;
670
920
  }
671
921
  } else {
672
- result = await this.runtime.vm.evalCode(code, "main.js");
673
- }
674
-
675
- if (result instanceof Promise || (result && typeof result === "object" && typeof result.then === "function")) {
676
- result = await result;
922
+ const evalRes = this.context.evalCode(code, "main.js");
923
+ if (evalRes.error) {
924
+ const err = this.context.dump(evalRes.error);
925
+ evalRes.error.dispose();
926
+ throw err;
927
+ }
928
+ let resultHandle = evalRes.unwrap();
929
+ const state = this.context.getPromiseState(resultHandle);
930
+ if (state && state.type === "pending") {
931
+ while (true) {
932
+ this.runtime.executePendingJobs();
933
+ const curState = this.context.getPromiseState(resultHandle);
934
+ if (curState.type !== "pending") {
935
+ if (curState.type === "fulfilled") {
936
+ resultHandle.dispose();
937
+ resultHandle = curState.value;
938
+ } else {
939
+ const errHandle = curState.error;
940
+ const err = this.context.dump(errHandle);
941
+ errHandle.dispose();
942
+ resultHandle.dispose();
943
+ throw err;
944
+ }
945
+ break;
946
+ }
947
+ await new Promise(resolve => setTimeout(resolve, 1));
948
+ }
949
+ }
950
+ result = this.context.dump(resultHandle);
951
+ resultHandle.dispose();
677
952
  }
678
953
 
679
954
  await this._syncScopes();
680
955
  return result;
681
956
  } catch (error) {
682
- // Try to extract line/col from stack trace
683
957
  const stack = error.stack || "";
684
958
  const match = stack.match(/main\.js:(\d+):(\d+)/) || stack.match(/:(\d+):(\d+)/);
685
959
 
@@ -695,44 +969,46 @@ class EvaluatorState {
695
969
  }
696
970
  }
697
971
 
698
- /**
699
- * Disposal.
700
- */
701
972
  destroy() {
702
973
  if (this.runtime) {
703
- try {
704
- // Execute any lingering jobs & trigger the QuickJS garbage collector
705
- if (this.runtime.vm?.context?.runtime) {
706
- this.runtime.vm.context.runtime.executePendingJobs();
707
- this.runtime.vm.context.runtime.gc();
974
+ if (this.pendingDeferreds) {
975
+ for (const deferred of this.pendingDeferreds) {
976
+ try {
977
+ if (deferred.alive) {
978
+ deferred.dispose();
979
+ }
980
+ } catch (e) {}
708
981
  }
709
- } catch (e) { }
982
+ this.pendingDeferreds.clear();
983
+ }
984
+
985
+ try {
986
+ this.runtime.executePendingJobs();
987
+ } catch (e) {}
710
988
 
711
989
  try {
990
+ if (this.context) {
991
+ this.context.dispose();
992
+ }
712
993
  this.runtime.dispose();
713
994
  } catch (e) {
714
- // Graceful logging for minor Emscripten reference delays
715
- console.warn("<$yellow:Warning:$> Safe context disposal warning: " + e.message);
995
+ console.warn(formatMessage("<$yellow:Warning:$> Safe context disposal warning: " + e.message));
716
996
  }
717
997
  this.runtime = null;
998
+ this.context = null;
718
999
  }
719
1000
  }
720
1001
  }
721
1002
 
722
- /**
723
- * Evaluator
724
- *
725
- * Acts as a router/proxy singleton that routes VM calls to a stack of active isolated runtimes.
726
- * This guarantees concurrent and recursive safety across all compiler runs.
727
- */
728
1003
  class Evaluator {
729
1004
  constructor() {
730
1005
  this.instances = [];
731
1006
  }
732
1007
 
733
- /**
734
- * Get the active logic engine state instance at the top of the stack.
735
- */
1008
+ setDefaultFs(fs) {
1009
+ defaultFs = fs;
1010
+ }
1011
+
736
1012
  get active() {
737
1013
  if (this.instances.length === 0) {
738
1014
  throw new Error("No active EvaluatorState instance. Did you call init()?");