dsh-ptc-plus 0.4.1 → 0.4.2

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.
Files changed (53) hide show
  1. package/README.en.md +6 -10
  2. package/README.md +6 -10
  3. package/client.js +262 -31
  4. package/compiler-core.cjs +132 -130
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/README.md +1 -0
  7. package/docs/adr/0023-global-user-bindings.md +6 -6
  8. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  9. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +5 -5
  10. package/docs/architecture.md +9 -5
  11. package/docs/client-ui.md +1 -1
  12. package/docs/durability-design.md +6 -3
  13. package/docs/installation.md +4 -4
  14. package/docs/runtime-reference.md +5 -1
  15. package/docs/stateful-binding-updates-proposal.md +8 -4
  16. package/docs/user-bindings.en.md +5 -3
  17. package/docs/user-bindings.md +4 -2
  18. package/docs/verification-optimization-work.md +41 -0
  19. package/docs/verification.md +15 -1
  20. package/internal/async-local-storage-intrinsics.js +38 -0
  21. package/internal/binding-declaration-contract.js +8 -0
  22. package/internal/binding-source-analysis.js +381 -64
  23. package/internal/binding-update-policy.js +7 -1
  24. package/internal/cell-analysis.js +21 -8
  25. package/internal/compiler-intrinsics.js +9 -43
  26. package/internal/compiler-module-links.js +7 -4
  27. package/internal/dynamic-environment-integration.js +8 -4
  28. package/internal/dynamic-environment-runtime.js +24 -3
  29. package/internal/dynamic-native-calls.js +4 -1
  30. package/internal/dynamic-native-runtime.js +15 -1
  31. package/internal/failure-reporting.js +38 -30
  32. package/internal/isolated-worker.js +17 -10
  33. package/internal/kernel-worker.js +493 -331
  34. package/internal/module-compilation.js +5 -4
  35. package/internal/module-transform-contract.js +32 -2
  36. package/internal/record-utils.js +24 -15
  37. package/internal/repl-value-observer.js +6 -3
  38. package/internal/runtime-intrinsics.js +101 -0
  39. package/internal/session-cell-executor.js +2 -1
  40. package/internal/session-journal-schema.js +30 -13
  41. package/internal/session-journal.js +19 -4
  42. package/internal/session-runtime.js +1 -0
  43. package/internal/source-position-map.js +19 -0
  44. package/internal/stateful-root-compiler.js +3 -1
  45. package/internal/typescript-transform.js +4 -2
  46. package/internal/user-binding-console-worker.js +101 -41
  47. package/internal/user-bindings.js +10 -2
  48. package/internal/value-wire.js +101 -71
  49. package/internal/worker-cwd-virtualization.js +80 -53
  50. package/internal/worker-environment.js +20 -0
  51. package/internal/worker-repl-realm.js +134 -0
  52. package/package.json +2 -1
  53. package/src/client-activity.js +8 -1
@@ -1,6 +1,7 @@
1
1
  import { LIVE_MODULE_SEMANTICS } from './repl-rewrite-contract.js'
2
2
  import { LEGACY_LANGUAGE_SEMANTICS, STATEFUL_LANGUAGE_SEMANTICS,
3
3
  PROTECTED_LANGUAGE_SEMANTICS, normalizeLanguageSemantics } from './language-semantics.js'
4
+ import { moduleTransformForLanguage, normalizeModuleTransform } from './module-transform-contract.js'
4
5
 
5
6
  /**
6
7
  * Translate the public compatibility settings into the execution policy used
@@ -15,8 +16,10 @@ export function executionPolicies(config, replayRecord = undefined) {
15
16
  const bindingPolicy = Object.freeze({ ...replayRecord.bindingPolicy })
16
17
  const rewritesEnabled = Object.freeze({ ...replayRecord.rewritePolicy })
17
18
  const moduleSemantics = Object.freeze({ ...replayRecord.moduleSemantics })
19
+ const languageSemantics = normalizeLanguageSemantics(replayRecord.languageSemantics ?? LEGACY_LANGUAGE_SEMANTICS)
18
20
  return Object.freeze({
19
- languageSemantics: normalizeLanguageSemantics(replayRecord.languageSemantics ?? LEGACY_LANGUAGE_SEMANTICS),
21
+ languageSemantics,
22
+ moduleTransform: normalizeModuleTransform(replayRecord.moduleTransform, languageSemantics),
20
23
  bindingPolicy,
21
24
  rewritesEnabled,
22
25
  moduleSemantics,
@@ -25,6 +28,7 @@ export function executionPolicies(config, replayRecord = undefined) {
25
28
  if (config.bindingUpdates === 'stateful' && config.legacyBindingSettings !== true) {
26
29
  return Object.freeze({
27
30
  languageSemantics: STATEFUL_LANGUAGE_SEMANTICS,
31
+ moduleTransform: moduleTransformForLanguage(STATEFUL_LANGUAGE_SEMANTICS),
28
32
  bindingPolicy: Object.freeze({ variableRedeclarations: true, functionClassRedeclarations: true }),
29
33
  rewritesEnabled: Object.freeze({ autoRewriteImports: true, autoStripExports: true, autoSplitRedeclarations: true }),
30
34
  moduleSemantics: LIVE_MODULE_SEMANTICS,
@@ -33,6 +37,7 @@ export function executionPolicies(config, replayRecord = undefined) {
33
37
  if (config.bindingUpdates === 'protected' && config.legacyBindingSettings !== true) {
34
38
  return Object.freeze({
35
39
  languageSemantics: PROTECTED_LANGUAGE_SEMANTICS,
40
+ moduleTransform: moduleTransformForLanguage(PROTECTED_LANGUAGE_SEMANTICS),
36
41
  bindingPolicy: Object.freeze({ variableRedeclarations: false, functionClassRedeclarations: false }),
37
42
  // Module syntax is part of the language in protected mode too. The
38
43
  // policy protects existing names; it does not remove valid syntax.
@@ -42,6 +47,7 @@ export function executionPolicies(config, replayRecord = undefined) {
42
47
  }
43
48
  return Object.freeze({
44
49
  languageSemantics: LEGACY_LANGUAGE_SEMANTICS,
50
+ moduleTransform: moduleTransformForLanguage(LEGACY_LANGUAGE_SEMANTICS),
45
51
  bindingPolicy: Object.freeze({
46
52
  variableRedeclarations: config.looseTopLevelRedeclarations === true,
47
53
  functionClassRedeclarations: config.looseTopLevelFunctionClassRedeclarations === true,
@@ -17,7 +17,7 @@ import {
17
17
  renderDurabilityReason,
18
18
  renderDurabilityReasons,
19
19
  } from './module-policy.js'
20
- import { applySourceEdits, mapSourcePosition, mapSourceSpan } from './source-position-map.js'
20
+ import { applySourceEdits, mapSourcePosition, mapSourceSpan, sourceRangeHasOriginalText } from './source-position-map.js'
21
21
  import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
22
22
  import { compileStatefulRoot } from './stateful-root-compiler.js'
23
23
  import { CELL_PARSER_PLUGINS, bindCompilerIntrinsics, lowerStatefulDecorators, lowerStatefulResources, lowerNativeLanguageSource, normalizeStatefulScopes } from './repl-scope-normalizer.js'
@@ -240,7 +240,9 @@ function staticModuleClassification(moduleLoads) {
240
240
  }
241
241
 
242
242
  /** Conservatively classify a cell before giving it non-journalable capability. */
243
- export function classifyDurability(code, knownBindings = new Set(), { sourceType = 'script', internalModules = new Set() } = {}) {
243
+ export function classifyDurability(code, knownBindings = new Set(), {
244
+ sourceType = 'script', internalModules = new Set(), sourceMap = undefined, originalSource = undefined,
245
+ } = {}) {
244
246
  let body
245
247
  let moduleLoads = []
246
248
  if (sourceType === 'module') {
@@ -297,11 +299,12 @@ export function classifyDurability(code, knownBindings = new Set(), { sourceType
297
299
  }
298
300
  return false
299
301
  }
300
- walkAst(body, (node, parent, parentKey, { scopes, strict }) => {
302
+ walkAst(body, (node, parent, parentKey, { scopes, strict, thisIsGlobal }) => {
301
303
  let nestedScopes = scopes
302
304
  if (isFunction(node)) {
303
305
  strict ||= hasStrictDirective(node.body)
304
306
  nestedScopes = [...scopes, functionParameterBindings(node)]
307
+ if (node.type !== 'ArrowFunctionExpression') thisIsGlobal = false
305
308
  } else if (node.type === 'BlockStatement' && node !== body) {
306
309
  const names = directBlockBindings(node.body)
307
310
  if (isFunction(parent)) addFunctionVariables(node, names, strict)
@@ -326,6 +329,11 @@ export function classifyDurability(code, knownBindings = new Set(), { sourceType
326
329
  if (node.type === 'ImportExpression') {
327
330
  classifyModule(node.source)
328
331
  }
332
+ const sourceOwnedThis = sourceMap === undefined
333
+ || sourceRangeHasOriginalText(sourceMap, code, originalSource, node.start, node.end)
334
+ if (node.type === 'ThisExpression' && thisIsGlobal && sourceOwnedThis) {
335
+ addReason(Object.freeze({ kind: 'ambient', name: 'globalThis' }))
336
+ }
329
337
  if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'require'
330
338
  && !isBound('require', nestedScopes)) {
331
339
  classifyModule(node.arguments[0])
@@ -367,8 +375,8 @@ export function classifyDurability(code, knownBindings = new Set(), { sourceType
367
375
  addReason(Object.freeze({ kind: 'ambient', name: 'Math' }))
368
376
  }
369
377
  }
370
- return { scopes: nestedScopes, strict }
371
- }, undefined, { scopes: [rootBindings], strict: rootStrict })
378
+ return { scopes: nestedScopes, strict, thisIsGlobal }
379
+ }, undefined, { scopes: [rootBindings], strict: rootStrict, thisIsGlobal: sourceType === 'script' })
372
380
  return {
373
381
  durability: reasons.size === 0 ? 'durable' : 'volatile',
374
382
  reasons: Object.freeze([...reasons.values()]),
@@ -473,12 +481,17 @@ export function prepareProgram(program, options = {}) {
473
481
  prepared = { ...prepared, deferredHelpers: normalized.deferredHelpers }
474
482
  intrinsicContext.expression = `this[${JSON.stringify(prepared.rootRuntimeName)}].intrinsics`
475
483
  const classificationSource = bindCompilerIntrinsics(lowerStatefulDecorators(
476
- { ...prepared, code: prepared.classificationCode }, undefined, intrinsicContext), intrinsicContext).code
484
+ { ...prepared, code: prepared.classificationCode, sourceMap: prepared.classificationSourceMap },
485
+ undefined, intrinsicContext), intrinsicContext)
477
486
  prepared = bindCompilerIntrinsics(lowerStatefulDecorators(prepared, undefined, intrinsicContext), intrinsicContext)
478
487
  prepared = lowerStatefulResources(prepared, { intrinsicContext, nativeUsing: options.nativeUsing })
479
488
  const parsed = parseExecutableCell(prepared.code, { eraseTypes: true })
480
- const classification = classifyDurability(parseExecutableCell(classificationSource, { eraseTypes: true }).code,
481
- new Set([...(options.knownBindings ?? []), ...prepared.declared]))
489
+ const classificationCode = parseExecutableCell(classificationSource.code, { eraseTypes: true }).code
490
+ const classification = classifyDurability(classificationCode,
491
+ new Set([...(options.knownBindings ?? []), ...prepared.declared]), {
492
+ sourceMap: classificationSource.sourceMap,
493
+ originalSource: program,
494
+ })
482
495
  const reasons = [...staticModuleClassification(prepared.moduleLoads), ...classification.reasons]
483
496
  const lowered = rewriteCellReturns(parsed.code, prepared.sourceMap,
484
497
  new Set([...prepared.declared, ...(options.knownBindings ?? [])]))
@@ -1,15 +1,12 @@
1
1
  import { stringifyCompilerData } from './compiler-data.js'
2
2
  import { compilerStorageSource } from './compiler-storage-source.js'
3
3
  import { compilerDescriptorSource } from './compiler-descriptors.js'
4
+ import { runtimeIntrinsics } from './runtime-intrinsics.js'
4
5
 
5
6
  /** Compiler operations are captured before an owned realm executes user code. */
6
7
  const defaultRealmFunction = Function
7
8
  const realms = new WeakMap()
8
- const uncurry = Function.prototype.bind.bind(Function.prototype.call)
9
- const weakMapGet = uncurry(WeakMap.prototype.get)
10
- const weakMapSet = uncurry(WeakMap.prototype.set)
11
- const mapKeys = uncurry(Map.prototype.keys)
12
- const mapIteratorNext = uncurry(Object.getPrototypeOf(new Map().keys()).next)
9
+ const { weakMapGet, weakMapSet } = runtimeIntrinsics
13
10
 
14
11
  export function captureCompilerIntrinsics(realmFunction = defaultRealmFunction) {
15
12
  let intrinsics = weakMapGet(realms, realmFunction)
@@ -21,7 +18,8 @@ export function captureCompilerIntrinsics(realmFunction = defaultRealmFunction)
21
18
  const object=properties.Object;
22
19
  const symbol=Symbol;
23
20
  return {Object:object,WeakMap,WeakSet,unscopables:symbol.unscopables,Reflect:{apply:nativeReflect.apply,
24
- construct:nativeReflect.construct,ownKeys:nativeReflect.ownKeys,defineProperty:properties.reflectDefineProperty,
21
+ construct:nativeReflect.construct,deleteProperty:nativeReflect.deleteProperty,has:nativeReflect.has,
22
+ ownKeys:nativeReflect.ownKeys,defineProperty:properties.reflectDefineProperty,
25
23
  get decorate(){return nativeReflect.decorate}},TypeError,ReferenceError,SyntaxError,Error,Symbol,String,Number,
26
24
  Promise:{resolve:Promise.resolve.bind(Promise),reject:Promise.reject.bind(Promise)},
27
25
  SuppressedError:typeof SuppressedError==='function'?SuppressedError:undefined,
@@ -36,49 +34,17 @@ export const moduleCompilerIntrinsics = captureCompilerIntrinsics()
36
34
  /** Internal graph records use captured collection operations. Source-owned
37
35
  * objects and callbacks keep their original realm and observable methods. */
38
36
  export const moduleRuntimeIntrinsics = {
37
+ ...runtimeIntrinsics,
39
38
  Object: moduleCompilerIntrinsics.Object,
40
39
  Reflect: moduleCompilerIntrinsics.Reflect,
41
40
  Error: moduleCompilerIntrinsics.Error,
42
- Map, Set, WeakMap, Proxy,
43
- mapGet: uncurry(Map.prototype.get),
44
- mapHas: uncurry(Map.prototype.has),
45
- mapSet: uncurry(Map.prototype.set),
46
- mapDelete: uncurry(Map.prototype.delete),
47
- mapClear: uncurry(Map.prototype.clear),
48
- mapSize: uncurry(Object.getOwnPropertyDescriptor(Map.prototype, 'size').get),
49
- mapFirstKey: value => mapIteratorNext(mapKeys(value)).value,
50
- mapForEach: uncurry(Map.prototype.forEach),
51
- setHas: uncurry(Set.prototype.has),
52
- setAdd: uncurry(Set.prototype.add),
53
- setDelete: uncurry(Set.prototype.delete),
54
- setClear: uncurry(Set.prototype.clear),
55
- setForEach: uncurry(Set.prototype.forEach),
56
- weakMapHas: uncurry(WeakMap.prototype.has),
57
- weakMapGet, weakMapSet,
58
- isArray: Array.isArray,
59
- includes: uncurry(Array.prototype.includes),
60
- join: uncurry(Array.prototype.join),
61
- sliceString: uncurry(String.prototype.slice),
62
- replaceAllString: uncurry(String.prototype.replaceAll),
63
- includesString: uncurry(String.prototype.includes),
64
- floor: Math.floor,
65
- min: Math.min,
66
- max: Math.max,
41
+ TypeError: moduleCompilerIntrinsics.TypeError,
67
42
  // Only private native Promises enter this boundary. Source promises and the
68
43
  // public result keep their prototypes; private reactions cannot select a
69
44
  // source constructor/species or dynamically dispatch through then/catch.
70
45
  observeOwnedPromise: moduleCompilerIntrinsics.observeOwnedPromise,
71
- appendArray(target, value) {
72
- moduleCompilerIntrinsics.Object.defineProperty(target, target.length,
73
- { value, writable: true, enumerable: true, configurable: true })
74
- return target
75
- },
76
- copyArray(source, start = 0, end = source.length, target = []) {
77
- for (let index = start; index < end; index++) this.appendArray(target, source[index])
78
- return target
79
- },
80
- sort: uncurry(Array.prototype.sort),
81
- compare: uncurry(String.prototype.localeCompare),
82
- startsWith: uncurry(String.prototype.startsWith),
46
+ promiseThen: moduleCompilerIntrinsics.promiseThen,
47
+ promiseResolve: moduleCompilerIntrinsics.Promise.resolve,
48
+ promiseReject: moduleCompilerIntrinsics.Promise.reject,
83
49
  stringify: stringifyCompilerData,
84
50
  }
@@ -1,6 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
 
3
3
  const encodeReference = encodeURIComponent
4
+ const objectEntries = Object.entries
5
+ const sortArray = Function.prototype.call.bind(Array.prototype.sort)
6
+ const jsonStringify = JSON.stringify
4
7
 
5
8
  // These ambient inputs must remain outside source-owned CommonJS bindings.
6
9
  export const commonJsCompilerGlobals = new Set(['process'])
@@ -21,14 +24,14 @@ const ATTRIBUTED_LINK_KIND = 'static-attributed'
21
24
  * attribute-free links keep the reference they had before attributes were
22
25
  * distinguished. */
23
26
  export function staticModuleLinkReference(source, attributes) {
24
- const entries = Object.entries(attributes ?? {})
27
+ const entries = objectEntries(attributes ?? {})
25
28
  if (entries.length === 0) return compilerModuleReference(STATIC_LINK_KIND, source)
26
- entries.sort((left, right) => left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0)
27
- return compilerModuleReference(ATTRIBUTED_LINK_KIND, JSON.stringify([source, entries]))
29
+ sortArray(entries, (left, right) => left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0)
30
+ return compilerModuleReference(ATTRIBUTED_LINK_KIND, jsonStringify([source, entries]))
28
31
  }
29
32
 
30
33
  /** Compiler helpers resolve from their own URL, independently of source-owned
31
34
  * wrapper bindings and the ESM translator's restricted CommonJS require. */
32
35
  export function commonJsCompilerImport(url) {
33
- return `process.getBuiltinModule('module').createRequire(${JSON.stringify(url.href)})(${JSON.stringify(fileURLToPath(url))})`
36
+ return `process.getBuiltinModule('module').createRequire(${jsonStringify(url.href)})(${jsonStringify(fileURLToPath(url))})`
34
37
  }
@@ -34,15 +34,19 @@ export function adaptDynamicCell(code, sourceMap, options) {
34
34
  let setup
35
35
  if (options.module === true) {
36
36
  const runtimeUrl = new URL('./dynamic-environment-runtime.js', import.meta.url)
37
+ const interfaceOwner = allocate('dynamic_interface_owner')
38
+ const runtimeOptions = `{interfaceOwner:${interfaceOwner}${options.importOperation === undefined
39
+ ? '' : `,importModule:${options.importOperation}`}}`
37
40
  if (options.sourceType === 'module') {
38
41
  const factory = allocate('dynamic_factory')
39
- setup = `\nimport {createDynamicEnvironmentRuntime as ${factory}} from ${JSON.stringify(runtimeUrl.href)};\n`
40
- expression = `${factory}(${options.importOperation === undefined ? '' : `{importModule:${options.importOperation}}`}).installIntrinsics().environment({strict:true,getThis:()=>void 0})`
41
- options = { ...options, internalBindings: new Set([...options.internalBindings ?? [], factory]) }
42
+ setup = `\nimport {createDynamicEnvironmentRuntime as ${factory}} from ${JSON.stringify(runtimeUrl.href)};\nfunction ${interfaceOwner}(){}\n`
43
+ expression = `${factory}(${runtimeOptions}).installIntrinsics().environment({strict:true,getThis:()=>void 0})`
44
+ options = { ...options, internalBindings: new Set([...options.internalBindings ?? [], factory, interfaceOwner]) }
42
45
  } else {
43
46
  const names = ['exports', 'module', 'require', '__filename', '__dirname', 'arguments']
44
47
  const bindings = names.map(name => `[${JSON.stringify(name)},{kind:"param",get:()=>${name}${name === 'arguments' && strict ? '' : `,set:v=>${name}=v`}}]`).join(',')
45
- setup = `\nconst ${environmentName}=${commonJsCompilerImport(runtimeUrl)}.createDynamicEnvironmentRuntime(${options.importOperation === undefined ? '' : `{importModule:${options.importOperation}}`}).installIntrinsics().environment({strict:${strict},getThis:()=>this,getNewTarget:()=>new.target,allowNewTarget:true,nativeBindings:[${bindings}]});\n`
48
+ setup = `\nfunction ${interfaceOwner}(){}\nconst ${environmentName}=${commonJsCompilerImport(runtimeUrl)}.createDynamicEnvironmentRuntime(${runtimeOptions}).installIntrinsics().environment({strict:${strict},getThis:()=>this,getNewTarget:()=>new.target,allowNewTarget:true,nativeBindings:[${bindings}]});\n`
49
+ options = { ...options, internalBindings: new Set([...options.internalBindings ?? [], interfaceOwner]) }
46
50
  }
47
51
  } else setup = options.nativeRoot === true ? `\nconst ${environmentName}=${options.environmentGlobal};\n`
48
52
  : `\nconst ${environmentName}=${options.rootRuntime}.dynamic();\n`
@@ -16,6 +16,17 @@ function captureReflection(reflect) {
16
16
  return captured
17
17
  }
18
18
  const defaultIntrinsics = { intrinsicEval: eval, realmFunction: Function, globalObject: globalThis, reflect: captureReflection(Reflect) }
19
+ let programAmbientResolver
20
+
21
+ /** Install the worker-owned resolver used by explicitly adapted program modules. */
22
+ export function installProgramAmbientResolver(resolve) {
23
+ if (programAmbientResolver !== undefined) throw new Error('program ambient resolver is already installed')
24
+ if (typeof resolve !== 'function') throw new TypeError('program ambient resolver must be a function')
25
+ programAmbientResolver = resolve
26
+ return () => {
27
+ if (programAmbientResolver === resolve) programAmbientResolver = undefined
28
+ }
29
+ }
19
30
 
20
31
  const isObject = value => value !== null && (typeof value === 'object' || typeof value === 'function')
21
32
  const isMap = types.isMap
@@ -45,7 +56,7 @@ const asBindings = value => {
45
56
  export function createDynamicEnvironmentRuntime({
46
57
  importModule = (source, options) => import(source, options),
47
58
  intrinsicEval = defaultIntrinsics.intrinsicEval, realmFunction = defaultIntrinsics.realmFunction, globalObject = defaultIntrinsics.globalObject,
48
- errors, reflect = defaultIntrinsics.reflect, object, interfaceOwner,
59
+ errors, reflect = defaultIntrinsics.reflect, object, interfaceOwner, resolveAmbient = programAmbientResolver,
49
60
  } = {}) {
50
61
  const intrinsics = captureCompilerIntrinsics(realmFunction)
51
62
  const { unscopables, Object: intrinsicObject, array: internalArray } = intrinsics
@@ -143,7 +154,10 @@ export function createDynamicEnvironmentRuntime({
143
154
  return deleted
144
155
  },
145
156
  }, object, strict)
146
- const ambientReference = (name, strict) => reference(name, {
157
+ const ambientReference = (name, strict) => {
158
+ const resolved = resolveAmbient?.(name)
159
+ if (resolved !== undefined) return reference(name, resolved, undefined, strict)
160
+ return reference(name, {
147
161
  get() {
148
162
  if (!reflect.has(globalObject, name)) throw new errors.ReferenceError(`${name} is not defined`)
149
163
  return reflect.get(globalObject, name, globalObject)
@@ -154,7 +168,8 @@ export function createDynamicEnvironmentRuntime({
154
168
  },
155
169
  typeof: () => reflect.has(globalObject, name) ? typeof reflect.get(globalObject, name, globalObject) : 'undefined',
156
170
  delete: () => reflect.deleteProperty(globalObject, name),
157
- }, undefined, strict)
171
+ }, undefined, strict)
172
+ }
158
173
 
159
174
  function environment({ frames = [], varFrame = new internal.Map(), strict = false, origin, lexicalContext = {},
160
175
  getThis = () => globalObject, getNewTarget = () => undefined, allowNewTarget = false,
@@ -170,6 +185,12 @@ export function createDynamicEnvironmentRuntime({
170
185
  importModule,
171
186
  internalArray,
172
187
  expose: nativeCalls.exposedValue,
188
+ exposeProperty(receiver, key) {
189
+ if (receiver === null || receiver === undefined) {
190
+ throw new errors.TypeError(`Cannot read properties of ${receiver}`)
191
+ }
192
+ return nativeCalls.exposedMemberValue(receiver, reflect.get(object(receiver), key, receiver))
193
+ },
173
194
  prepareCall: nativeCalls.prepareCall,
174
195
  prepareInvocation: nativeCalls.prepareInvocation,
175
196
  releaseMemberReceiver: nativeCalls.releaseMemberReceiver,
@@ -42,7 +42,10 @@ export function rewriteNativeCalls(tree, environment, { origin, skip, operations
42
42
  parent.node.computed = true
43
43
  parent.node.key = t.stringLiteral(path.node.name)
44
44
  }
45
- path.replaceWith(t.inherits(helper('expose', [path.node]), path.node))
45
+ if (path.isMemberExpression() && !t.isSuper(path.node.object) && !t.isPrivateName(path.node.property)) {
46
+ const key = path.node.computed ? path.node.property : t.stringLiteral(path.node.property.name)
47
+ path.replaceWith(t.inherits(helper('exposeProperty', [path.node.object, key]), path.node))
48
+ } else path.replaceWith(t.inherits(helper('expose', [path.node]), path.node))
46
49
  path.skip()
47
50
  }
48
51
  const prepare = (callee, target, optional = false) => {
@@ -11,6 +11,7 @@ const exposedIntrinsics = new internal.WeakMap()
11
11
  const sources = createCallableSourceRegistry()
12
12
  const callableSources = new internal.WeakMap()
13
13
  const callableSourceTargets = new internal.WeakMap()
14
+ const callableInterfaces = new internal.WeakMap()
14
15
  const sourceTarget = origin => typeof origin === 'string' ? origin : origin?.target
15
16
  const copyArguments = (source, start = 0, result = []) => internal.copyArray(source, start, source.length, result)
16
17
  const invocationScopes = new AsyncLocalStorage()
@@ -114,7 +115,19 @@ export function createNativeCallAdapter({ realmFunction, intrinsicEval, reflect,
114
115
  internal.weakMapSet(state.callableSources, compiled, source)
115
116
  return compiled
116
117
  }
117
- const exposedValue = value => internal.mapGet(selected.exposed, value) ?? internal.weakMapGet(exposedIntrinsics, value) ?? value
118
+ const exposedValue = value => {
119
+ const exposed = internal.mapGet(selected.exposed, value) ?? internal.weakMapGet(exposedIntrinsics, value)
120
+ if (exposed !== undefined) return exposed
121
+ if (typeof value === 'function' && !internal.weakMapHas(callableInterfaces, value)) {
122
+ internal.weakMapSet(callableInterfaces, value, selected)
123
+ }
124
+ return value
125
+ }
126
+ const exposedMemberValue = (receiver, value) => {
127
+ const owner = typeof receiver === 'function' ? internal.weakMapGet(callableInterfaces, receiver) : undefined
128
+ const exposed = owner === undefined ? undefined : internal.mapGet(owner.exposed, value)
129
+ return exposed ?? exposedValue(value)
130
+ }
118
131
  const propagate = (error, origin) => {
119
132
  recordExceptionOrigin(error, sourceTarget(origin), { reset: true })
120
133
  for (let scope = currentScope(); scope !== undefined; scope = scope.parent) {
@@ -218,6 +231,7 @@ export function createNativeCallAdapter({ realmFunction, intrinsicEval, reflect,
218
231
  return {
219
232
  install,
220
233
  exposedValue,
234
+ exposedMemberValue,
221
235
  isEval: callee => callee === state.intrinsicEval || internal.mapHas(selected.exposed, state.intrinsicEval) && callee === internal.mapGet(selected.exposed, state.intrinsicEval),
222
236
  evaluate(compiled, environment) {
223
237
  // Bootstrap originals never leave this owner. Direct eval must use the
@@ -1,15 +1,19 @@
1
1
  import { diagnostic } from './diagnostic.js'
2
- import { AsyncLocalStorage } from 'node:async_hooks'
3
2
  import { createHash } from 'node:crypto'
4
- import { moduleRuntimeIntrinsics as internal } from './compiler-intrinsics.js'
3
+ import { createPrivateAsyncLocalStorage } from './async-local-storage-intrinsics.js'
4
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
5
5
 
6
+ const { Object, RegExp, isArray, numberIsSafeInteger, toNumber, toString,
7
+ splitString, startsWith, endsWith, sliceString, searchString, replaceString,
8
+ trimString, regexpExec, prependArray, join, bufferByteLength, bufferIsBuffer,
9
+ bufferSubarray, bufferToString } = internal
6
10
  export const MAX_ERROR_LOG_BYTES = 4 * 1024
7
11
  export const FAILURE_HINT_THRESHOLD = 3
8
12
  export const LONG_CELL_CODE_UNITS = 2_000
9
13
  const BINDING_FAILURE = Symbol('binding failure')
10
14
  const PROGRAM_FAILURES = new internal.WeakMap()
11
15
  const MAX_CAUSE_CODE_UNITS = 2048
12
- const exceptionOrigins = new AsyncLocalStorage()
16
+ const exceptionOrigins = createPrivateAsyncLocalStorage()
13
17
  const runExceptionScope = exceptionOrigins.run.bind(exceptionOrigins)
14
18
  const currentExceptionScope = exceptionOrigins.getStore.bind(exceptionOrigins)
15
19
 
@@ -36,14 +40,14 @@ export function recordExceptionOrigin(error, origin, { reset = false, sourceFail
36
40
  }
37
41
 
38
42
  const SOURCE_LINE_BREAK = /\r\n|[\n\r\u2028\u2029]/
39
- const sourceLines = source => source.split(SOURCE_LINE_BREAK)
43
+ const sourceLines = source => splitString(source, SOURCE_LINE_BREAK)
40
44
 
41
45
  /** A cell position is a 1-based line and column inside its source. The column
42
46
  * may address the position just after the last character of a line. */
43
47
  function boundedPosition(position, lines) {
44
48
  if (position === null || typeof position !== 'object'
45
- || !Number.isSafeInteger(position.line) || position.line < 1
46
- || !Number.isSafeInteger(position.column) || position.column < 1
49
+ || !numberIsSafeInteger(position.line) || position.line < 1
50
+ || !numberIsSafeInteger(position.column) || position.column < 1
47
51
  || position.line > lines.length || position.column > lines[position.line - 1].length + 1) return undefined
48
52
  return { line: position.line, column: position.column }
49
53
  }
@@ -55,14 +59,15 @@ export function cellPosition(position, source) {
55
59
 
56
60
  /** Only a fact belonging to the current original source can bypass its map. */
57
61
  export function exceptionOriginPosition(origins, source) {
58
- if (!Array.isArray(origins)) return undefined
62
+ if (!isArray(origins)) return undefined
59
63
  const prefix = `eval:${createHash('sha256').update(source).digest('hex')}:`
60
64
  const lines = sourceLines(source)
61
- for (const origin of origins) {
62
- if (typeof origin !== 'string' || !origin.startsWith(prefix)) continue
63
- const match = /^(\d+):(\d+)$/.exec(origin.slice(prefix.length))
65
+ for (let index = 0; index < origins.length; index += 1) {
66
+ const origin = origins[index]
67
+ if (typeof origin !== 'string' || !startsWith(origin, prefix)) continue
68
+ const match = regexpExec(/^(\d+):(\d+)$/, sliceString(origin, prefix.length))
64
69
  if (match === null) continue
65
- const position = boundedPosition({ line: Number(match[1]), column: Number(match[2]) }, lines)
70
+ const position = boundedPosition({ line: toNumber(match[1]), column: toNumber(match[2]) }, lines)
66
71
  if (position !== undefined) return position
67
72
  }
68
73
  return undefined
@@ -124,7 +129,7 @@ export function messageOf(error, fallback = 'Unprintable error') {
124
129
  const message = safeProperty(error, 'message')
125
130
  if (typeof message === 'string') return message
126
131
  try {
127
- return String(error)
132
+ return toString(error)
128
133
  } catch {
129
134
  return fallback
130
135
  }
@@ -139,20 +144,21 @@ export function processExitDescription(code, signal) {
139
144
 
140
145
  export function firstLine(value, fallback = undefined) {
141
146
  if (typeof value !== 'string') return fallback
142
- const line = value.split(/[\r\n]/, 1)[0]
147
+ const line = splitString(value, /[\r\n]/, 1)[0]
143
148
  return line.length > 0 ? line : fallback
144
149
  }
145
150
 
146
151
  export function oneLineMessage(error) {
147
- return firstLine(messageOf(error), 'Unknown error').replace(/\s+\(\d+:\d+\)$/, '')
152
+ return replaceString(firstLine(messageOf(error), 'Unknown error'), /\s+\(\d+:\d+\)$/, '')
148
153
  }
149
154
 
150
155
  /** Return DSH's reported missing-description property path when it is actionable. */
151
156
  export function missingDescriptionPath(error) {
152
- const violations = messageOf(error).matchAll(/missing required property ["']([^"']+)["']/g)
153
- for (const match of violations) {
157
+ const pattern = /missing required property ["']([^"']+)["']/g
158
+ const message = messageOf(error)
159
+ for (let match = regexpExec(pattern, message); match !== null; match = regexpExec(pattern, message)) {
154
160
  const path = match[1]
155
- if (path === 'description' || path.endsWith('.description')) return path
161
+ if (path === 'description' || endsWith(path, '.description')) return path
156
162
  }
157
163
  return undefined
158
164
  }
@@ -167,18 +173,18 @@ export function limitLogs(logs) {
167
173
  let bytes = 0
168
174
  const tail = []
169
175
  for (let index = logs.length - 1; index >= 0; index -= 1) {
170
- const size = Buffer.byteLength(logs[index], 'utf8') + 1
176
+ const size = bufferByteLength(logs[index], 'utf8') + 1
171
177
  if (bytes + size > MAX_ERROR_LOG_BYTES) break
172
178
  bytes += size
173
- tail.unshift(logs[index])
179
+ prependArray(tail, logs[index])
174
180
  }
175
181
  return tail
176
182
  }
177
183
 
178
184
  /** A frame is only useful when its 1-based line and column are safe integers. */
179
185
  function structuralPosition(position) {
180
- return Number.isSafeInteger(position.line) && position.line >= 1
181
- && Number.isSafeInteger(position.column) && position.column >= 1 ? position : undefined
186
+ return numberIsSafeInteger(position.line) && position.line >= 1
187
+ && numberIsSafeInteger(position.column) && position.column >= 1 ? position : undefined
182
188
  }
183
189
 
184
190
  /** Extract the first frame for the active cell. The frame is reported in the
@@ -187,10 +193,10 @@ function structuralPosition(position) {
187
193
  export function errorPosition(error, filename, source) {
188
194
  const stack = safeProperty(error, 'stack')
189
195
  if (typeof stack !== 'string') return undefined
190
- const escaped = filename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
191
- const match = new RegExp(`${escaped}:(\\d+):(\\d+)`).exec(stack)
196
+ const escaped = replaceString(filename, /[.*+?^${}()|[\]\\]/g, '\\$&')
197
+ const match = regexpExec(new RegExp(`${escaped}:(\\d+):(\\d+)`), stack)
192
198
  if (match === null) return undefined
193
- const position = { line: Number(match[1]), column: Number(match[2]) }
199
+ const position = { line: toNumber(match[1]), column: toNumber(match[2]) }
194
200
  return source === undefined ? structuralPosition(position) : cellPosition(position, source)
195
201
  }
196
202
 
@@ -206,12 +212,14 @@ export function errorDetails(error, filename, source) {
206
212
  const causeMessage = firstLine(safeProperty(candidate, 'message'))
207
213
  const causeCode = firstLine(safeProperty(candidate, 'code'))
208
214
  const stderr = safeProperty(error, 'stderr')
209
- const stderrText = typeof stderr === 'string' ? stderr.slice(0, MAX_CAUSE_CODE_UNITS)
210
- : Buffer.isBuffer(stderr) ? stderr.subarray(0, MAX_CAUSE_CODE_UNITS).toString('utf8') : ''
211
- const boundedMessage = message.slice(0, MAX_CAUSE_CODE_UNITS)
212
- const continuation = boundedMessage.search(/[\r\n]/)
213
- const detail = [continuation < 0 ? '' : boundedMessage.slice(continuation + 1), stderrText]
214
- .join(' ').slice(0, MAX_CAUSE_CODE_UNITS).replace(/\s+/g, ' ').trim()
215
+ const stderrText = typeof stderr === 'string' ? sliceString(stderr, 0, MAX_CAUSE_CODE_UNITS)
216
+ : bufferIsBuffer(stderr)
217
+ ? bufferToString(bufferSubarray(stderr, 0, MAX_CAUSE_CODE_UNITS), 'utf8') : ''
218
+ const boundedMessage = sliceString(message, 0, MAX_CAUSE_CODE_UNITS)
219
+ const continuation = searchString(boundedMessage, /[\r\n]/)
220
+ const detailParts = [continuation < 0 ? '' : sliceString(boundedMessage, continuation + 1), stderrText]
221
+ const detail = trimString(replaceString(
222
+ sliceString(join(detailParts, ' '), 0, MAX_CAUSE_CODE_UNITS), /\s+/g, ' '))
215
223
  const cause = causeMessage === undefined
216
224
  ? detail.length === 0 ? undefined : { message: detail }
217
225
  : {
@@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events'
3
3
  import { resolve } from 'node:path'
4
4
  import { PassThrough } from 'node:stream'
5
5
  import { pathToFileURL } from 'node:url'
6
+ import { helperProcessEnvironment, isElectronHost } from './worker-environment.js'
6
7
 
7
8
  /**
8
9
  * Fallback bound for a helper that answers the stop request and then keeps the
@@ -43,6 +44,7 @@ export class IsolatedWorker extends EventEmitter {
43
44
  this.stderr = new PassThrough()
44
45
  this.protocol = protocol
45
46
  this.exited = false
47
+ this.exitedPromise = new Promise(resolve => { this.resolveExited = resolve })
46
48
  this.exitCode = undefined
47
49
  this.exitSignal = undefined
48
50
  this.released = false
@@ -63,12 +65,15 @@ export class IsolatedWorker extends EventEmitter {
63
65
  }
64
66
  // child_process.fork mutates the env object it receives by copying
65
67
  // NODE_V8_COVERAGE into it. Snapshot the caller's projection first so the
66
- // inner Worker receives exactly the projected user environment, while the
67
- // helper process can still carry the host's coverage variable.
68
- const workerEnvironment = env === undefined ? undefined : { ...env }
68
+ // inner Worker receives exactly the projected user environment. An Electron
69
+ // Host also needs a helper-only startup override; it must not reach user code.
70
+ const electronHost = isElectronHost()
71
+ const projectedEnvironment = env === undefined && electronHost ? process.env : env
72
+ const workerEnvironment = projectedEnvironment === undefined ? undefined : { ...projectedEnvironment }
73
+ const forkEnvironment = helperProcessEnvironment(projectedEnvironment, process.platform, electronHost)
69
74
  this.child = fork(helper, [], {
70
75
  stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
71
- env,
76
+ env: forkEnvironment,
72
77
  serialization: 'advanced',
73
78
  // The helper starts with the host's own startup arguments removed, matching
74
79
  // the isolation the in-host worker had with execArgv: [].
@@ -80,6 +85,7 @@ export class IsolatedWorker extends EventEmitter {
80
85
  this.exited = true
81
86
  this.exitCode = code
82
87
  this.exitSignal = signal
88
+ this.resolveExited()
83
89
  this.#rejectUtilizationRequests(new Error('kernel helper exited before answering utilization sampling'))
84
90
  })
85
91
  // A ChildProcess 'close' follows 'exit' once its stdio has really ended, which
@@ -264,7 +270,7 @@ export class IsolatedWorker extends EventEmitter {
264
270
  const started = Date.now()
265
271
  const killAt = started + ISOLATED_RELEASE_MS
266
272
  const confirmBy = started + ISOLATED_STOP_MS
267
- const exited = new Promise(resolve => { this.child.once('exit', resolve) })
273
+ const exited = this.exitedPromise
268
274
  const onReleased = message => {
269
275
  if (message?.type === 'worker-message' && message.value?.type === 'shutdown-released') this.released = true
270
276
  }
@@ -278,18 +284,19 @@ export class IsolatedWorker extends EventEmitter {
278
284
  // The channel is closed; the kill path below owns the instance.
279
285
  }
280
286
  if (!await leftWithin(this, remaining(killAt))) {
287
+ let killFailure
281
288
  if (!this.exited) {
282
289
  try {
283
290
  await this.kill()
284
291
  } catch (error) {
285
- // Reclamation failed: keep the failure observable on the transport so
286
- // the owner can retain responsibility for this instance.
287
- this.reclamationFailure = error
288
- throw error
292
+ // On Windows a process that has already left can report a refused
293
+ // kill before Node delivers its exit event. The real exit observation
294
+ // decides whether this instance was reclaimed.
295
+ killFailure = error
289
296
  }
290
297
  }
291
298
  if (!this.exited && !await leftWithin(this, remaining(confirmBy))) {
292
- const failure = new Error('kernel helper did not exit before the close deadline')
299
+ const failure = killFailure ?? new Error('kernel helper did not exit before the close deadline')
293
300
  this.reclamationFailure = failure
294
301
  throw failure
295
302
  }