dsh-ptc-plus 0.4.1 → 0.4.3

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 (103) hide show
  1. package/README.en.md +164 -78
  2. package/README.md +165 -79
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-icon.svg +5 -0
  6. package/client.js +854 -336
  7. package/compiler-core.cjs +161 -157
  8. package/compiler-core.cjs.map +3 -3
  9. package/docs/README.md +4 -1
  10. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +8 -7
  11. package/docs/adr/0008-define-agent-operating-contract.md +2 -2
  12. package/docs/adr/0010-session-log-derived-recovery-tips.md +7 -0
  13. package/docs/adr/0016-archive-resolved-review-findings.md +3 -3
  14. package/docs/adr/0019-plugin-settings-and-kill-switch.md +3 -3
  15. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -0
  16. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +14 -3
  17. package/docs/adr/0023-global-user-bindings.md +7 -7
  18. package/docs/adr/0025-use-versioned-logical-binding-identities.md +3 -3
  19. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +23 -7
  20. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +9 -5
  21. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +26 -6
  22. package/docs/adr/0031-close-semantic-obligations-by-owner.md +29 -0
  23. package/docs/adr/README.md +4 -4
  24. package/docs/architecture.md +15 -10
  25. package/docs/binding-review-placement-design.md +1 -1
  26. package/docs/client-ui.md +9 -5
  27. package/docs/durability-design.md +12 -9
  28. package/docs/installation.md +27 -9
  29. package/docs/runtime-reference.md +23 -12
  30. package/docs/semantic-validation.md +7 -3
  31. package/docs/stateful-binding-updates-proposal.md +8 -4
  32. package/docs/user-bindings.en.md +10 -7
  33. package/docs/user-bindings.md +9 -6
  34. package/docs/value-wire.md +1 -1
  35. package/docs/verification-optimization-work.md +41 -0
  36. package/docs/verification.md +53 -2
  37. package/index.js +39 -34
  38. package/internal/async-local-storage-intrinsics.js +43 -0
  39. package/internal/binding-declaration-contract.js +8 -0
  40. package/internal/binding-source-analysis.js +381 -64
  41. package/internal/binding-update-policy.js +7 -1
  42. package/internal/callable-source-facts.js +37 -1
  43. package/internal/cell-analysis.js +85 -17
  44. package/internal/compiler-intrinsics.js +9 -43
  45. package/internal/compiler-module-links.js +7 -4
  46. package/internal/config-spec.js +1 -1
  47. package/internal/cordis-tools-owner.js +20 -10
  48. package/internal/dynamic-environment-integration.js +8 -4
  49. package/internal/dynamic-environment-runtime.js +24 -3
  50. package/internal/dynamic-native-calls.js +4 -1
  51. package/internal/dynamic-native-runtime.js +15 -1
  52. package/internal/edit-transport-owner.js +80 -20
  53. package/internal/execution-seam-compat.js +192 -30
  54. package/internal/failure-reporting.js +38 -30
  55. package/internal/isolated-worker.js +17 -10
  56. package/internal/kernel-worker.js +645 -393
  57. package/internal/message-sources.js +14 -0
  58. package/internal/module-compilation.js +7 -5
  59. package/internal/module-transform-contract.js +32 -2
  60. package/internal/record-utils.js +24 -15
  61. package/internal/repl-memory-projection.js +10 -0
  62. package/internal/repl-scope-normalizer.js +72 -35
  63. package/internal/repl-value-observer.js +145 -46
  64. package/internal/runtime-config.js +62 -2
  65. package/internal/runtime-intrinsics.js +104 -0
  66. package/internal/runtime-messages.js +6 -4
  67. package/internal/session-cell-executor.js +26 -2
  68. package/internal/session-journal-recovery.js +267 -67
  69. package/internal/session-journal-schema.js +30 -13
  70. package/internal/session-journal.js +485 -44
  71. package/internal/session-log-view.js +5 -4
  72. package/internal/session-runtime.js +46 -21
  73. package/internal/session-state.js +7 -29
  74. package/internal/settings-compat.js +66 -1
  75. package/internal/source-position-map.js +38 -0
  76. package/internal/stateful-root-compiler.js +3 -1
  77. package/internal/typescript-transform.js +4 -2
  78. package/internal/user-binding-console-worker.js +115 -41
  79. package/internal/user-binding-draft-projection.js +3 -2
  80. package/internal/user-bindings-owner.js +2 -1
  81. package/internal/user-bindings.js +16 -3
  82. package/internal/validated-parse-repair.js +6 -2
  83. package/internal/value-wire.js +189 -94
  84. package/internal/worker-client.js +99 -8
  85. package/internal/worker-cwd-virtualization.js +82 -54
  86. package/internal/worker-environment.js +20 -0
  87. package/internal/worker-output-capture.js +187 -0
  88. package/internal/worker-output-fence.js +26 -0
  89. package/internal/worker-realm-surfaces.js +197 -0
  90. package/internal/worker-repl-realm.js +140 -0
  91. package/package.json +37 -4
  92. package/src/client-activity.js +12 -2
  93. package/src/client-authoring-view.js +121 -62
  94. package/src/client-console.js +4 -2
  95. package/src/client-copy.js +4 -2
  96. package/src/client-feature-gates.js +7 -9
  97. package/src/client-host-compat.js +104 -27
  98. package/src/client-repl-view.js +3 -1
  99. package/src/client-styles.js +12 -1
  100. package/src/client-tool-view.js +3 -1
  101. package/src/client-transport.js +2 -2
  102. package/src/client-workbench.js +4 -3
  103. package/src/client.js +56 -17
@@ -1,4 +1,5 @@
1
1
  import { assertFields, isRecord } from './record-utils.js'
2
+ import { runtimeIntrinsics as internal } from './runtime-intrinsics.js'
2
3
  import {
3
4
  VALUE_CODEC,
4
5
  DEFAULT_VALUE_LIMITS,
@@ -12,12 +13,24 @@ import {
12
13
  } from './value-wire-schema.js'
13
14
 
14
15
  export { VALUE_CODEC, DEFAULT_VALUE_LIMITS } from './value-wire-schema.js'
16
+ const { Object, Reflect, Array, Map, Set, WeakMap, WeakSet, TypeError,
17
+ isArray, numberIsFinite, numberIsNaN, numberIsSafeInteger, toBigInt,
18
+ toNumber, toString, regexpTest, mapGet, mapHas, mapSet, setHas, setAdd,
19
+ setDelete, weakMapGet, weakMapSet, weakSetHas, weakSetAdd,
20
+ appendArray, popArray, join } = internal
15
21
  const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
22
+ const intrinsicFunctionToString = Function.prototype.toString
23
+ const reflectApply = Reflect.apply
24
+ const bufferByteLength = internal.bufferByteLength
25
+ const jsonStringify = JSON.stringify
16
26
 
17
27
  function limitsOf(options = {}) {
18
28
  const limits = { ...DEFAULT_VALUE_LIMITS, ...options }
19
- for (const [name, value] of Object.entries(limits)) {
20
- if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`invalid PTC value limit ${name}`)
29
+ const entries = Object.entries(limits)
30
+ for (let index = 0; index < entries.length; index += 1) {
31
+ const name = entries[index][0]
32
+ const value = entries[index][1]
33
+ if (!numberIsSafeInteger(value) || value < 1) throw new TypeError(`invalid PTC value limit ${name}`)
21
34
  }
22
35
  return limits
23
36
  }
@@ -28,7 +41,7 @@ function hasIntrinsicConstructor(prototype, name) {
28
41
  try {
29
42
  return constructor.name === name
30
43
  && constructor.prototype === prototype
31
- && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`
44
+ && reflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`
32
45
  } catch {
33
46
  return false
34
47
  }
@@ -47,7 +60,7 @@ function plainObjectPrototype(value) {
47
60
 
48
61
  function hasPlainArrayPrototype(value) {
49
62
  const prototype = Object.getPrototypeOf(value)
50
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
63
+ if (!isArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false
51
64
  const objectPrototype = Object.getPrototypeOf(prototype)
52
65
  return objectPrototype !== null && typeof objectPrototype === 'object'
53
66
  && isIntrinsicObjectPrototype(objectPrototype)
@@ -58,7 +71,7 @@ function invalid(path, detail) {
58
71
  }
59
72
 
60
73
  function stringBytes(value) {
61
- return Buffer.byteLength(value, 'utf8')
74
+ return bufferByteLength(value, 'utf8')
62
75
  }
63
76
 
64
77
  /** Encode one supported JavaScript graph into the project-owned JSON-safe wire. */
@@ -68,6 +81,7 @@ export function encodeValue(value, options = {}) {
68
81
  const pending = []
69
82
  const nodes = []
70
83
  let edgeCount = 0
84
+ let arraySlotCount = 0
71
85
  let textBytes = 0
72
86
 
73
87
  const accountText = (text, path) => {
@@ -82,7 +96,7 @@ export function encodeValue(value, options = {}) {
82
96
  return current
83
97
  }
84
98
  if (typeof current === 'number') {
85
- if (Number.isNaN(current)) return { tag: 'number', value: 'nan' }
99
+ if (numberIsNaN(current)) return { tag: 'number', value: 'nan' }
86
100
  if (current === Infinity) return { tag: 'number', value: 'infinity' }
87
101
  if (current === -Infinity) return { tag: 'number', value: '-infinity' }
88
102
  if (Object.is(current, -0)) return { tag: 'number', value: '-0' }
@@ -90,20 +104,20 @@ export function encodeValue(value, options = {}) {
90
104
  }
91
105
  if (current === undefined) return { tag: 'undefined' }
92
106
  if (typeof current === 'bigint') {
93
- const text = String(current)
107
+ const text = toString(current)
94
108
  const digits = text[0] === '-' ? text.length - 1 : text.length
95
109
  if (digits > limits.maxBigIntDigits) invalid(path, `BigInt exceeds ${limits.maxBigIntDigits} digits`)
96
110
  accountText(text, path)
97
111
  return { tag: 'bigint', value: text }
98
112
  }
99
113
  if (typeof current !== 'object') invalid(path, typeof current)
100
- const prior = seen.get(current)
114
+ const prior = weakMapGet(seen, current)
101
115
  if (prior !== undefined) return { tag: 'reference', index: prior }
102
116
  if (nodes.length >= limits.maxNodes) invalid(path, `node budget exceeds ${limits.maxNodes}`)
103
117
  const index = nodes.length
104
- seen.set(current, index)
105
- nodes.push(undefined)
106
- pending.push({ value: current, index, path })
118
+ weakMapSet(seen, current, index)
119
+ appendArray(nodes, undefined)
120
+ appendArray(pending, { value: current, index, path })
107
121
  return { tag: 'reference', index }
108
122
  }
109
123
 
@@ -111,24 +125,30 @@ export function encodeValue(value, options = {}) {
111
125
  for (let cursor = 0; cursor < pending.length; cursor += 1) {
112
126
  const item = pending[cursor]
113
127
  const current = item.value
114
- if (Array.isArray(current)) {
128
+ if (isArray(current)) {
115
129
  if (!hasPlainArrayPrototype(current)) invalid(item.path, 'non-plain array')
116
130
  if (current.length > limits.maxArrayLength) invalid(item.path, `array length exceeds ${limits.maxArrayLength}`)
131
+ arraySlotCount += current.length
132
+ if (arraySlotCount > limits.maxArrayLength) {
133
+ invalid(item.path, `array slot budget exceeds ${limits.maxArrayLength}`)
134
+ }
117
135
  const entries = []
118
- for (const key of Reflect.ownKeys(current)) {
136
+ const keys = Reflect.ownKeys(current)
137
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
138
+ const key = keys[keyIndex]
119
139
  if (key === 'length') continue
120
- if (typeof key !== 'string' || !/^(0|[1-9][0-9]*)$/.test(key)) {
140
+ if (typeof key !== 'string' || !regexpTest(/^(0|[1-9][0-9]*)$/, key)) {
121
141
  invalid(item.path, 'array has a symbol or non-index property')
122
142
  }
123
- const index = Number(key)
124
- if (!Number.isSafeInteger(index) || index >= current.length) invalid(`${item.path}[${key}]`, 'invalid array index')
143
+ const index = toNumber(key)
144
+ if (!numberIsSafeInteger(index) || index >= current.length) invalid(`${item.path}[${key}]`, 'invalid array index')
125
145
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
126
146
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
127
147
  invalid(`${item.path}[${key}]`, 'array index must be an enumerable data property')
128
148
  }
129
149
  edgeCount += 1
130
150
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
131
- entries.push([index, atom(descriptor.value, `${item.path}[${key}]`)])
151
+ appendArray(entries, [index, atom(descriptor.value, `${item.path}[${key}]`)])
132
152
  }
133
153
  nodes[item.index] = { type: 'array', length: current.length, entries }
134
154
  continue
@@ -137,7 +157,9 @@ export function encodeValue(value, options = {}) {
137
157
  const prototype = plainObjectPrototype(current)
138
158
  if (prototype === undefined) invalid(item.path, 'non-plain object')
139
159
  const entries = []
140
- for (const key of Reflect.ownKeys(current)) {
160
+ const keys = Reflect.ownKeys(current)
161
+ for (let keyIndex = 0; keyIndex < keys.length; keyIndex += 1) {
162
+ const key = keys[keyIndex]
141
163
  if (typeof key !== 'string') invalid(item.path, 'object has a symbol key')
142
164
  const descriptor = Object.getOwnPropertyDescriptor(current, key)
143
165
  if (descriptor === undefined || !Object.hasOwn(descriptor, 'value') || descriptor.enumerable !== true) {
@@ -146,14 +168,14 @@ export function encodeValue(value, options = {}) {
146
168
  accountText(key, `${item.path}.${key}`)
147
169
  edgeCount += 1
148
170
  if (edgeCount > limits.maxEdges) invalid(item.path, `edge budget exceeds ${limits.maxEdges}`)
149
- entries.push([key, atom(descriptor.value, `${item.path}.${key}`)])
171
+ appendArray(entries, [key, atom(descriptor.value, `${item.path}.${key}`)])
150
172
  }
151
173
  nodes[item.index] = { type: 'object', prototype, entries }
152
174
  }
153
175
  return { codec: VALUE_CODEC, root, nodes }
154
176
  }
155
177
 
156
- function decodeAtom(atom, nodes, limits, budget, path) {
178
+ function validateAtom(atom, nodeCount, limits, budget, path) {
157
179
  if (atom === null || typeof atom === 'boolean') return atom
158
180
  if (typeof atom === 'string') {
159
181
  budget.textBytes += stringBytes(atom)
@@ -161,114 +183,176 @@ function decodeAtom(atom, nodes, limits, budget, path) {
161
183
  return atom
162
184
  }
163
185
  if (typeof atom === 'number') {
164
- if (!Number.isFinite(atom) || Object.is(atom, -0)) throw new TypeError(`invalid PTC value number at ${path}`)
186
+ if (!numberIsFinite(atom) || Object.is(atom, -0)) throw new TypeError(`invalid PTC value number at ${path}`)
165
187
  return atom
166
188
  }
167
189
  if (!isRecord(atom) || typeof atom.tag !== 'string') throw new TypeError(`invalid PTC value atom at ${path}`)
168
190
  if (atom.tag === 'undefined') {
169
191
  assertFields(atom, UNDEFINED_FIELDS, 'PTC undefined atom')
170
- return undefined
192
+ return { tag: 'undefined' }
171
193
  }
172
194
  if (atom.tag === 'number') {
173
195
  assertFields(atom, NUMBER_FIELDS, 'PTC number atom')
174
- if (atom.value === 'nan') return NaN
175
- if (atom.value === 'infinity') return Infinity
176
- if (atom.value === '-infinity') return -Infinity
177
- if (atom.value === '-0') return -0
196
+ if (atom.value === 'nan' || atom.value === 'infinity'
197
+ || atom.value === '-infinity' || atom.value === '-0') {
198
+ return { tag: 'number', value: atom.value }
199
+ }
178
200
  throw new TypeError('invalid PTC special number')
179
201
  }
180
202
  if (atom.tag === 'bigint') {
181
203
  assertFields(atom, BIGINT_FIELDS, 'PTC BigInt atom')
182
- if (typeof atom.value !== 'string' || !/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/.test(atom.value)) {
204
+ if (typeof atom.value !== 'string' || !regexpTest(/^(0|-[1-9][0-9]*|[1-9][0-9]*)$/, atom.value)) {
183
205
  throw new TypeError('invalid PTC BigInt')
184
206
  }
185
207
  const digits = atom.value[0] === '-' ? atom.value.length - 1 : atom.value.length
186
208
  if (digits > limits.maxBigIntDigits) throw new TypeError('PTC BigInt digit budget exceeded')
187
209
  budget.textBytes += stringBytes(atom.value)
188
210
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
189
- return BigInt(atom.value)
211
+ return { tag: 'bigint', value: atom.value }
190
212
  }
191
213
  if (atom.tag === 'reference') {
192
214
  assertFields(atom, REFERENCE_FIELDS, 'PTC reference atom')
193
- if (!Number.isSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodes.length) {
215
+ if (!numberIsSafeInteger(atom.index) || atom.index < 0 || atom.index >= nodeCount) {
194
216
  throw new TypeError('dangling PTC value reference')
195
217
  }
196
- budget.reachable.add(atom.index)
197
- return nodes[atom.index]
218
+ if (atom.index >= budget.nextReference) {
219
+ if (atom.index !== budget.nextReference) throw new TypeError('non-canonical PTC value envelope')
220
+ budget.nextReference += 1
221
+ }
222
+ return { tag: 'reference', index: atom.index }
198
223
  }
199
- throw new TypeError(`unknown PTC value atom tag ${JSON.stringify(atom.tag)}`)
224
+ throw new TypeError(`unknown PTC value atom tag ${jsonStringify(atom.tag)}`)
200
225
  }
201
226
 
202
- /** Validate, hydrate, and own one canonical PTC value envelope without recursive stack growth. */
203
- function decodeCanonicalValue(wire, options = {}) {
204
- const limits = limitsOf(options)
227
+ function canonicalArrayIndex(key) {
228
+ if (!regexpTest(/^(0|[1-9][0-9]*)$/, key)) return undefined
229
+ const index = toNumber(key)
230
+ return numberIsSafeInteger(index) && index < 0xffffffff && toString(index) === key ? index : undefined
231
+ }
232
+
233
+ const MAX_ARRAY_LENGTH = 0xffffffff
234
+
235
+ /** Validate and detach the complete envelope before allocating hydrated arrays. */
236
+ function validateCanonicalWire(wire, limits) {
205
237
  assertFields(wire, ENVELOPE_FIELDS, 'PTC value envelope')
206
- if (wire.codec !== VALUE_CODEC || !Array.isArray(wire.nodes)) throw new TypeError('invalid PTC value codec')
238
+ if (wire.codec !== VALUE_CODEC || !isArray(wire.nodes)) throw new TypeError('invalid PTC value codec')
207
239
  if (wire.nodes.length > limits.maxNodes) throw new TypeError('PTC value node budget exceeded')
208
- const targets = new Array(wire.nodes.length)
240
+ const nodes = new Array(wire.nodes.length)
241
+ let arraySlots = 0
209
242
  for (let index = 0; index < wire.nodes.length; index += 1) {
210
243
  const node = wire.nodes[index]
211
244
  if (!isRecord(node)) throw new TypeError(`invalid PTC value node ${index}`)
212
245
  if (node.type === 'array') {
213
246
  assertFields(node, ARRAY_NODE_FIELDS, `PTC array node ${index}`)
214
- if (!Number.isSafeInteger(node.length) || node.length < 0 || node.length > limits.maxArrayLength) {
247
+ if (!numberIsSafeInteger(node.length) || node.length < 0
248
+ || node.length > MAX_ARRAY_LENGTH || node.length > limits.maxArrayLength) {
215
249
  throw new TypeError(`invalid PTC array length at node ${index}`)
216
250
  }
217
- if (!Array.isArray(node.entries)) throw new TypeError(`invalid PTC array entries at node ${index}`)
218
- targets[index] = new Array(node.length)
251
+ if (!isArray(node.entries)) throw new TypeError(`invalid PTC array entries at node ${index}`)
252
+ arraySlots += node.length
253
+ if (arraySlots > limits.maxArrayLength) throw new TypeError('PTC value array slot budget exceeded')
254
+ nodes[index] = { type: 'array', length: node.length, entries: node.entries }
219
255
  } else if (node.type === 'object') {
220
256
  assertFields(node, OBJECT_NODE_FIELDS, `PTC object node ${index}`)
221
- if (!['object', 'null'].includes(node.prototype) || !Array.isArray(node.entries)) {
257
+ if ((node.prototype !== 'object' && node.prototype !== 'null') || !isArray(node.entries)) {
222
258
  throw new TypeError(`invalid PTC object node ${index}`)
223
259
  }
224
- targets[index] = node.prototype === 'null' ? Object.create(null) : {}
260
+ nodes[index] = { type: 'object', prototype: node.prototype, entries: node.entries }
225
261
  } else {
226
262
  throw new TypeError(`unknown PTC value node type at ${index}`)
227
263
  }
228
264
  }
229
265
 
230
- const budget = { edges: 0, textBytes: 0, reachable: new Set() }
231
- const root = decodeAtom(wire.root, targets, limits, budget, '$')
232
- for (let nodeIndex = 0; nodeIndex < wire.nodes.length; nodeIndex += 1) {
233
- const node = wire.nodes[nodeIndex]
234
- const target = targets[nodeIndex]
266
+ const budget = { edges: 0, textBytes: 0, nextReference: 0 }
267
+ const root = validateAtom(wire.root, nodes.length, limits, budget, '$')
268
+ for (let nodeIndex = 0; nodeIndex < budget.nextReference; nodeIndex += 1) {
269
+ const node = nodes[nodeIndex]
270
+ const entries = []
235
271
  if (node.type === 'array') {
236
272
  let previous = -1
237
- for (const entry of node.entries) {
238
- if (!Array.isArray(entry) || entry.length !== 2 || !Number.isSafeInteger(entry[0])
273
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
274
+ const entry = node.entries[entryIndex]
275
+ if (!isArray(entry) || entry.length !== 2 || !numberIsSafeInteger(entry[0])
239
276
  || entry[0] <= previous || entry[0] < 0 || entry[0] >= node.length) {
240
277
  throw new TypeError(`invalid PTC array entry at node ${nodeIndex}`)
241
278
  }
242
279
  previous = entry[0]
243
280
  budget.edges += 1
244
281
  if (budget.edges > limits.maxEdges) throw new TypeError('PTC value edge budget exceeded')
245
- Object.defineProperty(target, entry[0], {
246
- value: decodeAtom(entry[1], targets, limits, budget, `$nodes[${nodeIndex}][${entry[0]}]`),
247
- enumerable: true, configurable: true, writable: true,
248
- })
282
+ appendArray(entries, [entry[0], validateAtom(entry[1], nodes.length, limits, budget,
283
+ `$nodes[${nodeIndex}][${entry[0]}]`)])
249
284
  }
285
+ nodes[nodeIndex] = { type: 'array', length: node.length, entries }
250
286
  continue
251
287
  }
252
288
  const keys = new Set()
253
- for (const entry of node.entries) {
254
- if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || keys.has(entry[0])) {
289
+ let previousIndex = -1
290
+ let ordinaryKeySeen = false
291
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
292
+ const entry = node.entries[entryIndex]
293
+ if (!isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || setHas(keys, entry[0])) {
255
294
  throw new TypeError(`invalid PTC object entry at node ${nodeIndex}`)
256
295
  }
257
- keys.add(entry[0])
296
+ const index = canonicalArrayIndex(entry[0])
297
+ if (index === undefined) ordinaryKeySeen = true
298
+ else if (ordinaryKeySeen || index <= previousIndex) {
299
+ throw new TypeError('non-canonical PTC value envelope')
300
+ } else previousIndex = index
301
+ setAdd(keys, entry[0])
258
302
  budget.textBytes += stringBytes(entry[0])
259
303
  if (budget.textBytes > limits.maxStringBytes) throw new TypeError('PTC value string budget exceeded')
260
304
  budget.edges += 1
261
305
  if (budget.edges > limits.maxEdges) throw new TypeError('PTC value edge budget exceeded')
306
+ appendArray(entries, [entry[0], validateAtom(entry[1], nodes.length, limits, budget,
307
+ `$nodes[${nodeIndex}].${entry[0]}`)])
308
+ }
309
+ nodes[nodeIndex] = { type: 'object', prototype: node.prototype, entries }
310
+ }
311
+ if (budget.nextReference !== nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
312
+ const normalized = { codec: VALUE_CODEC, root, nodes }
313
+ const serialization = jsonStringify(normalized)
314
+ if (serialization !== jsonStringify(wire)) throw new TypeError('non-canonical PTC value envelope')
315
+ return { wire: normalized, serialization }
316
+ }
317
+
318
+ function hydrateAtom(atom, nodes) {
319
+ if (atom === null || typeof atom === 'boolean' || typeof atom === 'string' || typeof atom === 'number') return atom
320
+ if (atom.tag === 'undefined') return undefined
321
+ if (atom.tag === 'number') {
322
+ if (atom.value === 'nan') return NaN
323
+ if (atom.value === 'infinity') return Infinity
324
+ if (atom.value === '-infinity') return -Infinity
325
+ return -0
326
+ }
327
+ if (atom.tag === 'bigint') return toBigInt(atom.value)
328
+ return nodes[atom.index]
329
+ }
330
+
331
+ /** Validate, hydrate, and own one canonical PTC value envelope without recursive stack growth. */
332
+ function decodeCanonicalValue(wire, options = {}) {
333
+ const limits = limitsOf(options)
334
+ const validated = validateCanonicalWire(wire, limits)
335
+ const targets = new Array(validated.wire.nodes.length)
336
+ for (let index = 0; index < validated.wire.nodes.length; index += 1) {
337
+ const node = validated.wire.nodes[index]
338
+ targets[index] = node.type === 'array' ? new Array(node.length)
339
+ : node.prototype === 'null' ? Object.create(null) : {}
340
+ }
341
+ const root = hydrateAtom(validated.wire.root, targets)
342
+ for (let nodeIndex = 0; nodeIndex < validated.wire.nodes.length; nodeIndex += 1) {
343
+ const node = validated.wire.nodes[nodeIndex]
344
+ const target = targets[nodeIndex]
345
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
346
+ const entry = node.entries[entryIndex]
262
347
  Object.defineProperty(target, entry[0], {
263
- value: decodeAtom(entry[1], targets, limits, budget, `$nodes[${nodeIndex}].${entry[0]}`),
348
+ value: hydrateAtom(entry[1], targets),
264
349
  enumerable: true, configurable: true, writable: true,
265
350
  })
266
351
  }
267
352
  }
268
- if (budget.reachable.size !== wire.nodes.length) throw new TypeError('PTC value envelope contains unreachable nodes')
269
353
  const canonical = encodeValue(root, limits)
270
- const serialization = JSON.stringify(canonical)
271
- if (serialization !== JSON.stringify(wire)) throw new TypeError('non-canonical PTC value envelope')
354
+ const serialization = jsonStringify(canonical)
355
+ if (serialization !== validated.serialization) throw new TypeError('non-canonical PTC value envelope')
272
356
  return { value: root, wire: canonical, serialization }
273
357
  }
274
358
 
@@ -295,31 +379,32 @@ export function isPlainJsonTree(value) {
295
379
  const pending = [value]
296
380
  const seen = new WeakSet()
297
381
  while (pending.length > 0) {
298
- const current = pending.pop()
382
+ const current = popArray(pending)
299
383
  if (current === null || typeof current === 'string' || typeof current === 'boolean') continue
300
- if (typeof current === 'number' && Number.isFinite(current) && !Object.is(current, -0)) continue
384
+ if (typeof current === 'number' && numberIsFinite(current) && !Object.is(current, -0)) continue
301
385
  if (typeof current !== 'object') return false
302
- if (seen.has(current)) return false
303
- seen.add(current)
304
- if (Array.isArray(current)) {
386
+ if (weakSetHas(seen, current)) return false
387
+ weakSetAdd(seen, current)
388
+ if (isArray(current)) {
305
389
  for (let index = 0; index < current.length; index += 1) {
306
390
  if (!Object.hasOwn(current, index)) return false
307
- pending.push(current[index])
391
+ appendArray(pending, current[index])
308
392
  }
309
393
  } else {
310
- for (const key of Object.keys(current)) pending.push(current[key])
394
+ const keys = Object.keys(current)
395
+ for (let index = 0; index < keys.length; index += 1) appendArray(pending, current[keys[index]])
311
396
  }
312
397
  }
313
398
  return true
314
399
  }
315
400
 
316
401
  function renderKey(key) {
317
- return IDENTIFIER.test(key) ? key : JSON.stringify(key)
402
+ return regexpTest(IDENTIFIER, key) ? key : jsonStringify(key)
318
403
  }
319
404
 
320
405
  function atomText(atom) {
321
- if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return String(atom)
322
- if (typeof atom === 'string') return JSON.stringify(atom)
406
+ if (atom === null || typeof atom === 'boolean' || typeof atom === 'number') return toString(atom)
407
+ if (typeof atom === 'string') return jsonStringify(atom)
323
408
  if (atom.tag === 'undefined') return 'undefined'
324
409
  if (atom.tag === 'number') {
325
410
  return atom.value === 'nan' ? 'NaN'
@@ -337,16 +422,20 @@ export function renderValueWire(wire, options = {}) {
337
422
  }
338
423
 
339
424
  function renderCanonicalValue(normalized, limits) {
340
- const references = new Array(normalized.nodes.length).fill(0)
425
+ const references = new Array(normalized.nodes.length)
426
+ for (let index = 0; index < references.length; index += 1) references[index] = 0
341
427
  const count = (atom) => {
342
428
  if (isRecord(atom) && atom.tag === 'reference') references[atom.index] += 1
343
429
  }
344
430
  count(normalized.root)
345
- for (const node of normalized.nodes) for (const entry of node.entries) count(entry[1])
431
+ for (let nodeIndex = 0; nodeIndex < normalized.nodes.length; nodeIndex += 1) {
432
+ const entries = normalized.nodes[nodeIndex].entries
433
+ for (let entryIndex = 0; entryIndex < entries.length; entryIndex += 1) count(entries[entryIndex][1])
434
+ }
346
435
  const labels = new Map()
347
436
  let nextLabel = 0
348
437
  for (let index = 0; index < references.length; index += 1) {
349
- if (references[index] > 1) labels.set(index, ++nextLabel)
438
+ if (references[index] > 1) mapSet(labels, index, ++nextLabel)
350
439
  }
351
440
 
352
441
  const chunks = []
@@ -354,19 +443,19 @@ function renderCanonicalValue(normalized, limits) {
354
443
  const append = (text) => {
355
444
  bytes += stringBytes(text)
356
445
  if (bytes > limits.maxStringBytes) throw new TypeError(`rendered PTC value exceeds ${limits.maxStringBytes} bytes`)
357
- chunks.push(text)
446
+ appendArray(chunks, text)
358
447
  }
359
448
  const emitted = new Set()
360
449
  const active = new Set()
361
450
  const tasks = [{ kind: 'atom', atom: normalized.root }]
362
451
  while (tasks.length > 0) {
363
- const task = tasks.pop()
452
+ const task = popArray(tasks)
364
453
  if (task.kind === 'text') {
365
454
  append(task.text)
366
455
  continue
367
456
  }
368
457
  if (task.kind === 'leave') {
369
- active.delete(task.index)
458
+ setDelete(active, task.index)
370
459
  continue
371
460
  }
372
461
  const primitive = atomText(task.atom)
@@ -375,38 +464,44 @@ function renderCanonicalValue(normalized, limits) {
375
464
  continue
376
465
  }
377
466
  const index = task.atom.index
378
- const label = labels.get(index)
379
- if (emitted.has(index)) {
380
- append(active.has(index) ? `[Circular *${label}]` : `[Reference *${label}]`)
467
+ const label = mapGet(labels, index)
468
+ if (setHas(emitted, index)) {
469
+ append(setHas(active, index) ? `[Circular *${label}]` : `[Reference *${label}]`)
381
470
  continue
382
471
  }
383
- emitted.add(index)
384
- active.add(index)
472
+ setAdd(emitted, index)
473
+ setAdd(active, index)
385
474
  if (label !== undefined) append(`<ref *${label}> `)
386
475
  const node = normalized.nodes[index]
387
- tasks.push({ kind: 'leave', index })
476
+ appendArray(tasks, { kind: 'leave', index })
388
477
  if (node.type === 'array') {
389
478
  append('[')
390
- tasks.push({ kind: 'text', text: ']' })
391
- const entries = new Map(node.entries)
392
- if (node.length > 0 && !entries.has(node.length - 1)) tasks.push({ kind: 'text', text: ',' })
479
+ appendArray(tasks, { kind: 'text', text: ']' })
480
+ const entries = new Map()
481
+ for (let entryIndex = 0; entryIndex < node.entries.length; entryIndex += 1) {
482
+ const entry = node.entries[entryIndex]
483
+ mapSet(entries, entry[0], entry[1])
484
+ }
485
+ if (node.length > 0 && !mapHas(entries, node.length - 1)) appendArray(tasks, { kind: 'text', text: ',' })
393
486
  for (let item = node.length - 1; item >= 0; item -= 1) {
394
- if (item < node.length - 1) tasks.push({ kind: 'text', text: ', ' })
395
- const value = entries.get(item)
396
- if (value !== undefined) tasks.push({ kind: 'atom', atom: value })
487
+ if (item < node.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
488
+ const value = mapGet(entries, item)
489
+ if (value !== undefined) appendArray(tasks, { kind: 'atom', atom: value })
397
490
  }
398
491
  continue
399
492
  }
400
493
  append('{')
401
- tasks.push({ kind: 'text', text: '}' })
494
+ appendArray(tasks, { kind: 'text', text: '}' })
402
495
  for (let entryIndex = node.entries.length - 1; entryIndex >= 0; entryIndex -= 1) {
403
- const [key, value] = node.entries[entryIndex]
404
- if (entryIndex < node.entries.length - 1) tasks.push({ kind: 'text', text: ', ' })
405
- tasks.push({ kind: 'atom', atom: value })
406
- tasks.push({ kind: 'text', text: `${renderKey(key)}: ` })
496
+ const entry = node.entries[entryIndex]
497
+ const key = entry[0]
498
+ const value = entry[1]
499
+ if (entryIndex < node.entries.length - 1) appendArray(tasks, { kind: 'text', text: ', ' })
500
+ appendArray(tasks, { kind: 'atom', atom: value })
501
+ appendArray(tasks, { kind: 'text', text: `${renderKey(key)}: ` })
407
502
  }
408
503
  }
409
- return chunks.join('')
504
+ return join(chunks, '')
410
505
  }
411
506
 
412
507
  /**