pyright 是微软开源的一个为 python 提供类型检查、自动补全、文档信息提示等语言服务的工具,用 typescript 写成,微软自家的 VS Code python 扩展 Pylance 就是基于 pyright 开发。
笔者在对 python 解释器进行中文化,实现草蟒中文编程语言之后,便打算对其小弟 micropython 进行中文化。但是,mpy 是针对单片机等小内存设备而实现的精简解释器,对 uft8 的支持天生有限。因此,笔者之前用 python 中文化思路尝试的 mpy 中文化并不成功。
近期,笔者利用 typescript——可以看作是 ts/js 的语言服务工具——实现了 ts/js 中文化(极速Web开发语言初具气象)。在此过程中,笔者发现,此类语言服务工具除了提供常见的语言服务之外,其实还有一个重要用途,那就是能够很方便地用来实现编程语言的中文化和中英互译。
本着这个想法,笔者开始了解 python 的类型检查和语言服务工具,刚好发现微软就有这么一款开源工具,而且是后起之秀。
不过,不像 typescript,pyright 未提供 API 文档和 d.ts 声明文件,代码注释也很少,github 及其他网站上关于它的使用寥寥无几,其最大的应用 pylance 还是闭源的。
幸好,笔者之前开发极速 Web 语言的经验派上了用场,在潜心阅读源代码之后,克服了上述困难。
下面就把笔者的探索成果分享出来,希望有助于推动中文编程语言的发展和崛起。
本文主要说明如何将中文代码翻译成英文代码,英译中大同小异,不再赘述。
一个中文代码文件其实就是一个字符串,我们需要做的是对这个字符串进行处理,将其中需要翻译的中文(包括保留字、库中的类名、函数名、参数名等)翻译成英文,然后生成英文代码文件,之后的处理和执行就是大家所熟悉的常规操作。
pyright 包含 tokenizer(用于将上述字符串中的一个个词元分门别类,形成一个带有丰富信息的词元或 token 流)和 parser(用于根据 token 流形成抽象语法树或 AST),略作修改使其能够识别并解析中文保留字。
// tokenizerTypes.ts
export const enum KeywordType {
And,
...
With,
Yield,
不是,
不在,
}
// tokenizer.ts
const _keywords: Map<string, KeywordType> = new Map([
['and', KeywordType.And],
['且', KeywordType.And],
...
['True', KeywordType.True],
['真', KeywordType.True],
['不是', KeywordType.不是],
['不在', KeywordType.不在],
]);
// parser.ts
// comparison: expr (comp_op expr)*
// comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
private _parseComparison(): ExpressionNode {
...
while (true) {
...
if (...
} else if (this._consumeTokenIfKeyword(KeywordType.In)) {
comparisonOperator = OperatorType.In;
} else if (this._consumeTokenIfKeyword(KeywordType.不是)) {
comparisonOperator = OperatorType.IsNot;
} else if (this._consumeTokenIfKeyword(KeywordType.不在)) {
comparisonOperator = OperatorType.NotIn;
} else if (...
}
...
return leftExpr;
}
然后,我们就可以使用 parser 处理中文代码,获得 AST。
const parser = new Parser();
let result = parser.parseSourceFile(code, new ParseOptions(), new DiagnosticSink())
pyright 提供了遍历 AST 的类 ParseTreeWalker,实现其一个子类并重写不同节点的处理函数,便可修改节点信息,达到我们的翻译目的。
class treeWalker extends ParseTreeWalker {
constructor(srcFile, program) {
super();
this._srcFile = srcFile;
this._program = program;
}
visitName(node/* : NameNode */) {
// NameNode 包括保留字、函数名等,是翻译的重点
let pos = {line: 0, character: node.start+1};
// 查找文档信息
let hoverResult = this._program.getHoverForPosition(this._srcFile, pos, 'plaintext', CancellationToken.None);
let sigHelp = this._program.getSignatureHelpForPosition(this._srcFile, pos, 'plaintext', CancellationToken.None);
if (hoverResult) {
// 替换中文保留字、函数名等
}
if (sigHelp) {
// 替换函数的中文参数名称等
}
return true;
}
}
上面的代码中,program 这个对象至关重要,它包含了所有相关文件(标准库、项目文件、各种 pyi 等)的信息,通过它可以获得特定位置的标识符(不包括保留字)的文档信息,由此我们才知道应该将其翻译成什么。创建 program 对象需要两个参数,如下例所示。
const configOptions = new ConfigOptions(dir, 'off');
// 详细配置信息在 configOptions.ts 中
configOptions.pythonPath = '';
configOptions.typeshedPath = '';
configOptions.stubPath = '';
configOptions.verboseOutput = true;
// configOptions.useLibraryCodeForTypes = true;
const fs = createFromRealFileSystem();
const importResolver = new ImportResolver(fs, configOptions, new FullAccessHost(fs));
const program = new Program(importResolver, configOptions);
program.setTrackedFiles([sourceFile]);
为了正确实现替换,文档信息须符合一定的规范,欢迎大家就此提出建议。笔者目前简单规定如下:
最后一步就是将修改后的 token 流再组装成代码字符串,保留字的翻译也是在此进行。这一步不难,但比较繁琐,感兴趣的话可以看我在码云 (gitee) 上的源代码:金蟒。