DSH / Atlas
2026-07-31implementedfeature

Code Mode language dispatch and the Python SDK renderer

Code Mode 语言分发与 Python SDK 渲染器

Code Mode generated one SDK flavor: TypeScript. `ToolRuntime` hard-coded `renderToolsSdk` for the `tools:sdk` section and `requireCodeRuntime` rejected any `ctx.codeRuntime.language !== 'typescript'`. Adding a CPython backend means a program's source language is no longer fixed: the same visible tool registry must project a Python SDK when a Python runtime is loaded, and the model-facing `run_code` schema strings ("E

English

Problem

Code Mode generated one SDK flavor: TypeScript. ToolRuntime hard-coded renderToolsSdk for the tools:sdk section and requireCodeRuntime rejected any ctx.codeRuntime.language !== 'typescript'. Adding a CPython backend means a program's source language is no longer fixed: the same visible tool registry must project a Python SDK when a Python runtime is loaded, and the model-facing run_code schema strings ("Execute a Python program …") must match the SDK section's language so the model never sees a TypeScript instruction over a Python runtime.

This is the tool-facing half of the multi-language Code Mode split; the code-runtime seam already carries CodeRuntime.language. This note owns only how dsh-tools dispatches on that field. The backend that implements language: 'python' is owned by its own note, delivered separately.

Decision

Language selection is a lookup on ctx.codeRuntime.language, resolved lazily at prompt assembly, against two parallel tables in dsh-tools:

  • SDK_RENDERERS (index.ts) maps a language to its tools:sdk renderer — typescript → renderToolsSdk, python → renderToolsSdkPy. The tools:sdk section reads the loaded runtime's language and picks the renderer; requireCodeRuntime rejects a mode: code/both runtime whose language is absent from the table, naming the known languages.
  • RUN_CODE_FLAVORS (code-mode.ts) maps a language to its two model-facing run_code strings (tool description and the code parameter description), so a language's SDK section and its transport schema always agree.

Both tables are read with Object.hasOwn before use so a language named toString/constructor cannot resolve an inherited Object.prototype member as a renderer. The two guards differ in reachability: SDK_RENDERERS' in-callback guard is unreachable because requireCodeRuntime validated the same const table earlier in the same callback (it carries a /* v8 ignore */), while RUN_CODE_FLAVORS' guard is the primary, publicly reachable rejection — any language absent from the flavor table hits it through run_code's language-aware getters, which the public schemas() reaches without passing requireCodeRuntime first; the test reads one of those getters off the definition directly, under a language absent from both tables. A language present in SDK_RENDERERS but not RUN_CODE_FLAVORS is drift the shared CodeSdkLanguage satisfies pins reject at typecheck, so it is not an input either guard can see; what the guards still own is a mounted runtime reporting a language absent from both tables. Schema emission reads the runtime through peekRuntime() rather than requireRuntime(): undefined (no runtime mounted, reached by definition readers and schemas(), of which the doc-catalog harvest is the only shipped one and none of which feeds a model because assembly passes requireCodeRuntime first) degrades to the TypeScript flavor, whereas a mounted unknown language fails loud — this is NOT the silent fallback rejected below, which concerns emitting a wrong-language SDK for a real runtime. Adding a backend language is three parallel edits — a CodeSdkLanguage member and the two table entries — plus its renderer and the prose that names the well-known values instead of deriving them (the seam's dsh-code-runtime README pair, its CodeRuntime.language JSDoc, and the docs/subsystems/code-runtime.md pair; this package's own README pair and its Config.mode JSDoc — no gate checks any of it), with no agent-loop or registry-structure change.

code-mode.ts depends only on the runtime Service Definition (@deepseek-ai/dsh-code-runtime), never on a concrete backend; dispatch is by runtime.language at run time. The tool layer is therefore independent of the Python protocol and backend — it needs only the service's language field.

The Python SDK renderer

py-types.ts renders the same unified tool-schema vocabulary jsonSchemaToTs covers, targeting Python: jsonSchemaToPy emits a type expression per JSON-schema node, and renderToolsSdkPy assembles named TypedDicts for each visible tool's arguments and canonical output plus a tools object with usage instructions equivalent to the TypeScript flavor. Unsupported raw constructs degrade rather than throwing during assembly, matching the TypeScript renderer's contract. The output is deterministic — lexicographic tool order, byte-identical text for an unchanged tool set — so the prompt stays prefix-cache-friendly. Lexicographic means one ordered member stream: a tool whose name is not a legal attribute is listed as a tools[name] comment in its sorted position rather than partitioned to the end, matching how the TypeScript flavor quotes an exotic key in place. That stream forces one thing directly — comment lines are not statements, so a tool set that emits no method at all still needs an explicit pass. Three further rules are Python-specific rather than consequences of the ordering. The usage contract states that the declarations are static stubs and arguments are plain dict/list values: a TypedDict reads as a constructible class, so a model that writes FooArgs(field=1) gets a NameError — TypeScript's interface is visibly a type, and the TS flavor's "runs type-stripped" clause already covers it. A description becomes the method's docstring emitted as the FIRST statement of its body: above the async def the first one would document the Tools class and the rest would be dead expressions, leaving every method undocumented. And a list[…] chain degrades to Any past MAX_LIST_NESTING, because CPython's tokenizer rejects a line with more than 200 open brackets and the block must stay parseable Python — the same reason docLines escapes quotes and backslashes. ts-types needs neither: TypeScript attaches a leading /** … */ to the member that follows it and bounds nesting nowhere in its grammar.

The standard that cap serves is grammatical validity, and the boundary is deliberate: a long A | B | … union is valid Python at any length and is left uncapped, even though CPython's compile() exhausts its C recursion walking the left-nested BinOp spine (measured on 3.9: 1,000 branches compile, 5,000 raise RecursionError). Nothing compiles this block — it is prompt text — so that limit costs nothing, whereas capping union length would retire the deep-chain tests that pin the walk's linear time and the class-name propagation cap. A future renderer that does need compilable output should flatten unions rather than truncate them.

renderType validates the whole schema once (assertSupportedJsonSchema) and then trusts it, wrapping the walk in one try/catch that degrades to Any — the same trusted-after-validation stance the sibling ts-types renderer takes at this typed same-process boundary (Trust TypeScript at typed same-process boundaries). It deliberately carries NO defenses against a schema whose accessors mutate between reads (post-validation cycles, TOCTOU on const/enum, self-referential functions): the input is a first-party registration (a defineTool literal or a raw registration) or a wire-derived plain JSON schema — the former is trusted per AGENTS.md, the latter is a JSON.parse product that physically cannot carry accessors, and renderType re-validates the whole tree on every call regardless — so such inputs are unreachable, and adding per-shape guards here would break symmetry with ts-types (which has none) for values the static interface forbids. jsonSchemaToPy(schema: unknown) accepts unknown and returns Any on a malformed schema — the Python counterpart of the TS flavor's unknown — but its contract is "degrade an unsupported schema", not "survive an adversarial mutating one".

Alternatives considered

  • A language config field on ToolRuntime. Deployment would then have two places to name the language (the loaded runtime and the tools config) that can disagree; the loaded runtime is the single source of truth, so the registry reads it rather than duplicating it.
  • Importing the Python backend into code-mode.ts to detect it. That would couple the tool layer to a concrete backend and force the protocol/backend PRs to land first. Runtime dispatch on language keeps the layer backend-agnostic and independently shippable.
  • A default renderer for an unknown language. A silent fallback would emit a TypeScript SDK over, e.g., a Ruby runtime — the model would see instructions in the wrong language. Failing loud at assembly is the repository's misconfiguration stance.

Consequences

Adding a backend language is three parallel edits — a CodeSdkLanguage member, an SDK_RENDERERS entry, and a RUN_CODE_FLAVORS entry — plus the renderer function the second points at, with no change to agent-loop or the registry structure. The two tables (SDK_RENDERERS, RUN_CODE_FLAVORS) must stay in step, and that invariant is checked statically rather than left to review: both are satisfies-checked against that one union, so a language added to one and not the other fails typecheck. This is the mechanical form the drift risk deserves — the runtime Object.hasOwn guards would catch it too, but only once a backend reporting that language ships: at the Consumer's integration point rather than where the drift was introduced — and never while no second backend exists. The tables keep their Record<string, …> declared type because CodeRuntime.language is an unconstrained string; the union pins what the harness ships, the guards reject what a runtime reports. What stays outside that check is the prose that names the well-known values instead of deriving them: dsh-code-runtime's README pair, its CodeRuntime.language JSDoc, and the docs/subsystems/code-runtime.md pair at the seam, plus this package's own README pair and its Config.mode JSDoc. Earlier notes name the values as they stood at the time and are not on that list. Two separate reasons keep it ungated. Prose is not type-checked at all, wherever the union lives. And no type-level pin can stand in for it here: the Service Definition package must not import its Consumer's table, and CodeRuntime.language stays an unconstrained string by design, so moving the union into the Service Definition would not apply it either. A unit test pinning the two key sets equal was rejected in favor of this: it would buy the same check at the cost of a test-only export of two private tables, and would run later than the compiler does. Which of the two runtime failures surfaces depends on the entry point, for a language absent from both tables: assembly reports the missing renderer, because wireSchemas calls requireCodeRuntime before projecting, while the public schemas() reaches run_code's language-aware getters first and reports the missing flavor. The tool layer stays free of any concrete backend dependency, so it ships and is testable ahead of the Python protocol and backend.

The cost is that the Python branch of both tables is unreachable in the shipped tree: CodeRuntime.language is set by the loaded backend, the only published backend is dsh-code-runtime-worker-thread ('typescript'), and the registry reads the loaded runtime rather than a config field, so no assembled application can select renderToolsSdkPy or PYTHON_FLAVOR. The model-visible surface is therefore unchanged by this note's work until a backend reporting 'python' is published, and this change's coverage is unit-level — the renderer output plus the dispatch and rejection paths. The keyless snapshot for the Python model interface belongs to the change that publishes that backend, because only there does a real cordis.yml over published plugins produce a Python assembly; a snapshot example that mounted a fixture runtime here would assert against a test double, which docs/testing.md rejects as a substitute for the assembled application transcript.

Two runtime contracts the Python SDK text asserts are owed by that same backend PR. First, the instructions tell the model that exactly tools and ToolCallError are bound and that the declared TypedDict classes are not, so the backend must inject those two names — with ToolCallError.toolName populated per the seam's errorClass contract — and must NOT bind the declared class names into the program's globals; injecting them "helpfully" would make the SDK text false. Second, the language has to be bound to the request: requireCodeRuntime resolves ctx.codeRuntime separately at assembly and at run_code execution, so a reload that swapped the runtime between those two points would hand a program written against one flavor to the other. The split is finer than those two points — run_code's description and parameters getters each call resolveFlavor(peekRuntime()), and schemaOf destructures both, so one projection reads the runtime twice; both reads are for run_code's own schema, since the getters are installed on that one definition and every other definition carries plain data properties. A reload between those two reads yields a single schema whose two halves name different languages. Neither is reachable here — one published backend means both reads return the same flavor and no program ever runs against this renderer's output — and the cross-language rejection is not testable until a second language exists.

Third, that PR owns the CPython floor, and with it the renderer's Unicode-table skew. Four expressions read the running engine's tables (Node 22.23.1: Unicode 17.0) while the interpreter uses its own (CPython 3.9.6: 13.0.0): isBareIdentifier's IDENTIFIER, and camelCase's split set, head test, and toUpperCase(). An interpreter older than the engine is the failing direction — the engine emits a character its tokenizer refuses, taking the whole block down — and it arrives by three independent paths. Through the predicate, a bare method or field name carrying a character added between the two versions — to XID_Start at its head, or to XID_Continue in any tail position, the middle of a name included. Through camelCase's XID reads, a class name, which reaches emitted text whenever any object shape in the tool's schema declares a TypedDict, and which the predicate's verdict on the tool name does not gate: zz- plus U+1E4D0 never reaches the predicate's skew, since the - rejects it outright, yet it still declares class Zz𞓐xArgs. Through the case mapping, a class name derived from a tool the predicate accepted — a different table and a wider window than XID membership: U+019B is XID_Start and NFKC-stable, so async def ƛ compiles on 3.9.6, but Node uppercases it to U+A7DC (unassigned there; CPython's own .upper() is the identity) and class ꟜArgs fails with invalid non-printable character U+A7DC. The exposure window is the characters and mappings that changed between the two versions, so the PR that names a supported CPython range must decide explicitly between accepting it and pinning all four read points to tables for that floor — pinning the predicate alone leaves both class-name paths open. Nothing here can decide it: the floor does not exist yet, and a table pinned to a guess would be a deployment-varying constant with no configurability behind it. A second axis rides along with the floor and is not one of the four: the names and syntax the block would evaluate at definition time. TypedDict needs 3.8, the PEP 585 builtin generics dict[str, Any] and list[…] need 3.9, an A | B annotation 3.10, and NotRequired 3.11. These are not parse failures — the block parses on any version, which is the standard the MAX_LIST_NESTING cap serves — but definition-time evaluation failures, and nothing in the product evaluates this text. Recording them with the read points keeps "parseable on the supported range" from being read as "executable on it".

中文

问题

Code Mode 只生成一种 SDK 形态:TypeScript。ToolRuntimetools:sdk 段硬编码了 renderToolsSdk,且 requireCodeRuntime 会拒绝任何 ctx.codeRuntime.language !== 'typescript'。引入 CPython 后端后,程序的源语言不再固定:同一个可见工具注册表在加载 Python 运行时时必须投射出 Python SDK,而面向模型的 run_code schema 字符串("Execute a Python program …")也必须与 SDK 段的语言一致,模型才不会在 Python 运行时下看到 TypeScript 指令。

这是多语言 Code Mode 拆分中面向工具的那一半;代码运行时 seam 已经携带 CodeRuntime.language。本 Note 只负责 dsh-tools 如何在该字段上分发。实现 language: 'python' 的后端由它自己的 Note 负责,单独交付。

决策

语言选择就是对 ctx.codeRuntime.language 的查表,在提示词装配时惰性解析,查 dsh-tools 里两张平行的表:

  • SDK_RENDERERS(index.ts)把语言映射到它的 tools:sdk 渲染器——typescript → renderToolsSdkpython → renderToolsSdkPytools:sdk 段读取所加载运行时的语言并选出渲染器;requireCodeRuntime 拒绝其语言不在表中的 mode: code/both 运行时,并列出已知语言。
  • RUN_CODE_FLAVORS(code-mode.ts)把语言映射到它那两条面向模型的 run_code 字符串(工具 descriptioncode 参数描述),使一种语言的 SDK 段与它的传输 schema 始终一致。

两张表在使用前都以 Object.hasOwn 读取,这样名为 toString/constructor 的语言不会把继承自 Object.prototype 的成员解析成渲染器。两个守卫的可达性不同:SDK_RENDERERS 的回调内守卫不可达,因为 requireCodeRuntime 已在同一回调更早处校验过同一张 const 表(它带 /* v8 ignore */);而 RUN_CODE_FLAVORS 的守卫是主要的、可公开到达的拒绝路径——任何缺席 flavor 表的语言都经 run_code 的语言感知 getter 到达它,而公共 schemas() 抵达那些 getter 时并未先过 requireCodeRuntime;测试直读 definition 上的其中一个 getter,用的是对两张表都缺席的语言。「在 SDK_RENDERERS 里却不在 RUN_CODE_FLAVORS 里」这种漂移已由共享的 CodeSdkLanguage satisfiestypecheck 处拒绝,两个守卫都看不到这种输入;它们如今负责的是所挂载运行时报告了一门两张表都缺席的语言。schema 发射通过 peekRuntime() 而非 requireRuntime() 读取运行时:undefined(无运行时,由直读 definition 的读者与 schemas() 到达,其中 doc-catalog 采集是唯一已交付的一个,而它们都不会喂给模型,因为组装路径先过 requireCodeRuntime)降级到 TypeScript flavor,而挂载了未知语言则 fail loud——这不是下方被否决的静默回退,那指的是为真实运行时发出错误语言的 SDK。新增一门后端语言是三处并列编辑——一个 CodeSdkLanguage 成员加两条表项——再加它的渲染器,以及点名已知值而非从中派生的散文(seam 侧的 dsh-code-runtime README 双语对、它的 CodeRuntime.language JSDoc 与 docs/subsystems/code-runtime.md 双语对;本包自己的 README 双语对与它的 Config.mode JSDoc,无任何 gate 检查其中任何一处),不动 agent-loop,也不动注册表结构。

code-mode.ts 只依赖运行时 Service Definition(@deepseek-ai/dsh-code-runtime),绝不依赖具体后端;分发在运行时按 runtime.language 进行。因此工具层独立于 Python 协议和后端——它只需要服务的 language 字段。

Python SDK 渲染器

py-types.ts 渲染 jsonSchemaToTs 所覆盖的同一套统一工具 schema 词汇,目标为 Python:jsonSchemaToPy 为每个 JSON-schema 节点发出一个类型表达式,renderToolsSdkPy 为每个可见工具的参数与规范输出装配具名 TypedDict,再加一个带用法说明的 tools 对象,与 TypeScript 形态等价。不支持的原始构造在装配时降级而非抛错,与 TypeScript 渲染器的约定一致。输出是确定性的——工具按字典序排列,工具集不变时文本逐字节相同——因此提示词保持 prefix-cache 友好。字典序意味着单一有序的成员流:名字不是合法属性的工具以 tools[name] 注释出现在它排序后的位置上,而不是被分拣到末尾,与 TypeScript 形态就地为异常键加引号的做法一致。这个成员流直接决定了一件事:注释行不是语句,所以一个不发出任何方法的工具集仍需显式 pass。另有三条规则并非源自排序,而是 Python 特有。其一,用法约定声明这些声明只是静态存根、参数为普通 dict/list 值:TypedDict 读起来像一个可构造的类,模型若写 FooArgs(field=1) 会得到 NameError——TypeScript 的 interface 一眼就是类型,且 TS 形态的「runs type-stripped」一句已经覆盖了它。其二,描述会成为方法的 docstring,且必须作为方法体的第一条语句发出:放在 async def 之上,第一条会变成 Tools 的类文档、其余都是无效果表达式,导致每个方法都没有文档。其三,list[…] 链超过 MAX_LIST_NESTING 后降级为 Any,因为 CPython 的 tokenizer 拒绝一行中超过 200 个同时未闭合的括号,而这个块必须是可解析的 Python——与 docLines 转义引号和反斜杠是同一个理由。ts-types 两者都不需要:TypeScript 会把前置的 /** … */ 附着到其后的成员上,其语法也不对嵌套设限。

该上限服务的标准是语法合法性,这条边界是有意划定的:长的 A | B | … union 在任何长度下都是合法 Python,故不设上限——尽管 CPython 的 compile() 在沿左嵌套 BinOp 脊柱下降时会耗尽 C 递归(在 3.9 上实测:1,000 个分支可编译,5,000 个抛 RecursionError)。没有任何东西会编译这个块——它是提示词文本——所以那条限制在这里没有代价;而给 union 长度封顶会作废那几个钉住 walk 线性时间与类名传播上限的深链测试。将来若有渲染器确实需要可编译的输出,应当把 union 拍平,而不是截断。

renderType 先用 assertSupportedJsonSchema 整树校验一次、随后信任它,用单个 try/catch 把整个遍历兜住并降级为 Any——与姊妹渲染器 ts-types 在这个 typed 同进程边界上采取的「校验后信任」姿态一致(Trust TypeScript at typed same-process boundaries)。它有意不设任何针对「访问器在多次读取间变值」的防御(校验后成环、const/enum 的 TOCTOU、自引用函数):输入是第一方注册(defineTool 字面量或 raw 注册)或从 wire 桥接而来的纯 JSON schema——前者按 AGENTS.md 受信任,后者是 JSON.parse 产物、物理上不可能携带访问器,且每次调用 renderType 都会整树重新校验——这类输入不可达,而在此加逐形态守卫会为静态接口所禁止的值破坏与 ts-types(没有这类守卫)的对称。jsonSchemaToPy(schema: unknown) 接受 unknown 并对畸形 schema 返回 Any——TypeScript 形态 unknown 的对应物——但它的约定是「降级不支持的 schema」,而非「扛住对抗性的可变 schema」。

考虑过的替代方案

  • ToolRuntime 上加一个 language 配置字段。 那样部署方就会有两处命名语言(所加载的运行时与 tools 配置)且可能相互矛盾;所加载的运行时是唯一真源,故注册表读取它而不复制它。
  • 把 Python 后端 import 进 code-mode.ts 来检测它。 那会把工具层耦合到具体后端,并迫使协议/后端 PR(Pull Request)先落地。按 language 运行时分发使该层保持后端无关、可独立发布。
  • 为未知语言提供默认渲染器。 静默回退会在比如 Ruby 运行时上发出 TypeScript SDK——模型会看到错误语言的指令。在装配处 fail loud 是本仓库对错误配置的立场。

后果

新增一门后端语言是三处并列编辑——一个 CodeSdkLanguage 成员、一个 SDK_RENDERERS 表项、一个 RUN_CODE_FLAVORS 表项——再加第二处所指向的渲染器函数,不动 agent-loop,也不动注册表结构。两张表(SDK_RENDERERSRUN_CODE_FLAVORS)必须同步,且这条不变式由静态检查把关,而非交给 review:两张表都以 satisfies 对上述同一个 union 校验,因此只加其一而漏掉另一会在 typecheck 处失败。这正是该漂移风险应有的机械形式——运行时的 Object.hasOwn 守卫同样能捕获,但要等到有后端报告该语言之后:触发点在消费方的集成处而非漂移引入处——而只要不存在第二个后端,就永远不会触发。两张表的声明类型仍是 Record<string, …>,因为 CodeRuntime.language 是不受约束的 string:union 钉住 harness 交付了什么,守卫拒绝运行时报告了什么。落在这条检查之外的是点名已知值而非从中派生的散文:seam 侧的 dsh-code-runtime README 双语对、它的 CodeRuntime.language JSDoc 与 docs/subsystems/code-runtime.md 双语对,再加本包自己的 README 双语对与它的 Config.mode JSDoc。更早的 note 点名这些值时记的是当时的状态,不在此列。让它无 gate 的是两条独立理由。其一,散文根本不受类型检查,union 放在哪里都一样。其二,类型级替代在这里也不可用:Service Definition 包不得 import 其消费方的表,而 CodeRuntime.language 按设计保持不受约束的 string,即便把 union 迁进 Service Definition 也不会作用到它。用一个断言两张表键集相等的 unit test 的方案被否决:它买到的是同一条检查,代价却是把两张私有表做测试专用导出,且运行时机晚于编译器。对两张表都缺席的语言,两种运行时失败中报出哪一条随入口而异:组装路径报缺渲染器,因为 wireSchemas 在投影前先调 requireCodeRuntime;而公共 schemas() 先经过 run_code 的语言感知 getter,报的是缺 flavor 表项。工具层不依赖任何具体后端,因此它能先于 Python 协议和后端交付并可测。

代价是两张表的 Python 分支在已交付的代码树中不可达:CodeRuntime.language 由所加载的后端设定,已发布的后端只有 dsh-code-runtime-worker-thread'typescript'),而注册表读取的是所加载的运行时而非某个配置字段,因此没有任何一份组装好的应用能选中 renderToolsSdkPyPYTHON_FLAVOR。也就是说,在报告 'python' 的后端发布之前,本 note 的工作不改变模型可见表面,本变更的覆盖因此是 unit 级——渲染器输出加分发与拒绝路径。Python 模型界面的 keyless 快照归属于发布该后端的那个变更,因为只有在那里,一份基于已发布插件的真实 cordis.yml 才会产出 Python 组装;在此处挂载 fixture(测试前置数据)运行时的快照示例断言的是测试替身,而 docs/testing.md 明确拒绝以此替代组装好的应用 transcript(文本记录)。

Python SDK 文本断言的两条运行时约定同样归属那个后端 PR。其一,说明文字告诉模型运行时恰好绑定 toolsToolCallError 两个名字、所声明的 TypedDict 类不绑定,因此后端必须注入这两个名字(并按 seam 的 errorClass 约定填充 ToolCallError.toolName),且不得把所声明的类名绑进程序全局——「好心」注入会使这段 SDK 文本变成假话。其二,语言必须绑定到请求上:requireCodeRuntime 在组装时与 run_code 执行时分别解析 ctx.codeRuntime,若在这两点之间发生重载并换掉运行时,就会把针对一种形态写成的程序交给另一种形态执行。分裂比这两点更细——run_codedescriptionparameters 两个 getter 各自调用 resolveFlavor(peekRuntime()),而 schemaOf 会解构这两个字段,因此一次投影读两次运行时;两次都属于 run_code 自己的 schema,因为这两个 getter 只装在那一个 definition 上,其余 definition 携带的都是普通数据属性。在这两次读取之间重载会产出单个 schema 的两半分属不同语言。两者在此处都不可达——只有一个已发布后端意味着两次读取返回同一形态,且没有任何程序会针对本渲染器的输出运行——而跨语言拒绝在第二门语言存在之前也无法测试。

其三,那个 PR 拥有 CPython 版本下限,连带拥有本渲染器的 Unicode 表偏斜。有四处表达式读所运行引擎的表(Node 22.23.1:Unicode 17.0),而解释器用它自己的表(CPython 3.9.6:13.0.0):isBareIdentifierIDENTIFIER,以及 camelCase 的切分集、头部测试与 toUpperCase()。解释器旧于引擎是会失败的那个方向——引擎发出的字符被其 tokenizer 拒收,整个块随之不可解析——而它经三条独立路径抵达。经判据抵达的是不加引号发出的方法名或字段名,其中带有一个在两个版本之间新增的字符——首位加进 XID_Start,或尾部任意位置(含名字中部)加进 XID_Continue。经 camelCase 的 XID 读取抵达的是类名:只要工具 schema 中有任一对象形态声明 TypedDict,该类名就进入发出的文本,且判据对工具名的裁决并不对它设闸——工具名 zz- 加 U+1E4D0 因 - 被判据直接拒绝、从不触及那里的偏斜,却照样声明 class Zz𞓐xArgs。经大写映射抵达的是由判据已接受的工具派生出的类名——这是另一张表,窗口也比 XID 归属更宽:U+019B 既是 XID_Start 又 NFKC 稳定,故 async def ƛ 在 3.9.6 上可编译,但 Node 将其大写为 U+A7DC(在那里未分配;CPython 自己的 .upper() 在此是恒等),于是 class ꟜArgsinvalid non-printable character U+A7DC 失败。暴露窗口是两个版本之间发生变化的那些字符与映射,所以宣布支持某个 CPython 范围的那个 PR 必须在「接受该暴露」与「按该下限的表钉住全部四个读取点」之间显式作出决定——只钉判据会同时留下两条类名路径。此处无法决定:下限尚不存在,而按猜测钉死一张表会成为一个随部署而变、却没有可配置性支撑的常量。还有第二条轴随该下限一同确定,且不属于那四个读取点:本块在定义期会被求值的那些名字与语法。TypedDict 需要 3.8,PEP 585 的内建泛型 dict[str, Any]list[…] 需要 3.9,A | B 形式的注解需要 3.10,NotRequired 需要 3.11。这些不是解析失败——本块在任何版本上都能解析,这正是 MAX_LIST_NESTING 上限所服务的标准——而是定义期求值失败,且产品中没有任何东西会求值这段文本。把它们与那四个读取点记在一起,可避免把「在所支持范围上可解析」读成「在其上可执行」。