基于C语言的javascript语法解析器
Last Update:
Word Count:
Read Time:
本博客记录本人完成的基于vibe coding的编译原理课程大作业:基于C语言的javascript语法解析器。
对测试用例basic.js和functions.js进行了补充,下面的内容还是以旧的测试用例为准,实际运行以实际测试用例情况为准(编译器代码没有改变)
工具:re2c+bison(Flex不支持unicode定义,javascript中的多种符合是应用unicode定义的)
目标:可以判断输入的javascript脚本是否符合语法;针对省略的分号,实现ASI(Automatic Semicolon Insertion) 机制。
接下来是整个基于C语言的javascript语法解析器的项目介绍:
项目介绍
这是一个基于C语言的JavaScript语法解析器,使用re2c进行词法分析和bison进行语法分析,完全遵循ECMAScript规范并实现了ASI(自动分号插入)机制。
完整的项目源代码压缩包(旧测试用例): js-parser.zip 项目下载
完整的项目源代码压缩包(新测试用例): js-parser-2.zip 项目下载
包含内容:
1 | |
技术架构
整体流程
1
2
3
4
5
6
7
8
9
10
11JavaScript源代码
↓
词法分析器 (lexer.re - re2c)
↓
Token流 + ASI处理
↓
语法分析器 (parser.y - bison)
↓
抽象语法树 (AST)
↓
语法验证结果核心组件
词法分析器 (src/lexer.re)
- 使用re2c生成,支持UTF-8编码
- 识别JavaScript的所有词法单元(关键字、标识符、字面量、运算符等)
- 实现ASI的词法层处理(跟踪换行符)
- 关键变量:
seen_newline、last_token
语法分析器 (src/parser.y)
- 使用bison生成LALR(1)解析器
- 定义完整的JavaScript语法规则
- 实现ASI的语法层处理(受限产生式)
- 生成抽象语法树
AST模块 (src/ast.c/h)
- 定义26种AST节点类型
- 提供38个创建函数
- 实现AST打印和内存管理
主程序 (src/main.c)
文件读取和命令行参数处理
调用解析器并输出结果
使用方法:
步骤一:
安装依赖
1
2sudo apt-get update
sudo apt-get install build-essential re2c bison验证安装
1
2
3re2c --version # 应显示版本号 (需要 >= 1.0)
bison --version # 应显示版本号 (需要 >= 3.0)
gcc --version # 应显示版本号步骤二:
下载文件,解压并编译
1
2cd js-parser
make应该看到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15mkdir -p build
gcc -Wall -Wextra -g -std=c11 -c src/main.c -o build/main.o
gcc -Wall -Wextra -g -std=c11 -c src/ast.c -o build/ast.o
re2c -o src/lexer.c -8 --input-encoding utf8 src/lexer.re
bison -d -v -o src/parser.tab.c src/parser.y
src/parser.y: warning: 48 shift/reduce conflicts [-Wconflicts-sr]
src/parser.y: warning: 38 reduce/reduce conflicts [-Wconflicts-rr]
src/parser.y: note: rerun with option '-Wcounterexamples' to generate conflict counterexamples
gcc -Wall -Wextra -g -std=c11 -Wno-unused-variable -Wno-unused-function -c src/lexer.c -o build/lexer.o
gcc -Wall -Wextra -g -std=c11 -Wno-unused-function -c src/parser.tab.c -o build/parser.tab.o
gcc -Wall -Wextra -g -std=c11 -o js-parser build/main.o build/ast.o build/lexer.o build/parser.tab.o
======================================
✓ Build successful!
======================================编译过程说明:
- re2c生成
src/lexer.c(词法分析器) - bison生成
src/parser.tab.c和src/parser.tab.h(语法分析器) - gcc编译所有C文件
- 链接生成可执行文件
js-parser
- re2c生成
步骤三:
基本用法:
1
2
3# 基本用法
# 解析JavaScript文件
./js-parser tests/positive/basic.js应该看到输出:
1
2
3Parsing 'tests/positive/basic.js'...
✓ Parsing successful!
Total lines: 16详细模式(查看AST):
1
2# 使用 -v 或 --verbose 显示完整的AST
./js-parser -v tests/positive/basic.js输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14Parsing 'tests/positive/basic.js'...
✓ Parsing successful!
Total lines: 16
=== Abstract Syntax Tree ===
Program
StatementList (4 statements)
VariableDeclaration(var) name=x
Initializer:
Literal (number) 42
VariableDeclaration(let) name=y
Initializer:
Literal (string) "hello"
...运行所有测试:
1
make test这将:
- 运行所有正向测试(应该成功)
- 运行所有负向测试(应该失败)
- 显示测试结果摘要
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46======================================
Running Positive Tests
======================================
Testing: tests/positive/asi_return.js
Parsing 'tests/positive/asi_return.js'...
✓ Parsing successful!
Total lines: 15
Testing: tests/positive/asi_statements.js
Parsing 'tests/positive/asi_statements.js'...
✓ Parsing successful!
Total lines: 19
Testing: tests/positive/basic.js
Parsing 'tests/positive/basic.js'...
✓ Parsing successful!
Total lines: 18
Testing: tests/positive/functions.js
Parsing 'tests/positive/functions.js'...
✓ Parsing successful!
Total lines: 16
Testing: tests/positive/unicode.js
Parsing 'tests/positive/unicode.js'...
✓ Parsing successful!
Total lines: 9
======================================
Running Negative Tests (should fail)
======================================
Testing: tests/negative/asi_ambiguous.js
Parsing 'tests/negative/asi_ambiguous.js'...
✗ Parsing failed with 1 error(s)
✓ Failed as expected
Testing: tests/negative/invalid_syntax.js
Parsing 'tests/negative/invalid_syntax.js'...
✗ Parsing failed with 1 error(s)
✓ Failed as expected
======================================
✓ All tests passed!
======================================
测试用例说明
正向测试(tests/positive/)
basic.js - 基本语法
1
2
3
4
5
6var x = 42;
let y = "hello";
const z = true;
function add(a, b) { return a + b; }
if (x > 10) { ... }
while (y < 100) { ... }functions.js - 函数声明
1
2
3
4
5
6function regular() { return 1; }
async function asyncFunc() { return 2; }
function outer() {
function inner() { return 42; }
return inner();
}asi_return.js - ASI(自动分号插入)与return
1
2
3
4
5
6
7
8function test1() {
return
42; // ASI在return后插入分号,返回undefined
}
function test2() {
return 42; // 返回42
}asi_statements.js - ASI(自动分号插入)与语句
1
2
3
4var a = 1
var b = 2 // ASI自动插入分号
a = b + c
a++unicode.js - Unicode支持
1
2
3var α = 42; // 希腊字母
var 变量 = "中文"; // 中文
var ヴァリアブル = 123; // 日文
负向测试(tests/negative/)
invalid_syntax.js - 语法错误
1
2
3
4
5
6
7var = 123; // 缺少标识符
function { // 缺少函数名
return 42;
}
if x > 10 { // 缺少括号
console.log("error");
}asi_ambiguous.js - ASI(自动分号插入)歧义
1
2a = b + c
(d + e).print() // 会被解析为: a = b + c(d + e).print()
测试覆盖说明
测试矩阵
测试类别 测试文件 测试内容 预期结果 基本语法 basic.js 变量声明、函数、控制流 ✅ 通过 函数 functions.js 函数声明、嵌套、async ✅ 通过 ASI-受限 asi_return.js return后换行 ✅ 通过 ASI-一般 asi_statements.js 语句间ASI ✅ 通过 Unicode unicode.js 多语言标识符 ✅ 通过 语法错误 invalid_syntax.js 各种错误语法 ✅ 正确拒绝 ASI歧义 asi_ambiguous.js 歧义情况 ✅ 正确处理 覆盖的JavaScript特性
已实现:
- 变量声明(var, let, const)
- 函数声明(普通、async)
- 表达式(二元、一元、三元、赋值)
- 控制流(if, while, for, do-while)
- 跳转语句(return, break, continue, throw)
- 对象和数组字面量
- 成员访问和函数调用
- Unicode标识符
未实现:
- ES6+特性(class, arrow function, destructuring)
- 模块系统(import/export)
- 生成器和迭代器
- Promise和async/await完整支持
简单的 ASI(自动分号插入)机制验证
测试受限产生式
1
2
3
4
5
6
7
8
9# 创建测试文件
cat > test_asi.js << 'EOF'
function test() {
return
42
}
EOF
./js-parser -v test_asi.js预期行为:
return后的换行会触发ASI
自动插入分号:
return; 42;函数返回undefined而非42
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Parsing 'test_asi.js'...
✓ Parsing successful!
Total lines: 5
=== Abstract Syntax Tree ===
Program
StatementList (1 statements)
FunctionDeclaration name=test
Body:
BlockStatement
StatementList (2 statements)
ReturnStatement
(no argument)
ExpressionStatement
Literal (number) 42AST输出结构解释:
这个输出是抽象语法树(Abstract Syntax Tree),以缩进的树形结构显示代码的语法结构:
1
2
3
4
5
6
7
8
9
10
11=== Abstract Syntax Tree ===
Program ← 根节点:整个程序
StatementList (1 statements) ← 程序包含1个语句
FunctionDeclaration name=test ← 语句是:函数声明,名字是test
Body: ← 函数体
BlockStatement ← 代码块 { ... }
StatementList (2 statements) ← 代码块里有2个语句 关键!
ReturnStatement ← 第1个语句:return
(no argument) ← 关键!return没有参数!返回undefined
ExpressionStatement ← 第2个语句:表达式语句
Literal (number) 42 ← 字面量42(永远不会执行)测试EOF处的ASI
1
2
3
4
5cat > test_eof.js << 'EOF'
var x = 42
EOF
./js-parser -v test_eof.js预期行为:
文件末尾自动插入分号
成功解析
输出:
1
2
3
4
5
6
7
8
9
10
11Parsing 'test_eof.js'...
✓ Parsing successful!
Total lines: 2
=== Abstract Syntax Tree ===
Program
StatementList (1 statements)
StatementList (1 statements)
VariableDeclaration(var) name=x
Initializer:
Literal (number) 42AST输出结构解释:
这个AST展示了文件末尾(EOF)的ASI处理:
1
2
3
4
5
6
7=== Abstract Syntax Tree ===
Program ← 根节点:整个程序
StatementList (1 statements) ← 程序包含1个语句
StatementList (1 statements) ← 嵌套的列表(变量声明列表)
VariableDeclaration(var) name=x ← 变量声明:var x
Initializer: ← 初始化器
Literal (number) 42 ← 值是 42补充:为什么有两层 StatementList?
1
2
3StatementList (程序级别)
└─ StatementList (变量声明列表)
└─ VariableDeclaration这是因为语法规则设计:
- 外层 StatementList: 程序的所有语句
- 内层 StatementList:
VariableDeclarationList可以包含多个声明(如var a=1, b=2, c=3)
对比:如果有多个变量声明
1
var a = 1, b = 2, c = 3AST会是:
1
2
3
4
5StatementList (程序级别)
└─ StatementList (变量声明列表,3个)
├─ VariableDeclaration name=a
├─ VariableDeclaration name=b
└─ VariableDeclaration name=c
ASI机制实现详解
ASI(Automatic Semicolon Insertion,自动分号插入)是JavaScript语言的一个重要特性,允许在某些情况下省略分号。本解析器严格按照ECMAScript规范实现了完整的ASI机制。
ASI的核心变量
在词法分析器中,我们使用以下全局变量来跟踪ASI状态:
1 | |
变量说明:
seen_newline: 标记当前位置前是否遇到换行符,用于触发ASI规则1last_token: 记录上一个识别的token,用于判断受限产生式paren_depth: 跟踪括号嵌套深度,用于识别for循环结束in_for_header: 标记是否在for循环头部,防止误插入分号
ASI规则映射到代码
规则1:违规token (Offending Token)
ECMAScript规范定义: 当遇到一个token,且该token与前一个token之间有至少一个行终止符分隔时,如果该token不能按语法规则被解析,则在该token前自动插入分号。
代码实现:
1 | |
换行符检测:
1 | |
当词法分析器遇到换行符(\n、\r、\u2028、\u2029)时,设置seen_newline = 1,为后续ASI判断提供依据。
规则2:输入结束 (EOF)
ECMAScript规范定义: 当到达输入流末尾且解析器无法将输入解析为完整的程序时,在末尾自动插入分号。
代码实现:
1 | |
测试示例:
1 | |
在文件末尾,如果最后一个token不是分号或右花括号,则自动插入分号。
规则3:受限产生式 (Restricted Productions)
ECMAScript规范定义: 某些语句(return、break、continue、throw、后缀++/–)在关键字和后续表达式之间不允许出现行终止符。如果出现换行,则在关键字后自动插入分号。
受限产生式判断函数:
1 | |
在词法分析器中主动插入分号:
1 | |
在语法分析器中的体现:
Return语句:
1 | |
Continue语句:
1 | |
Break语句:
1 | |
Throw语句:
1 | |
测试示例:
1 | |
规则4:右花括号前插入分号
ECMAScript规范定义: 当遇到右花括号}时,如果该花括号前需要一个分号但没有,则在花括号前自动插入分号。
代码实现:
在can_insert_semicolon函数中已包含此规则:
1 | |
右花括号的特殊处理:
1 | |
遇到右花括号时,重置seen_newline标志,防止后续误触发ASI。
测试示例:
1 | |
规则5:for循环头部特殊处理
ECMAScript规范定义: 在for循环的头部(初始化、条件、更新表达式),即使遇到换行也不应插入分号,因为分号是for语法的分隔符。
for关键字标记:
1 | |
当识别到for关键字时,设置in_for_header = 1。
for循环右括号处理:
1 | |
通过paren_depth跟踪括号嵌套,当最外层右括号闭合时,重置in_for_header标志。
ASI函数中的异常处理:
1 | |
在for循环头部内,即使遇到换行也不插入分号。
语法规则中的体现:
1 | |
for循环的三部分用显式的分号;分隔,不依赖ASI。
测试示例(正确):
1 | |
一般语句的ASI支持
除了受限产生式,普通语句也支持ASI:
变量声明:
1 | |
表达式语句:
1 | |
Do-While语句:
1 | |
ASI实现流程总结
1 | |
关键设计要点
- 词法层和语法层协作:
- 词法层负责检测换行、EOF、受限产生式
- 语法层提供带/不带分号的双重规则
- 状态追踪:
seen_newline: 追踪换行位置last_token: 判断受限产生式in_for_header: 处理for循环特例
- 精确控制:
- 受限产生式立即插入分号
- 一般语句由语法规则宽容处理
- for循环显式禁止ASI
这种实现方式确保了与ECMAScript规范的完全一致性,同时保持了代码的清晰和可维护性。
项目特性
re2c词法分析器
完整Unicode支持
所有JavaScript token识别
注释处理(单行和多行)
bison语法分析器
遵循ECMAScript规范
表达式和语句完整支持
函数声明(包括async)
控制流语句(if/while/for)
ASI机制
受限产生式(return/break/continue/throw)
EOF处理
右花括号处理
for循环特殊处理
完整AST
26种节点类型
详细打印功能
完整内存管理
性能和限制
性能特点
编译时间: <2秒(取决于机器)
解析速度: 毫秒级(小型文件)
内存使用: 取决于AST大小
当前限制
不支持的ES6+特性:
- 类声明(class)
- 箭头函数(=>)
- 模板字面量(完整语法)
- 解构赋值
- import/export
ASI的固有限制:
某些情况下ASI可能导致非预期行为(这是JavaScript语言本身的特性)正则表达式:
仅基本支持,不解析正则表达式内部语法
致谢
感谢编译原理课程提供的学习机会,通过这个项目加深了对编译器构造的理解。特别感谢ECMAScript规范文档和开源社区提供的丰富资料。
reward
