Web编程技术营地
研究、演示、创新

实现JavaScript在线编译器

撰写时间:2025-02-18

修订时间:2025-09-06

目标设定

下面是一段C语言的代码:

int sum(int a, int b) { return a + b; } int main() { int x = 2 * 3; int y = sum(x, 4); printf(y); }

我们准备使用流的方式,将这段C语言代码分别编译为JavaScript代码及Wasm代码,并在浏览器中分别予以运行。

要点:

  1. 两套语法。是否符合契约约定?函数声明、赋值语句、返回语句。源语法及目标语法。
  2. 能识别为函数。按函数语法解析。包括:返回值、函数名、参数列表、函数体、返回值
  3. 变量。开辟内存空间、存储数值。数据类型、值。
  4. 关键词。实现特定的小功能。int, return, function, let

第一步:词条扫描

编写Lexer.js文件内容如下:

import { CharUtils } from '/js/esm/CharUtils.js'; class PrintStream extends WritableStream { constructor(pc) { super({ write(chunk) { pc.log('token: "%s"', chunk); } }); } } const Token = Object.freeze({ EOF: '\0', DEF: -2, EXTERN: -3, IDENTIFIER: -4, NUMBER: -5 }); let gIdentifierStr = ""; let gNumStr = ""; export class Lexer { static async Parse(src, pc) { let singleCharStream = new ReadableStream({ offset: 0, CHUNK_SIZE: 1, pull(controller) { let char = src.slice(this.offset, this.offset + this.CHUNK_SIZE); if (char === '') { controller.close(); } else { controller.enqueue(char); this.offset += this.CHUNK_SIZE; } } }); let ts = new TransformStream({ prevChar: "", transform(char, controller) { let currChar = char; if (Lexer.IsDelimiter(currChar)) { Lexer.checkToCloseToken(controller); } if (CharUtils.IsAlpha(currChar)) { Lexer.DealWithAlpha(this.prevChar, currChar); } if (CharUtils.IsDigit(currChar) || CharUtils.IsDigitDot(currChar)) { Lexer.DealWithNumber(this.prevChar, currChar); } if (Lexer.IsOperator(currChar)) { controller.enqueue(currChar); } this.prevChar = currChar; } }); singleCharStream.pipeThrough(ts) .pipeTo(new PrintStream(pc)); } static IsDelimiter(char) { return CharUtils.IsWhiteSpace(char) || /[=+;(){}]/.test(char); } static IsOperator(char) { return /\+|-|\*|\/|=/.test(char); } static checkToCloseToken(controller) { if (gIdentifierStr.length > 0) { controller.enqueue(gIdentifierStr); gIdentifierStr = ''; } if (gNumStr.length > 0) { controller.enqueue(gNumStr); gNumStr = ''; } } static DealWithAlpha(prevChar, currChar) { if (Lexer.IsDelimiter(prevChar) || prevChar === '') { gIdentifierStr = currChar; } else if (CharUtils.IsAlpha(prevChar)) { gIdentifierStr += currChar; } } static DealWithNumber(prevChar, currChar) { if (Lexer.IsDelimiter(prevChar) || prevChar === '') { gNumStr = currChar; } else if (CharUtils.IsDigit(prevChar) || CharUtils.IsDigitDot(prevChar)) { gNumStr += currChar; } } }

客户端代码:

const { Lexer } = await import('./Lexer.js'); let src = ` int sum(int a, int b) { return a + b; } int main() { int x = (2 + 3) * 4 / 5; float y = sum(x, 6.78); printf(y); } `; Lexer.Parse(src, pc);

上面去掉了相应的分隔符,所提取出来的词条,严格按源代码的顺序进行提取,并且没有漏掉任何关键信息。这些词条是构建抽象语法树AST, Abstract Syntax Tree)的必要信息。

输入流为单字符的可读流,输出流为多字符的可写流。通过应用流的管道操作,逻辑清晰,代码简练。

词法分析术语

数据类型

数据类型的作用在于在编译阶段为相应的变量分配内存空间。包括:

int
整数
float
浮点数

变量

变量用于在内存中存储数值。在C语言或汇编语言中,先在内存中存储数值,然后在编译阶段建立起符号表,在符号表中将数值的内存地址赋值于相应的变量。如果目标语言直接支持内存地址的操作,则可使用这种方式。

但对于JavaScriptWasm来讲,不需要直接操控数值的内存地址,故变量的AST只需变量名及数值就行了。