Merge pull request #1079 from deepseek-harness/feat/code-runtime-multilang-seam
feat(code-runtime): own portable-identifier exclusions at the seam
This commit is contained in:
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url'
|
||||
import { Context } from 'cordis'
|
||||
import z from 'schemastery'
|
||||
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
|
||||
import { CodeRuntime } from '@deepseek-ai/dsh-code-runtime'
|
||||
import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from '@deepseek-ai/dsh-code-runtime'
|
||||
import type { CodeBindingNamespace, CodeJsonValue, CodeRunFailure, CodeRunRequest, CodeRunResult } from '@deepseek-ai/dsh-code-runtime'
|
||||
import { snapshotJsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts'
|
||||
@@ -65,20 +65,12 @@ 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
|
||||
|
||||
/** ECMAScript reserved words that cannot be async-function parameter names — rejected as binding globals. */
|
||||
const RESERVED_WORDS = new Set([
|
||||
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
||||
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
|
||||
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
|
||||
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
|
||||
'private', 'protected', 'public', 'arguments', 'eval',
|
||||
])
|
||||
|
||||
/** Valid async-function parameter name (the binding global becomes one). */
|
||||
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
||||
|
||||
/** Error properties whose binding-member replacement would destroy the promised Error contract. */
|
||||
const RESERVED_ERROR_PROPERTIES = new Set(['name', 'message', 'stack'])
|
||||
/**
|
||||
* The seam's language-portable identifier subset (see
|
||||
* `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same
|
||||
* namespace list must be usable against every backend regardless of language.
|
||||
*/
|
||||
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/
|
||||
|
||||
/**
|
||||
* The shell a program is wrapped in for the type-strip, matching the
|
||||
@@ -328,10 +320,19 @@ export class WorkerCodeRuntime extends CodeRuntime {
|
||||
private validateBindings(request: CodeRunRequest): Map<string, CodeBindingNamespace> {
|
||||
const bindings = new Map<string, CodeBindingNamespace>()
|
||||
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`)
|
||||
}
|
||||
if (namespace.global === 'console' || bindings.has(namespace.global)) {
|
||||
// RESERVED_BINDING_GLOBALS is the seam's shared backend-owned set:
|
||||
// `console` is THIS backend's log-capture slot; the dunder entries exist
|
||||
// for the Python side — its seeded/wrapped slots plus the `__debug__`
|
||||
// compile-time constant — refused here too so the namespace list stays
|
||||
// portable across backends. The seam declaration is the single home for
|
||||
// why each entry is reserved.
|
||||
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)
|
||||
@@ -341,13 +342,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 (descriptor.name === 'console' || 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)}`)
|
||||
}
|
||||
if (descriptor.memberNameProperty.length === 0 || RESERVED_ERROR_PROPERTIES.has(descriptor.memberNameProperty)) {
|
||||
const member = descriptor.memberNameProperty
|
||||
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)
|
||||
|
||||
@@ -787,7 +787,17 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => {
|
||||
const cases: [string, RegExp][] = [
|
||||
['not valid!', /not a usable identifier/],
|
||||
['await', /not a usable identifier/],
|
||||
['console', /duplicate binding global/],
|
||||
// `$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/],
|
||||
// `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)
|
||||
@@ -814,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'),
|
||||
@@ -822,6 +832,14 @@ describe('WorkerCodeRuntime — seam misuse and lifecycle', () => {
|
||||
])).rejects.toThrow(/duplicate injected global/)
|
||||
await expect(run([namespace('tools', 'CallError', '')])).rejects.toThrow(/member property.*not usable/)
|
||||
await expect(run([namespace('tools', 'CallError', 'message')])).rejects.toThrow(/member property.*not usable/)
|
||||
// The shared exclusion set covers Python's exception-protocol members and
|
||||
// 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 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(/reserved binding global/)
|
||||
})
|
||||
|
||||
it('rejects config values that are not positive numbers', async () => {
|
||||
|
||||
@@ -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 packages/code-runtime/code-runtime/README.md
|
||||
README.md: e9641041af76b60606f999783f29224d8d79c743
|
||||
README.zh.md: cc97b6b6cf7c8c5aedb58e40d06ee6dd962ac3b2
|
||||
README.md: bb1c20d00a260f643f601c42c6e48722437d5aab
|
||||
README.zh.md: 15fbcecf77b2318acf3b09101802cd032ae426d2
|
||||
|
||||
@@ -20,6 +20,8 @@ Semantics every implementation must honor (contract details in the class JSDoc):
|
||||
|
||||
`CodeRunRequest` (`program`, `bindings`, `signal?`) carries everything the runtime acts on — defaulting (time budgets and outer-output cap) is the implementation's validated config, never a hidden `??` inside `run()`. `bindings` is a list of `CodeBindingNamespace`s (`global` + `functions` + optional `errorClass`), each exposed to the program as one global object of async callables returning `CodeJsonValue`, the seam-local structural equivalent of canonical `JsonValue` that keeps this interface package independent of sessions. An `errorClass` descriptor names a real program-global constructor and the own property that receives the rejected member name; runtimes remain independent of consumer terms such as `ToolCallError`. `CodeRunResult` reports the lossless JSON completion `value?`, ordered `logs: string[]`, and the `error?` (`CodeRunFailure`: `kind` + model-feedable `message`). See `src/types.ts` for the full contracts.
|
||||
|
||||
Binding-global and error-class names are **language-portable**: they must match the identifier subset `[A-Za-z_][A-Za-z0-9_]*` (no JS-only `$`) and clear the seam-exported exclusion sets, so one `bindings` list is valid against every backend regardless of its `language`. The package exports the contract every backend enforces — `PORTABLE_RESERVED_WORDS` (ECMAScript ∪ Python reserved words), `RESERVED_BINDING_GLOBALS` (backend-owned globals such as `console`), `RESERVED_ERROR_MEMBERS` and `DUNDER_MEMBER` (error-member exclusions) — so a name like `$tools`, `lambda`, or `__dsh_main__` makes `run()` reject as seam misuse on any backend, not just some. See `src/index.ts` for the exact sets and rationale.
|
||||
|
||||
## Model Experience
|
||||
|
||||
Indirectly, through Code Mode in `dsh-tools`, which exposes `run_code` and returns program logs, values, or failures as retained tool-result tokens.
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
`CodeRunRequest`(`program`、`bindings`、`signal?`)携带运行时操作所需的全部内容;默认值解析(时间预算与外层输出上限)属于实现的已验证配置,绝不能是隐藏的 `??`,更不能藏在 `run()` 内部。`bindings` 是 `CodeBindingNamespace` 列表(`global` + `functions` + 可选 `errorClass`);每个命名空间会作为一个由异步可调用函数组成的全局对象公开给程序,这些函数返回 `CodeJsonValue`。后者是 seam 本地、与规范 `JsonValue` 结构等价的类型,使接口包保持独立于会话。`errorClass` 描述符点名真实的程序全局构造器,以及用于接收被拒绝成员名称的自有属性;运行时不依赖 `ToolCallError` 等消费方术语。`CodeRunResult` 报告无损 JSON 完成值 `value?`、有序的 `logs: string[]` 和 `error?`(`CodeRunFailure`:`kind` + 可反馈给模型的 `message`)。完整契约见 `src/types.ts`。
|
||||
|
||||
binding-global 与 error-class 名称是**语言可移植**的:必须匹配标识符子集 `[A-Za-z_][A-Za-z0-9_]*`(不含 JS 专有的 `$`)并通过 seam 导出的排除集,因此同一份 `bindings` 列表对每个后端都有效,无论其 `language` 为何。本包导出每个后端都执行的契约——`PORTABLE_RESERVED_WORDS`(ECMAScript ∪ Python 保留字)、`RESERVED_BINDING_GLOBALS`(如 `console` 等后端拥有的 global)、`RESERVED_ERROR_MEMBERS` 与 `DUNDER_MEMBER`(error-member 排除)——因此 `$tools`、`lambda`、`__dsh_main__` 之类的名称会让 `run()` 在任何后端上作为 seam 误用而 reject,而非只在某些后端。确切集合与理由见 `src/index.ts`。
|
||||
|
||||
## 模型体验
|
||||
|
||||
通过 `dsh-tools` 中的 Code Mode 间接提供;后者公开 `run_code`,并将程序日志、值或失败作为保留的工具结果 token 返回。
|
||||
|
||||
@@ -17,6 +17,77 @@ export type {
|
||||
CodeRunResult,
|
||||
} from './types.ts'
|
||||
|
||||
/**
|
||||
* 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 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<string> = new Set([
|
||||
'console',
|
||||
'__dsh_main__', '__builtins__', '__name__', '__debug__',
|
||||
])
|
||||
|
||||
/**
|
||||
* `CodeBindingErrorClass.memberNameProperty` names EVERY backend refuses, as
|
||||
* one shared contract so a request valid on one backend is valid on all. The
|
||||
* JS `Error` exclusions (`name`, `message`, `stack`) and Python's
|
||||
* exception-protocol members (`args`, `with_traceback`, `add_note`) are
|
||||
* listed by name; dunder-form names (`__x__`, non-empty middle) are refused
|
||||
* wholesale — several are constrained CPython descriptors whose `setattr`
|
||||
* raises while constructing the rejection, and the exact set is an interpreter
|
||||
* version detail. Any other non-empty own property name is accepted everywhere.
|
||||
*/
|
||||
export const RESERVED_ERROR_MEMBERS: ReadonlySet<string> = new Set([
|
||||
'name', 'message', 'stack',
|
||||
'args', 'with_traceback', 'add_note',
|
||||
])
|
||||
|
||||
/**
|
||||
* 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 portable target language (ECMAScript ∪ Python),
|
||||
* refused as {@link CodeBindingNamespace.global} / error-class names by all
|
||||
* backends. Python is a portability target here even though only the
|
||||
* TypeScript worker ships in this PR (the CPython backend is a later PR in the
|
||||
* stack). The portable-identifier contract promises a namespace list valid
|
||||
* on one backend is valid on every backend; a per-language check would let
|
||||
* `lambda` pass the TypeScript backend and fail the Python one. Extending the
|
||||
* seam with a new language means widening this union (a breaking review of
|
||||
* existing binding names, by design).
|
||||
*/
|
||||
export const PORTABLE_RESERVED_WORDS: ReadonlySet<string> = new Set([
|
||||
// ECMAScript reserved words and reserved-in-strict-mode names.
|
||||
'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
||||
'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import', 'in',
|
||||
'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof',
|
||||
'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'implements', 'interface', 'package',
|
||||
'private', 'protected', 'public', 'arguments', 'eval',
|
||||
// Python 3.x keywords and soft keywords not already above ('type' and '_'
|
||||
// are soft keywords: legal names in practice, reserved here for safety).
|
||||
'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'def', 'del', 'elif', 'except', 'from',
|
||||
'global', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'match', 'type', '_',
|
||||
])
|
||||
|
||||
declare module 'cordis' {
|
||||
interface Context {
|
||||
codeRuntime: CodeRuntime
|
||||
|
||||
@@ -28,9 +28,14 @@ export type CodeJsonValue = null | boolean | number | string | CodeJsonValue[] |
|
||||
* of a particular consumer such as Code Mode.
|
||||
*/
|
||||
export interface CodeBindingErrorClass {
|
||||
/** Constructor global and resulting `Error.name` (must be a usable JS identifier). */
|
||||
/** Constructor global and resulting `Error.name`; same portable identifier rule as {@link CodeBindingNamespace.global}. */
|
||||
name: string
|
||||
/** Non-empty own property for the member name; cannot replace `name`, `message`, or `stack`. */
|
||||
/**
|
||||
* Non-empty own property for the member name. The portable exclusion set is
|
||||
* `RESERVED_ERROR_MEMBERS` plus dunder-form names (`__x__`, non-empty
|
||||
* middle), enforced identically by every backend; any other name —
|
||||
* identifiers or not — is accepted everywhere.
|
||||
*/
|
||||
memberNameProperty: string
|
||||
}
|
||||
|
||||
@@ -42,7 +47,16 @@ export interface CodeBindingErrorClass {
|
||||
* collisions.
|
||||
*/
|
||||
export interface CodeBindingNamespace {
|
||||
/** The global identifier the program sees (must be a valid JS identifier). */
|
||||
/**
|
||||
* The global identifier the program sees. Must match the LANGUAGE-PORTABLE
|
||||
* identifier subset `[A-Za-z_][A-Za-z0-9_]*` and no language's reserved
|
||||
* words, so the same namespace list works against every backend regardless
|
||||
* of `language` — a JS-only spelling like `$tools` is rejected by design,
|
||||
* not just by the Python backend. Names that satisfy the identifier rule but
|
||||
* name a backend-owned slot (`RESERVED_BINDING_GLOBALS`, e.g. `console`,
|
||||
* `__dsh_main__`) are also refused everywhere; see its declaration for the
|
||||
* exact set and why each entry is reserved.
|
||||
*/
|
||||
global: string
|
||||
/** The callable members, keyed by the exact name the program calls. */
|
||||
functions: Record<string, CodeBindingFunction>
|
||||
|
||||
57
packages/code-runtime/code-runtime/tests/reserved.spec.ts
Normal file
57
packages/code-runtime/code-runtime/tests/reserved.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DUNDER_MEMBER,
|
||||
PORTABLE_RESERVED_WORDS,
|
||||
RESERVED_BINDING_GLOBALS,
|
||||
RESERVED_ERROR_MEMBERS,
|
||||
} from '@deepseek-ai/dsh-code-runtime'
|
||||
|
||||
/**
|
||||
* The seam owns the portable-identifier exclusion sets so every backend
|
||||
* enforces one contract: a namespace list valid on one backend is valid on
|
||||
* all. These assertions pin the shared membership backends import rather than
|
||||
* re-declare.
|
||||
*/
|
||||
describe('seam-owned portable identifier exclusions', () => {
|
||||
it('RESERVED_BINDING_GLOBALS covers each backend-owned slot', () => {
|
||||
expect(RESERVED_BINDING_GLOBALS.has('console')).toBe(true)
|
||||
expect(RESERVED_BINDING_GLOBALS.has('__dsh_main__')).toBe(true)
|
||||
expect(RESERVED_BINDING_GLOBALS.has('__builtins__')).toBe(true)
|
||||
expect(RESERVED_BINDING_GLOBALS.has('__name__')).toBe(true)
|
||||
expect(RESERVED_BINDING_GLOBALS.has('__debug__')).toBe(true)
|
||||
expect(RESERVED_BINDING_GLOBALS.has('tools')).toBe(false)
|
||||
})
|
||||
|
||||
it('RESERVED_ERROR_MEMBERS covers the JS Error and Python exception-protocol members', () => {
|
||||
for (const name of ['name', 'message', 'stack', 'args', 'with_traceback', 'add_note']) {
|
||||
expect(RESERVED_ERROR_MEMBERS.has(name)).toBe(true)
|
||||
}
|
||||
expect(RESERVED_ERROR_MEMBERS.has('code')).toBe(false)
|
||||
})
|
||||
|
||||
it('DUNDER_MEMBER matches dunder-form names only', () => {
|
||||
expect(DUNDER_MEMBER.test('__dict__')).toBe(true)
|
||||
expect(DUNDER_MEMBER.test('__init__')).toBe(true)
|
||||
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)
|
||||
// `____` also has an empty middle between the two `__` pairs — not matched.
|
||||
expect(DUNDER_MEMBER.test('____')).toBe(false)
|
||||
// A single character between the pairs is the shortest real dunder form.
|
||||
expect(DUNDER_MEMBER.test('__x__')).toBe(true)
|
||||
})
|
||||
|
||||
it('PORTABLE_RESERVED_WORDS is the union of ECMAScript and Python reserved words', () => {
|
||||
// ECMAScript-only keyword.
|
||||
expect(PORTABLE_RESERVED_WORDS.has('function')).toBe(true)
|
||||
// Python-only keyword — refused here so the list stays portable.
|
||||
expect(PORTABLE_RESERVED_WORDS.has('lambda')).toBe(true)
|
||||
expect(PORTABLE_RESERVED_WORDS.has('nonlocal')).toBe(true)
|
||||
// Shared keyword.
|
||||
expect(PORTABLE_RESERVED_WORDS.has('class')).toBe(true)
|
||||
// Ordinary identifier is not reserved.
|
||||
expect(PORTABLE_RESERVED_WORDS.has('tools')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user