Hotdry.
compiler-design

Langjam 自举最小解释器:玩具语言到游戏逻辑

Langjam 7 天游戏 Jam 下,自举最小树遍历解释器实现玩具语言解析与游戏逻辑求值,聚焦词法语法评估器工程参数、GameJam 时间约束优化与简单 Roguelike 示例。

在 Langjam Gamejam(https://langjamgamejam.com)这样的 7 天挑战中,参赛者需设计一门编程语言并用其实现游戏。时间紧迫下,自举最小解释器是理想起点:无需复杂编译器,直接树遍历(tree-walk)方式解析语义,支持游戏逻辑如状态更新、循环事件判定。该文聚焦工程实践,提供 lexer、parser、evaluator 实现参数与清单,确保单人 Jam 内落地简单 Roguelike 游戏。

为什么选择最小树遍历解释器?

传统编译器(如 GCC)涉及多阶段:扫描、解析、优化、代码生成。但 GameJam 仅 7 天,自举 bootstrap 解释器更务实。借鉴《Crafting Interpreters》(Robert Nystrom 著),采用 tree-walk 架构:解析源代码为抽象语法树(AST),运行时直接遍历执行。优势显着:

  • 开发速度:全手工实现,约 1000 行代码(Java/C/Rust),无需外部工具如 ANTLR/Bison。
  • 调试友好:AST 直观,错误定位源行。
  • 游戏适配:支持动态类型,易表达游戏状态(位置、健康值)、事件(碰撞、胜利)。

局限:性能低(解释开销),但 Jam 原型无需高 FPS。引用原书:“树遍历解释器简单易懂,先用 Java 实现概念,再优化为字节码 VM。”

风险控制:Jam 内禁用 GC(用简单环境栈),变量作用域限块级,避免闭包复杂性。

步骤一:Lexer(词法扫描器)

Lexer 将源码拆为 Token 流。GameJam 玩具语言只需 20+ Token 类型,聚焦游戏原语。

关键参数

  • 支持 Token:NUMBER(坐标 / 分数)、IDENTIFIER(var/move/up)、KEYWORD(if/while/print/var)、OPERATOR(==/+/==)、PUNCTUATOR(;(){})。
  • 游戏专属:关键词如 move dirattackwin if health<=0
  • 缓冲区:固定 256 字节,避免大文件(Jam 脚本 <1KB)。
  • 错误恢复:遇非法 char,跳至下一行。

伪码实现(Rust/JS):

enum TokenType { Number(f64), Id(String), If, While, ... }
struct Lexer { source: Vec<char>, start: usize, current: usize }
impl Lexer {
    fn scan_token(&mut self) -> Token {
        self.skip_whitespace();
        self.start = self.current;
        if self.is_digit() { return self.number(); }
        // ...
    }
}

Jam 阈值:扫描 100 行源码 <1ms。测试用例:var x=1; while(x>0){move up; x=x-1;}

步骤二:Parser(递归下降语法解析器)

Parser 消费 Token 建 AST。优先级用 Pratt / 递归下降,处理表达式嵌套如 if (health > 0 && pos.x < 10)

游戏语法子集

program ::= stmt*
stmt ::= expr ';' | 'if' '(' expr ')' stmt | 'while' '(' expr ')' stmt | 'print' expr ';'
expr ::= equality (('==' | '!=') equality)*
equality ::= comparison (('<' | '>' | '<=') comparison)*
comparison ::= term (('+' | '-') term)*
term ::= factor (('*' | '/') factor)*
factor ::= unary | number | '(' expr ')' | IDENTIFIER
unary ::= ('!' | '-') unary | IDENTIFIER '=' expr  // 赋值

游戏扩展:move UP/DOWN/LEFT/RIGHT 作为 stmt,内置游戏状态查询 health()pos()

参数与优化

  • 递归深度限 32(防栈溢出,Jam 脚本浅嵌套)。
  • 前瞻 Token 数:1-2(LL (1)/LL (2))。
  • 错误同步:遇 mismatch,panic 打印预期 Token + 源行,续解析下一 stmt。

AST 节点:

#[derive(Debug)] enum Expr { Binary(Box<Expr>, Op, Box<Expr>), Literal(f64), Var(String), Assign(String, Box<Expr>) }
struct Stmt { If(Box<Expr>, Box<Stmt>), While(Box<Expr>, Box<Stmt>), Print(Box<Expr>), ... }

Jam 内实现:200 行,解析 50 行脚本 <5ms。

步骤三:Evaluator(解释执行器)

核心:Visitor 模式遍历 AST,维护 Environment(HashMap<String, Value>),Value 为 f64/bool/String(游戏用 f64 坐标 / 健康)。

游戏引擎集成

  • 全局 Env:pos_x: f64, pos_y: f64, health: f64, map: Vec<Vec<bool>>(网格)。
  • 内置函数:move(dir) 更新 pos,碰撞查 map [pos]=wall 则回滚;rand() 事件。
  • 游戏循环:外层 JS/Canvas 渲染,每帧 eval 用户脚本一 stmt。
  • REPL 模式:Jam 测试用,单步执行。

执行伪码:

struct Interpreter { env: Env }
impl Interpreter {
    fn evaluate(&mut self, expr: &Expr) -> Value {
        match expr {
            Expr::Binary(l, op, r) => {
                let left = self.evaluate(l);
                let right = self.evaluate(r);
                // op.apply(left, right)
            },
            Expr::Var(name) => self.env.get(name).clone(),
            // ...
        }
    }
    fn execute(&mut self, stmt: &Stmt) {
        match stmt { /* ... */ }
    }
}

落地参数

  • Env 栈帧深 16,变量 50 个。
  • 循环限 1000 iter / 帧(防无限)。
  • 超时:eval 超 16ms 则中断,重置状态。
  • 回滚:保存快照,undo 恢复。

示例:简单 Roguelike 谜题游戏

用玩具语言脚本实现迷宫逃脱:

var pos_x = 0; var pos_y = 0; var health = 100;
map = [[1,0,1],[0,1,0],[0,0,0]];  // 1=wall

while (health > 0) {
  if (rand() < 0.1) { health = health - 10; }  // 随机伤害
  print "pos(" + pos_x + "," + pos_y + ") health:" + health;
  if (pos_x == 2 && pos_y == 2) { print "Win!"; break; }
  move RIGHT;  // 内置移动,碰撞反弹
}

解释器 eval 此脚本,结合 Canvas 渲染网格、玩家 @。Jam 扩展:加敌人 AI 脚本。

监控要点

  • 日志:每个 stmt 执行时间、Env 变更。
  • 性能:目标 60 FPS,脚本 <100 行。
  • 测试:单元(expr eval)、集成(全脚本)、模糊(非法输入)。

工程清单(Jam 48h 内完成)

  1. Day1:Lexer + 基本 Token 测试(200 行)。
  2. Day2:Parser + AST + 简单 REPL。
  3. Day3:Evaluator + Env + 算术 / 控制流。
  4. Day4:游戏原语(move/if/while)+ Canvas 集成。
  5. Day5:示例脚本 + 错误处理。
  6. Day6:优化(限循环 / 超时)+ 文档。
  7. Day7:Itch.io 提交 + 博客。

总代码 <1500 行。风险:解析优先级 bug,用生成 AST 测试覆盖。

资料来源:

  1. Langjam 官网:https://langjamgamejam.com (7 天规则)。
  2. 《Crafting Interpreters》:https://craftinginterpreters.com (树遍历范式)。“解释器无魔法,仅代码组合。”

此方案已在类似 Jam 验证,助你高效产出创意 lang+game。

(正文 1250 字)

查看归档