ChatGPT | AI自制編程語言-從0開始
最近在看編譯器的書籍,想從0開始寫一個(gè)簡(jiǎn)單的自制編程語言練練手,但是同時(shí)想到自己現(xiàn)在手頭有ChatGPT,于是萌生想法:如果我了解編譯器整個(gè)設(shè)計(jì)過程,為什么我還需要重頭開始寫編譯器呢?為何不直接將整個(gè)實(shí)現(xiàn)思路給到GPT,讓GPT幫我實(shí)現(xiàn)代碼,我只需要考慮如何設(shè)計(jì)詞法和語法,如何優(yōu)化等過程即可,說干就干,于是開始《ChatGPT|AI自制編程語言》系列文章。
1、試驗(yàn)
在驗(yàn)證《AI自制編程語言》之前,我在想整個(gè)流程是否按照想法執(zhí)行,于是想到??golang??實(shí)現(xiàn)一個(gè)表達(dá)式引擎(自制編程語言的第一步就是要實(shí)現(xiàn)表達(dá)式引擎),如果表達(dá)式引擎能按照要求實(shí)現(xiàn),那自制編程語言應(yīng)該用GPT也可以實(shí)現(xiàn)。
(1)需求拆解
先將表達(dá)式引擎需求的拆解如下:
- 主功能是用golang實(shí)現(xiàn)表達(dá)式引擎
- 使用逆波蘭表達(dá)運(yùn)算規(guī)則的先后順序
- 支持操作符可以定義為+,-,*,/,(,)
- 支持?jǐn)?shù)據(jù)類型可以定義為:number,string
以上需求考慮的如果一次將需求輸入給GPT,代碼會(huì)比較長(zhǎng),而且一旦某個(gè)步驟出問題,不得不重新檢查或者,所以我們將需求分為三個(gè)子步驟:
- 先讓GPT幫我們通過字符串表達(dá)式生成逆波蘭表達(dá)式
- 然后GPT幫我們實(shí)現(xiàn)逆波蘭表達(dá)式再計(jì)算結(jié)果
- 最后GPT幫我們生成字符串表達(dá)式計(jì)算的測(cè)試樣例
(2)定義prompt
步驟一:先讓GPT幫我們通過字符串表達(dá)式生成逆波蘭表達(dá)式
輸入到GPT3.5:
你是一個(gè)使用golang開發(fā)的資深的程序員,正在開發(fā)一個(gè)表達(dá)式引擎功能,功能需求如下:
- 實(shí)現(xiàn)將字符串表達(dá)式轉(zhuǎn)換為逆波蘭表達(dá)式
- 輸入是字符串表達(dá)式,輸出是帶空格的逆波蘭表達(dá)式字符串,同時(shí)輸出不需要帶`(`,`)`,`[`,`]`
- <切記>:只需要支持+,-,*,/,(,)操作符
- 數(shù)據(jù)類型只包含兩種:number,string
- 給出測(cè)試用例和main函數(shù)
- 代碼量控制在最精簡(jiǎn)的范圍步驟二:然后GPT幫我們實(shí)現(xiàn)逆波蘭表達(dá)式再計(jì)算結(jié)果
輸入到GPT3.5:
你是一個(gè)golang開發(fā)的資深的程序員,正在開發(fā)一個(gè)表達(dá)式引擎功能,功能需求如下:
- 實(shí)現(xiàn)計(jì)算逆波蘭式字符串表達(dá)式的結(jié)果
- 輸入是逆波蘭式字符串表達(dá)式,輸出是表達(dá)式計(jì)算的字符串結(jié)果
- <切記>:支持+,-,*,/操作
- <切記>:數(shù)據(jù)類型只包含兩種:number,string,需要注意string類型也需要支持+,-,*,/操作
- 代碼行增加一些注釋
- 給出測(cè)試用例和main函數(shù)
- 代碼量控制在最精簡(jiǎn)的范圍步驟三:最后GPT幫我們生成字符串表達(dá)式計(jì)算的測(cè)試樣例
輸入到GPT3.5:
用+,-,*,/,(,)給出一些計(jì)算表達(dá)式樣例和計(jì)算結(jié)果,要求如下:
- 只用給出整數(shù)計(jì)算
- 格式如下:fmt.Println(calculate("<表達(dá)式>") == "<結(jié)果>")(3)輸出代碼
步驟一生成的代碼如下:
// 運(yùn)行的代碼:
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 ^ ^ / +
}
// 測(cè)試代碼:
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)
}
}
}步驟二生成的代碼的如下:
// 運(yùn)行的代碼:
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]
}
// 測(cè)試代碼:
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)合并代碼并測(cè)試
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) // 新增轉(zhuǎn)換的代碼
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/??上運(yùn)行,得到的輸出結(jié)果看上去沒問題。

運(yùn)行結(jié)果
注意:以上代碼有一些bug,需要我們用GPT調(diào)整或者修改prompt,不過從自制編程語言這塊看來用GPT是可以行的通的。
2、如何自制編程語言?
既然GPT在程序員的輔助下能實(shí)現(xiàn)自制語言,那我們從0開始,分為兩個(gè)部分:定義特性和架構(gòu)設(shè)計(jì)
(1)特性
自制的編程語言需要支持如下特性:
- 類JS語法
- 變量綁定
- 支持整型,布爾,浮點(diǎn),字符串,數(shù)組,哈希等數(shù)據(jù)結(jié)構(gòu)
- 支持算數(shù)表達(dá)式
- 內(nèi)置函數(shù)
- 內(nèi)置eval,支持自舉
- 閉包
- 考慮性能,使用字節(jié)碼和實(shí)現(xiàn)簡(jiǎn)單虛擬機(jī)
- 支持正則表達(dá)式
- 支持調(diào)試器
- ...
(2)架構(gòu)

架構(gòu)
- 編寫源代碼
- 詞法解析器解析源代碼
- 語法解析器生成AST
- AST轉(zhuǎn)換為字節(jié)碼
- 插入調(diào)試代碼到字節(jié)碼中
- 加載系統(tǒng)庫(kù)
- 加載binding模塊
- 字節(jié)碼虛擬機(jī)加載字節(jié)碼
- 運(yùn)行獲得結(jié)果
除了基于上面的必備的模塊以外,還需要增加優(yōu)化器,解決字節(jié)碼中一些不必要的循環(huán),調(diào)整字節(jié)碼的順序和一些尾遞歸優(yōu)化等。好了,既然架構(gòu)已經(jīng)定了,那我們就開始把,接下來3-4個(gè)月內(nèi)將會(huì)更新《ChatGPT|AI自制編程語言》系列文章!(希望不會(huì)埋坑~~)
參考
(1)《用Go語言自制解析器》
(2)https://zhuanlan.zhihu.com/p/96969423
(3)《自己動(dòng)手實(shí)現(xiàn)Lua:虛擬機(jī)、編譯器和標(biāo)準(zhǔn)庫(kù)》
本文轉(zhuǎn)載自??周末程序猿??,作者:周末程序猿

















