The Super Tiny Compiler(1)- Tokenizer

117 阅读8分钟

Tokenizer

Token 类型

定义一个名为 TokenType 的枚举和一个名为 Token 的接口。

  • export const enum TokenType {...} 是一个常量的,并且在编译时被内联,以减少运行时的开销。这里定义了四个令牌类型:PARENNUMBERSTRINGNAME,它们分别代表括号、数字、字符串和名称。在 Tokenizer 类中,这些类型可以用来表示不同类型的标记,以便于对输入的代码进行分类和解析。
  • export type Token = {...} 是一个类型别名,它定义了一个名为 Token 的接口。这个接口有两个属性:typevaluetype 属性的类型是 TokenType,这意味着它只能是枚举 TokenType 中的四个值之一。value 属性是一个字符串,表示令牌的值。在使用这个接口时,你可以创建符合这个接口定义的对象,这些对象可以被 TypeScript 的类型系统理解和约束。
export const enum TokenType {
  PAREN,
  NUMBER,
  STRING,
  NAME,
} 

export type Token = {
  type: TokenType,
  value: string,
}

工具类

先实现一些工具类以方便后面使用。

TokenList

首先定义了一个名为 TokenList 的类。这个类的主要功能是管理一个名为 _tokens 的私有属性,这个属性是一个数组,用来存储类型为 T 的标记(tokens)。

  • get value(): T[] {...}:这是一个 getter 方法,允许其他代码片段访问 _tokens 数组。
  • constructor(tokens?: T[]){...}:这是类的构造函数。它接受一个可选的 tokens 参数。如果在创建 TokenList 实例时提供了 tokens 数组,它将复制这个数组来初始化 _tokens 属性。
  • addToken(token: T){...}:上添加新的标记。新的标记会被添加到 _tokens 数组的末尾。
  • clear(){...}:这个方法重置 _tokens 数组为空。
export class TokenList<T> {
  private _tokens: T[] = []
  get value() {
    return this._tokens
  }
  constructor(tokens?: T[]) {
    if (tokens) {
      this._tokens = [...tokens]
    }
  }
  addToken(token: T) {
    this._tokens.push(token)
  }
  clear() {
    this._tokens = []
  }
}

TokenList 类提供了一个灵活的接口,让我们可以轻松地管理一组标记,在实际开发中可以帮助你组织和操作标记化的代码或数据。

Char

编写的一个名为 Char 的类。它有一个私有属性 _value,用于存储一个字符。类中定义了 getter 和 setter 方法,允许外部代码片段访问和修改 _value 属性。Char 类提供了一种清晰的方式来处理字符数据,通过封装字符值和相关操作,使得代码更加模块化和可维护。

value 获取器允许外部读取 Char 实例的 _value 属性,is 和 not 方法用于检查 _value 属性是否等于或不等于某个特定字符。这种设计模式使得 Char 类可以作为一个封装字符的容器,并且可以灵活地与其他部分代码交互。例如,你可以创建一个 Char 实例来表示某个特定的字符,然后使用它的方法来执行各种逻辑,例如比较、匹配或转换。

export class Char {
  private _value: string
  get value() {
    return this._value
  }
  set value(c: string) {
    this._value = c
  }
  constructor(c?: string) {
    if (c) {
      this._value = c
    }
  }
  is(c: string) {
    return this._value === c
  }
  not(c: string) {
    return !this.is(c)
  }
}

具体实现

这里写了一个 BaseTokenizer 的抽象类,后面的 Tokenizer 仅需实现里面的 tokenize() 方法即可。这个类作为一个基类,用于创建特定的标记生成器,这些标记生成器可以处理不同类型的标记。

  1. export abstract class BaseTokenizer<T> {...}:这行代码声明了一个可导出的抽象基类 BaseTokenizer,并引入了一个泛型类型 T。这意味着任何继承自 BaseTokenizer 的类都需要指定 T 的具体类型,以表示标记的类型。

  2. private _tokens: T[] = []:这个私有属性是一个泛型数组,用于存储标记。它被初始化为一个空数组,以便在 tokenize 方法中生成的标记可以被存储在这里。

  3. private _rawSourceCode: string = ```:另一个私有属性,用于保存原始源代码字符串。这个字符串将由 tokenize` 方法解析以生成标记。

  4. constructor(rawSourceCode: string) {...}:构造函数用于初始化 BaseTokenizer 类的实例,它接收一个字符串参数 rawSourceCode。这个字符串代表了需要被标记化的源代码。

  5. public run(): T[] {...}run 方法作为一个公共方法,允许外部代码片段调用它来执行标记化过程。当调用 run 方法时,它调用了受保护的抽象方法 tokenize,并将返回的标记数组存储在 _tokens 属性中。然后,run 方法返回这个标记数组,使得 BaseTokenizer 的用户可以访问生成的标记。

  6. protected abstract tokenize(rawSourceCode: string): T[] {...}tokenize 方法是一个抽象的受保护方法,它必须在继承 BaseTokenizer 的子类中被实现。这个方法负责解析原始源代码字符串,并返回一个类型为 T 的标记数组。因为它是抽象的,所以 BaseTokenizer 类本身并不提供这个方法的具体实现,而是留给子类来完成。

BaseTokenizer 类提供了一个框架,让开发者可以创建自定义的标记器,这些标记器可以解析和生成不同类型的标记。通过继承 BaseTokenizer 并实现 tokenize 方法,开发者可以根据具体的需求来定制标记化逻辑。run 方法提供了一个标准化的接口,使得标记化过程可以很容易地被触发和访问。这种设计模式在软件开发中非常有用,特别是在需要处理和分析代码或文本数据的领域,如编译器、文本编辑器、自然语言处理等。

export abstract class BaseTokenizer<T> {
  private _tokens: T[] = []
  private _rawSourceCode: string = ``

  constructor(rawSourceCode: string) {
    this._rawSourceCode = rawSourceCode
  }

  public run(): T[] {
    this._tokens = this.tokenize(this._rawSourceCode)
    return this._tokens
  }

  protected abstract tokenize(rawSourceCode: string): T[]
}

最后重点是实现 BaseTokenizer 中的 tokenize() 方法。 定义了一个名为 Tokenizer 的类,它继承自 BaseTokenizer 类,并实现了 tokenize 方法。这个 Tokenizer 类用于将原始源代码字符串转换为 Token 对象的数组。

  • import { Token, TokenType } from '../types': 导入 Token 类型和 TokenType 枚举。这允许 Tokenizer 类访问这些类型,以便创建表示不同类型标记的 Token 对象。
  • import { Char, TokenList } from "../utils": 导入 CharTokenListChar 类允许 Tokenizer 类在处理源代码时创建和检查单个字符。TokenList 类提供了管理生成的 Token 对象的数组的功能。
  • import { BaseTokenizer } from '../utils/baseTokenizer': 导入 BaseTokenizer 类作为超类。Tokenizer 类将继承 BaseTokenizer 的属性和方法,并需要实现 tokenize 方法。
  • const RegExpHelper = {...}': 创建一个名为 RegExpHelper 的常量对象,其中包含用于检查空格、数字和字母的正则表达式。这些正则表达式将帮助 Tokenizer 类识别标记的类型。
  • export class: 定义 Tokenizer类。它继承自BaseTokenizer,并使用 Token类型进行泛型。通过继承BaseTokenizerTokenizer` 类获得了处理标记的基本功能。
  • protected tokenize(rawSourceCode: string): Token[] {...}: 实现 tokenize 方法。这是一个受保护的抽象方法,它负责将原始源代码字符串分解成 Token 对象的数组。Tokenizer 类中的实现对源代码进行逐字分析,根据字符的类型或值创建不同类型的 Token
  • while (current < codeLen) {...}: 一个 while 循环,用于遍历原始源代码字符串的每个字符。current 变量表示当前正在检查的字符位置,而 codeLen 表示字符串的总长度。循环将继续,直到 current 达到字符串的末尾。
  • 各种 if/else 块用于检查字符类型并据此创建 Token
    • 根据 RegExpHelper.WHITESPACE 正则表达式,忽略空格字符。
    • 使用自定义的 Char 类检查括号字符,并创建 TokenType.PAREN 类型的 Token
    • 根据 RegExpHelper.NUMBERS 正则表达式,识别数字序列并创建 TokenType.NUMBER 类型的 Token
    • 检查双引号字符,以捕获字符串值。循环持续直到遇到另一个双引号,然后创建 TokenType.STRING 类型的 Token
    • 根据 RegExpHelper.LETTERS 正则表达式,识别字母序列并创建 TokenType.NAME 类型的 Token
  • throw new TypeError(...): 如果遇到一个字符,它不符合上述任何条件,Tokenizer 类将抛出一个错误,指示它无法识别该字符。
  • return tokens.value: 当 tokenize 方法完成时,它返回 TokenList 实例中存储的 Token 对象的数组。value 获取器可能是 TokenList 类的一个方法,用于返回内部 _tokens 数组。

这个 Tokenizer 类提供了一个灵活的方式来将源代码字符串转换为一个有序的 Token 对象数组。通过解析字符和创建适当的 Token,它为后续的语法分析、代码生成或其他需要将源代码表示为结构化数据的任务做准备。

import { Token, TokenType } from '../types'
import { Char, TokenList } from "../utils"
import { BaseTokenizer } from '../utils/baseTokenizer'

const RegExpHelper = {
  WHITESPACE: /\s/,
  NUMBERS: /[0-9]/,
  LETTERS: /[a-z]/i,
} as const

export class Tokenizer extends BaseTokenizer<Token> {
  protected tokenize(rawSourceCode: string): Token[] {
    let current: number = 0
    const codeLen: number = rawSourceCode.length
    const tokens = new TokenList<Token>()
    while (current < codeLen) {
      const char = new Char(rawSourceCode[current])
      if (RegExpHelper.WHITESPACE.test(char.value)) {
        current++
        continue
      }
      if (char.is('(')) {
        tokens.addToken({ type: TokenType.PAREN, value: '(' } as Token)
        current++
        continue
      }
      if (char.is(')')) {
        tokens.addToken({ type: TokenType.PAREN, value: ')' } as Token)
        current++
        continue
      }
      if (RegExpHelper.NUMBERS.test(char.value)) {
        let value = ''
        while (RegExpHelper.NUMBERS.test(char.value)) {
          value += char.value
          char.value = rawSourceCode[++current]
        }
        tokens.addToken({ type: TokenType.NUMBER, value } as Token)
        continue
      }
      if (char.is('"')) {
        let value = ''
        char.value = rawSourceCode[++current]
        while (char.not('"')) {
          value += char.value
          char.value = rawSourceCode[++current]
        }
        char.value = rawSourceCode[++current]
        tokens.addToken({ type: TokenType.STRING, value } as Token)
        continue
      }
      if (RegExpHelper.LETTERS.test(char.value)) {
        let value = ''
        while (RegExpHelper.LETTERS.test(char.value) && current < codeLen) {
          value += char.value
          char.value = rawSourceCode[++current]
        }
        tokens.addToken({ type: TokenType.NAME, value } as Token)
        continue
      }
      throw new TypeError(`I don't know what this character: ${char.value} is.`)
    }
    return tokens.value
  }
}