这篇文章包含两个部分:
A 部分:类型系统编译器概述(包括 TypeScript)
B 部分:构建我们自己的类型系统编译器
语法和语义之间的区别对于早期的运行很重要。
语法通常是指 JavaScript 本机代码。本质上是询问给定的 JavaScript 代码在运行时是否正确。
例如,下面的语法是正确的:
var foo: number = "not a number";
这是特定于类型系统的代码。本质上是询问附加到代码中的给定类型是否正确。
例如,上面的代码在语法上是正确的,但在语义上是错误的(将变量定义为一个数字类型,但是值是一个字符串)。
接下来是 JavaScript 生态系统中的 AST 和编译器。
在进一步讨论之前,我们需要快速了解一下 JavaScript 编译器中的一个重要机制 AST。
关于 AST 详细介绍请看这篇文章。
AST 的意思是抽象语法树 ,它是一个表示程序代码的节点树。Node 是最小单元,基本上是一个具有 type
和 location
属性的 POJO(即普通 JavaScript 对象)。所有节点都有这两个属性,但根据类型,它们也可以具有其他各种属性。
在 AST 格式中,代码非常容易操作,因此可以执行添加、删除甚至替换等操作。
例如下面这段代码:
function add(number) {
return number + 1;
}
将解析成以下 AST:
在 JavaScript 生态系统中有两种主要的编译器类型:
原生编译器将代码转换为可由服务器或计算机运行的代码格式(即机器代码)。类似于 Java 生态系统中的编译器 - 将代码转换为字节码,然后将字节码转换为本机代码。
语言编译器扮演着不同的角色。TypeScript 和 Flow 的编译器在将代码输出到 JavaScript 时都算作语言编译器。
语言编译器与原生编译器的主要区别在于,前者的编译目的是 tooling-sake
(例如优化代码性能或添加附加功能),而不是为了生成机器代码。
在类型系统编译器中,总结的两个最基本的核心职责是:
引入类型(通常是通过显式注解或隐式推理),以及检查一种类型是否匹配另一种类型的方法,例如 string
和 number
。
对于一个在开发环境中工作的类型系统(type system)来说,最好能在 IDE 中运行任何类型检查,并为用户提供即时反馈。
语言服务器将类型系统连接到 IDE,它们可以在后台运行编译器,并在用户保存文件时重新运行。流行的语言,如 TypeScript 和 Flow 都包含一个语言服务器。
许多类型系统包含原生 JavaScript 不支持的代码(例如不支持类型注解) ,因此它们必须将不受支持的 JavaScript 转换为受支持的 JavaScript 代码。
关于代码转换更详细的介绍,可以参考原作者的这两篇文章 Web Bundler 和 Source Maps。
对于大多数编译器来说,在某种形式上有三个共同的阶段。
1. 将源代码解析为 AST
解析器检查给定代码的语法。类型系统必须有自己的解析器,通常包含数千行代码。
Babel 解析器 中的 2200+
行代码,仅用于处理 statement
语句(请参阅此处)。
Hegel
解析器将 typeAnnotation
属性设置为具有类型注解的代码(可以在这里看到)。
TypeScript 的解析器拥有 8900+
行代码(这里是它开始遍历树的地方)。它包含了一个完整的 JavaScript 超集,所有这些都需要解析器来理解。
2. 在 AST 上转换节点
这里将执行应用于 AST 的任何转换。
3. 生成源代码
类型系统必须将任何非 js 兼容的 AST 映射回原生 JavaScript。
类型系统如何处理这种情况呢?
除了上述步骤之外,类型系统编译器通常还会在解析之后包括一个或两个额外步骤,其中包括特定于类型的工作。
顺便说一下,TypeScript 的编译器实际上有 5
个阶段,它们是:
正如上面看到的,语言服务器包含一个预处理器,它触发类型编译器只在已更改的文件上运行。这会监听任意的 import
语句,来确定还有哪些内容可能发生了更改,并且需要在下次重新运行时携带这些内容。
此外,编译器只能重新处理 AST 结构中已更改的分支。关于更多 lazy compilation
,请参阅下文。
类型系统编译器有两个常见的职责:
1. 推导 - Inferring
对于没有注解的代码需要进行推断。关于这点,这里推荐一篇关于何时使用类型注解和何时让引擎使用推断的文章。
使用预定义的算法,引擎将计算给定变量或者函数的类型。
TypeScript 在其 Binding 阶段(两次语义传递中的第一次)中使用最佳公共类型算法。它考虑每个候选类型并选择与所有其他候选类型兼容的类型。上下文类型在这里起作用,也会做为最佳通用类型的候选类型。在这里的 TypeScript 规范中有更多的帮助。
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
TypeScript 实际上引入了 Symbols
(interface)的概念,这些命名声明将 AST 中的声明节点与其他声明进行连接,从而形成相同的实体。它们是 TypeScript 语义系统的基本构成。
2. 检查 - Checking
现在类型推断已经完成,类型已经分配,引擎可以运行它的类型检查。他们检查给定代码的 semantics
。这些类型的检查有很多种,从类型错误匹配到类型不存在。
对于 TypeScript 来说,这是 Checker (第二个语义传递) ,它有 20000+
行代码。
我觉得这给出了一个非常强大的 idea
,即在如此多的不同场景中检查如此多的不同类型是多么的复杂和困难。
类型检查器不依赖于调用代码,即如果一个文件中的任何代码被执行(例如,在运行时)。类型检查器将处理给定文件中的每一行,并运行适当的检查。
由于这些概念的复杂性,我们今天不深入探讨以下几个概念:
懒编译 - Lazy compilation
现代编译的一个共同特征是延迟加载。他们不会重新计算或重新编译文件或 AST 分支,除非绝对需要。
TypeScript 预处理程序可以使用缓存在内存中的前一次运行的 AST 代码。这将大大提高性能,因为它只需要关注程序或节点树的一小部分已更改的内容。
TypeScript 使用不可变的只读数据结构,这些数据结构存储在它所称的 look aside tables
中。这样很容易知道什么已经改变,什么没有改变。
稳健性
在编译时,有些操作编译器不确定是安全的,必须等待运行时。每个编译器都必须做出困难的选择,以确定哪些内容将被包含,哪些不会被包含。TypeScript 有一些被称为不健全的区域(即需要运行时类型检查)。
我们不会在编译器中讨论上述特性,因为它们增加了额外的复杂性,对于我们的小 POC 来说不值得。
现在令人兴奋的是,我们自己也要实现一个编译器。
我们将构建一个编译器,它可以对三个不同的场景运行类型检查,并为每个场景抛出特定的信息。
我们将其限制在三个场景中的原因是,我们可以关注每一个场景中的具体机制,并希望到最后能够对如何引入更复杂的类型检查有一个更好的构思。
我们将在编译器中使用函数声明和表达式(调用该函数)。
这些场景包括:
1. 字符串与数字的类型匹配问题
fn("craig-string"); // throw with string vs number
function fn(a: number) {}
2. 使用未定义的未知类型
fn("craig-string"); // throw with string vs ?
function fn(a: made_up_type) {} // throw with bad type
3. 使用代码中未定义的属性名
interface Person {
name: string;
}
fn({ nam: "craig" }); // throw with "nam" vs "name"
function fn(a: Person) {}
实现我们的编译器,需要两部分:解析器和检查器。
前面提到,我们今天不会关注解析器。我们将遵循 Hegel
的解析方法,假设一个 typeAnnotation
对象已经附加到所有带注解的 AST 节点中。我已经硬编码了 AST 对象。
场景 1
将使用以下解析器:
字符串与数字的类型匹配问题
function parser(code) {
// fn("craig-string");
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn"
},
arguments: [
{
type: "StringLiteral", // Parser "Inference" for type.
value: "craig-string"
}
]
}
};
// function fn(a: number) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn"
},
params: [
{
type: "Identifier",
name: "a",
// 参数标识
typeAnnotation: {
// our only type annotation
type: "TypeAnnotation",
typeAnnotation: {
// 数字类型
type: "NumberTypeAnnotation"
}
}
}
],
body: {
type: "BlockStatement",
body: [] // "body" === block/line of code. Ours is empty
}
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [expressionAst, declarationAst]
}
};
// normal AST except with typeAnnotations on
return programAst;
}
可以看到场景 1
中,第一行 fn("craig-string")
语句的 AST 对应 expressionAst
,第二行声明函数的 AST 对应 declarationAst
。最后返回一个 programmast
,它是一个包含两个 AST 块的程序。
在AST中,您可以看到参数标识符 a
上的 typeAnnotation
,与它在代码中的位置相匹配。
场景 2
将使用以下解析器:
使用未定义的未知类型
function parser(code) {
// fn("craig-string");
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn"
},
arguments: [
{
type: "StringLiteral", // Parser "Inference" for type.
value: "craig-string"
}
]
}
};
// function fn(a: made_up_type) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn"
},
params: [
{
type: "Identifier",
name: "a",
typeAnnotation: {
// our only type annotation
type: "TypeAnnotation",
typeAnnotation: {
// 参数类型不同于场景 1
type: "made_up_type" // BREAKS
}
}
}
],
body: {
type: "BlockStatement",
body: [] // "body" === block/line of code. Ours is empty
}
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [expressionAst, declarationAst]
}
};
// normal AST except with typeAnnotations on
return programAst;
}
场景 2
的解析器的表达式、声明和程序 AST 块非常类似于场景 1
。然而,区别在于 params
内部的 typeAnnotation
是 made_up_type
,而不是场景 1
中的 NumberTypeAnnotation
。
type: "made_up_type" // BREAKS
}
场景 3
使用以下解析器:
使用代码中未定义的属性名
function parser(code) {
// interface Person {
// name: string;
// }
const interfaceAst = {
type: "InterfaceDeclaration",
id: {
type: "Identifier",
name: "Person",
},
body: {
type: "ObjectTypeAnnotation",
properties: [
{
type: "ObjectTypeProperty",
key: {
type: "Identifier",
name: "name",
},
kind: "init",
method: false,
value: {
type: "StringTypeAnnotation",
},
},
],
},
};
// fn({nam: "craig"});
const expressionAst = {
type: "ExpressionStatement",
expression: {
type: "CallExpression",
callee: {
type: "Identifier",
name: "fn",
},
arguments: [
{
type: "ObjectExpression",
properties: [
{
type: "ObjectProperty",
method: false,
key: {
type: "Identifier",
name: "nam",
},
value: {
type: "StringLiteral",
value: "craig",
},
},
],
},
],
},
};
// function fn(a: Person) {}
const declarationAst = {
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "fn",
},
params: [
{
type: "Identifier",
name: "a",
//
typeAnnotation: {
type: "TypeAnnotation",
typeAnnotation: {
type: "GenericTypeAnnotation",
id: {
type: "Identifier",
name: "Person",
},
},
},
},
],
body: {
type: "BlockStatement",
body: [], // Empty function
},
};
const programAst = {
type: "File",
program: {
type: "Program",
body: [interfaceAst, expressionAst, declarationAst],
},
};
// normal AST except with typeAnnotations on
return programAst;
}
除了表达式、声明和程序 AST 块之外,还有一个 interfaceAst
块,它负责保存 InterfaceDeclaration
AST。
在declarationAst
块的 typeAnnotation
节点上有一个 GenericType
,因为它接受一个对象标识符,即 Person
。在这个场景中,programAst
将返回这三个对象的数组。
解析器的相似性
从上面可以得知,这三种有共同点, 3
个场景中保存所有的类型注解的主要区域是 declaration
。
现在来看编译器的类型检查部分。
它需要遍历所有程序主体的 AST 对象,并根据节点类型进行适当的类型检查。我们将把所有错误添加到一个数组中,并返回给调用者以便打印。
在我们进一步讨论之前,对于每种类型,我们将使用的基本逻辑是:
以下代码中包含 typeChecks
对象(和 errors
数组) ,它将用于表达式检查和基本的注解(annotation)检查。
const errors = [];
// 注解类型
const ANNOTATED_TYPES = {
NumberTypeAnnotation: "number",
GenericTypeAnnotation: true
};
// 类型检查的逻辑
const typeChecks = {
// 比较形参和实参的类型
expression: (declarationFullType, callerFullArg) => {
switch (declarationFullType.typeAnnotation.type) {
// 注解为 number 类型
case "NumberTypeAnnotation":
// 如果调用时传入的是数字,返回 true
return callerFullArg.type === "NumericLiteral";
// 注解为通用类型
case "GenericTypeAnnotation": // non-native
// 如果是对象,检查对象的属性
if (callerFullArg.type === "ObjectExpression") {
// 获取接口节点
const interfaceNode = ast.program.body.find(
node => node.type === "InterfaceDeclaration"
);
const properties = interfaceNode.body.properties;
//遍历检查调用时的每个属性
properties.map((prop, index) => {
const name = prop.key.name;
const associatedName = callerFullArg.properties[index].key.name;
// 没有匹配,将错误信息存入 errors
if (name !== associatedName) {
errors.push(
`Property "${associatedName}" does not exist on interface "${interfaceNode.id.name}". Did you mean Property "${name}"?`
);
}
});
}
return true; // as already logged
}
},
annotationCheck: arg => {
return !!ANNOTATED_TYPES[arg];
}
};
让我们来看一下代码,我们的 expression
有两种类型的检查:
NumberTypeAnnotation;
调用时类型应为 AnumericTeral
(即,如果注解为数字,则调用时类型应为数字)。场景 1
将在此处失败,但未记录任何错误信息。GenericTypeAnnotation;
如果是一个对象,我们将在 AST 中查找 InterfaceDeclaration
节点,然后检查该接口上调用者的每个属性。之后将所有错误信息都会被存到 errors
数组中,场景 3
将在这里失败并得到这个错误。我们的处理仅限于这个文件中,大多数类型检查器都有作用域的概念,因此它们能够确定声明在运行时的准确位置。我们的工作更简单,因为它只是一个
POC
。
以下代码包含程序体中每个节点类型的处理。这就是上面调用类型检查逻辑的地方。
// Process program
ast.program.body.map(stnmt => {
switch (stnmt.type) {
case "FunctionDeclaration":
stnmt.params.map(arg => {
// Does arg has a type annotation?
if (arg.typeAnnotation) {
const argType = arg.typeAnnotation.typeAnnotation.type;
// Is type annotation valid
const isValid = typeChecks.annotationCheck(argType);
if (!isValid) {
errors.push(
`Type "${argType}" for argument "${arg.name}" does not exist`
);
}
}
});
// Process function "block" code here
stnmt.body.body.map(line => {
// Ours has none
});
return;
case "ExpressionStatement":
const functionCalled = stnmt.expression.callee.name;
const declationForName = ast.program.body.find(
node =>
node.type === "FunctionDeclaration" &&
node.id.name === functionCalled
);
// Get declaration
if (!declationForName) {
errors.push(`Function "${functionCalled}" does not exist`);
return;
}
// Array of arg-to-type. e.g. 0 = NumberTypeAnnotation
const argTypeMap = declationForName.params.map(param => {
if (param.typeAnnotation) {
return param.typeAnnotation;
}
});
// Check exp caller "arg type" with declaration "arg type"
stnmt.expression.arguments.map((arg, index) => {
const declarationType = argTypeMap[index].typeAnnotation.type;
const callerType = arg.type;
const callerValue = arg.value;
// Declaration annotation more important here
const isValid = typeChecks.expression(
argTypeMap[index], // declaration details
arg // caller details
);
if (!isValid) {
const annotatedType = ANNOTATED_TYPES[declarationType];
// Show values to user, more explanatory than types
errors.push(
`Type "${callerValue}" is incompatible with "${annotatedType}"`
);
}
});
return;
}
});
让我们再次遍历代码,按类型对其进行分解。
FunctionDeclaration (即 function hello(){}
)
首先处理 arguments/params
。如果找到类型注解,就检查给定参数的类型 argType
是否存在。如果不进行错误处理,场景 2
会在这里报错误。
之后处理函数体,但是我们知道没有函数体需要处理,所以我把它留空了。
stnmt.body.body.map(line => {
// Ours has none
});
ExpressionStatement (即 hello()
)
首先检查程序中函数的声明。这就是作用域将应用于实际类型检查器的地方。如果找不到声明,就将错误信息添加到 errors
数组中。
接下来,我们针对调用时传入的参数类型(实参类型)检查每个已定义的参数类型。如果发现类型不匹配,则向 errors
数组中添加一个错误。场景 1
和场景 2
在这里都会报错。
源码存放在这里,该文件一次性处理所有三个 AST 节点对象并记录错误。
运行它时,我得到以下信息:
总而言之:
场景 1:
fn("craig-string"); // throw with string vs number
function fn(a: number) {}
我们定义参数为 number 的类型,然后用字符串调用它。
场景 2:
fn("craig-string"); // throw with string vs ?
function fn(a: made_up_type) {} // throw with bad type
我们在函数参数上定义了一个不存在的类型,然后调用我们的函数,所以我们得到了两个错误(一个是定义的错误类型,另一个是类型不匹配的错误)。
场景 3:
interface Person {
name: string;
}
fn({ nam: "craig" }); // throw with "nam" vs "name"
function fn(a: Person) {}
我们定义了一个接口,但是使用了一个名为 nam
的属性,这个属性不在对象上,错误提示我们是否要使用 name
。
如前所述,类型编译器还有许多其他部分,我们在编译器中省略了这些部分。其中包括:
非常感谢您的阅读和观看,我从这项研究中了解了大量关于类型系统的知识,希望对您有所帮助。以上完整代码您可以在这里找到。(给原作者 start)
备注:
原作者在源码中使用的 Node 模块方式为 ESM(ES Module),在将源码克隆到本地后,如果运行不成功,需要修改 start
指令,添加启动参数 --experimental-modules
。
"start": "node --experimental-modules src/index.mjs",
END
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/1DWjPk_7rI3-W5EGPAHYMA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。