到目前为止,您可能已经熟悉了一种或多种编程语言。但是您是否曾经想过如何创建自己的编程语言?我的意思是
编程语言是任何将字符串转换为各种机器代码输出的规则集。
简而言之,编程语言只是一组预定义的规则。为了使其有用,您需要一些能够理解这些规则的东西。这些东西就是编译器、解释器等。因此,我们可以简单地定义一些规则,然后,为了使其工作,我们可以使用任何现有的编程语言来编写一个可以理解这些规则的程序,这将是我们的解释器。
编译器
编译器将代码转换为处理器可以执行的机器代码(例如 C++ 编译器)。
解释器
解释器逐行遍历程序并执行每个命令。
想试一试吗?让我们一起创建一个超级简单的编程语言,它在控制台中输出品红色输出。我们将其称为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
变量提供的任何文本将文本记录到控制台。并且,目前,我们在文件中直接定义了该codes
变量,其中包含几个“hello”消息。

好的,现在我们需要创建一个称为词法分析器的东西。
什么是词法分析器?
好的,让我们先谈谈英语。以以下短语为例
你好吗?
这里,“How”是副词,“are”是动词,“you”是代词。我们最后还有一个问号(“?”)。我们可以像这样将任何句子或短语划分为许多语法成分在 JavaScript 中。另一种区分这些部分的方法是将它们分成小的标记。将文本划分为标记的程序是我们的词法分析器。

由于我们的语言非常小,它只有两种类型的标记,每种都有一个值
关键字
字符串
我们可以使用正则表达式从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)) {
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
运行它,我们应该会看到控制台中打印出一系列标记。

定义规则和语法
我们想看看我们的代码的顺序是否与某种规则或语法匹配。但首先我们需要定义这些规则和语法是什么。由于我们的语言非常小,它只有一个简单的语法,即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)
}
}
你看,我们已经拥有了一门可以工作的语言了!

好的,但是将代码放在字符串变量中并不那么有趣。所以让我们将我们的Magenta代码放在一个名为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, "")
const magenta = new Magenta(codes)
magenta.run()
去创建一个编程语言!
就这样,我们成功地从头开始创建了一个微小的编程语言。看,编程语言可以像完成一项特定任务一样简单。当然,像这里的 Magenta 这样的语言不太可能有用到足以成为流行框架的一部分或其他什么,但现在您了解了创建它需要哪些步骤。
天空才是极限。如果您想更深入地了解,请尝试观看我制作的这段视频,其中介绍了一个更高级的示例。这段视频还展示了如何向您的语言添加变量。
非常棒的解释器入门介绍!我不常看到这个主题被介绍,而且没有这么清晰。感谢您创建这个!
太棒了
多么复杂的语言。这个例子对我来说是一个很好的参考,感谢您分享这些信息。
喜欢它!
生活本来就很简单,我们只是用恐惧把它复杂化了。
我不会说这个例子解决了所有问题,但视频非常好。
非常感谢!
当您想向您的语言添加数学运算时,所有这些代码都变得毫无用处,您需要单独创建一个解析器才能真正做出好的东西,而不是硬编码所有内容
对于网站速度来说,最好的方法是什么?
为 c++ 构建我自己的编译器,还是使用 golang,还是创建一种私有语言?
我用 Python 创建了一个名为 Arbito 的版本,但您只能键入
disp Hello World!
。否则,您将收到“语法错误:第 (行) 行出错
”消息。