控制流:if / while 与代码块

目标

表达式语言还不够用。本节课把语言升级成能写"程序":支持 if / else、while 循环、用 {} 包裹的代码块,以及变量重新赋值。我们用它算出 1 到 10 的和。

运行环境:Node.js v18+。运行方式:保存为 flow.js,执行 node flow.js。

// flow.js —— 完整可运行:词法 + 语法 + 环境 + 控制流
const KEYWORDS = new Set(['var', 'if', 'else', 'while'])
function tokenize(src) {
  const tokens = []; let i = 0
  const isD = c => c >= '0' && c <= '9'
  const isA = c => (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
  while (i < src.length) {
    const c = src[i]
    if (c === ' ' || c === '\n' || c === '\t' || c === '\r') { i++; continue }
    if (isD(c)) { const s = i; while (i < src.length && isD(src[i])) i++; tokens.push({ type: 'num', value: Number(src.slice(s, i)) }); continue }
    if (isA(c)) { const s = i; while (i < src.length && (isA(src[i]) || isD(src[i]))) i++; const w = src.slice(s, i); tokens.push(KEYWORDS.has(w) ? { type: 'kw', value: w } : { type: 'id', value: w }); continue }
    if (c === '<' || c === '>') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: c + '=' }); i += 2; continue } tokens.push({ type: 'op', value: c }); i++; continue }
    if (c === '=') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: '==' }); i += 2; continue } tokens.push({ type: 'eq', value: c }); i++; continue }
    if (c === '!') { if (src[i + 1] === '=') { tokens.push({ type: 'op', value: '!=' }); i += 2; continue } throw new Error('不支持单独的 !') }
    const one = { '+': 'plus', '-': 'minus', '*': 'star', '/': 'slash', '(': 'lparen', ')': 'rparen', ';': 'semi', '{': 'lbrace', '}': 'rbrace' }[c]
    if (one) { tokens.push({ type: one, value: c }); i++; continue }
    throw new Error('非法字符: ' + c)
  }
  tokens.push({ type: 'eof', value: null }); return tokens
}
function parse(src) {
  const tokens = tokenize(src); let pos = 0
  const peek = () => tokens[pos], next = () => tokens[pos++]
  const expect = t => { if (peek().type !== t) throw new Error('语法错误:期望 ' + t); return next() }
  function parseExpr() {
    let n = parseCmp()
    while (peek().type === 'plus' || peek().type === 'minus') { const op = next().value; n = { type: 'binary', op, left: n, right: parseCmp() } }
    return n
  }
  function parseCmp() {
    let n = parseTerm()
    const cmp = new Set(['<', '>', '<=', '>=', '==', '!='])
    while (cmp.has(peek().value)) { const op = next().value; n = { type: 'binary', op, left: n, right: parseTerm() } }
    return n
  }
  function parseTerm() {
    let n = parseFactor()
    while (peek().type === 'star' || peek().type === 'slash') { const op = next().value; n = { type: 'binary', op, left: n, right: parseFactor() } }
    return n
  }
  function parseFactor() {
    const t = peek()
    if (t.type === 'minus') { next(); return { type: 'unary', op: '-', operand: parseFactor() } }
    if (t.type === 'num') { next(); return { type: 'num', value: t.value } }
    if (t.type === 'id') {
      next()
      if (peek().type === 'eq') { next(); return { type: 'assign', name: t.value, value: parseExpr() } }
      return { type: 'ident', name: t.value }
    }
    if (t.type === 'lparen') { next(); const e = parseExpr(); expect('rparen'); return e }
    throw new Error('无法解析表达式: ' + t.type)
  }
  function parseStmt() {
    const t = peek()
    if (t.type === 'kw' && t.value === 'var') {
      next(); const name = expect('id').value; expect('eq'); const init = parseExpr(); expect('semi'); return { type: 'var', name, init }
    }
    if (t.type === 'kw' && t.value === 'if') {
      next(); expect('lparen'); const test = parseExpr(); expect('rparen')
      const cons = parseStmt(); let alt = null
      if (peek().type === 'kw' && peek().value === 'else') { next(); alt = parseStmt() }
      return { type: 'if', test, cons, alt }
    }
    if (t.type === 'kw' && t.value === 'while') {
      next(); expect('lparen'); const test = parseExpr(); expect('rparen'); const body = parseStmt()
      return { type: 'while', test, body }
    }
    if (t.type === 'lbrace') { next(); const stmts = []; while (peek().type !== 'rbrace') stmts.push(parseStmt()); expect('rbrace'); return { type: 'block', stmts } }
    const e = parseExpr(); expect('semi'); return { type: 'exprstmt', expr: e }
  }
  const prog = []; while (peek().type !== 'eof') prog.push(parseStmt()); return prog
}
class Environment {
  constructor(parent = null) { this.vars = new Map(); this.parent = parent }
  define(n, v) { this.vars.set(n, v) }
  get(n) { let e = this; while (e) { if (e.vars.has(n)) return e.vars.get(n); e = e.parent } throw new Error('未定义变量: ' + n) }
  assign(n, v) { let e = this; while (e) { if (e.vars.has(n)) { e.vars.set(n, v); return } e = e.parent } this.vars.set(n, v) }
}
function ev(node, env) {
  switch (node.type) {
    case 'num': return node.value
    case 'ident': return env.get(node.name)
    case 'unary': return -ev(node.operand, env)
    case 'binary': {
      const a = ev(node.left, env), b = ev(node.right, env)
      switch (node.op) {
        case '+': return a + b; case '-': return a - b; case '*': return a * b; case '/': return a / b
        case '<': return a < b; case '>': return a > b; case '<=': return a <= b; case '>=': return a >= b
        case '==': return a === b; case '!=': return a !== b
      }
    }
    case 'assign': { const v = ev(node.value, env); env.assign(node.name, v); return v }
    case 'var': { const v = ev(node.init, env); env.define(node.name, v); return v }
    case 'exprstmt': return ev(node.expr, env)
    case 'block': { const b = new Environment(env); let r; for (const s of node.stmts) r = ev(s, b); return r }
    case 'if': return ev(node.test, env) ? ev(node.cons, env) : (node.alt ? ev(node.alt, env) : undefined)
    case 'while': { let r; while (ev(node.test, env)) r = ev(node.body, env); return r }
  }
  throw new Error('未知节点: ' + node.type)
}
function run(src) {
  const env = new Environment()
  let last
  for (const s of parse(src)) last = ev(s, env)
  return { last, env }
}

// ===== 调用示例:求 1 到 10 的和 =====
const { env } = run(`
var sum = 0;
var i = 1;
while (i <= 10) {
  sum = sum + i;
  i = i + 1;
}
`)
console.log('1 到 10 的和 =', env.get('sum')) // 55

名词解释

控制流(Control Flow):程序执行的路径分支,靠 if / while / for 等语句改变"下一步执行哪一段"。没有它,代码只能从上往下直线跑。
代码块(Block):用 {} 括起来的一组语句,拥有自己的作用域。进入块时新建 Environment,离开即销毁——这正是 let 块级作用域的来源。
重新赋值(assign):x = x + 1 不是声明,而是沿作用域链找到已有 x 并改写。它和 var x = ...(声明)走的是 assign / define 两条不同路径。

课后练习

练习:用本课的解释器算 1 到 100 中所有偶数的和。

答案:把上面的调用示例换成:

const { env } = run(`
var s = 0;
var i = 1;
while (i <= 100) {
  if (i != (i / 2) * 2) { s = s + i; }
  i = i + 1;
}
`)
console.log('偶数和 =', env.get('s')) // 2550

总结

控制流让"表达式语言"升级为"能解决问题的程序语言"。关键设计有两点:其一,if / while 的 body 通过 parseStmt 复用,所以 if 里可以再套 while、再套 if——语法的递归性在这里体现为"语句能无限嵌套";其二,代码块每次进入都 new 一个 Environment,这让"局部变量只在块内有效"成为天然结果,不需要特殊规则。你写的 while 循环能正确累加,本质上是解释器在反复 ev(node.body),而 assign 沿作用域链把新值写回同一个 i。语言的能力,就是这样一层层叠出来的。