ChatGPT | AI自制編程語言-從0開始
最近在看編譯器的書籍,想從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自制編程語言》系列文章?。ㄏM粫窨觺~)
參考
(1)《用Go語言自制解析器》
(2)https://zhuanlan.zhihu.com/p/96969423
(3)《自己動手實現Lua:虛擬機、編譯器和標準庫》
本文轉載自??周末程序猿??,作者:周末程序猿
