dsh-ptc-plus 0.2.1
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/LICENSE +21 -0
- package/README.md +137 -0
- package/README.zh.md +137 -0
- package/assets/dsh-ptc-plus-banner.webp +0 -0
- package/assets/ptc-plus-repair.png +0 -0
- package/assets/ptc-plus-settings.png +0 -0
- package/client.js +366 -0
- package/cordis.patch.yml +5 -0
- package/docs/README.md +21 -0
- package/docs/adr/0001-delegate-governance-to-dsh.md +3 -0
- package/docs/adr/0002-limit-work-map-scope.md +3 -0
- package/docs/adr/0003-prefer-native-program-surfaces.md +3 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +5 -0
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +129 -0
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +45 -0
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +39 -0
- package/docs/adr/0008-define-agent-operating-contract.md +25 -0
- package/docs/adr/0009-bound-expensive-acceptance-default.md +39 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +26 -0
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +20 -0
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +23 -0
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +21 -0
- package/docs/adr/0014-persist-cell-rewrite-policy.md +25 -0
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +25 -0
- package/docs/adr/0016-archive-resolved-review-findings.md +43 -0
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +23 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +25 -0
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +32 -0
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +36 -0
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +68 -0
- package/docs/adr/README.md +18 -0
- package/docs/architecture.md +134 -0
- package/docs/capability-projection.md +48 -0
- package/docs/client-ui.md +47 -0
- package/docs/durability-design.md +265 -0
- package/docs/evaluation.md +62 -0
- package/docs/installation.md +82 -0
- package/docs/program-data-plane.md +61 -0
- package/docs/publishing.md +37 -0
- package/docs/runtime-reference.md +119 -0
- package/docs/value-wire.md +61 -0
- package/index.js +532 -0
- package/internal/ast-traversal.js +20 -0
- package/internal/binding-descriptors.js +95 -0
- package/internal/binding-pattern.js +54 -0
- package/internal/builtin-esm-sync.js +9 -0
- package/internal/cell-analysis.js +467 -0
- package/internal/cell-rewriter.js +520 -0
- package/internal/config-spec.js +179 -0
- package/internal/cordis-tools-owner.js +363 -0
- package/internal/diagnostic.js +133 -0
- package/internal/direct-surface-owner.js +326 -0
- package/internal/edit-transport-owner.js +287 -0
- package/internal/failure-reporting.js +139 -0
- package/internal/kernel-worker.js +484 -0
- package/internal/module-policy.js +46 -0
- package/internal/program-bindings.js +187 -0
- package/internal/record-utils.js +53 -0
- package/internal/recovery-tips.js +81 -0
- package/internal/rejected-cell-editor.js +328 -0
- package/internal/repl-convenience.js +201 -0
- package/internal/runtime-bridge-owner.js +417 -0
- package/internal/runtime-config.js +77 -0
- package/internal/runtime-contexts.js +31 -0
- package/internal/sdk-projection.js +88 -0
- package/internal/session-cell-executor.js +612 -0
- package/internal/session-journal.js +889 -0
- package/internal/session-log-view.js +80 -0
- package/internal/session-runtime.js +536 -0
- package/internal/session-state.js +45 -0
- package/internal/source-position-map.js +224 -0
- package/internal/tool-call-canonicalizer.js +339 -0
- package/internal/value-wire.js +421 -0
- package/internal/worker-client.js +218 -0
- package/internal/worker-cwd-virtualization.js +155 -0
- package/package.json +100 -0
- package/src/client.js +165 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Shared binding-pattern traversal used by cell analysis and REPL policy code. */
|
|
2
|
+
|
|
3
|
+
export function walkBindingPattern(pattern, visit) {
|
|
4
|
+
if (pattern === null || typeof pattern !== 'object') return
|
|
5
|
+
if (pattern.type === 'Identifier') {
|
|
6
|
+
visit(pattern)
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
if (pattern.type === 'RestElement') return walkBindingPattern(pattern.argument, visit)
|
|
10
|
+
if (pattern.type === 'AssignmentPattern') return walkBindingPattern(pattern.left, visit)
|
|
11
|
+
if (pattern.type === 'ArrayPattern') {
|
|
12
|
+
for (const element of pattern.elements) walkBindingPattern(element, visit)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
if (pattern.type === 'ObjectPattern') {
|
|
16
|
+
for (const property of pattern.properties) {
|
|
17
|
+
walkBindingPattern(property.type === 'RestElement' ? property.argument : property.value, visit)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function bindingNodes(pattern) {
|
|
23
|
+
const nodes = []
|
|
24
|
+
walkBindingPattern(pattern, node => nodes.push(node))
|
|
25
|
+
return nodes
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Allocate private identifiers outside both source and caller-owned name sets. */
|
|
29
|
+
export function createGeneratedNameAllocator(root, unavailableNames = []) {
|
|
30
|
+
const used = new Set(unavailableNames)
|
|
31
|
+
const seen = new Set()
|
|
32
|
+
const pending = [root]
|
|
33
|
+
while (pending.length > 0) {
|
|
34
|
+
const value = pending.pop()
|
|
35
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) continue
|
|
36
|
+
seen.add(value)
|
|
37
|
+
if (value.type === 'Identifier') used.add(value.name)
|
|
38
|
+
for (const child of Object.values(value)) {
|
|
39
|
+
if (Array.isArray(child)) pending.push(...child)
|
|
40
|
+
else if (child !== null && typeof child === 'object') pending.push(child)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const sequences = new Map()
|
|
44
|
+
return (purpose) => {
|
|
45
|
+
let sequence = sequences.get(purpose) ?? 0
|
|
46
|
+
let name
|
|
47
|
+
do {
|
|
48
|
+
name = `__dsh_ptc_${purpose}_${sequence++}__`
|
|
49
|
+
} while (used.has(name))
|
|
50
|
+
sequences.set(purpose, sequence)
|
|
51
|
+
used.add(name)
|
|
52
|
+
return name
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { syncBuiltinESMExports } from 'node:module'
|
|
2
|
+
|
|
3
|
+
export function synchronizeBuiltinEsmExports(sync = syncBuiltinESMExports) {
|
|
4
|
+
try {
|
|
5
|
+
sync()
|
|
6
|
+
} catch (cause) {
|
|
7
|
+
throw new Error('ptc-plus: failed to synchronize patched Node filesystem exports', { cause })
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { stripTypeScriptTypes } from 'node:module'
|
|
2
|
+
import { parse } from 'acorn'
|
|
3
|
+
import {
|
|
4
|
+
STRIP_PREFIX,
|
|
5
|
+
STRIP_SUFFIX,
|
|
6
|
+
rewriteModuleImportsExports,
|
|
7
|
+
} from './cell-rewriter.js'
|
|
8
|
+
import {
|
|
9
|
+
bindingNodes,
|
|
10
|
+
createGeneratedNameAllocator,
|
|
11
|
+
walkBindingPattern as walkPattern,
|
|
12
|
+
} from './binding-pattern.js'
|
|
13
|
+
import {
|
|
14
|
+
AMBIENT_GLOBALS,
|
|
15
|
+
DYNAMIC_MODULE_REASON,
|
|
16
|
+
classifyModuleSource,
|
|
17
|
+
renderDurabilityReason,
|
|
18
|
+
renderDurabilityReasons,
|
|
19
|
+
} from './module-policy.js'
|
|
20
|
+
import { rewriteLooseRedeclarations } from './repl-convenience.js'
|
|
21
|
+
import { applySourceEdits, mapSourceSpan } from './source-position-map.js'
|
|
22
|
+
import { SKIP_AST_CHILDREN, walkAst } from './ast-traversal.js'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Pure AST analysis for PTC cells: binding inventory, durability classification,
|
|
26
|
+
* return rewriting, and program preparation. This module owns no worker, journal,
|
|
27
|
+
* or session state so its behavior is fully testable in isolation.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export function declarationSpan(node) {
|
|
31
|
+
const start = node.loc?.start
|
|
32
|
+
/* c8 ignore next */
|
|
33
|
+
const end = node.loc?.end ?? start
|
|
34
|
+
/* c8 ignore next */
|
|
35
|
+
if (start === undefined) return undefined
|
|
36
|
+
return {
|
|
37
|
+
line: Math.max(1, start.line - 1),
|
|
38
|
+
column: start.column + 1,
|
|
39
|
+
/* c8 ignore next */
|
|
40
|
+
...(end === undefined ? {} : {
|
|
41
|
+
end: {
|
|
42
|
+
line: Math.max(1, end.line - 1),
|
|
43
|
+
column: end.column + 1,
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function addPatternBindings(pattern, names) {
|
|
50
|
+
walkPattern(pattern, node => names.add(node.name))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addPatternDeclarations(pattern, declarations) {
|
|
54
|
+
bindingNodes(pattern).forEach(node => declarations.push({
|
|
55
|
+
name: node.name,
|
|
56
|
+
span: declarationSpan(node),
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function topLevelBindings(body) {
|
|
61
|
+
return new Set(topLevelDeclarations(body).map(declaration => declaration.name))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function topLevelDeclarations(body) {
|
|
65
|
+
const declarations = []
|
|
66
|
+
for (const statement of body) {
|
|
67
|
+
if (statement.type === 'VariableDeclaration') {
|
|
68
|
+
for (const declaration of statement.declarations) addPatternDeclarations(declaration.id, declarations)
|
|
69
|
+
} else if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
|
|
70
|
+
&& statement.id !== null) {
|
|
71
|
+
declarations.push({
|
|
72
|
+
name: statement.id.name,
|
|
73
|
+
span: declarationSpan(statement.id),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return declarations
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function directBlockBindings(body) {
|
|
81
|
+
const names = new Set()
|
|
82
|
+
for (const statement of body) {
|
|
83
|
+
if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
|
|
84
|
+
for (const declaration of statement.declarations) addPatternBindings(declaration.id, names)
|
|
85
|
+
} else if ((statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
|
|
86
|
+
&& statement.id !== null) {
|
|
87
|
+
names.add(statement.id.name)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return names
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function functionBindings(node) {
|
|
94
|
+
const names = new Set()
|
|
95
|
+
if (node.id !== null && node.id !== undefined) names.add(node.id.name)
|
|
96
|
+
/* c8 ignore next */
|
|
97
|
+
for (const param of node.params ?? []) addPatternBindings(param, names)
|
|
98
|
+
walkAst(node.body, (current) => {
|
|
99
|
+
if (current !== node && isFunction(current)) {
|
|
100
|
+
if (current.type === 'FunctionDeclaration' && current.id !== null) names.add(current.id.name)
|
|
101
|
+
return SKIP_AST_CHILDREN
|
|
102
|
+
}
|
|
103
|
+
if (current.type === 'VariableDeclaration' && current.kind === 'var') {
|
|
104
|
+
for (const declaration of current.declarations) addPatternBindings(declaration.id, names)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
return names
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loopBindings(node) {
|
|
111
|
+
const declaration = node.type === 'ForStatement' ? node.init : node.left
|
|
112
|
+
if (declaration?.type !== 'VariableDeclaration' || declaration.kind === 'var') return new Set()
|
|
113
|
+
const names = new Set()
|
|
114
|
+
for (const entry of declaration.declarations) addPatternBindings(entry.id, names)
|
|
115
|
+
return names
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isReferenceIdentifier(node, parent, key) {
|
|
119
|
+
/* c8 ignore next */
|
|
120
|
+
if (parent === undefined) return false
|
|
121
|
+
if ((parent.type === 'MemberExpression' || parent.type === 'OptionalMemberExpression')
|
|
122
|
+
&& key === 'property' && !parent.computed) return false
|
|
123
|
+
if ((parent.type === 'Property' || parent.type === 'MethodDefinition' || parent.type === 'PropertyDefinition')
|
|
124
|
+
&& key === 'key' && !parent.computed && !parent.shorthand) return false
|
|
125
|
+
if (['VariableDeclarator', 'FunctionDeclaration', 'FunctionExpression', 'ClassDeclaration', 'ClassExpression']
|
|
126
|
+
.includes(parent.type) && key === 'id') return false
|
|
127
|
+
if ((parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression')
|
|
128
|
+
&& key === 'params') return false
|
|
129
|
+
if (parent.type === 'CatchClause' && key === 'param') return false
|
|
130
|
+
if (['LabeledStatement', 'BreakStatement', 'ContinueStatement'].includes(parent.type) && key === 'label') return false
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isStableProcessMember(node, parent) {
|
|
135
|
+
/* c8 ignore next */
|
|
136
|
+
if (node.name !== 'process' || parent?.type !== 'MemberExpression' || parent.object !== node) return false
|
|
137
|
+
const member = parent.computed
|
|
138
|
+
? parent.property?.type === 'Literal' ? parent.property.value : undefined
|
|
139
|
+
/* c8 ignore next */
|
|
140
|
+
: parent.property?.type === 'Identifier' ? parent.property.name : undefined
|
|
141
|
+
return ['stdout', 'stderr', 'cwd'].includes(member)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function staticMemberName(node) {
|
|
145
|
+
if (node?.type !== 'MemberExpression') return undefined
|
|
146
|
+
if (!node.computed && node.property?.type === 'Identifier') return node.property.name
|
|
147
|
+
if (node.computed && node.property?.type === 'Literal' && typeof node.property.value === 'string') {
|
|
148
|
+
return node.property.value
|
|
149
|
+
}
|
|
150
|
+
return undefined
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function globalThisMember(node) {
|
|
154
|
+
return node?.type === 'MemberExpression'
|
|
155
|
+
&& node.object?.type === 'Identifier'
|
|
156
|
+
&& node.object.name === 'globalThis'
|
|
157
|
+
? staticMemberName(node)
|
|
158
|
+
: undefined
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isFunction(node) {
|
|
162
|
+
return node.type === 'FunctionDeclaration'
|
|
163
|
+
|| node.type === 'FunctionExpression'
|
|
164
|
+
|| node.type === 'ArrowFunctionExpression'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export class PreflightError extends Error {
|
|
168
|
+
constructor(message, node, span = undefined) {
|
|
169
|
+
super(message)
|
|
170
|
+
this.span = span ?? declarationSpan(node)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function staticModuleClassification(moduleLoads) {
|
|
175
|
+
const reasons = new Map()
|
|
176
|
+
for (const load of moduleLoads) {
|
|
177
|
+
const classification = classifyModuleSource(load.source)
|
|
178
|
+
if (classification.status === 'forbidden') {
|
|
179
|
+
throw new PreflightError(
|
|
180
|
+
`cell import of ${load.source} is forbidden because it exposes kernel control`,
|
|
181
|
+
undefined,
|
|
182
|
+
load.position,
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (classification.reason !== undefined) {
|
|
186
|
+
reasons.set(renderDurabilityReason(classification.reason), classification.reason)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...reasons.values()]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Conservatively classify a cell before giving it non-journalable capability. */
|
|
193
|
+
export function classifyDurability(code, knownBindings = new Set()) {
|
|
194
|
+
const tree = parse(`${STRIP_PREFIX}${code}${STRIP_SUFFIX}`, { ecmaVersion: 'latest', sourceType: 'script', locations: true })
|
|
195
|
+
const outer = tree.body[0]
|
|
196
|
+
/* c8 ignore next */
|
|
197
|
+
if (outer?.type !== 'FunctionDeclaration') throw new Error('ptc-plus: failed to parse cell wrapper')
|
|
198
|
+
const declared = topLevelBindings(outer.body.body)
|
|
199
|
+
const rootBindings = new Set([...knownBindings, ...declared])
|
|
200
|
+
const reasons = new Map()
|
|
201
|
+
const addReason = reason => reasons.set(renderDurabilityReason(reason), reason)
|
|
202
|
+
const classifyModule = (source) => {
|
|
203
|
+
if (source?.type !== 'Literal' || typeof source.value !== 'string') {
|
|
204
|
+
addReason(DYNAMIC_MODULE_REASON)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
const classification = classifyModuleSource(source.value)
|
|
208
|
+
if (classification.status === 'forbidden') {
|
|
209
|
+
throw new PreflightError(`cell import of ${source.value} is forbidden because it exposes kernel control`, source)
|
|
210
|
+
}
|
|
211
|
+
if (classification.reason !== undefined) addReason(classification.reason)
|
|
212
|
+
}
|
|
213
|
+
const isBound = (name, scopes) => {
|
|
214
|
+
for (let index = scopes.length - 1; index >= 0; index -= 1) {
|
|
215
|
+
if (scopes[index].has(name)) return true
|
|
216
|
+
}
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
walkAst(outer.body, (node, parent, parentKey, scopes) => {
|
|
220
|
+
let nestedScopes = scopes
|
|
221
|
+
if (isFunction(node)) {
|
|
222
|
+
nestedScopes = [...scopes, functionBindings(node)]
|
|
223
|
+
} else if (node.type === 'BlockStatement' && node !== outer.body) {
|
|
224
|
+
nestedScopes = [...scopes, directBlockBindings(node.body)]
|
|
225
|
+
} else if (node.type === 'CatchClause') {
|
|
226
|
+
const names = new Set()
|
|
227
|
+
addPatternBindings(node.param, names)
|
|
228
|
+
nestedScopes = [...scopes, names]
|
|
229
|
+
} else if (['ForStatement', 'ForInStatement', 'ForOfStatement'].includes(node.type)) {
|
|
230
|
+
nestedScopes = [...scopes, loopBindings(node)]
|
|
231
|
+
}
|
|
232
|
+
if (node.type === 'ImportExpression') {
|
|
233
|
+
classifyModule(node.source)
|
|
234
|
+
}
|
|
235
|
+
if (node.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'require'
|
|
236
|
+
&& !isBound('require', nestedScopes)) {
|
|
237
|
+
classifyModule(node.arguments[0])
|
|
238
|
+
}
|
|
239
|
+
if (node.type === 'Identifier' && isReferenceIdentifier(node, parent, parentKey)
|
|
240
|
+
&& !isBound(node.name, nestedScopes) && AMBIENT_GLOBALS.has(node.name)
|
|
241
|
+
&& !(node.name === 'require' && parent?.type === 'CallExpression' && parent.callee === node)) {
|
|
242
|
+
addReason(Object.freeze({ kind: 'ambient', name: node.name }))
|
|
243
|
+
}
|
|
244
|
+
if (node.type === 'Identifier' && isReferenceIdentifier(node, parent, parentKey)
|
|
245
|
+
&& !isBound('process', nestedScopes) && node.name === 'process' && !isStableProcessMember(node, parent)) {
|
|
246
|
+
addReason(Object.freeze({ kind: 'ambient', name: 'process' }))
|
|
247
|
+
}
|
|
248
|
+
if (node.type === 'MemberExpression'
|
|
249
|
+
&& ((node.object?.type === 'Identifier' && node.object.name === 'Math' && !isBound('Math', nestedScopes))
|
|
250
|
+
|| (globalThisMember(node.object) === 'Math' && !isBound('globalThis', nestedScopes)))
|
|
251
|
+
&& staticMemberName(node) === 'random') {
|
|
252
|
+
addReason(Object.freeze({ kind: 'math-random' }))
|
|
253
|
+
}
|
|
254
|
+
if (node.type === 'MemberExpression' && node.object?.type === 'Identifier'
|
|
255
|
+
&& node.object.name === 'globalThis' && !isBound('globalThis', nestedScopes)) {
|
|
256
|
+
const member = staticMemberName(node)
|
|
257
|
+
if (member === undefined) addReason(Object.freeze({ kind: 'computed-global-access' }))
|
|
258
|
+
else if (member === 'process') addReason(Object.freeze({ kind: 'ambient', name: 'process' }))
|
|
259
|
+
else if (AMBIENT_GLOBALS.has(member)) addReason(Object.freeze({ kind: 'ambient', name: member }))
|
|
260
|
+
}
|
|
261
|
+
return nestedScopes
|
|
262
|
+
}, undefined, [rootBindings], outer, 'body')
|
|
263
|
+
return {
|
|
264
|
+
durability: reasons.size === 0 ? 'durable' : 'volatile',
|
|
265
|
+
reasons: Object.freeze([...reasons.values()]),
|
|
266
|
+
declared,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function rewriteCellReturns(code, sourceMap, unavailableNames) {
|
|
271
|
+
const wrapped = STRIP_PREFIX + code + STRIP_SUFFIX
|
|
272
|
+
const tree = parse(wrapped, { ecmaVersion: 'latest', sourceType: 'script' })
|
|
273
|
+
const outer = tree.body[0]
|
|
274
|
+
/* c8 ignore next */
|
|
275
|
+
if (outer?.type !== 'FunctionDeclaration') throw new Error('ptc-plus: failed to parse cell wrapper')
|
|
276
|
+
const offset = STRIP_PREFIX.length
|
|
277
|
+
const edits = []
|
|
278
|
+
const allocateName = createGeneratedNameAllocator(tree, unavailableNames)
|
|
279
|
+
const returnSignal = allocateName('return_signal')
|
|
280
|
+
const signalReference = `this[${JSON.stringify(returnSignal)}]`
|
|
281
|
+
|
|
282
|
+
walkAst(outer.body, (node) => {
|
|
283
|
+
if (node !== outer.body && isFunction(node)) return SKIP_AST_CHILDREN
|
|
284
|
+
if (node.type === 'ReturnStatement') {
|
|
285
|
+
const start = node.start - offset
|
|
286
|
+
const end = node.end - offset
|
|
287
|
+
const argument = node.argument === null
|
|
288
|
+
? ''
|
|
289
|
+
: code.slice(node.argument.start - offset, node.argument.end - offset)
|
|
290
|
+
// A ReturnStatement may be the last clause body in a switch case. The
|
|
291
|
+
// generated throw must terminate explicitly because the original return
|
|
292
|
+
// statement's semicolon is part of the replaced AST range.
|
|
293
|
+
edits.push({ start, end, text: `throw new ${signalReference}(${argument});` })
|
|
294
|
+
return SKIP_AST_CHILDREN
|
|
295
|
+
}
|
|
296
|
+
if (node.type === 'CatchClause') {
|
|
297
|
+
const bodyStart = node.body.start - offset + 1
|
|
298
|
+
const temporary = allocateName('caught')
|
|
299
|
+
if (node.param === null) {
|
|
300
|
+
edits.push({ start: node.start - offset + 5, end: node.start - offset + 5, text: ` (${temporary})` })
|
|
301
|
+
edits.push({ start: bodyStart, end: bodyStart, text: `\nif (${temporary} instanceof ${signalReference}) throw ${temporary};` })
|
|
302
|
+
} else if (node.param.type === 'Identifier') {
|
|
303
|
+
edits.push({
|
|
304
|
+
start: bodyStart,
|
|
305
|
+
end: bodyStart,
|
|
306
|
+
text: `\nif (${node.param.name} instanceof ${signalReference}) throw ${node.param.name};`,
|
|
307
|
+
})
|
|
308
|
+
} else {
|
|
309
|
+
const pattern = code.slice(node.param.start - offset, node.param.end - offset)
|
|
310
|
+
edits.push({ start: node.param.start - offset, end: node.param.end - offset, text: temporary })
|
|
311
|
+
edits.push({
|
|
312
|
+
start: bodyStart,
|
|
313
|
+
end: bodyStart,
|
|
314
|
+
text: `\nif (${temporary} instanceof ${signalReference}) throw ${temporary};\nconst ${pattern} = ${temporary};`,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
/* c8 ignore next */
|
|
321
|
+
edits.sort((left, right) => right.start - left.start || right.end - left.end)
|
|
322
|
+
return { ...applySourceEdits(code, sourceMap, edits), returnSignal }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function prepareProgram(program, knownBindings, looseTopLevelRedeclarations, reservedBindings = new Set(), rewritesEnabled, importBindings = new Map(), importNamespaces = new Set()) {
|
|
326
|
+
if (typeof program !== 'string') throw new TypeError('ptc-plus: program must be a string')
|
|
327
|
+
if (rewritesEnabled === null || typeof rewritesEnabled !== 'object' || Array.isArray(rewritesEnabled)
|
|
328
|
+
|| typeof rewritesEnabled.autoRewriteImports !== 'boolean'
|
|
329
|
+
|| typeof rewritesEnabled.autoStripExports !== 'boolean'
|
|
330
|
+
|| typeof rewritesEnabled.autoSplitRedeclarations !== 'boolean') {
|
|
331
|
+
throw new TypeError('ptc-plus: rewrite policy must define autoRewriteImports, autoStripExports, and autoSplitRedeclarations booleans')
|
|
332
|
+
}
|
|
333
|
+
const unavailableGeneratedNames = new Set([
|
|
334
|
+
...knownBindings,
|
|
335
|
+
...reservedBindings,
|
|
336
|
+
...importBindings.keys(),
|
|
337
|
+
...importNamespaces,
|
|
338
|
+
])
|
|
339
|
+
const moduleRewrite = rewriteModuleImportsExports(
|
|
340
|
+
program,
|
|
341
|
+
rewritesEnabled,
|
|
342
|
+
importBindings,
|
|
343
|
+
importNamespaces,
|
|
344
|
+
unavailableGeneratedNames,
|
|
345
|
+
)
|
|
346
|
+
const staticModuleReasons = staticModuleClassification(moduleRewrite.moduleLoads)
|
|
347
|
+
const wrapped = STRIP_PREFIX + moduleRewrite.code + STRIP_SUFFIX
|
|
348
|
+
let stripped = wrapped
|
|
349
|
+
let tree
|
|
350
|
+
try {
|
|
351
|
+
tree = parse(wrapped, { ecmaVersion: 'latest', sourceType: 'script', locations: true })
|
|
352
|
+
} catch (javascriptError) {
|
|
353
|
+
try {
|
|
354
|
+
stripped = stripTypeScriptTypes(wrapped)
|
|
355
|
+
tree = parse(stripped, { ecmaVersion: 'latest', sourceType: 'script', locations: true })
|
|
356
|
+
} catch {
|
|
357
|
+
throw javascriptError
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const code = stripped.slice(STRIP_PREFIX.length, stripped.length - STRIP_SUFFIX.length)
|
|
361
|
+
const sourceMap = moduleRewrite.sourceMap
|
|
362
|
+
const outer = tree.body[0]
|
|
363
|
+
/* c8 ignore next */
|
|
364
|
+
const generatedDeclarations = outer?.type === 'FunctionDeclaration' ? topLevelDeclarations(outer.body.body) : []
|
|
365
|
+
const collisionFor = (declaration) => {
|
|
366
|
+
/* c8 ignore next */
|
|
367
|
+
const mapped = declaration.original === true ? declaration.span : declaration.span === undefined
|
|
368
|
+
? { line: 1, column: 1 }
|
|
369
|
+
: mapSourceSpan(declaration.span, code, program, sourceMap)
|
|
370
|
+
return {
|
|
371
|
+
name: declaration.name,
|
|
372
|
+
start: { line: mapped.line, column: mapped.column },
|
|
373
|
+
/* c8 ignore next */
|
|
374
|
+
...(mapped.end === undefined ? {} : { end: mapped.end }),
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const classifyPrepared = (preparedCode, preparedSourceMap) => {
|
|
378
|
+
try {
|
|
379
|
+
const classification = classifyDurability(preparedCode, knownBindings)
|
|
380
|
+
const reasons = new Map()
|
|
381
|
+
for (const reason of [...staticModuleReasons, ...classification.reasons]) {
|
|
382
|
+
reasons.set(renderDurabilityReason(reason), reason)
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
...classification,
|
|
386
|
+
durability: reasons.size === 0 ? 'durable' : 'volatile',
|
|
387
|
+
reasons: Object.freeze([...reasons.values()]),
|
|
388
|
+
reason: renderDurabilityReasons([...reasons.values()]),
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error instanceof PreflightError && error.span !== undefined) {
|
|
392
|
+
error.span = mapSourceSpan(error.span, preparedCode, program, preparedSourceMap)
|
|
393
|
+
}
|
|
394
|
+
throw error
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const importedNames = new Set(moduleRewrite.imports.keys())
|
|
398
|
+
const declarations = [
|
|
399
|
+
...moduleRewrite.importDeclarations,
|
|
400
|
+
...generatedDeclarations.filter(declaration => !moduleRewrite.generatedNamespaces.has(declaration.name)),
|
|
401
|
+
]
|
|
402
|
+
const normalizeClassification = classification => {
|
|
403
|
+
const imports = new Map(moduleRewrite.imports)
|
|
404
|
+
for (const name of classification.declared) {
|
|
405
|
+
if (!moduleRewrite.generatedNamespaces.has(name)) imports.delete(name)
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
...classification,
|
|
409
|
+
imports,
|
|
410
|
+
declared: new Set([...classification.declared, ...importedNames]),
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const reserved = declarations.filter(declaration => (
|
|
414
|
+
reservedBindings.has(declaration.name) || moduleRewrite.importNamespaces.has(declaration.name)
|
|
415
|
+
))
|
|
416
|
+
if (reserved.length > 0) {
|
|
417
|
+
const classification = normalizeClassification(classifyPrepared(code, sourceMap))
|
|
418
|
+
return {
|
|
419
|
+
code,
|
|
420
|
+
...classification,
|
|
421
|
+
declarations,
|
|
422
|
+
imports: classification.imports,
|
|
423
|
+
importNamespaces: moduleRewrite.importNamespaces,
|
|
424
|
+
collisions: reserved.map(collisionFor),
|
|
425
|
+
redeclared: [],
|
|
426
|
+
rewrites: moduleRewrite.rewrites,
|
|
427
|
+
moduleLoads: moduleRewrite.moduleLoads,
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const convenience = rewriteLooseRedeclarations({
|
|
431
|
+
code,
|
|
432
|
+
sourceMap,
|
|
433
|
+
body: looseTopLevelRedeclarations && outer?.type === 'FunctionDeclaration' ? outer.body.body : undefined,
|
|
434
|
+
offset: STRIP_PREFIX.length,
|
|
435
|
+
knownBindings: new Set([...knownBindings].filter(name => !importBindings.has(name))),
|
|
436
|
+
declarations,
|
|
437
|
+
declarationSpan,
|
|
438
|
+
collisionFor,
|
|
439
|
+
autoSplitRedeclarations: rewritesEnabled.autoSplitRedeclarations,
|
|
440
|
+
})
|
|
441
|
+
const { executableCode, executableSourceMap, collisions, redeclared, rewrites } = convenience
|
|
442
|
+
// Classification sees the rewritten program so declared bindings reflect the
|
|
443
|
+
// split form; it must never see the return rewrite, whose computed
|
|
444
|
+
// globalThis access would mark every returning cell volatile.
|
|
445
|
+
const classification = normalizeClassification(classifyPrepared(executableCode, executableSourceMap))
|
|
446
|
+
let lowered = { code, sourceMap }
|
|
447
|
+
if (collisions.length === 0) {
|
|
448
|
+
lowered = rewriteCellReturns(
|
|
449
|
+
executableCode,
|
|
450
|
+
executableSourceMap,
|
|
451
|
+
unavailableGeneratedNames,
|
|
452
|
+
)
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
code: lowered.code,
|
|
456
|
+
sourceMap: lowered.sourceMap,
|
|
457
|
+
...(lowered.returnSignal === undefined ? {} : { returnSignal: lowered.returnSignal }),
|
|
458
|
+
...classification,
|
|
459
|
+
declarations,
|
|
460
|
+
imports: classification.imports,
|
|
461
|
+
importNamespaces: moduleRewrite.importNamespaces,
|
|
462
|
+
collisions,
|
|
463
|
+
redeclared,
|
|
464
|
+
rewrites: [...moduleRewrite.rewrites, ...rewrites],
|
|
465
|
+
moduleLoads: moduleRewrite.moduleLoads,
|
|
466
|
+
}
|
|
467
|
+
}
|