创建一个编程语言的方法

172 阅读5分钟

现在,你可能已经熟悉了一种或多种编程语言。但你有没有想过,你如何能创造你自己的编程语言?我的意思是说。

一种编程语言是将字符串转换为各种机器码输出的任何一组规则。

简而言之,一种编程语言只是一组预定义的规则。为了使它们有用,你需要一些能理解这些规则的东西。而这些东西就是编译器解释器,等等。因此,我们可以简单地定义一些规则,然后,为了使其发挥作用,我们可以使用任何现有的编程语言来制作一个能够理解这些规则的程序,这将是我们的解释器。

编译器

编译器将代码转换为处理器可以执行的机器代码(例如,C++编译器)。

解释器

解释器逐行浏览程序并执行每条命令。

想试一试吗?让我们一起创建一个超级简单的编程语言,在控制台输出品红色的输出。我们就叫它洋红色

Screenshot of terminal output in color magenta.

我们的简单编程语言创建了一个包含文本的代码变量,这些文本被打印到控制台......当然是洋红色的。

设置我们的编程语言

我将使用Node.js,但你可以使用任何语言来跟随,其概念将保持不变。让我从创建一个index.js 文件开始,把事情设置好。

class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  run() {
    console.log(this.codes)
  }
}

// For now, we are storing codes in a string variable called `codes`
// Later, we will read codes from a file
const codes = 
`print "hello world"
print "hello again"`
const magenta = new Magenta(codes)
magenta.run()

我们在这里要做的是声明一个名为Magenta 的类。该类定义并启动了一个对象,负责将我们通过codes 变量提供给它的任何文本记录到控制台。而且,就目前而言,我们直接在文件中用几个 "hello "信息定义了那个codes 变量。

Screenshot of terminal output.

如果我们运行这段代码,我们将得到存储在代码中的文本记录在控制台中。

好了,现在我们需要创建一个所谓的Lexer。

什么是Lexer?

好吧,让我们先来谈谈英语。以下面这个短语为例。

How are you?

这里,"How "是副词,"are "是动词,而 "you "是代词。我们在结尾处还有一个问号("?")。我们可以在JavaScript中把任何像这样的句子或短语划分为许多语法成分。另一种我们可以区分这些部分的方法是将它们分成小的标记。将文本划分为标记的程序就是我们的词典

Diagram showing command going through a lexer.

由于我们的语言非常小,它只有两种类型的标记,每种标记都有一个值。

  1. keyword
  2. string

我们本来可以用正则表达式从codes 字符串中提取托克,但性能会非常慢。一个更好的方法是循环浏览code 字符串的每个字符,并抓取标记。因此,让我们在我们的Magenta 类中创建一个tokenize 方法--它将是我们的词典。

完整的代码

class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  tokenize() {
    const length = this.codes.length
    // pos keeps track of current position/index
    let pos = 0
    let tokens = []
    const BUILT_IN_KEYWORDS = ["print"]
    // allowed characters for variable/keyword
    const varChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
    while (pos < length) {
      let currentChar = this.codes[pos]
      // if current char is space or newline,  continue
      if (currentChar === " " || currentChar === "\n") {
        pos++
        continue
      } else if (currentChar === '"') {
        // if current char is " then we have a string
        let res = ""
        pos++
        // while next char is not " or \n and we are not at the end of the code
        while (this.codes[pos] !== '"' && this.codes[pos] !== '\n' && pos < length) {
          // adding the char to the string
          res += this.codes[pos]
          pos++
        }
        // if the loop ended because of the end of the code and we didn't find the closing "
        if (this.codes[pos] !== '"') {
          return {
            error: `Unterminated string`
          }
        }
        pos++
        // adding the string to the tokens
        tokens.push({
          type: "string",
          value: res
        })
      } else if (varChars.includes(currentChar)) { arater
        let res = currentChar
        pos++
        // while the next char is a valid variable/keyword charater
        while (varChars.includes(this.codes[pos]) && pos < length) {
          // adding the char to the string
          res += this.codes[pos]
          pos++
        }
        // if the keyword is not a built in keyword
        if (!BUILT_IN_KEYWORDS.includes(res)) {
          return {
            error: `Unexpected token ${res}`
          }
        }
        // adding the keyword to the tokens
        tokens.push({
          type: "keyword",
          value: res
        })
      } else { // we have a invalid character in our code
        return {
          error: `Unexpected character ${this.codes[pos]}`
        }
      }
    }
    // returning the tokens
    return {
      error: false,
      tokens
    }
  }
  run() {
    const {
      tokens,
      error
    } = this.tokenize()
    if (error) {
      console.log(error)
      return
    }
    console.log(tokens)
  }
}

如果我们用node index.js ,在终端运行这个方法,我们应该看到控制台中打印出一个标记的列表。

Screenshot of code.

很好的东西!

定义规则和句法

我们想看看我们的代码的顺序是否符合某种规则或语法。但首先我们需要定义这些规则和句法是什么。由于我们的语言是如此之小,它只有一种简单的语法,即一个print 关键字,后面是一个字符串。

keyword:print string

因此,让我们创建一个parse 方法,循环浏览我们的令牌,看看我们是否有一个有效的语法形成。如果是的话,它将采取必要的行动。

class Magenta {
  constructor(codes) {
    this.codes = codes
  }
  tokenize(){
    /* previous codes for tokenizer */
  }
  parse(tokens){
    const len = tokens.length
    let pos = 0
    while(pos < len) {
      const token = tokens[pos]
      // if token is a print keyword
      if(token.type === "keyword" && token.value === "print") {
        // if the next token doesn't exist
        if(!tokens[pos + 1]) {
          return console.log("Unexpected end of line, expected string")
        }
        // check if the next token is a string
        let isString = tokens[pos + 1].type === "string"
        // if the next token is not a string
        if(!isString) {
          return console.log(`Unexpected token ${tokens[pos + 1].type}, expected string`)
        }
        // if we reach this point, we have valid syntax
        // so we can print the string
        console.log('\x1b[35m%s\x1b[0m', tokens[pos + 1].value)
        // we add 2 because we also check the token after print keyword
        pos += 2
      } else{ // if we didn't match any rules
        return console.log(`Unexpected token ${token.type}`)
      }
    }
  }
  run(){
    const {tokens, error} = this.tokenize()
    if(error){
      console.log(error)
      return
    }
    this.parse(tokens)
  }
}

你看--我们已经有了一种有效的语言!

Screenshot of terminal output.

好吧,但是把代码放在一个字符串变量中并不那么有趣。因此,让我们把我们的洋红色代码放在一个叫做code.m 的文件中。这样我们就可以把我们的洋红色代码与编译器的逻辑分开。我们使用.m 作为文件扩展名,以表明该文件包含我们语言的代码。

让我们从该文件中读取代码。

// importing file system module
const fs = require('fs')
//importing path module for convenient path joining
const path = require('path')
class Magenta{
  constructor(codes){
    this.codes = codes
  }
  tokenize(){
    /* previous codes for tokenizer */
 }
  parse(tokens){
    /* previous codes for parse method */
 }
  run(){
    /* previous codes for run method */
  }
}

// Reading code.m file
// Some text editors use \r\n for new line instead of \n, so we are removing \r
const codes = fs.readFileSync(path.join(__dirname, 'code.m'), 'utf8').toString().replace(/\r/g, &quot;&quot;)
const magenta = new Magenta(codes)
magenta.run()

去创造一种编程语言吧!

就这样,我们成功地从头创建了一个小小的编程语言。看,一种编程语言可以简单到只完成一件特定的事情。当然,像Magenta这样的语言不太可能有用到成为流行框架或任何东西的一部分,但现在你看到了制作一个语言需要什么。

天空真的是无限的。如果你想再深入一点,可以试着跟着我做的这个视频来学习一个更高级的例子。这段视频中我还展示了如何在你的语言中添加变量。