数据类型:字符串、数组与内建操作

目标

数字之外,真实语言要有字符串和数组。本节课给解释器加上字符串字面量、[1,2,3] 数组字面量,以及几个内建操作 len / push / map。下面给出完整可运行程序。

运行环境:Node.js v18+。运行方式:保存为 data.js,执行 node data.js(在 func.js 基础上扩展字符串/数组与内建)。

// data.js —— 完整可运行:在 func.js 基础上增加 str / 数组 / 内建
const KEYWORDS = new Set(['var', 'if', 'else', 'while', 'func', 'return'])
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 (c === '"') { i++; let str = ''; while (i < src.length && src[i] !== '"') { str += src[i]; i++ } i++; tokens.push({ type: 'str', value: str }); 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 }
    const one = { '+': 'plus', '-': 'minus', '*': 'star', '/': 'slash', '(': 'lparen', ')': 'rparen', ';': 'semi', '{': 'lbrace', '}': 'rbrace', ',': 'comma', '[': 'lbracket', ']': 'rbracket' }[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 = parseTerm()
    while (peek().type === 'plus' || peek().type === 'minus') { 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 === 'str') { next(); return { type: 'str', value: t.value } }
    if (t.type === 'lbracket') {
      next(); const els = []
      if (peek().type !== 'rbracket') { els.push(parseExpr()); while (peek().type === 'comma') { next(); els.push(parseExpr()) } }
      expect('rbracket'); return { type: 'arr', elements: els }
    }
    if (t.type === 'id') {
      next()
      if (peek().type === 'lparen') {
        next(); const args = []
        if (peek().type !== 'rparen') { args.push(parseExpr()); while (peek().type === 'comma') { next(); args.push(parseExpr()) } }
        expect('rparen'); return { type: 'call', callee: { type: 'ident', name: t.value }, args }
      }
      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 === 'func') {
      next(); const name = expect('id').value; expect('lparen')
      const params = []
      if (peek().type !== 'rparen') { params.push(expect('id').value); while (peek().type === 'comma') { next(); params.push(expect('id').value) } }
      expect('rparen'); const body = parseStmt(); return { type: 'func', name, params, 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 'str': return node.value
    case 'arr': return node.elements.map(e => ev(e, env))
    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)
      if (node.op === '+' && (typeof a === 'string' || typeof b === 'string')) return String(a) + String(b)
      switch (node.op) {
        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 'func': { env.define(node.name, { type: 'UserFn', params: node.params, body: node.body, closure: env }); return node.name }
    case 'call': {
      const callee = ev(node.callee, env)
      const args = node.args.map(a => ev(a, env))
      if (typeof callee === 'function') return callee(...args)
      if (callee && callee.type === 'UserFn') {
        const fnEnv = new Environment(callee.closure)
        callee.params.forEach((p, i) => fnEnv.define(p, args[i]))
        try { ev(callee.body, fnEnv) } catch (e) { if (e && e.__ret) return e.value; throw e }
        return undefined
      }
      throw new Error('不是可调用的对象')
    }
  }
  throw new Error('未知节点: ' + node.type)
}
function run(src) {
  const env = new Environment()
  env.define('print', (...xs) => console.log(...xs))
  env.define('len', x => x.length)
  env.define('push', (a, x) => { a.push(x); return a })
  env.define('map', (a, fn) => a.map(e => fn(e)))
  for (const s of parse(src)) ev(s, env)
}

// ===== 调用示例 =====
run(`
print(len("hello"));
var a = [1, 2, 3];
print(map(a, func(x) { return x * 2; }));
print(push(a, 4));
`)

map 把一个"用户函数"当参数传进去,解释器在原生 map 里通过 fn(e) 回调它——这又是一次"函数是一等公民"的实战。

名词解释

字面量(Literal):直接写进代码里的值,如 "hello"(字符串字面量)、[1,2,3](数组字面量)。它不需要计算,本身就是值。
内建函数(Builtin):语言自带的、由宿主(这里是 JS)实现的函数,如 len/push/map。它们和 UserFn 的区别是:内建函数直接是 JS 函数,不需要 AST 就能执行。
高阶函数:接收或返回函数的函数。map 是高阶函数——它接收一个"怎么变换每个元素"的函数,这正是函数式编程的基石。

课后练习

练习:用 map 和 len 计算数组 [10,20,30] 每个元素加 1 后的长度。

答案:

run(`
var a = [10, 20, 30];
var b = map(a, func(x) { return x + 1; });
print(len(b));  // 3
`)

总结

数据类型是语言"能表达什么世界"的边界。加了字符串和数组,你的语言才从"计算器"变成"能处理真实数据"的工具。而 map/filter/reduce 这类高阶内建函数,是把"循环 + 副作用"升级为"声明式变换"的关键——你不再说"怎么一步步改",而是说"我想要每个元素变成什么样"。注意一个设计取舍:数组我用 JS 原生数组承载,字符串用原生字符串,这意味着我们的语言直接复用了宿主语言的数据结构。现实编译器往往不会这么做(为了跨平台/可控内存),但它极其适合教学:让你把精力放在"语言机制"而不是"内存管理"上。