Jazor 的 WhiteList:编译器里的四层 API 映射系统
Jazor 的 WhiteList:编译器里的四层 API 映射系统
Alias、Inline、Import、Compile——不是你说"这个方法叫
Math.abs"就完了,编译器要知道怎么把调用点变成正确的 JavaScript。
一、为什么需要一个白名单?
Jazor 的任务是把 C# 编译成 JavaScript。但 C# 代码天然依赖 BCL——string.IsNullOrEmpty、List<T>.Add、Console.WriteLine,这些方法在 .NET 运行时里都有完整实现。Jazor 不包含 .NET 运行时,输出的是零依赖的纯 JavaScript。
所以 Jazor 面临一个根本问题:当编译器遇到一个不属于当前模块的方法调用时,它怎么知道这个方法在 JavaScript 里对应什么?
答案是一个叫 WhiteList 的映射表。它记录了每一个"外部 API"在 JavaScript 端的长相——方法名、所在的宿主对象、参数怎么传递。
二、四层映射策略:从简单到复杂
WhiteList 里的每个条目都标记了它属于哪一层映射。消费端的分派顺序是:
Compile → Alias → Inline → Import → 常规 lowering
每一层解决一个问题类型。下面逐一展开。
Alias:纯名字替换
最简单的映射。C# 端叫 A,JavaScript 端叫 B。没有任何逻辑变化。
// WhiteList 声明(在 ECMAScript.Contract 中):
[Jazor(Op.Alias, "")]
string.Empty // → ""
Alias 适合的场景:常量映射、简单名称转换。string.Empty → "" 是典型。Math.PI → Math.PI 也是一样——后端已经知道宿主是 Math(通过 extension(Math)),这里只需要确认名字。
Alias 的语义是:生成代码时,用目标名字替换源名字。没有参数处理,没有逻辑注入。
Inline:可内联的表达式模板
当映射不只是换名字,而是需要参数参与时,Alias 不够用了。Inline 允许定义一个 JavaScript 表达式模板,把调用点的参数填入模板。
// WhiteList:
[Jazor(Op.Inline, "($0 == null || $0.length === 0)")]
bool string.IsNullOrEmpty(string value)
// 调用端 string.IsNullOrEmpty(s) → (s == null || s.length === 0)
模板里的 $0 指代第一个参数,$1 指代第二个,以此类推。
Inline 有两个重要限制:
- 模板必须能放进一个表达式位置。 不能包含语句(
if、for、return),只能是表达式。 - 模板必须简单。 如果模板开始膨胀——多行、嵌套、局部变量——就该考虑升级到 Import 或 Compile。
Jazor 的文档里有一条明确的迁移线:Inline 模板应该短且可读。如果需要非平凡控制流或跨模块共享,就应该升级。
Import:跨模块的 helper 方法
当逻辑复杂到不适合内联时,就轮到 Import。Import 会在编译输出中引入一个 import 语句,从指定的运行时模块引入一个 helper 函数。
// Jazor.CLR 声明:
[Jazor(Op.Import, "defaultRuntime", "contains")]
bool List<T>.Contains(T item)
编译结果:
import { contains } from "./defaultRuntime.mjs";
// 调用端:
list.contains(item);
Import 适合的场景:
- 逻辑涉及控制流(if/else、循环)
- 需要在多处复用的通用逻辑
- 和 JavaScript 原生行为的语义差异需要额外处理
Import 和 Inline 之间的选择还有一个重要原则:同一个 API 的所有调用点必须走同一层映射。 不能在模块 A 里对 List<T>.Contains 走 Inline、在模块 B 里走 Import——输出一致性是编译器的一个硬要求。
Compile:AST 级的宿主语义
最高一层的映射——Compile。它完全接管了某个 API 的代码生成,不是做模板替换,而是直接用 C# 代码构造目标 AST 节点。
Compile 的注册签名是一个委托:
Func<ISymbol, SenseArgument, Expression?, Expression?[], IOperation?, Expression?>
参数分别是:调用的符号、语义上下文、实例表达式(静态调用的为 null)、参数表达式数组、原始 IOperation。
Compile 适合的场景:
- 需要根据上下文做不同处理的调用(如
base.Method()→super.method()) - 需要生成复杂 AST 结构的方法(如构造函数重载分派)
- 需要在生成的代码中插入额外的语义检查(如类型转换、空值保护)
一个典型的 Compile 场景是 base.Property = value 的 lowering。C# 里访问基类属性用的是 base.Property,但 JS 里要写 super.Property = value。这不是简单的名字替换——编译器需要在 lowering 时识别调用上下文,构造不同的 AST 节点。Inline 做不到这一点,Compile 可以。
什么情况下升级到 Compile
ImplementationPrinciples.md 里有一条清晰的升级判据:
如果某个宿主能力继续停留在
Inline会导致 AST 结构脆弱、sourcemap 难以维持、调试体验变差、规则组合性下降,那么它就应该升级到Compile。
Compile 不是"Inline 不够用了的兜底",它是一种等价的、更结构化的宿主语义表达方式。升级的标准不是"模板太长",而是"AST 层面需要上下文信息来做出正确的决策"。
三、白名单的生成流程:从 Attribute 到代码
白名单不是手写的。Jazor 有一个代码生成器(Jazor.Compiler.Generator)负责从 Attribute 标注中自动产生 WhiteList。
流程是:
- 开发者用
[Jazor(Op.*, ...)]标注 CLR 运行时模块中的方法 Jazor.Compiler.Generator扫描这些 Attribute,生成WhiteList.cs.Generate.csWhiteList.cs.Generate.cs被编译进Jazor.Compiler,在运行时提供查找服务
生成器有一条关键的合约规则:白名单的 key 必须稳定。 如果从 Roslyn Symbol 推导 key,使用 symbol.OriginalDefinition.ToDisplayString(Format.NameFormat) 作为规范形式。不搞额外的规范化(不剥离 extern、不改写修饰符)——key 是什么就是什么。
这个"key 稳定"规则的工程意义是:白名单是自动生成的产出物。如果每次重新生成后 key 都变了,所有的查找都会断裂。所以生成器必须保证同一个符号在每次扫描中产出的 key 完全一致。
四、查找链:编译器怎么找到对应的映射
当 SemanticWalker 遇到一个外部方法调用时,它走一条多级查找链:
- 检查是否是当前模块内部声明的方法 → 直接 lowering,不走 WhiteList
- 从
Compile表里查 - 从
Alias表里查 - 从
Inline表里查 - 从
Import表里查 - 如果都没找到 → 进入普通 lowering 或报错:不支持的外部 API
关键的是,第 5 步(Compile)失败时不能回退——一旦注册了 Compile 但执行失败,说明是语义层面的问题,必须报错。编译器不允许"Compile 失败了但我用 Inline 兜底"。从设计原则上说,Compile 的失败是 claimed-and-failed,而不是 silent fallback。
查找支持多种 fallback 策略:
- 静态扩展方法的查找:用
StaticExtensionNameFormat做 key override和virtual的兼容查找:找到基类声明- 泛型参数的结构匹配:
List<T>的 key 匹配T的引用
但这些都是 consumer-side 的查找补丁——不影响白名单 key 的生成。key 始终是规范形式。这条"key 不漂移"的原则是 Jazor WhiteList 设计里最重要的合约。
五、一条红线:不支持的类型必须报错
WhiteList 系统的一个重要职责是"拒绝"——编译器要知道什么不能做。
- 不在白名单里的外部类型 →
JAZOR001诊断错误 - 白名单里有映射但类型不兼容 →
JAZOR002模糊类型过滤 - 泛型类型在运行时没有对应映射 → 编译报错
这些都是"明确的拒绝",不是"静默 fallback"。Jazor 的设计原则是:宁可少支持,不可瞎映射。
判断"是否支持一个外部类型"的标准是使用点裁决,而不是类型出现裁决。也就是说,List<Unsupported> 作为泛型参数出现时不报错——只有当你真的在这个 List<Unsupported> 上调用了某个需要运行时语义的方法时,编译器才会在那一刻拒绝。这个"延迟到使用点"的策略让 WhiteList 的检查粒度更精确,减少误报。
六、WhiteList 和 Analyzer 的分工
WhiteList 不是唯一的守卫。Jazor 还有一个 Jazor.Analyzer(Roslyn 诊断分析器),它在编译更早的阶段做部分检查。
两者的分工是:
- Analyzer 做"前置收口"——在 IDE 里实时检查用户代码,诊断
JAZOR001到JAZOR004。Analyzer 可以比编译器更严格,因为它的目标是在编译之前尽早发现问题。 - SemanticWalker + WhiteList 做"最终裁决"——在 lowering 阶段,针对每一个具体的使用点判断支持情况。WhiteList 的 lookup 是在 lowering 时发生的,不是在分析阶段。
这个分层的好处是:用户写代码时 IDE 就能看到红色波浪线(Analyzer),不需要等到编译完才发现问题。而编译器层面的 WhiteList 保持精确——只在实际需要 lowering 时才做最终判断。
七、小结
Jazor 的 WhiteList 系统本质上是一个API 边界管理框架。它的四层结构(Alias/Inline/Import/Compile)覆盖了从简单名字替换到复杂 AST 构造的所有映射场景,同时用明确的"拒绝"策略防止不可靠的隐式映射。
这个系统还有几个重要的工程属性:
- 自动生成:由
Jazor.Compiler.Generator从 Attribute 标注自动产生,没有手写映射散落在各处 - key 稳定:生成器保证同一符号的 key 在每次生成时完全一致
- 使用点裁决:不在类型出现时报错,在实际使用时才判断
- 分层守卫:Analyzer 做前置收口,WhiteList + SemanticWalker 做最终裁决

浙公网安备 33010602011771号