最近在看编译器的书籍,想从0开始写一个简单的自制编程语言练练手,但是同时想到自己现在手头有ChatGPT,于是萌生想法:如果我了解编译器整个设计过程,为什么我还需要重头开始写编译器呢?为何不直接将整个实现思路给到GPT,让GPT帮我实现代码,我只需要考虑如何设计词法和语法,如何优化等过程即可,说干就干,于是开始《ChatGPT|AI自制编程语言》系列文章。
1、试验
在验证《AI自制编程语言》之前,我在想整个流程是否按照想法执行,于是想到golang
实现一个表达式引擎(自制编程语言的第一步就是要实现表达式引擎),如果表达式引擎能按照要求实现,那自制编程语言应该用GPT也可以实现。
(1)需求拆解
先将表达式引擎需求的拆解如下:
- 主功能是用golang实现表达式引擎
- 使用逆波兰表达运算规则的先后顺序
- 支持操作符可以定义为+,-,*,/,(,)
- 支持数据类型可以定义为:number,string
以上需求考虑的如果一次将需求输入给GPT,代码会比较长,而且一旦某个步骤出问题,不得不重新检查或者,所以我们将需求分为三个子步骤:
- 先让GPT帮我们通过字符串表达式生成逆波兰表达式
- 然后GPT帮我们实现逆波兰表达式再计算结果
- 最后GPT帮我们生成字符串表达式计算的测试样例
(2)定义prompt
步骤一:先让GPT帮我们通过字符串表达式生成逆波兰表达式
输入到GPT3.5:
你是一个使用golang开发的资深的程序员,正在开发一个表达式引擎功能,功能需求如下:
- 实现将字符串表达式转换为逆波兰表达式
- 输入是字符串表达式,输出是带空格的逆波兰表达式字符串,同时输出不需要带`(`,`)`,`[`,`]`
- <切记>:只需要支持+,-,*,/,(,)操作符
- 数据类型只包含两种:number,string
- 给出测试用例和main函数
- 代码量控制在最精简的范围
步骤二:然后GPT帮我们实现逆波兰表达式再计算结果
输入到GPT3.5:
你是一个golang开发的资深的程序员,正在开发一个表达式引擎功能,功能需求如下:
- 实现计算逆波兰式字符串表达式的结果
- 输入是逆波兰式字符串表达式,输出是表达式计算的字符串结果
- <切记>:支持+,-,*,/操作
- <切记>:数据类型只包含两种:number,string,需要注意string类型也需要支持+,-,*,/操作
- 代码行增加一些注释
- 给出测试用例和main函数
- 代码量控制在最精简的范围
步骤三:最后GPT帮我们生成字符串表达式计算的测试样例
输入到GPT3.5:
用+,-,*,/,(,)给出一些计算表达式样例和计算结果,要求如下:
- 只用给出整数计算
- 格式如下:fmt.Println(calculate("<表达式>") == "<结果>")
(3)输出代码
步骤一生成的代码如下:
// 运行的代码:
package main
import (
"fmt"
"strings"
)
func isOperator(ch byte) bool {
return ch == '+' || ch == '-' || ch == '*' || ch == '/'
}
func isDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func isLetter(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}
func isSpace(ch byte) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func precedence(ch byte) int {
if ch == '+' || ch == '-' {
return 1
} else if ch == '*' || ch == '/' {
return 2
} else {
return 0
}
}
func toRPN(expr string) string {
var stack []byte
var output []string
for i := 0; i < len(expr); {
if isSpace(expr[i]) {
i++
} else if isOperator(expr[i]) {
for len(stack) > 0 && isOperator(stack[len(stack)-1]) && precedence(stack[len(stack)-1]) >= precedence(expr[i]) {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
stack = append(stack, expr[i])
i++
} else if expr[i] == '(' {
stack = append(stack, expr[i])
i++
} else if expr[i] == ')' {
for len(stack) > 0 && stack[len(stack)-1] != '(' {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
if len(stack) > 0 && stack[len(stack)-1] == '(' {
stack = stack[:len(stack)-1]
}
i++
} else if isDigit(expr[i]) {
start := i
for i < len(expr) && isDigit(expr[i]) {
i++
}
output = append(output, expr[start:i])
} else if isLetter(expr[i]) {
start := i
for i < len(expr) && (isLetter(expr[i]) || isDigit(expr[i])) {
i++
}
output = append(output, expr[start:i])
} else {
panic("Invalid character")
}
}
for len(stack) > 0 {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
return strings.Join(output, " ")
}
func main() {
expr := "3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3"
rpn := toRPN(expr)
fmt.Println(rpn) // Output: 3 4 2 * 1 5 - 2 3 ^ ^ / +
}
// 测试代码:
func TestToRPN(t *testing.T) {
testCases := []struct {
expr string
rpn string
}{
{"3 + 4", "3 4 +"},
{"3 + 4 * 2 / ( 1 - 5 ) ^ 2 ^ 3", "3 4 2 * 1 5 - 2 3 ^ ^ / +"},
{"( 1 + 2 ) * 3 - 4 / 5", "1 2 + 3 * 4 5 / -"},
{"a + b * c - d / e", "a b c * + d e / -"},
}
for _, tc := range testCases {
rpn := toRPN(tc.expr)
if rpn != tc.rpn {
t.Errorf("toRPN(%v) = %v, want %v", tc.expr, rpn, tc.rpn)
}
}
}
步骤二生成的代码的如下:
// 运行的代码:
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
expr := "5 1 2 + 4 * + 3 -"
result := calculate(expr)
fmt.Println(result)
}
func calculate(expr string) string {
stack := make([]string, 0)
operators := map[string]bool{"+": true, "-": true, "*": true, "/": true}
for _, token := range strings.Split(expr, " ") {
if !operators[token] {
stack = append(stack, token)
} else {
b, _ := strconv.Atoi(stack[len(stack)-1])
a, _ := strconv.Atoi(stack[len(stack)-2])
stack = stack[:len(stack)-2]
switch token {
case "+":
stack = append(stack, strconv.Itoa(a+b))
case "-":
stack = append(stack, strconv.Itoa(a-b))
case "*":
stack = append(stack, strconv.Itoa(a*b))
case "/":
stack = append(stack, strconv.Itoa(a/b))
}
}
}
return stack[0]
}
// 测试代码:
func TestCalculate(t *testing.T) {
expr1 := "5 1 2 + 4 * + 3 -"
result1 := calculate(expr1)
if result1 != "14" {
t.Errorf("Expected result to be 14, but got %s", result1)
}
expr2 := "2 3 4 + *"
result2 := calculate(expr2)
if result2 != "14" {
t.Errorf("Expected result to be 14, but got %s", result2)
}
expr3 := "5 2 /"
result3 := calculate(expr3)
if result3 != "2" {
t.Errorf("Expected result to be 2, but got %s", result3)
}
expr4 := "hello world +"
result4 := calculate(expr4)
if result4 != "helloworld" {
t.Errorf("Expected result to be helloworld, but got %s", result4)
}
expr5 := "5 hello *"
result5 := calculate(expr5)
if result5 != "hellohellohellohellohello" {
t.Errorf("Expected result to be hellohellohellohellohello, but got %s", result5)
}
}
步骤三输出如下:
fmt.Println(calculate("1+2") == "3")
fmt.Println(calculate("3-4") == "-1")
fmt.Println(calculate("5*6") == "30")
fmt.Println(calculate("8/4") == "2")
fmt.Println(calculate("(1+2)*(3-4)") == "-3")
fmt.Println(calculate("10/(2+3)*4") == "8")
fmt.Println(calculate("2*3+(4-1)/5") == "6")
fmt.Println(calculate("(2+3)*(4-1)/5") == "3")
fmt.Println(calculate("1+2*3+4") == "11")
fmt.Println(calculate("1+(2*3)+4") == "11")
fmt.Println(calculate("1+2*(3+4)") == "15")
fmt.Println(calculate("(1+2)*3+4") == "13")
fmt.Println(calculate("(1+2)*(3+4)") == "21")
(4)合并代码并测试
package main
import (
"fmt"
"strings"
)
func isOperator(ch byte) bool {
return ch == '+' || ch == '-' || ch == '*' || ch == '/'
}
func isDigit(ch byte) bool {
return ch >= '0' && ch <= '9'
}
func isLetter(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')
}
func isSpace(ch byte) bool {
return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'
}
func precedence(ch byte) int {
if ch == '+' || ch == '-' {
return 1
} else if ch == '*' || ch == '/' {
return 2
} else {
return 0
}
}
func toRPN(expr string) string {
var stack []byte
var output []string
for i := 0; i < len(expr); {
if isSpace(expr[i]) {
i++
} else if isOperator(expr[i]) {
for len(stack) > 0 && isOperator(stack[len(stack)-1]) && precedence(stack[len(stack)-1]) >= precedence(expr[i]) {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
stack = append(stack, expr[i])
i++
} else if expr[i] == '(' {
stack = append(stack, expr[i])
i++
} else if expr[i] == ')' {
for len(stack) > 0 && stack[len(stack)-1] != '(' {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
if len(stack) > 0 && stack[len(stack)-1] == '(' {
stack = stack[:len(stack)-1]
}
i++
} else if isDigit(expr[i]) {
start := i
for i < len(expr) && isDigit(expr[i]) {
i++
}
output = append(output, expr[start:i])
} else if isLetter(expr[i]) {
start := i
for i < len(expr) && (isLetter(expr[i]) || isDigit(expr[i])) {
i++
}
output = append(output, expr[start:i])
} else {
panic("Invalid character")
}
}
for len(stack) > 0 {
output = append(output, string(stack[len(stack)-1]))
stack = stack[:len(stack)-1]
}
return strings.Join(output, " ")
}
func calculate(expr string) string {
expr = toRPN(expr) // 新增转换的代码
stack := make([]string, 0)
operators := map[string]bool{"+": true, "-": true, "*": true, "/": true}
for _, token := range strings.Split(expr, " ") {
if !operators[token] {
stack = append(stack, token)
} else {
b, _ := strconv.Atoi(stack[len(stack)-1])
a, _ := strconv.Atoi(stack[len(stack)-2])
stack = stack[:len(stack)-2]
switch token {
case "+":
stack = append(stack, strconv.Itoa(a+b))
case "-":
stack = append(stack, strconv.Itoa(a-b))
case "*":
stack = append(stack, strconv.Itoa(a*b))
case "/":
stack = append(stack, strconv.Itoa(a/b))
}
}
}
return stack[0]
}
func main() {
fmt.Println(calculate("1+2") == "3")
fmt.Println(calculate("3-4") == "-1")
fmt.Println(calculate("5*6") == "30")
fmt.Println(calculate("8/4") == "2")
fmt.Println(calculate("(1+2)*(3-4)") == "-3")
fmt.Println(calculate("10/(2+3)*4") == "8")
fmt.Println(calculate("2*3+(4-1)/5") == "6")
fmt.Println(calculate("(2+3)*(4-1)/5") == "3")
fmt.Println(calculate("1+2*3+4") == "11")
fmt.Println(calculate("1+(2*3)+4") == "11")
fmt.Println(calculate("1+2*(3+4)") == "15")
fmt.Println(calculate("(1+2)*3+4") == "13")
fmt.Println(calculate("(1+2)*(3+4)") == "21")
}
在https://go.dev/play/
上运行,得到的输出结果看上去没问题。
运行结果
注意:以上代码有一些bug,需要我们用GPT调整或者修改prompt,不过从自制编程语言这块看来用GPT是可以行的通的。
2、如何自制编程语言?
既然GPT在程序员的辅助下能实现自制语言,那我们从0开始,分为两个部分:定义特性和架构设计
(1)特性
自制的编程语言需要支持如下特性:
- 类JS语法
- 变量绑定
- 支持整型,布尔,浮点,字符串,数组,哈希等数据结构
- 支持算数表达式
- 内置函数
- 内置eval,支持自举
- 闭包
- 考虑性能,使用字节码和实现简单虚拟机
- 支持正则表达式
- 支持调试器
- ...
(2)架构
架构
- 编写源代码
- 词法解析器解析源代码
- 语法解析器生成AST
- AST转换为字节码
- 插入调试代码到字节码中
- 加载系统库
- 加载binding模块
- 字节码虚拟机加载字节码
- 运行获得结果
除了基于上面的必备的模块以外,还需要增加优化器,解决字节码中一些不必要的循环,调整字节码的顺序和一些尾递归优化等。好了,既然架构已经定了,那我们就开始把,接下来3-4个月内将会更新《ChatGPT|AI自制编程语言》系列文章!(希望不会埋坑~~)
参考
(1)《用Go语言自制解析器》
(2)zhuanlan.zhihu.com/p/96969423
(3)《自己动手实现Lua:虚拟机、编译器和标准库》