编译原理之词法分析(一)

476 阅读3分钟

需求: 实现 jsx 转 js 的编译器 github: github.com/vaynevayne/…

之前

<h1 id='title'><span>hello</span> world </h1>

之后

Rect.createElement("h1",{
 id:"title"
}, React.createElement('span',null,"hello"),"world")

编译器工作流:

  1. 解析(Parse)将源代码转换成抽象语法书 ast
  2. 遍历(Traversal) 遍历 ast
  3. 转换(transformation) 编辑 ast,例如js转java
  4. 代码生成(Code generate) 将 ast 转换成新代码
Parse

分为两个阶段: 词法分析 和 语法分析

  • 词法分析: 源代码转 token
  • 语法分析: token 转 ast

原始jsx代码

<h1 id='title'><span>hello</span> world </h1>

token:

[
    { type: 'Punctuator', value: '<' },
    { type: 'JSXIdentifier', value: 'h1' },
    { type: 'JSXIdentifier', value: 'id' },
    { type: 'Punctuator', value: '=' },
    { type: 'String', value: "'title'" },
    { type: 'Punctuator', value: '>' },
    { type: 'Punctuator', value: '<' },
    { type: 'JSXIdentifier', value: 'span' },
    { type: 'Punctuator', value: '>' },
    { type: 'JSXText', value: 'hello' },
    { type: 'Punctuator', value: '<' },
    { type: 'Punctuator', value: '/' },
    { type: 'JSXIdentifier', value: 'span' },
    { type: 'Punctuator', value: '>' },
    { type: 'JSXText', value: ' world ' },
    { type: 'Punctuator', value: '<' },
    { type: 'Punctuator', value: '/' },
    { type: 'JSXIdentifier', value: 'h1' },
    { type: 'Punctuator', value: '>' }
  ]

可以借助 esprima 看一下效果

/**
 * 1. 把源代码进行分词, 得到一个token数组
 * 2. 把token数组转成一个抽象语法树
 */
let esprima = require("esprima");
let sourceCode = `<h1 id='title'><span>hello</span> world </h1>`;

let ast = esprima.parseModule(sourceCode, { jsx: true, tokens: true }); // 打印token

console.log(ast);

如何分词? 需要借助有限状态机的概念

有限状态机

  • 每一个状态都是一个机器, 每个机器都可以接受输入和计算输出
  • 机器本身没有状态,每一个机器会根据输入决定下一个状态

先看一个简单的demo, 支持 10+20 的分词

let NUMBERS = /[0-9]/;
const Numeric = "Numeric";
const Punctuator = "Punctuator";

let tokens = [];

let currentToken;
// 确定一个新token
function emit(token) {
  currentToken = { type: "", value: "" };
  tokens.push(token);
}

/**
 *
 * start 表示开始状态
 * 它是一个函数, 接受一个字符,返回下一个状态函数
 */
function start(char) {
  //char =1
  if (NUMBERS.test(char)) {
    // 如果char是一个数字的话
    currentToken = { type: Numeric, value: "" };
  }
  // 进入新的状态, 收集number数字的状态
  return number(char);
}

function number(char) {
  if (NUMBERS.test(char)) {
    // 如果char是一个数字的话
    currentToken.value += char;
  } else if (char === "+" || char === "-") {
    emit(currentToken);
    emit({
      type: Punctuator,
      value: char,
    });
    currentToken = { type: Numeric, value: "" };
  }
  return number;
}

function tokenizer(input) {
  // 刚开始的时候是char的状态
  let state = start;
  for (let char of input) {
    state = state(char);
  }
  if (currentToken.value.length > 0) {
    emit(currentToken);
  }
}
// 10 + 20
tokenizer("10+20");
console.log(tokens);
/**
* [
*  { type: 'Numeric', value: '10' },
*  { type: 'Punctuator', value: '+' },
*  { type: 'Numeric', value: '20' }
* ]
*/

词法分析实现

// ./tokenTypes
exports.LeftParentheses = "LeftParentheses"; // <
exports.JSXIdentifier = "JSXIdentifier"; //标识符
exports.AttributeKey = "AttributeKey"; // 属性的key
exports.AttributeStringValue = "AttributeStringValue"; // 字符串格式的属性值
exports.AttributeExpressionValue = "AttributeExpressionValue"; // 变量的属性值
exports.RightParentheses = "RightParentheses"; // > 开始标签的结束
exports.JSXText = "JSXText"; // 文本
exports.BackSlash = "BackSlash"; // 反斜杠


// tokenizer.js
const LETTERS = /[a-z0-9]/;
const tokenTypes = require("./tokenTypes");
let currentToken = { type: "", value: "" };



const tokens = [];
function emit(token) {
  currentToken = { type: "", value: "" };
  tokens.push(token);
}

function start(char) {
  if (char === "<") {
    emit({ type: tokenTypes.LeftParentheses, value: "<" });
    return foundLeftParentheses; // 找到<
  }
  throw new Error("第一个字符必须是<");
}
// end of fire
function eof() {
  if (currentToken.value.length > 0) {
    emit(currentToken);
  }
}
function foundLeftParentheses(char) {
  // h1
  console.log("foundLeftParentheses", char);
  if (LETTERS.test(char)) {
    // 如果char是一个小写字母或数字
    currentToken.type = tokenTypes.JSXIdentifier;
    currentToken.value += char; // h
    return jsxIdentifier; // 继续收集标识符
  } else if (char === "/") {
    emit({ type: tokenTypes.BackSlash, value: "/" });
    console.log("tokens", tokens);
    return foundLeftParentheses; // 这里借助左边的来找
  }
}

function jsxIdentifier(char) {
  if (LETTERS.test(char)) {
    currentToken.value += char;
    return jsxIdentifier;
  } else if (char === " ") {
    // 遇到空格
    emit(currentToken);
    return attribute;
  } else if (char === ">") {
    // 说明没有属性 直接结束
    emit(currentToken);
    emit({ type: tokenTypes.RightParentheses, value: ">" });
    return foundRightParentheses;
  }
  //   return eof;
}

function attribute(char) {
  // i
  if (LETTERS.test(char)) {
    // 将会是key
    currentToken.type = tokenTypes.AttributeKey;
    currentToken.value += char;
    return attributeKey;
  }
  throw new TypeError("Error");
}

function attributeKey(char) {
  if (LETTERS.test(char)) {
    currentToken.value += char;
    return attributeKey;
  } else if (char === "=") {
    // 属性key的名字已经结束了
    emit(currentToken);
    return attributeValue;
  }
}

function attributeValue(char) {
  // char = "
  if (char === '"') {
    currentToken.type = tokenTypes.AttributeStringValue;
    currentToken.value = char;
    return attributeStringValue; // 开始读字符串属性值
  } else if (char === "{") {
    currentToken.type = tokenTypes.AttributeExpressionValue;
    currentToken.value = char;
    return attributeExpressionValue;
  }
}
function attributeExpressionValue(char) {
  if (LETTERS.test(char)) {
    currentToken.value += char;
    return attributeExpressionValue;
  } else if (char === "}") {
    // 说明字符串的值结束了
    currentToken.value += char;
    emit(currentToken); // {type:'AttributeStringValue', value:'title' }
    return tryLeaveAttribute;
  }
  throw new TypeError("Error");
}
function attributeStringValue(char) {
  // t
  if (LETTERS.test(char)) {
    currentToken.value += char;
    return attributeStringValue;
  } else if (char === '"') {
    // 说明字符串的值结束了
    currentToken.value += char;
    emit(currentToken); // {type:'AttributeStringValue', value:'title' }
    return tryLeaveAttribute;
  }
  throw new TypeError("Error");
}
// 后面可能是一个新属性,也坑是开始标签的结束
function tryLeaveAttribute(char) {
  if (char === " ") {
    return attribute; // 后面是空格, 说明后面是一个新属性
  } else if (char === ">") {
    // '<h1 id="title">
    emit({
      type: tokenTypes.RightParentheses,
      value: ">",
    });
    return foundRightParentheses;
  }
}
function foundRightParentheses(char) {
  if (char === "<") {
    // '<h1 id="title"><
    emit({ type: tokenTypes.LeftParentheses, value: "<" });
    return foundLeftParentheses; // 找到<
  } else {
    // <h1 id='title'><span>h
    currentToken.type = tokenTypes.JSXText;
    currentToken.value += char;
    return jsxText;
  }
}
function jsxText(char) {
  if (char === "<") {
    emit(currentToken); // {type:'JSXText',value:'hello'}
    emit({ type: tokenTypes.LeftParentheses, value: "<" });
    return foundLeftParentheses;
  } else {
    currentToken.value += char;
    return jsxText;
  }
}

function tokenizer(input) {
  let state = start;
  for (let char of input) {
    if (state) state = state(char);
  }
  return tokens;
}

module.export = {
  tokenizer,
};

let sourceCode = `<h1 id="title" name={name}><span>hello</span>world</h1>`;
console.log(tokenizer(sourceCode));