Status: Active Updated: 2026-04-26 Positioning: 从现有实现反推编译器路线、价值排序、硬边界与扩展判据。
Jazor.Compiler 现有文档已经覆盖了较多模块、能力和转换细节,但仍然缺一份专门回答下面这些问题的文档:
- 这个 compiler 真正把什么当成第一等公民?
- 当“完整语义等价”不可能时,优先保什么,允许丢什么?
- 为什么有些特性被擦除、有些特性走协议模拟、有些场景直接失败?
- 新增一个语法或宿主能力时,应该落到哪一层,以及按什么判据设计?
本文件不试图替代功能规格,也不试图成为测试清单。它的目标是记录 Jazor.Compiler 反复出现、且已经由实现与测试共同证明的设计立场。
Jazor.Compiler 不是“把任意 .NET 程序完整编译成 JavaScript”的通用 CLR-to-JS 编译器。
它更接近下面这个定义:
- 在受控输入域内工作;
- 以 Roslyn
IOperation语义为主输入; - 以 ESTree 为核心输出;
- 在声明支持的边界内追求使用点可观察行为等价;
- 允许引入大量 synthetic lowering;
- 但要求宿主语义边界、命名、作用域、导入和调试锚点都保持确定且可解释。
换句话说,Jazor.Compiler 的目标既不是“尽量像 C#”,也不是“尽量像手写 JS”,而是三件事:
- 使用点可观察行为
- 宿主语义边界
- 发射结果确定性
编译器首先要保证的是用户在使用点上能观察到的行为,而不是底层 runtime 结构。
这里的“可观察行为”主要包括:
- 求值顺序
- 副作用次数
- 最终结果值
- 分支路径
- 抛错/异常行为
- 调用前后可见的状态变化
编译器必须明确区分:
- 哪些用户代码允许进入编译域;
- 哪些外部类型和成员是已支持的宿主能力;
- 哪些语义必须通过
WhiteList/Compile显式声明; - 哪些能力不支持,必须直接失败。
边界的清晰度比“多支持一点点”更重要。没有边界,所有 lowering 最终都会被 fallback 污染。
同一份输入应尽量得到稳定、可复现、可比对的输出。
这里的确定性包括:
- lowering 临时名稳定
- import 绑定和 alias 稳定
- 作用域内名字不抖动
- 模块级导入顺序稳定
- sourcemap 锚点可预测
这不是测试附属品,而是 compiler 的产品属性。
“语义等价”在跨语言编译里不是单层概念。对 Jazor.Compiler 来说,应明确区分三层。
这是第一目标,也是必须优先保护的一层。
只要某个 lowering 可能破坏:
- 求值顺序
- 副作用次数
- 最终返回值
- 调用前后可见状态
就必须优先修正,即便代价是多引入若干 temp、SequenceExpression 或 IIFE。
当某个语言特性在当前支持边界内只需要保“可用行为”时,compiler 可以不保其完整 runtime 身份。
例如 tuple:
- 保投影
- 保解构
- 保比较
- 保视图 remap
但不保 System.ValueTuple 的 CLR runtime 身份。
这一层不是 Jazor.Compiler 的默认承诺。
只有当某个 runtime 结构对于目标平台语义同样重要,且保留成本合理时,才值得保留。否则优先考虑擦除或协议模拟。
当“完整复刻”做不到时,保真优先级应按下面排序理解:
- 求值顺序
- 副作用次数
- 最终结果
- 使用点语义
- 运行时结构身份
这条优先级解释了许多现有实现取舍:
- tuple 可以擦除
ref/out可以协议化- conditional access 可以先缓存再 guard
- object initializer 可以包进 IIFE
Jazor.Compiler 不是 best-effort transpiler。
当外部类型、外部成员或某类运行时语义不在支持边界内时,编译器的默认行为应是:
- 明确拒绝
- 给出清晰错误
- 禁止静默退化成“看起来像 JS”的近似实现
特别是下面这类情况,不能回退成 raw JavaScript fallback:
- unsupported external member access
- unsupported runtime host invocation
- unsupported property/indexer mutation path
- unsupported pattern/runtime property probing
支持与否不由一个类型是否“出现过”决定,而由它是否在当前 lowering 点需要真实运行时语义决定。
因此:
List<Unsupported>可以允许Task<Unsupported>可以允许Dictionary<TKey, Unsupported>可以允许Unsupported[]可以允许
但以下使用点应严格失败:
new Unsupported()- 需要 concrete lowering 的
default(Unsupported) - 对
Unsupported的静态/实例成员访问 - 需要 runtime-sensitive materialization 的场景
职责不应描述为“Analyzer 负责所有合法性,SemanticWalker 只负责 lowering”。
更准确的说法是:
Analyzer负责尽早收紧输入域;SemanticWalker负责在具体使用点做最终可落地裁决。
原因很直接:很多“能不能支持”只有在 lowering 现场才知道,因为它依赖:
- 当前语义位置
- 当前目标 AST 形状
- 当前宿主映射是否存在
- 当前是否要进入 runtime-sensitive lowering
适用场景:
- 某个特性本质上可以被更简单的值组合替代;
- 用户真正依赖的是使用点行为,而不是 runtime 类型身份。
典型代表是 tuple。
tuple 在 Jazor 中应视为“可擦除的复合值”:
- 保位置语义
- 保目标视图 remap
- 保解构和比较结果
- 不保 CLR runtime 身份
适用场景:
- 目标平台没有原生等价 runtime 机制;
- 但使用点行为仍然值得保留;
- 可以通过 caller/callee 双端一致协议保持行为闭环。
典型代表:
ref/out- 自定义
Deconstruct
这类 lowering 的重点不是“复刻底层模型”,而是定义一个清晰且内部一致的协议。
适用场景:
- 语义必须保留在表达式位置;
- 但正确实现它需要 statement 级能力;
- 或者需要先缓存再继续组合。
典型手法:
SequenceExpressionConditionalExpression- IIFE
- 局部 temp cache
典型场景:
- conditional access
- switch expression
- pattern matching 分支
- object initializer
- 带回写的调用表达式
适用场景:
- 某个行为本质上不是纯 C# 语法问题,而是“外部 API 如何落到 JS”。
这类问题应优先落到:
AliasInlineImportCompile
而不是散落在普通语义分支里。
除了方法体内的表达式与语句 lowering,Jazor.Compiler 还必须对声明级对象模型做明确裁决。当前最关键的三类是:
- enum
- interface
- class inheritance
- record
record 在当前编译器里应视为“结构化值对象声明”,而不是“语法更短的 class”。
这条路线的约束是:
record创建默认 lower 成对象字面量,而不是new Record(...)with表达式默认 lower 成对象 spread- record 位置模式/属性模式默认按结构属性键匹配
- record 解构赋值默认按结构属性键展开,而不是依赖实例
Deconstruct()runtime protocol - 模块层与成员层都不为
record发射 runtime class 声明 Description/ECMAScriptName之类命名配置只影响结构属性键或编译期名称,不会把record抬升回 nominal class 语义
换句话说,record 保的是:
- 属性 shape
- 位置/属性匹配行为
with的非破坏性更新语义
而不保:
instanceof Record- 模块级/成员级 runtime class 声明
- 默认的 nominal runtime type identity
如果用户需要普通 class 的运行时语义,应显式写 class,而不是依赖“带名字的 record”“带配置特性的 record”或其他隐式升级路径。
这条 record 路线还需要明确三条操作性规则:
-
structural-lowered record 支持
[Spread]语法糖:- 标记在 record 实例属性上时,含义是“把该成员 lower 后的对象形状 flatten 到当前外层对象”;
- 若该成员可直接 lower 为 object literal,则内联展开;
- 若不能直接 lower 为 object literal,则生成标准 JS spread
...expr; - 这是一层通用 record structural sugar,不是 Vue 特例。
-
structural-lowered record 支持静态
null省略优化:- 对主构造参数和 object initializer 成员,只要 Roslyn 能静态证明赋值值为
null,该成员就不生成; - 这条规则的目标是消除结构对象里的无意义字面量垃圾,而不是引入通用运行时 omit-null 协议;
- 因此非静态、非字面量的
null流值仍按普通成员保留。
- 对主构造参数和 object initializer 成员,只要 Roslyn 能静态证明赋值值为
-
ECMAScript.Vue3.VueObject/VueObject<TProps>只是建立在 record structural lowering 之上的 authoring surface:- 编译器不应为它们再开一条 Vue-only lowering 分支;
Props/Attrs/Dataset/Raw的 flatten 行为都应通过通用[Spread]路线获得;VueObject允许直接写字符串索引键,但这仍然应落到同一套 object-literal lowering;- 如果
VueObject所需能力在一般 record 上不成立,应优先增强通用 structural lowering,而不是追加 Vue 特路。
它们不能再被视为“以后再看”的零散实现点,因为三者直接决定模块级输出模型是否自洽。
enum 的路线应定义为:
- 默认不发射独立的 JS runtime declaration;
- 定义只作为编译期值域类型存在;
- 使用点统一降级为底层标量常量或标量表达式;
- enum typed local/field/parameter/return 在运行时都只是标量;
- 不提供 CLR enum runtime 身份;
- 不提供 reverse-map 风格的 JS 枚举对象。
也就是说,推荐模型不是“保一个模块级枚举对象再在运行时引用它”,而是:
- enum declaration -> compile-time only domain declaration
- enum member usage -> scalar literal / scalar expression
- enum typed runtime value -> scalar runtime value
这条路线的好处是:
- 与 tuple 一样,把重点放在使用点行为,而不是 runtime 身份;
- 与比较、赋值、
switch、default(enum)、Flags位运算等高频语义天然一致; - 避免为了少量名字语义去伪造一个其实并不稳定的 runtime enum object;
- 让模块级对象模型更清晰,避免把声明级 artifact 和使用点语义混在一起。
需要额外写清楚的边界:
Flags只是 bitmask 值域,不是特殊 runtime 对象;E.A、显式/隐式到底层数值类型的转换、default(E)、nullable enum 的值语义,都应按底层标量处理;typeof(E)、反射、装箱身份、System.Enum家族 API、按名字取值/格式化等能力,不应被默认视为仍然成立;- 当前
typeof(E)仅允许一个窄例外:当目标类型能稳定对应到 JavaScript 构造器或运行时宿主时,可以把它 lower 成“runtime type token”;这不是完整 CLRSystem.Type反射对象,也不自动为 shaped carrier / structural lowering 类型建立 nominal type identity; - 如果未来确实要支持“按名字看 enum”的能力,应通过显式 host seam 或编译期元数据映射引入,而不是反向恢复一个伪 CLR enum object;
- 对于超出 JS
Number安全整数范围的底层枚举值,编译器不能继续假装“普通 number 就够了”; - 这类场景必须二选一:
- 升级到精确的
BigInt枚举路线; - 或显式拒绝,直到存在稳定实现。
- 升级到精确的
如果当前实现里仍然存在 Object.freeze({...}) 式的枚举声明输出,它只能被视为过渡态,而不是长期路线本身。
interface 的路线应定义为:
- 一律不发射 JS runtime declaration;
- 一律不生成 JS interface object;
- 默认只作为编译期契约存在。
它在 compiler 中仍然可以参与两类事情:
- 约束与合法性判断
- 宿主语义投影与白名单/实现面查找
因此,interface 的正确理解不是“尚未支持输出”,而是:
- 它本来就不是主要输出 artifact;
- 它是 contract / projection seam。
这条路线同时解释了为什么:
- 某些
IEnumerable<T>/IList<T>/ runtime host 接口能参与 lowering; - 但 nested interface declaration 本身仍然不应被输出成 JS 声明。
JavaScript 原生支持 class extends、super(...) 和 prototype dispatch。
因此对于与 JS 语义足够接近的 C# class inheritance,compiler 的目标不应是永久拒绝,而应是支持一个清晰、受控的子集。
当前已经落地、并应视为正式路线一部分的子集是:
- 同一模块成员类之间的单继承;
- declaration emission 必须先基类、后派生类,即使源码书写顺序相反;
extends;: base(args)->super(...);- 派生类未显式声明构造函数时,合成
constructor() { super(); },避免静默擦除基类链; base.Method(...)->super.Method(...);base.Property/base.Property = value->super.Property/super.Property = value;base.Method方法组引用可通过局部 forwarder 保留“调用基类实现”的语义;- override 后的普通实例方法沿 JS prototype dispatch 运行。
当前必须继续显式拒绝的路径是:
base.Field;this(...)构造函数链;- 外部基类;
- 需要独立协议设计的构造函数初始化器能力,例如 named/ref/out 参数传递以及其他非常规参数绑定;
- 任何仍依赖 CLR 运行时身份或 metadata shape 的继承相关语义。
这里要特别记录一个实现约束:
- 只要
basereceiver 还没有独立 lowering,绝不能把它复用成this。
原因是当前实例引用 lowering 默认天然偏向 this 语义。
如果在 base 路径上偷用同一路径,会把“调用基类实现”静默降级成“调用当前覆盖后的实现”,这是错误语义。
同时也要区分成员种类:
- base method / accessor 与 JS prototype 语义相近,可以走
super; - base field 与当前 member-class field lowering 不相近,不能假装
super.Field就等价。
当前 member-class field 的运行时形态是实例自有状态,而不是原型上的 descriptor。
JS 的 super.Field 查找的是基类原型链,不会自然命中这类实例字段。
因此在 base field 还没有单独协议前,显式失败比生成错误 JavaScript 更正确。
需要明确排除或单独设计的内容:
- 多继承
- interface runtime emission
- event 体系
- 依赖 CLR 元数据身份的 abstract/runtime 检查
- 与 JS 无直接对位的可见性或运行时约束
最重要的一条工程规则是:
- 在继承模型没有真正接通前,不能静默擦除继承。
也就是说,如果某个成员类声明了非 object 基类,但当前场景不在上述已支持子集内,就必须直接失败,而不是生成 superClass: null、省略 super(...)、或者把 base 悄悄变成 this 的错误 class shape。
成员类构造函数重载不能照搬普通方法重载路线。
原因不是“签名 hash 不够”,而是 JavaScript class runtime shape 只允许一个真实 constructor。
因此当前正式路线应定义为:
- 运行时始终只发射一个真实
constructor; - 每个 C# 实例构造函数 body 下降为一个稳定命名的 helper method,例如
$ctor_<hash>; - 真实
constructor负责分派,不直接承载多套 body; - Jazor 编译出来的
new C(...)/super(...)在需要重载分派时把已绑定构造函数的$ctor_<hash>作为第一个内部 selector 参数传入; - 真实
constructor只按 selector 分派,不保留arguments.lengthfallback; - optional parameter 的省略值在命中分支后按该 overload 自身的默认值补齐;
- 对派生类,每个分支各自先执行对应的
super(...),再进入 helper body; - helper / dispatcher 的插入位置保持稳定,跟随源码中第一处显式构造函数的位置,不额外把构造函数重排到 class 顶部。
这条路线支持的子集必须收紧为:
- overload 集合按 Roslyn 已绑定的构造函数符号确定目标 helper;
- 同 arity 重载和 optional parameter 区间重叠允许存在,因为选择发生在 C# 绑定阶段;
- 没有 selector 的外部直接
new C(...)不属于成员类构造函数重载协议,运行时应命中“No matching constructor overload”失败路径。
当前必须继续显式拒绝的路径是:
this(...)构造函数链;ref/out/in/params参与的构造函数重载分派;- 命名
base(...)构造函数初始值设定项; - 外部基类上的构造函数协议模拟。
这条设计的重点不是“尽量像手写 JS”,而是:
- 单一 JS class runtime shape 保持合法;
- 调用点 observable 的 overload 选择结果保持确定;
- 默认值补齐、
super(...)调用、body 执行顺序保持稳定; - helper 名称和 class element 顺序保持确定性,不受遍历偶然性影响。
ref/out 在 Jazor.Compiler 中不应被定义为“地址语义复刻”,而应被定义为“调用协议模拟”。
核心原则:
- callee 端负责把返回值和回写值打包;
- caller 端负责按稳定槽位解包并回写;
- 整个调用仍然可以参与外层表达式组合;
- 内部协议可以 synthetic,但必须全链路一致。
这里要保的是:
- 实参求值顺序
ref初值传入out结果回写- 回写发生在调用之后、后续消费之前
不保的是:
- CLR 地址语义本体
- 引用存储模型本体
tuple 在 Jazor.Compiler 中不应被定义为“runtime tuple 模拟”,而应被定义为“值擦除型 lowering”。
核心原则:
- tuple 视图按使用点和目标位置映射;
- tuple projection 可以重构对象形状;
- tuple compare 可以递归展开为逐槽位比较;
- tuple deconstruct 可以拆成赋值序列;
- 复杂源值宁可缓存一次,也不能重复求值。
这里要保的是:
- 位置语义
- 目标字段名落地
- 解构结果
- 比较结果
不保的是:
System.ValueTupleruntime 身份- 反射形态
- CLR 布局与装箱细节
为了正确 lowering,编译器可以并且应该引入大量 synthetic 节点,例如:
- temp identifier
- synthetic catch 参数
- IIFE
- sequence 包装
- packed return array
- tuple/cache helper
虽然 synthetic node 可以很多,但不应引入模糊或偷偷改变用户边界的 synthetic semantics。
允许的事情:
- 为了正确求值而插 temp
- 为了表达式位置而包 IIFE
- 为了
ref/out协议而返回数组
不允许的事情:
- unsupported external member 假装可以普通访问
- unsupported runtime host 假装可以普通调用
- 通过静默 fallback 发明一套看似合理但未经声明的用户可见语义
WhiteList 的本质不是“映射表文本替换”,而是 compiler 与宿主语义之间的正式缝。
匹配应围绕下面这些东西展开:
OriginalDefinition- override / reduced extension 链
- structural generic matching
- API 家族,而非单个偶然的绑定实例
Compile 的地位应高于传统 Alias/Inline/Import。
推荐理解是:
Alias: 简单名字映射Inline: 局部、稳定、可模板化的表达式替换Import: 外部 helper/module 接缝Compile: 需要 AST 级决策、上下文参与、局部协议或复杂结构构造的宿主语义出口
但这里有一条额外边界:Compile 仍然是基础宿主绑定映射的 producer-side 出口,不应被外部库 authoring sugar 滥用。像 Vue H(component, child) 这种建立在公开 overload contract 上的 authoring 便利,应优先落到 compiler 内部 intrinsic / lowering 识别,而不是再让外部库用 [Jazor] / Op.Compile 声明一套语法糖协议。
如果某个宿主能力继续停留在 Inline 会导致:
- AST 结构脆弱
- sourcemap 难以维持
- 调试体验变差
- 规则组合性下降
那么它就应该升级到 Compile。
Jazor.Compiler 不是在保留 C# 原树结构,而是在按 JS 发射规则重建作用域。
关键对象:
SenseArgumentEmissionScopeContextUniqueNameSessionScopeSiteLoweringSite
这些设施共同表达的不是“当前语法在哪”,而是:
- 当前 lowering 属于哪个作用域;
- 当前 temp 应该落在什么边界内;
- 当前名字是否会与其它 lowering 位点冲突。
lowering 名称不应依赖遍历顺序、递增计数器或偶然上下文。
稳定命名应由三部分共同决定:
- lowering owner 的语义身份
- 当前 emission scope
- 当前 lowering site
因此临时名不是“为了测试方便造出来的字符串”,而是 compiler 确定性的一部分。
模块级导入状态应在整个模块转换过程中共享。
这样可以保证:
- 同一外部符号在不同成员中得到同一本地名;
- 名称冲突时 alias 分配稳定;
- import dedupe 和排序稳定;
- 输出不因访问顺序不同而抖动。
Jazor.Compiler 的主原则应是:
- 默认通过
SenseArgument显式传递语义上下文; - 禁止依赖父链做隐式语义控制流;
- 允许少量局部、封闭、纯结构性的父节点查询。
也就是说,这条规则是工程纪律,不是形式洁癖。
允许的例外应满足:
- 只读取局部结构信息;
- 不把父链当作隐式业务规则系统;
- 不把重要语义判定偷偷转移到
operation.Parent链上。
“这个节点来自哪里”不是 emit 末端再猜出来的,而是在 lowering 时显式附着到 AST 节点上。
因此:
- lowering 不只是产出 AST shape;
- lowering 同时产出调试锚点。
sourcemap 的目标不是隐藏 lowering,也不是把 synthetic 节点伪装成原生存在。
它服务的是:
- 源级调试体验
- 断点与观察点对齐
- “这个生成位置应该归属于哪个真实源语义”的判断
因此:
- synthetic node 可以存在;
- synthetic origin 不应污染真实源锚点;
- child origin 应优先于 parent origin;
- 降级后的内部细节不要求被完整还原为源结构。
Optimizer 只能做语义保守的清理,不能承担主语义修复责任。
这意味着:
- 关键行为必须在 lowering 阶段定对;
- optimizer 只能做可证明安全的局部整理;
- 不能指望 optimizer 去补
SemanticWalker的语义债。
换句话说,Optimizer 不是第二套编译器。
主 lowering 路径应坚持直接构造目标 AST。
Parser 可以存在,但只能作为少数受控语法孤岛的工具,例如:
- import declaration/specifier 语法片段
- inline template 的局部实例化
它不应成为主编译策略。否则:
- 语义约束会后移;
- AST 结构控制会减弱;
- deterministic 和 sourcemap 维护成本会上升。
新增一个语法或能力时,应按下面顺序判断:
- 它是在扩展输入域,还是在扩展已有输入域中的 lowering?
- 它是用户代码合法性问题,还是宿主映射问题?
- 它是模块/类型 shape 问题,还是方法体内语义问题?
- 它是否要求真实 runtime 结构,还是可擦除?
- 它是否需要 caller/callee 协议模拟?
- 它是否必须保持表达式位置?
- 它是否可能导致重复求值或副作用次数变化?
- 它是否应该进入
Compile,而不是继续停留在Inline? - 它是否会破坏稳定命名、稳定导入或 sourcemap 锚点?
- 如果不支持,是否应该明确失败,而不是静默 fallback?
根据回答落点应大致是:
- 输入域与前置约束:
Analyzer - 宿主语义声明:
WhiteList/Compile - 模块级结构:
AstConverter - 方法体语义:
SemanticWalker - 后处理的小幅清理:
Optimizer
在提交任何 compiler 语义改动前,至少应追问下面这些问题:
- 这个改动保住了求值顺序吗?
- 这个改动会增加或减少副作用次数吗?
- 这个改动是否引入了新的隐式 fallback?
- 这个改动是否把应失败的场景错误地放宽了?
- 这个改动是否把应允许的 erased 场景错误地收紧了?
- 这个改动是否需要稳定 temp,但目前名字依赖遍历顺序?
- 这个改动是否把复杂宿主语义错误地塞进了
Inline? - 这个改动是否破坏了 import alias 的模块级一致性?
- 这个改动是否插入了 synthetic node,但没有处理 source-origin?
- 这个改动是否把应由 lowering 负责的工作推给了 optimizer?
- 这个改动是否又把 enum 推回成 runtime declaration/object,而不是编译期值域类型?
- 这个改动是否把 interface 错误地推成了 runtime artifact?
- 这个改动是否在继承未打通时静默擦除了
extends/super语义?
下列目标不应被误认为当前 compiler 的核心追求:
- 完整 CLR runtime 模拟
- 任意 C# 语法的无条件支持
- 最少 AST 节点数
- 看起来最像人手写的 JavaScript
- 通过 parser 驱动主转换流程
- 通过静默 fallback 提升表面兼容率
Jazor.Compiler 的核心路线可以压缩成一句话:
在受控输入域内,优先保证使用点可观察行为、宿主语义边界和发射确定性;当完整 runtime 结构不值得保留时,允许擦除或协议模拟,但不允许静默伪造用户可见语义。