ChatGPT|AI自制编程语言-从0开始

1,022 阅读8分钟

最近在看编译器的书籍,想从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([]string0)
 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([]string0)
 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:虚拟机、编译器和标准库》