From eb03aa86fec91dd92cb303834596141c0ad1f566 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 18:30:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(code-runtime):=20address=20seam=20revi?= =?UTF-8?q?ew=20=E2=80=94=20drop=20worker=20aliases,=20tighten=20dunder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Worker consumes PORTABLE_RESERVED_WORDS / RESERVED_ERROR_MEMBERS by their seam names directly, dropping the local re-alias (symmetry with the other two imported constants). - Split the reserved-vs-duplicate diagnostics: a backend-owned global now reports "reserved binding global", not the misleading "duplicate". - DUNDER_MEMBER uses `__.+__` so a bare `__` (empty middle, not a real CPython dunder) is not matched; add coverage. - Worker misuse tests add `a$b` (second-char `$`) and `lambda` (Python keyword) so the identifier narrowing and reserved-word adoption are each pinned directly, not only transitively. - Clarify the seam JSDoc (dunder-vs-explicit-set wording, Python backend is a later stack PR) and record in the Agent Note the obligation to widen RESERVED_BINDING_GLOBALS when the bootstrap seeds more globals. --- ...runtime-portable-identifier-seam.i18n.yaml | 4 +- ...1-code-runtime-portable-identifier-seam.md | 2 + ...ode-runtime-portable-identifier-seam.zh.md | 2 + .../code-runtime-worker/src/index.ts | 33 ++++++----------- .../code-runtime-worker/tests/runtime.spec.ts | 15 ++++++-- .../code-runtime/code-runtime/src/index.ts | 37 +++++++++++-------- .../code-runtime/tests/reserved.spec.ts | 3 ++ 7 files changed, 54 insertions(+), 42 deletions(-) diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml index 73bf26efee..a2a0848eab 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md -2026-07-31-code-runtime-portable-identifier-seam.md: a24a2c03c937a7d569528504b5bd5fba812009a2 -2026-07-31-code-runtime-portable-identifier-seam.zh.md: 1bef607e31a5820ba849f09eb7ae4cc781c45a84 +2026-07-31-code-runtime-portable-identifier-seam.md: bd1876fc459dc85bb5d9f42ac04f4cc801d6398a +2026-07-31-code-runtime-portable-identifier-seam.zh.md: 2fd5e97c562d1e51cbad906cd37998b8ffccc58a diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md index a24a2c03c9..bd1876fc45 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md @@ -27,6 +27,8 @@ The constants live at the seam even though only one backend ships in this PR: th This PR delivers only the seam extension and the worker's adoption of it. No Python backend, `py-types` renderer, or Code Mode language dispatch ships here — they are later PRs in the stack that depend on these exports. The seam README's worker-only wording is left unchanged for the same reason: linking to a `dsh-code-runtime-python` README that does not yet exist would break the dead-link gate. +`RESERVED_BINDING_GLOBALS` currently encodes the not-yet-merged Python bootstrap's concrete design: it seeds exactly `__builtins__`/`__name__` and wraps the program under `__dsh_main__`. The Python-backend PR that seeds any additional module global (`__doc__`, `__loader__`, `__spec__`, `__file__`, `__package__`, …) MUST widen this set in the same change, exactly as adding a language widens `PORTABLE_RESERVED_WORDS` — a name the bootstrap seeds but the set omits is the portability split this contract exists to prevent. + ## Alternatives considered **Each backend declares its own exclusions.** Rejected: it makes the portability promise per-backend. A binding list the caller tested on the worker could be refused by Python, which is exactly the split the seam exists to prevent. diff --git a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md index 1bef607e31..2fd5e97c56 100644 --- a/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.zh.md @@ -27,6 +27,8 @@ seam 同时把可移植标识符子集收窄为 `[A-Za-z_][A-Za-z0-9_]*`(记 本 PR 只交付 seam 扩展与 worker 对它的采用。这里不交付任何 Python 后端、`py-types` 渲染器或 Code Mode 的语言分发——它们是本 stack 中依赖这些导出的后续 PR。seam README 中仅描述 worker 的措辞保持不变,理由相同:链接到一个尚不存在的 `dsh-code-runtime-python` README 会破坏死链 gate。 +`RESERVED_BINDING_GLOBALS` 当前编码了尚未合并的 Python bootstrap 的具体设计:它恰好 seed `__builtins__`/`__name__`,并把程序包装在 `__dsh_main__` 之下。任何 seed 额外模块 global(`__doc__`、`__loader__`、`__spec__`、`__file__`、`__package__` 等)的 Python 后端 PR 必须在同一改动中扩宽此集合,正如新增一门语言即扩宽 `PORTABLE_RESERVED_WORDS`——bootstrap 会 seed 却不在集合中的名称,正是本契约要防止的可移植性分裂。 + ## Alternatives considered **每个后端声明自己的排除集。** 拒绝:这让可移植承诺变成逐后端成立。调用方在 worker 上测过的绑定列表可能被 Python 拒绝,而这正是 seam 存在要防止的分裂。 diff --git a/packages/code-runtime/code-runtime-worker/src/index.ts b/packages/code-runtime/code-runtime-worker/src/index.ts index 3c198ca94e..9773d3c341 100644 --- a/packages/code-runtime/code-runtime-worker/src/index.ts +++ b/packages/code-runtime/code-runtime-worker/src/index.ts @@ -65,14 +65,6 @@ const ELU_POLL_INTERVAL_MS = 25 /** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */ const MIN_OUTPUT_BYTES = 4 -/** - * The seam's cross-language reserved-word union: the portable-identifier - * contract promises a namespace list valid here is valid on every backend, so - * a Python keyword like `lambda` is refused even though it is a legal JS - * parameter name. - */ -const RESERVED_WORDS = PORTABLE_RESERVED_WORDS - /** * The seam's language-portable identifier subset (see * `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same @@ -80,13 +72,6 @@ const RESERVED_WORDS = PORTABLE_RESERVED_WORDS */ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/ -/** - * The seam's shared error-member exclusions (plus the dunder rule below): - * enforced identically here and in the Python backend so an errorClass valid - * on one backend is valid on all. - */ -const RESERVED_ERROR_PROPERTIES = RESERVED_ERROR_MEMBERS - /** * The shell a program is wrapped in for the type-strip, matching the * grammatical context it will execute in (an async function body, where @@ -335,14 +320,17 @@ export class WorkerCodeRuntime extends CodeRuntime { private validateBindings(request: CodeRunRequest): Map { const bindings = new Map() for (const namespace of request.bindings) { - if (!IDENTIFIER.test(namespace.global) || RESERVED_WORDS.has(namespace.global)) { + if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`) } // RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set: // `console` is THIS backend's log-capture slot; the dunder entries are - // the Python bootstrap's — refused here too so the namespace list stays + // the Python backend's — refused here too so the namespace list stays // portable across backends. - if (RESERVED_BINDING_GLOBALS.has(namespace.global) || bindings.has(namespace.global)) { + if (RESERVED_BINDING_GLOBALS.has(namespace.global)) { + throw new Error(`dsh-code-runtime-worker: reserved binding global ${JSON.stringify(namespace.global)}`) + } + if (bindings.has(namespace.global)) { throw new Error(`dsh-code-runtime-worker: duplicate binding global ${JSON.stringify(namespace.global)}`) } bindings.set(namespace.global, namespace) @@ -352,14 +340,17 @@ export class WorkerCodeRuntime extends CodeRuntime { for (const namespace of request.bindings) { const descriptor = namespace.errorClass if (!descriptor) continue - if (!IDENTIFIER.test(descriptor.name) || RESERVED_WORDS.has(descriptor.name)) { + if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`) } - if (RESERVED_BINDING_GLOBALS.has(descriptor.name) || bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { + if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) { + throw new Error(`dsh-code-runtime-worker: reserved binding global ${JSON.stringify(descriptor.name)}`) + } + if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) { throw new Error(`dsh-code-runtime-worker: duplicate injected global ${JSON.stringify(descriptor.name)}`) } const member = descriptor.memberNameProperty - if (member.length === 0 || RESERVED_ERROR_PROPERTIES.has(member) || DUNDER_MEMBER.test(member)) { + if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) { throw new Error(`dsh-code-runtime-worker: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`) } errorClassNames.add(descriptor.name) diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index ce97faadb3..54f58eb414 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -790,7 +790,14 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { // `$tools` is legal JS but outside the seam's language-portable subset: // the same namespace list must work against every backend's language. ['$tools', /not a usable identifier/], - ['console', /duplicate binding global/], + // `a$b` pins the second character class too: the old identifier regex + // `[A-Za-z0-9_$]*` would have accepted a `$` after the first character. + ['a$b', /not a usable identifier/], + // `lambda` is a Python keyword, refused here directly (not just + // transitively) so the worker's adoption of PORTABLE_RESERVED_WORDS is + // its own regression, symmetric with the `$tools` case. + ['lambda', /not a usable identifier/], + ['console', /reserved binding global/], ] for (const [global, message] of cases) { await expect(runtime.run({ program: 'return 1', bindings: [{ global, functions: {} }] })).rejects.toThrow(message) @@ -817,7 +824,7 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { await expect(run([namespace('tools', 'not valid!')])).rejects.toThrow(/error class.*not a usable identifier/) await expect(run([namespace('tools', 'await')])).rejects.toThrow(/error class.*not a usable identifier/) - await expect(run([namespace('tools', 'console')])).rejects.toThrow(/duplicate injected global/) + await expect(run([namespace('tools', 'console')])).rejects.toThrow(/reserved binding global/) await expect(run([namespace('tools', 'tools')])).rejects.toThrow(/duplicate injected global/) await expect(run([ namespace('tools', 'CallError'), @@ -829,10 +836,10 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => { // dunders too, so the same errorClass is valid (or not) on every backend. await expect(run([namespace('tools', 'CallError', 'args')])).rejects.toThrow(/member property.*not usable/) await expect(run([namespace('tools', 'CallError', '__dict__')])).rejects.toThrow(/member property.*not usable/) - // The Python bootstrap's owned globals are refused here too (shared + // The Python backend's owned globals are refused here too (shared // RESERVED_BINDING_GLOBALS), keeping namespace lists backend-portable. await expect(runtime.run({ program: 'return 1', bindings: [{ global: '__dsh_main__', functions: {} }] })) - .rejects.toThrow(/duplicate binding global/) + .rejects.toThrow(/reserved binding global/) }) it('rejects config values that are not positive numbers', async () => { diff --git a/packages/code-runtime/code-runtime/src/index.ts b/packages/code-runtime/code-runtime/src/index.ts index 681c0465a7..2e4f18a2e2 100644 --- a/packages/code-runtime/code-runtime/src/index.ts +++ b/packages/code-runtime/code-runtime/src/index.ts @@ -20,19 +20,23 @@ export type { /** * Binding globals EVERY backend refuses because SOME backend owns the slot in * the program's namespace: `console` (the worker's log capture), and - * `__dsh_main__`/`__builtins__`/`__name__` (the Python bootstrap's wrapper - * and seeded module globals), and `__debug__`. One shared set — rather than each backend - * refusing only its own slots — keeps the portability promise real: a - * namespace list valid on one backend is valid on all, so a caller cannot - * pick a name that works on the worker and collides on Python (or vice - * versa). Dunder-form names are additionally covered by the identifier rule - * on `CodeBindingNamespace.global` only when they fail it; `__name__` et al. - * ARE valid identifiers, hence this explicit set. `__debug__` is listed for a - * different reason than a collision: CPython compiles a bare `__debug__` - * reference to the constant `True` and rejects any assignment to the name at - * COMPILE time, so an injected global under that name is unreachable from the - * program — accepted by validation, unusable on the Python backend, which is - * exactly the split the shared set exists to prevent. + * `__dsh_main__`/`__builtins__`/`__name__` (the Python backend's bootstrap + * wrapper and seeded module globals — that backend is a later PR in this + * stack, see the [portable-identifier Agent + * Note](../../../../.agents/notes/implemented/architecture/2026-07-31-code-runtime-portable-identifier-seam.md)), + * and `__debug__`. One shared set — rather than each backend refusing only its + * own slots — keeps the portability promise real: a namespace list valid on + * one backend is valid on all, so a caller cannot pick a name that works on + * the worker and collides on Python (or vice versa). `__name__` et al. ARE + * valid portable identifiers, so the identifier rule on + * `CodeBindingNamespace.global` never rejects them — hence this explicit set. + * (Error members differ: {@link DUNDER_MEMBER} refuses every dunder form + * wholesale; binding globals refuse only the names listed here.) `__debug__` + * is listed for a different reason than a collision: CPython compiles a bare + * `__debug__` reference to the constant `True` and rejects any assignment to + * the name at COMPILE time, so an injected global under that name is + * unreachable from the program — accepted by validation, unusable on the + * Python backend, which is exactly the split the shared set exists to prevent. */ export const RESERVED_BINDING_GLOBALS: ReadonlySet = new Set([ 'console', @@ -54,8 +58,11 @@ export const RESERVED_ERROR_MEMBERS: ReadonlySet = new Set([ 'args', 'with_traceback', 'add_note', ]) -/** Dunder form (`__*__`): object-protocol slots in Python, refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. */ -export const DUNDER_MEMBER = /^__.*__$/ +/** + * Dunder form (`__x__`, non-empty middle): object-protocol slots in Python, + * refused as {@link RESERVED_ERROR_MEMBERS | error members} on every backend. + */ +export const DUNDER_MEMBER = /^__.+__$/ /** * Reserved words of EVERY shipped backend language (ECMAScript ∪ Python), diff --git a/packages/code-runtime/code-runtime/tests/reserved.spec.ts b/packages/code-runtime/code-runtime/tests/reserved.spec.ts index 31868252a1..ccd2c89211 100644 --- a/packages/code-runtime/code-runtime/tests/reserved.spec.ts +++ b/packages/code-runtime/code-runtime/tests/reserved.spec.ts @@ -35,6 +35,9 @@ describe('seam-owned portable identifier exclusions', () => { expect(DUNDER_MEMBER.test('_private')).toBe(false) expect(DUNDER_MEMBER.test('name')).toBe(false) expect(DUNDER_MEMBER.test('__mid')).toBe(false) + // `__` has an empty middle — not a real CPython dunder, so not matched. + expect(DUNDER_MEMBER.test('__')).toBe(false) + expect(DUNDER_MEMBER.test('____')).toBe(true) }) it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => {