Go手写四则运算计算器:实现Yacc归约逻辑

Aria
Aria
发布时间:2026-10-15| 5人浏览过
原创

本文介绍如何在Go语言中手动实现一个简单的四则运算计算器,核心在于模拟Yacc编译器的移进/归约(Shift/Reduce)思想。通过设计状态机进行词法分析,识别数字、操作符及括号,构建记号流;随后使用栈结构进行语法分析,根据优先级规则实时执行乘除或括号的归约计算,最终输出结果。

项目目标与核心原理

本项目旨在编写一个基于Go语言的命令行计算器,能够接收形如 1+2*(3-4) 的输入并返回计算结果。为了实现这一目标,我们深入理解编译原理中的“移进/归约”算法,将其从复杂的语法解析简化为支持加、减、乘、除以及左右括号的数值计算。

整体架构流程

系统的运行分为两个主要阶段:词法扫描(Lexer)和语法分析/计算(Parser)。

  1. 读取输入:程序从标准输入循环读取一行表达式字符串。
  2. 词法扫描:逐字符遍历字符串,利用有限状态机将字符序列转换为记号(Token)队列,包括整数、浮点数、运算符和括号。
  3. 语法分析:将生成的记号逐一压入计算栈。每压入一个新记号,立即检查栈顶是否存在满足优先级的“归约条件”。
  4. 执行归约:若检测到可归约模式(如连续的数字与运算符),则弹出相应节点进行运算,并将结果作为一个新的值节点重新压回栈中。
  5. 输出结果:当所有记号处理完毕后,对栈内剩余内容进行最终清理与计算,得到最终结果。

第一阶段:记号扫描 (Lexer)

词法分析的核心是将输入的文本拆解为具有语义的类型。本例采用简单的手动有限状态机来实现,而非依赖外部工具。

第1步:定义状态与记号类型

首先定义必要的状态常量和记号标识。记号主要用于区分整型字面量、浮点字面量以及各种二元运算符。

package tokens
type TOKENS string
const IntLiteral TOKENS = "INT"
const DoubleLiteral TOKENS = "DBL"
const ADD TOKENS = "ADD"
const SUB TOKENS = "SUB"
const MUL TOKENS = "MUL"
const DIV TOKENS = "DIV"
const LB TOKENS = "LB"
const RB TOKENS = "RB"
const UNKNOWN TOKENS = "UNKNOWN"
type Token struct {
    Token TOKENS
    Value string
    Position int
}
func OfRune(t TOKENS, r rune, from int)  *Token {
    return &Token {
        Token: t,
        Value : string(r),
        Position: from,
    }
}
func OfString(t TOKENS, s string, from int)  *Token {
    return &Token {
        Token: t,
        Value : s,
        Position: from,
    }
}

第2步:实现有限状态转换 (States & Lexer)

状态机主要包含三种内部状态:INITIAL(初始/空闲态)、INT_STATUS(正在解析整数)和 FRAG_STATUS(正在解析小数部分)。遇到空格时触发当前数字的归约并重置状态。以下是解析主函数:

type STATES int
const INITIAL STATES = 1
const INT_STATUS STATES = 11
const DOT_STATUS STATES = 12
const FRAG_STATUS STATES = 13

在 lexer.go 中,通过遍历每一行输入字符串(并在末尾附加 '\n' 以确保最后一个数字被正确捕获),驱动状态机跳转:

func Parse(line string) ([]*tokens.Token, error) {
    var state = &(tLexerState{
        state: states.INITIAL,
        tokens: make([]*tokens.Token, 0),
        buffer: make([]rune, 0),
        i0 : 0,
        i1 : 0,
        d0: 0,
        d1: 0,
    })
    for i, it := range line + "\n" {
        e := parseChar(state, i, it)
        if e != nil {
            return nil, e
        }
    }
    return state.tokens, nil
}

具体逻辑如下:

  • 初始状态 (INITIAL):如果是数字或负号,转入 INT_STATUS;如果是一元运算符(如 + - * / ( )),直接生成对应的符号记号。
  • 整数状态 (INT_STATUS):继续吸收数字字符;如果遇到小数点,存入缓冲并转入 DOT_STATUS;如果遇到运算符或右括号,将缓冲区的内容作为 IntLiteral 或 DoubleLiteral 提交,并重置状态。
  • 小数状态 (DOT_STATUS & FRAG_STATUS):负责解析小数点后的尾数片段,直至遇到非数字字符时提交浮点记号。

第二阶段:栈式语法分析与计算 (Parser)

解析器通过维护一个混合类型的栈来工作,栈中既可以存放待处理的记号(Token Node),也可以存放已经计算出的中间结果(Value Node)。

第3步:栈节点设计与类型映射

为了支持不同类型的节点,我们定义了统一的栈节点结构,并提供了方便的工厂方法:

type tStackNodeType int
const NODE_VALUE tStackNodeType = 1
const NODE_TOKEN tStackNodeType = 2
type tStackNode struct {
    flag tStackNodeType
    token *tokens.Token
    value float64
} // ... 省略工厂函数代码 

tStackNode 还提供了 getTokenType() 方法,用于统一判断节点在栈中的角色,例如将整型和浮点型都视为 DoubleLiteral,以便匹配优先级相同的相邻数值。

第4步:核心的归约循环

在每次获取新记号并压栈后,都会调用 reduce() 方法来尝试执行归约。这是体现移进/归约思想的关键步骤:

func (me *tParser) reduce() {
    // ... 打印堆栈状态 ...
    for {
        // 1. 优先处理高优先级的乘法或除法
        if me.reduceMulOrDiv() { continue }
        // 2. 处理括号内的内容
        if me.reduceBrackets() { continue }
        // 3. 如果没有更多记号了,执行最后的加减合并
        if !me.moreToken() {
             if me.reduceAddOrSub() { break }
        }
        // ... 结束本轮检测 ...
        break
    }
}
  • 乘法归约 (reduceMulOrDiv):检测栈顶是否为 “数值 -> 乘号 -> 数值”。如果是,弹出这三个节点,执行数学运算,并将结果作为 NODE_VALUE 压回栈顶。此优先级最高。
  • 括号归约 (reduceBrackets):当检测到栈顶是左括号(LB)时,开始持续弹出元素直到遇见右括号(RB)。在弹出的过程中,依次执行加法或减法,计算出括号内的总和,最后将总和压回栈并丢弃括号。
  • 加减归约 (reduceAddOrSub):通常在没有高优先级算子时可推迟,但在无新记号输入时(!me.moreToken())需要强制清空栈内剩余的加减序列,得出最终答案。

主程序入口与交互体验

结合上述模块,在主程序中建立无限循环以提供交互式命令行体验:

func main() {
    reader := bufio.NewReader(os.Stdin)
    for {
        fmt.Printf("=> ")
        arrBytes, _, err := reader.ReadLine()
        if err != nil {
            panic(err.Error())
        }
        line := strings.TrimSpace(string(arrBytes))
        expression := line
        tokens, e := lexer.Parse(expression)
        if e != nil {
            println(e.Error())
        } else {
            e,v := parser.Parse(tokens)
            if e != nil {
                println(e.Error())
            }
            fmt.Println(strconv.FormatFloat(v, 'f', 10, 64))
        }
    }
}

程序会打印每一步的归约过程,展示栈的动态变化,便于调试和学习。以下是复杂表达式的运行示例:

=> 1+2*(3-4*(5/6+(7-8*9)))
stack => 1
stack => 1 +
stack => 1 + 2
stack => 1 + 2 *
stack => 1 + 2 * (
stack => 1 + 2 * ( 3
... (中间过程省略) ...
stack => 1 + 519.3333333333
520.3333333333

以上就是Go手写四则运算计算器yacc归约实现的详细内容,更多关于Golang基础教程的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。