逐行识别下的编译原理实验:词法分析与语法分析器实现 1. 逐行识别到底是个什么设计思路先把标题里的核心约束拎出来只能逐行识别一个正确的语法如果跨行存在则默认为两行语法这两行可能出现语法错误。这句话第一次读会觉得别扭但做过编译原理实验的人一眼就能看懂——这是在给词法分析器和语法分析器划定一条非常明确的边界分析器每次只吃一行输入行与行之间不共享任何语法上下文。这个约束听起来像是“功能阉割”但它其实是很多教学场景下的刻意设计。原因很简单如果允许跨行识别你就必须处理换行符的语义归属问题——换行到底是空白字符、语句终止符还是可以被忽略的分隔符一旦这个问题放开词法分析的状态机就要维护跨行的缓冲区语法分析器也要处理“当前行没读完但已经遇到不完整结构”的回溯逻辑。对于刚接触编译原理的人来说这些复杂度会直接淹没真正要学的东西有限状态自动机怎么跑、正则表达式怎么转NFA再转DFA、递归下降怎么处理一个完整的产生式。所以逐行识别的本质是把“跨行”这个变量从实验里彻底拿掉让每一行成为一个独立的、自包含的分析单元。你输入一行分析器就给你一个结论这一行是合法的还是非法的。合法就继续下一行非法就报错。行与行之间没有任何记忆也没有任何“上一行没写完这一行接着写”的机制。这个设计带来的直接后果就是标题里说的那句话一个本来正确的语法如果被拆到两行里写那么这两行各自看都可能不合法。举个最直观的例子假设某语言的赋值语句允许写成total price * quantity这是一行分析器能正确识别。但如果你写成total price * quantity第一行以乘号结尾在逐行识别的规则下这一行就是一个不完整的表达式语法分析器会直接报错第二行只有一个标识符单独看也不是一个合法的语句。两行都错但合在一起本来是对的。这就是逐行识别最典型的“误伤”场景。理解了这个前提后面所有的设计决策就都有了解释的锚点。你要做的不是去修复这个“缺陷”而是在这个约束下把词法分析和语法分析做扎实让每一行的识别结果尽可能准确、报错信息尽可能有用。1.1 为什么教学实验偏爱这种“不完整”的设定从教学角度来说逐行识别有三个非常实际的好处。第一状态机的边界清晰。词法分析器的核心是一个有限状态自动机它从行首开始读字符遇到换行符就结束当前行的扫描把状态重置到初始状态。你不需要考虑“这个字符串字面量还没闭合下一行继续”的情况因为逐行识别下未闭合的字符串就是当前行的错误直接报出来就行。这让DFA的状态转移表设计变得非常干净。第二语法分析的递归下降实现更直接。递归下降的核心是每个非终结符对应一个函数函数内部按照产生式右部依次调用其他函数或匹配终结符。如果允许跨行你就需要在某个函数里处理“当前token流结束了但产生式还没匹配完”的情况然后去读下一行。逐行识别下token流就是当前行的token序列读完了就是读完了匹配不上就报错逻辑非常线性。第三测试用例的构造和判定更简单。每一行输入对应一个明确的预期结果合法或非法。你可以写一个测试脚本逐行喂给分析器逐行比对输出。不需要构造多行联动的复杂用例也不需要处理“这个错误到底该归到哪一行”的歧义。注意逐行识别不等于“每行只能有一个语句”。一行里写多个语句只要它们都在同一行内闭合分析器照样能识别。约束的核心是“不跨行”而不是“一行一个”。1.2 这个约束对词法分析的具体影响词法分析阶段逐行识别意味着换行符是硬边界。你在设计词法规则时需要明确换行符的处理方式。通常有两种做法一种是把换行符当作空白字符但扫描到换行符时强制结束当前行的token序列并重置所有跨行可能残留的状态比如注释状态、字符串状态。另一种是把换行符当作语句终止符每行末尾自动补一个分号或类似的分隔符让语法分析器把每行当作一个完整的语句来处理。两种做法各有适用场景。如果实验的语言本身有明确的分号作为语句终止符那换行符当空白处理更自然因为语句的边界由分号决定换行只是排版。如果实验的语言像Python那样用缩进和换行来划分语句那换行符就必须被赋予语法意义逐行识别反而成了天然的优势。我在实际做这类实验时倾向于第一种做法换行符在词法阶段被识别为行结束标记词法分析器在遇到它时输出当前行的token列表然后清空缓冲区准备下一行。这样做的好处是词法分析器和语法分析器之间的接口非常清晰——语法分析器每次拿到的是一个完整的token列表不需要关心这个列表是怎么来的。1.3 这个约束对语法分析的具体影响语法分析阶段逐行识别意味着每个语句必须是自包含的。你不能设计那种“左部在第一行、右部在第二行”的产生式。比如C语言里的函数定义如果写成int add(int a, int b) { return a b; }在逐行识别的规则下第一行int add(int a, int b)单独看是一个不完整的函数定义因为没有函数体第二行{单独看也不是一个合法的语句。整个函数定义跨了多行逐行分析必然报错。这就倒逼你在设计实验用的文法时把所有产生式都设计成单行可闭合的。比如赋值语句、表达式语句、if语句的单行形式、while语句的单行形式等。如果实验要求支持复合语句那就必须允许用某种方式把多个语句写在同一行比如用分号分隔或者用花括号但花括号和内容都在同一行。这种约束其实是在训练一种能力在有限的语法范围内把分析逻辑做完整。你不需要处理复杂的跨行结构但你需要确保单行内的所有合法结构都能被正确识别所有非法结构都能被准确报错。2. 词法分析器的逐行实现细节词法分析是逐行识别的第一道关卡。它的任务很明确读入一行字符输出这一行的token序列遇到非法字符或非法token就报错。听起来简单但实际写起来有几个关键点需要仔细处理。2.1 输入缓冲与行边界处理逐行识别的第一步是按行读取输入。不管你用什么语言实现核心逻辑都是一样的从输入源文件或标准输入逐行读取每次读一行去掉行尾的换行符然后把这一行交给词法分析器。这里有个细节容易被忽略行尾的换行符到底要不要保留。我的建议是去掉。因为换行符本身不是语言的一部分它只是输入的物理分隔。去掉之后词法分析器面对的是一行纯字符不需要在规则里专门处理换行符。当然如果你需要记录行号用于报错那就在读取时维护一个行号计数器每读一行加一报错时带上行号。另一个细节是空行的处理。空行就是长度为0的行词法分析器遇到空行时应该直接返回一个空的token列表语法分析器收到空列表后直接跳过不报错。这符合大多数语言的直觉空行是合法的它不包含任何语句。还有一种情况是行尾有空白字符。比如一行以几个空格或制表符结尾。这些空白字符在词法阶段应该被跳过不产生token。所以词法分析器的扫描循环里遇到空白字符空格、制表符就继续往后读直到遇到非空白字符或行尾。2.2 正则规则与状态机设计词法分析器的核心是一组正则规则每条规则对应一类token。比如标识符的正则是[a-zA-Z_][a-zA-Z0-9_]*整数的正则是[0-9]运算符的正则是、-、*、/等。在逐行识别的约束下这些正则规则不需要考虑跨行匹配。比如字符串字面量的正则通常是([^\\]|\\.)*它要求字符串在同一行内闭合。如果一行里有一个未闭合的引号词法分析器扫描到行尾时发现字符串状态还没结束就直接报“未闭合的字符串”错误。这比跨行字符串的处理简单得多因为跨行字符串需要维护一个“字符串是否闭合”的状态并在多行之间传递。实现状态机时我推荐用表驱动的方式而不是写一堆if-else。表驱动的核心是维护一个状态转移表行是当前状态列是输入字符类别表项是下一个状态和要执行的动作。这样做的好处是逻辑清晰容易调试也容易扩展。比如下面这个简化的状态转移表当前状态字母/下划线数字运算符空白行尾初始标识符整数运算符跳过结束标识符标识符标识符输出标识符输出标识符输出标识符整数输出整数整数输出整数输出整数输出整数运算符输出运算符输出运算符输出运算符输出运算符输出运算符这张表虽然简化了很多但已经能说明问题每个状态在遇到不同类别的字符时要么继续留在当前状态要么输出当前token并转移到新状态。行尾是一个特殊的“字符”它触发当前token的输出并结束这一行的扫描。2.3 错误处理与报错信息设计逐行识别下词法错误主要有三类非法字符、未闭合的字符串、非法数字格式。非法字符是指那些不属于任何token规则的字符。比如语言里没有定义这个运算符但输入里出现了词法分析器就应该报“非法字符位于第X行第Y列”。报错信息里带上行号和列号非常重要因为逐行识别下用户需要快速定位到具体是哪一行的哪个位置出了问题。未闭合的字符串是指一行里出现了引号但行尾之前没有闭合。这种情况下词法分析器扫描到行尾时如果当前状态还在“字符串内部”就报“未闭合的字符串起始于第X行第Y列”。注意这里报的是起始位置而不是行尾位置因为起始位置对用户更有用。非法数字格式是指那些看起来像数字但格式不对的输入。比如123abc词法分析器读到123时可能已经输出了一个整数token然后继续读到abc又输出一个标识符token。这其实不算错误因为词法分析器通常采用“最长匹配”原则123abc会被拆成123和abc两个token。但如果语言规定数字后面不能直接跟字母那就需要在语法分析阶段报错而不是词法阶段。实操心得词法分析器的报错信息里列号从1开始计数比从0开始更符合人的直觉。另外报错后不要立即终止整个程序而是应该记录错误、跳过当前行、继续分析下一行。这样用户一次运行就能看到所有行的错误而不是改一个错再跑一次。2.4 逐行词法分析的代码骨架下面用Python写一个简化的逐行词法分析器骨架重点展示行边界处理和状态重置的逻辑import re # token规则按优先级排列 TOKEN_RULES [ (NUMBER, r\d), (ID, r[a-zA-Z_][a-zA-Z0-9_]*), (OP, r[\-*/!]), (LPAREN, r\(), (RPAREN, r\)), (SEMI, r;), (SKIP, r[ \t]), ] def tokenize_line(line, line_no): tokens [] pos 0 while pos len(line): matched False for token_type, pattern in TOKEN_RULES: regex re.compile(pattern) m regex.match(line, pos) if m: matched True if token_type ! SKIP: tokens.append((token_type, m.group(), line_no, pos 1)) pos m.end() break if not matched: raise SyntaxError(f第{line_no}行第{pos1}列非法字符 {line[pos]}) return tokens def tokenize_file(filepath): all_tokens [] with open(filepath, r, encodingutf-8) as f: for line_no, line in enumerate(f, start1): line line.rstrip(\n) if not line.strip(): continue try: tokens tokenize_line(line, line_no) all_tokens.append((line_no, tokens)) except SyntaxError as e: print(f词法错误{e}) all_tokens.append((line_no, [])) return all_tokens这段代码的核心逻辑是每次处理一行行内扫描遇到非法字符就报错并跳过该行。tokenize_file函数逐行读取文件把每行的token列表和行号一起存起来供语法分析器使用。注意line.rstrip(\n)去掉了行尾换行符空行直接跳过。3. 语法分析器的逐行实现细节语法分析器拿到的是每行的token列表它的任务是判断这一行的token序列是否符合文法规则。逐行识别下语法分析器不需要维护跨行的状态每行独立分析独立报错。3.1 文法设计与单行闭合原则设计实验用的文法时核心原则是每个产生式都必须能在单行内完成匹配。这意味着你不能设计那种“左部在第一行、右部在第二行”的产生式。比如下面这个赋值语句的文法stmt - ID expr ; expr - term (( | -) term)* term - factor ((* | /) factor)* factor - NUMBER | ID | ( expr )这个文法里stmt产生式要求ID expr ;全部在同一行内。如果用户写成total price * quantity;第一行total price *在匹配到*之后期望后面跟一个factor但行尾已经到了没有更多的token所以语法分析器会报“第1行表达式不完整期望一个因子”。第二行quantity;单独看quantity是一个ID但后面跟的是;不符合stmt产生式的开头stmt要求以ID开头然后跟所以也会报错。这就是逐行识别的典型表现本来正确的跨行语法被拆成两行后两行都报错。这不是bug而是设计约束的必然结果。你在写实验报告时需要明确说明这一点并解释为什么在逐行识别的约束下这种报错是合理的。3.2 递归下降分析器的逐行适配递归下降是语法分析实验中最常用的方法因为它直观、易实现、易调试。在逐行识别的约束下递归下降分析器的结构需要做一点调整。标准的递归下降分析器通常维护一个全局的token流和当前位置指针每个非终结符对应一个函数函数内部按照产生式右部依次调用其他函数或匹配终结符。在逐行识别下token流就是当前行的token列表分析器每次只处理一行处理完就重置。具体来说你可以这样组织代码class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def peek(self): if self.pos len(self.tokens): return self.tokens[self.pos] return None def match(self, expected_type): token self.peek() if token and token[0] expected_type: self.pos 1 return token return None def parse_stmt(self): # stmt - ID expr ; id_token self.match(ID) if not id_token: return None eq_token self.match(OP) if not eq_token or eq_token[1] ! : return None expr_result self.parse_expr() if expr_result is None: return None semi_token self.match(SEMI) if not semi_token: return None return (assign, id_token, expr_result) def parse_expr(self): # expr - term (( | -) term)* left self.parse_term() if left is None: return None while True: op_token self.peek() if op_token and op_token[0] OP and op_token[1] in (, -): self.pos 1 right self.parse_term() if right is None: return None left (binop, op_token[1], left, right) else: break return left def parse_term(self): # term - factor ((* | /) factor)* left self.parse_factor() if left is None: return None while True: op_token self.peek() if op_token and op_token[0] OP and op_token[1] in (*, /): self.pos 1 right self.parse_factor() if right is None: return None left (binop, op_token[1], left, right) else: break return left def parse_factor(self): # factor - NUMBER | ID | ( expr ) token self.peek() if not token: return None if token[0] NUMBER: self.pos 1 return (number, token[1]) if token[0] ID: self.pos 1 return (id, token[1]) if token[0] LPAREN: self.pos 1 expr_result self.parse_expr() if expr_result is None: return None rparen self.match(RPAREN) if not rparen: return None return expr_result return None这个分析器的核心特点是每次分析一行分析完就丢弃状态。parse_stmt函数尝试匹配一个完整的赋值语句如果匹配成功返回语法树节点如果匹配失败返回None表示这一行有语法错误。注意parse_expr和parse_term里的循环逻辑它们会尽可能多地匹配运算符和操作数直到遇到不匹配的token为止。这种“贪婪匹配”是递归下降的典型特征它保证了表达式能被完整解析。3.3 错误恢复与报错定位逐行识别下语法错误主要有两类token序列不符合任何产生式、token序列符合产生式开头但中途失败。第一类错误通常发生在行首。比如一行以;开头而文法里没有任何产生式以;开头分析器就会报“第X行意外的token ;期望语句开头”。这种报错比较直接用户一看就知道是行首的问题。第二类错误发生在产生式匹配过程中。比如total price * ;分析器在匹配expr时读到*后期望后面跟一个factor但实际读到的是;于是报“第X行表达式不完整期望一个因子但遇到;”。这种报错需要带上期望的token类型和实际遇到的token类型用户才能快速定位问题。实操心得报错信息里带上行号、列号、期望的token类型、实际的token类型这四个信息缺一不可。行号和列号帮用户定位位置期望和实际帮用户理解错误原因。如果只报“语法错误”用户根本不知道错在哪。错误恢复方面逐行识别下最简单的策略是一行出错就跳过该行继续分析下一行。因为行与行之间没有依赖关系跳过一行不会影响后续行的分析。这比跨行分析下的错误恢复简单得多不需要考虑“当前行出错后下一行是否还能继续”的问题。3.4 逐行语法分析的完整流程把词法分析和语法分析串起来完整的逐行分析流程是这样的从输入源读取一行去掉行尾换行符。如果行是空的或只包含空白字符跳过读下一行。调用词法分析器把这一行转换成token列表。如果词法分析报错记录错误跳过该行读下一行。调用语法分析器把token列表转换成语法树。如果语法分析报错记录错误跳过该行读下一行。如果词法和语法都通过记录这一行的语法树读下一行。重复以上步骤直到输入结束。输出所有行的分析结果和错误信息。这个流程的核心是逐行独立处理行与行之间不共享状态。每一行都从初始状态开始分析完就结束。这保证了分析器的行为是可预测的、可复现的。4. 常见问题与排查技巧实录做逐行识别的编译原理实验踩坑是必然的。下面整理几个我实际遇到过的问题和解决方法供参考。4.1 跨行语法被误报为错误这是逐行识别最典型的问题也是标题里明确提到的场景。比如用户输入total price * quantity;第一行报“表达式不完整”第二行报“意外的token quantity”。用户会觉得“我明明写的是对的为什么报错”。这时候你需要向用户解释逐行识别的约束下跨行语法不被支持每一行必须自包含。如果用户希望正确识别需要把语句写在同一行total price * quantity;这个问题没有技术上的解决方法因为它是设计约束的必然结果。你能做的是在实验文档里明确说明这个约束并在报错信息里给出提示比如“第1行表达式不完整请检查是否跨行书写”。4.2 词法分析器的状态重置遗漏逐行识别下词法分析器每处理完一行必须把所有状态重置到初始状态。如果忘记重置上一行的状态可能会影响下一行。比如上一行有一个未闭合的字符串词法分析器进入“字符串内部”状态如果这一行开始时没有重置分析器会继续把这一行当作字符串内容处理导致整行都被吞掉。解决方法很简单在每行开始扫描之前显式地把所有状态变量重置为初始值。包括当前状态、当前token的起始位置、缓冲区等。这个操作最好封装成一个reset()函数每行开始时调用一次。4.3 报错信息缺少行号和列号很多同学写词法分析器时报错只写“非法字符”不写行号和列号。用户拿到报错后完全不知道去哪找问题。逐行识别下行号是天然存在的你逐行读取时就有行号列号就是当前扫描位置。把这两个信息加到报错里用户体验会好很多。问题现象可能原因排查方法解决方法跨行语法报错逐行识别约束检查语句是否跨行把语句写在同一行上一行状态影响下一行状态未重置在每行开始处打印状态变量每行开始时调用reset()报错无行号列号报错信息不完整检查报错字符串加上行号、列号、期望和实际token空行报错空行未跳过检查是否处理空行空行直接跳过不调用分析器行尾空白导致报错空白未跳过检查词法规则是否包含空白在词法规则里跳过空格和制表符4.4 递归下降分析器的回溯问题递归下降分析器在处理某些文法时可能需要回溯。比如factor - NUMBER | ID | ( expr )分析器读到(时需要决定是匹配( expr )还是报错。如果(后面不是合法的表达式分析器需要回溯到(之前尝试其他产生式。但在逐行识别下回溯的代价很小因为token列表就在内存里重置pos指针就行。不过过度回溯会导致性能问题。如果文法设计得不好分析器可能在一行内反复回溯导致指数级的时间复杂度。解决方法是提取左公因子和消除左递归让文法变成LL(1)文法这样分析器只需要向前看一个token就能决定用哪个产生式不需要回溯。实操心得实验用的文法通常比较简单LL(1)化之后基本不需要回溯。如果你发现分析器在一行内反复回溯先检查文法是否有左递归或左公因子而不是急着优化代码。4.5 多语句同行的处理逐行识别允许一行内有多个语句只要它们都在同一行内闭合。比如a 1; b 2; c 3;这一行有三个赋值语句用分号分隔。语法分析器需要能识别这种多语句行。实现方法是在parse_stmt之上加一个parse_line函数循环调用parse_stmt直到token用完def parse_line(self): stmts [] while self.pos len(self.tokens): stmt self.parse_stmt() if stmt is None: return None stmts.append(stmt) return stmts这个函数会尽可能多地解析语句直到token用完或解析失败。如果解析失败返回None表示这一行有语法错误。4.6 注释的处理如果实验的语言支持注释逐行识别下注释的处理需要特别注意。单行注释如//或#开头比较简单词法分析器遇到注释起始符就跳过整行剩余内容。多行注释如/* */在逐行识别下会变得复杂因为注释可能跨行。如果实验不要求支持多行注释那就只实现单行注释并在文档里说明多行注释不被支持。如果要求支持多行注释那词法分析器需要维护一个“是否在注释内部”的状态并在行与行之间传递这个状态。这就打破了“逐行独立”的约束需要额外处理。我的建议是实验阶段只实现单行注释多行注释留到后续实验或作为扩展功能。5. 实验报告与测试用例设计做完实验后实验报告和测试用例是展示成果的关键。逐行识别下测试用例的设计需要覆盖各种边界情况。5.1 测试用例的分类测试用例可以分为以下几类合法单行语句各种类型的语句都在一行内闭合。比如赋值、表达式、if单行、while单行等。非法单行语句各种语法错误比如缺少分号、括号不匹配、运算符缺少操作数等。跨行语句本来正确的语句被拆到多行验证逐行识别下是否报错。空行和空白行验证空行是否被正确跳过。多语句同行一行内有多个语句验证是否能正确解析。词法错误非法字符、未闭合字符串等。每类用例至少准备3-5个覆盖典型场景和边界情况。测试时逐行输入逐行比对预期结果和实际结果。5.2 测试用例表格示例用例编号输入行预期结果实际结果备注1a 1;合法合法基本赋值2a 1非法非法缺少分号3a ;非法非法缺少表达式4a 1 2 * 3;合法合法运算符优先级5a (1 2) * 3;合法合法括号6a 1 非法非法表达式不完整7a 1; b 2;合法合法多语句同行8a 1 换行2;两行都非法两行都非法跨行语法9空行跳过跳过空行处理10a ;非法非法非法字符这个表格可以直接用在实验报告里清晰展示测试覆盖情况和分析器的正确性。5.3 实验报告的结构建议实验报告通常包括以下几个部分实验目的说明本次实验要掌握的知识点比如词法分析、语法分析、递归下降等。实验环境编程语言、工具、运行平台等。实验设计文法设计、词法规则、语法分析器结构等。实验过程关键代码、调试过程、遇到的问题和解决方法。测试结果测试用例表格、运行截图、结果分析。实验总结个人体会、收获、不足和改进方向。其中实验设计和测试结果是重点。实验设计要说明为什么选择逐行识别、文法为什么这样设计、分析器为什么用递归下降。测试结果要展示分析器在各种情况下的表现特别是跨行语法的报错情况。5.4 常见问题速查表问题原因解决方法跨行语法报错逐行识别约束把语句写在同一行上一行状态影响下一行状态未重置每行开始时重置状态报错无行号列号报错信息不完整加上行号、列号、期望和实际token空行报错空行未跳过空行直接跳过行尾空白导致报错空白未跳过词法规则里跳过空白多语句同行解析失败未实现parse_line在parse_stmt之上加parse_line注释导致报错注释未处理词法阶段跳过注释递归下降回溯过多文法非LL(1)提取左公因子、消除左递归这张表可以作为实验报告的附录方便查阅。6. 个人实操体会与扩展方向逐行识别的编译原理实验表面上看是在做一个“功能受限”的分析器但实际上它训练的是在明确约束下把核心逻辑做扎实的能力。我在带实验和实际写分析器的过程中有几个体会比较深。第一约束不是敌人而是帮手。逐行识别把跨行这个变量拿掉之后词法分析和语法分析的逻辑变得非常纯粹。你不需要处理复杂的跨行状态传递可以把全部精力放在状态机设计、文法设计、错误处理这些核心问题上。等你把这些核心问题搞清楚了再去处理跨行就会发现只是多了一层状态管理而已。第二报错信息的设计比分析逻辑本身更重要。很多同学把分析器写对了但报错信息写得很烂用户拿到报错后完全不知道问题在哪。逐行识别下行号是天然存在的列号也不难获取把这两个信息加上再带上期望和实际的token类型报错信息就足够有用了。这个习惯在后续做任何工具时都适用。第三测试用例要覆盖边界情况。空行、行尾空白、多语句同行、跨行语法、非法字符这些边界情况最容易出bug。写测试用例时不要只写“正常情况”要把这些边界情况都覆盖到。我通常会在实验报告里附一个测试用例表格逐行列出输入、预期结果、实际结果这样既展示了测试覆盖情况也方便排查问题。第四逐行识别可以自然扩展到多行识别。如果你后续想支持跨行语法可以在词法分析器里维护一个跨行状态比如“是否在字符串内部”“是否在注释内部”在语法分析器里维护一个跨行token缓冲区。当一行分析失败时尝试把下一行的token拼接到当前行重新分析。这种“逐行优先、跨行回退”的策略可以在保持逐行识别简单性的同时支持跨行语法。当然这会增加复杂度需要权衡。最后分享一个小技巧在分析器里加一个调试模式每处理一行就打印出这一行的token列表、语法树、分析结果。这样调试时一目了然不用靠猜。调试模式可以用命令行参数控制比如--debug默认关闭需要时打开。这个习惯能帮你省下大量调试时间。这个实验后续还可以这样扩展支持多行注释、支持跨行表达式、支持更复杂的语句类型如for循环、switch语句、支持语义分析如类型检查、变量未定义检查。每扩展一个功能你都会对编译原理有更深的理解。逐行识别是起点不是终点。