fix: 数字字面量与位宽符号之间不允许有空白符

This commit is contained in:
Luke 2025-06-30 16:21:01 +08:00
parent 6a247f456c
commit 3eacdf6d39
8 changed files with 106 additions and 32 deletions

View File

@ -1,7 +1,7 @@
function: main function: main
return_type: int return_type: int
body: body:
3 L declare num1 :int = 3.1 G
return 65537 return 65537
end body end body
end function end function

View File

@ -1,8 +1,8 @@
module: Math module: Math
function: factorial function: factorial
parameter: parameter:
declare n1: long declare n1: int
declare n2: long declare n2: int
return_type: long return_type: long
body: body:
return n1+n2 return n1+n2

View File

@ -91,7 +91,7 @@ public class SnowCLI {
System.exit(exitCode); System.exit(exitCode);
} catch (Exception e) { } catch (Exception e) {
// 捕获命令执行过程中的异常并打印错误消息 // 捕获命令执行过程中的异常并打印错误消息
System.err.println("Error: " + e.getMessage()); // System.err.println("Error: " + e.getMessage());
System.exit(1); System.exit(1);
} }
} }

View File

@ -4,6 +4,7 @@ import org.jcnc.snow.compiler.lexer.base.TokenScanner;
import org.jcnc.snow.compiler.lexer.scanners.*; import org.jcnc.snow.compiler.lexer.scanners.*;
import org.jcnc.snow.compiler.lexer.token.Token; import org.jcnc.snow.compiler.lexer.token.Token;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -24,6 +25,7 @@ public class LexerEngine {
*/ */
private final List<Token> tokens = new ArrayList<>(); private final List<Token> tokens = new ArrayList<>();
private final String absPath;
/** /**
* 词法上下文提供字符流读取与位置信息 * 词法上下文提供字符流读取与位置信息
*/ */
@ -34,6 +36,8 @@ public class LexerEngine {
*/ */
private final List<TokenScanner> scanners; private final List<TokenScanner> scanners;
private final List<LexicalError> errors = new ArrayList<>();
/** /**
* 构造词法分析器假定输入源自标准输入文件名默认为 <stdin> * 构造词法分析器假定输入源自标准输入文件名默认为 <stdin>
* *
@ -43,6 +47,7 @@ public class LexerEngine {
this(source, "<stdin>"); this(source, "<stdin>");
} }
/** /**
* 构造词法分析器并指定源文件名用于诊断信息 * 构造词法分析器并指定源文件名用于诊断信息
* 构造时立即进行全量扫描 * 构造时立即进行全量扫描
@ -51,6 +56,7 @@ public class LexerEngine {
* @param sourceName 文件名或来源描述"Main.snow" * @param sourceName 文件名或来源描述"Main.snow"
*/ */
public LexerEngine(String source, String sourceName) { public LexerEngine(String source, String sourceName) {
this.absPath = new File(sourceName).getAbsolutePath();
this.context = new LexerContext(source); this.context = new LexerContext(source);
this.scanners = List.of( this.scanners = List.of(
new WhitespaceTokenScanner(), // 跳过空格制表符等 new WhitespaceTokenScanner(), // 跳过空格制表符等
@ -68,15 +74,28 @@ public class LexerEngine {
try { try {
scanAllTokens(); scanAllTokens();
} catch (LexicalException le) { } catch (LexicalException le) {
// 输出文件名::: 错误信息简洁明了 // 输出绝对路径: x, y: 错误信息
System.err.printf( System.err.printf(
"%s:%d:%d: %s%n", "%s: 行 %d, 列 %d: %s%n",
sourceName, absPath,
le.getLine(), // 获取出错行号 le.getLine(),
le.getColumn(), // 获取出错列号 le.getColumn(),
le.getMessage() // 错误描述 le.getReason()
); );
System.exit(65); // 65 = EX_DATAERR标准数据错误退出码 System.exit(65); // 65 = EX_DATAERR
}
LexerEngine.report(this.getErrors());
}
/**
* 静态报告方法
*/
public static void report(List<LexicalError> errors) {
if (errors != null && !errors.isEmpty()) {
System.err.println("\n词法分析发现 " + errors.size() + " 个错误:");
errors.forEach(err -> System.err.println(" " + err));
} else {
System.out.println("## 词法分析通过,没有发现错误\n");
} }
} }
@ -88,15 +107,28 @@ public class LexerEngine {
private void scanAllTokens() { private void scanAllTokens() {
while (!context.isAtEnd()) { while (!context.isAtEnd()) {
char currentChar = context.peek(); char currentChar = context.peek();
// 依次查找能处理当前字符的扫描器 boolean handled = false;
for (TokenScanner scanner : scanners) { for (TokenScanner scanner : scanners) {
if (scanner.canHandle(currentChar, context)) { if (scanner.canHandle(currentChar, context)) {
scanner.handle(context, tokens); try {
break; // 已处理跳到下一个字符 scanner.handle(context, tokens);
} catch (LexicalException le) {
// 收集词法错误不直接退出
errors.add(new LexicalError(
absPath, le.getLine(), le.getColumn(), le.getReason()
));
// 跳过当前字符防止死循环
context.advance();
}
handled = true;
break;
} }
} }
if (!handled) {
// 万一没有任何扫描器能处理跳过一个字符防止死循环
context.advance();
}
} }
// 末尾补一个 EOF 标记
tokens.add(Token.eof(context.getLine())); tokens.add(Token.eof(context.getLine()));
} }
@ -108,4 +140,11 @@ public class LexerEngine {
public List<Token> getAllTokens() { public List<Token> getAllTokens() {
return List.copyOf(tokens); return List.copyOf(tokens);
} }
/**
* 返回全部词法错误
*/
public List<LexicalError> getErrors() {
return List.copyOf(errors);
}
} }

View File

@ -0,0 +1,20 @@
package org.jcnc.snow.compiler.lexer.core;
public class LexicalError {
private final String file;
private final int line;
private final int column;
private final String message;
public LexicalError(String file, int line, int column, String message) {
this.file = file;
this.line = line;
this.column = column;
this.message = message;
}
@Override
public String toString() {
return file + ": 行 " + line + ", 列 " + column + ": " + message;
}
}

View File

@ -19,6 +19,8 @@ public class LexicalException extends RuntimeException {
private final int line; private final int line;
/** 错误发生的列号从1开始 */ /** 错误发生的列号从1开始 */
private final int column; private final int column;
/** 错误原因 */
private final String reason;
/** /**
* 构造词法异常 * 构造词法异常
@ -27,13 +29,14 @@ public class LexicalException extends RuntimeException {
* @param column 出错列号 * @param column 出错列号
*/ */
public LexicalException(String reason, int line, int column) { public LexicalException(String reason, int line, int column) {
// 构造出错消息禁止异常堆栈打印 // 错误描述直接为 reason禁止异常堆栈打印
super(String.format("Lexical error: %s at %d:%d", reason, line, column), super(reason, null, false, false);
null, false, false); this.reason = reason;
this.line = line; this.line = line;
this.column = column; this.column = column;
} }
/** /**
* 屏蔽异常堆栈填充始终不打印堆栈信息 * 屏蔽异常堆栈填充始终不打印堆栈信息
*/ */
@ -51,4 +54,10 @@ public class LexicalException extends RuntimeException {
* @return 列号 * @return 列号
*/ */
public int getColumn() { return column; } public int getColumn() { return column; }
/**
* 获取出错的描述
* @return 出错描述
*/
public String getReason() { return reason; }
} }

View File

@ -35,51 +35,56 @@ import org.jcnc.snow.compiler.lexer.token.TokenType;
*/ */
public class NumberTokenScanner extends AbstractTokenScanner { public class NumberTokenScanner extends AbstractTokenScanner {
/** 合法类型后缀字符集合 */ /** 合法类型后缀字符集合(单字符,大小写均可) */
private static final String SUFFIX_CHARS = "bslfdBSLFD"; private static final String SUFFIX_CHARS = "bslfdBSLFD";
@Override @Override
public boolean canHandle(char c, LexerContext ctx) { public boolean canHandle(char c, LexerContext ctx) {
// 仅当遇到数字时本扫描器才处理
return Character.isDigit(c); return Character.isDigit(c);
} }
@Override @Override
protected Token scanToken(LexerContext ctx, int line, int col) { protected Token scanToken(LexerContext ctx, int line, int col) {
StringBuilder literal = new StringBuilder(); StringBuilder literal = new StringBuilder();
boolean hasDot = false; // 是否已遇到小数点 boolean hasDot = false; // 标记是否已出现过小数点
/* 1. 读取数字主体(整数 / 小数) */ /* 1. 读取数字主体部分(包括整数、小数) */
while (!ctx.isAtEnd()) { while (!ctx.isAtEnd()) {
char c = ctx.peek(); char c = ctx.peek();
if (c == '.' && !hasDot) { if (c == '.' && !hasDot) {
// 遇到第一个小数点
hasDot = true; hasDot = true;
literal.append(ctx.advance()); literal.append(ctx.advance());
} else if (Character.isDigit(c)) { } else if (Character.isDigit(c)) {
// 吸收数字字符
literal.append(ctx.advance()); literal.append(ctx.advance());
} else { } else {
// 非数字/非小数点终止主体读取
break; break;
} }
} }
/* 2. 处理后缀或非法跟随字符 */ /* 2. 检查数字字面量后的字符,决定是否继续吸收或抛出异常 */
if (!ctx.isAtEnd()) { if (!ctx.isAtEnd()) {
char next = ctx.peek(); char next = ctx.peek();
/* 2-A: 合法类型后缀,直接吸收 */ /* 2-A: 合法类型后缀,直接吸收(如 42L、3.0F */
if (SUFFIX_CHARS.indexOf(next) >= 0) { if (SUFFIX_CHARS.indexOf(next) >= 0) {
literal.append(ctx.advance()); literal.append(ctx.advance());
} }
/* 2-B: 未知字母紧邻 → 抛异常 */ /* 2-B: 若紧跟未知字母(如 42X抛出词法异常 */
else if (Character.isLetter(next)) { else if (Character.isLetter(next)) {
throw new LexicalException( throw new LexicalException(
"Unknown numeric suffix '" + next + "'", "未知的数字类型后缀 '" + next + "'",
line, col line, col
); );
} }
/* 2-C: 数字后空白(非换行)→ 若空白后跟字母,抛异常 */ /* 2-C: 若数字后有空白,且空白后紧跟字母(如 3 L也为非法 */
else if (Character.isWhitespace(next) && next != '\n') { else if (Character.isWhitespace(next) && next != '\n') {
int off = 1; int off = 1;
char look; char look;
// 跳过所有空白字符找到第一个非空白字符
do { do {
look = ctx.peekAhead(off); look = ctx.peekAhead(off);
if (look == '\n' || look == '\0') break; if (look == '\n' || look == '\0') break;
@ -88,20 +93,21 @@ public class NumberTokenScanner extends AbstractTokenScanner {
} while (true); } while (true);
if (Character.isLetter(look)) { if (Character.isLetter(look)) {
// 抛出数字字面量与位宽符号之间不允许有空白符
throw new LexicalException( throw new LexicalException(
"Whitespace between numeric literal and an alphabetic character is not allowed", "数字字面量与位宽符号之间不允许有空白符",
line, col line, col
); );
} }
} }
/* 2-D: 紧邻字符为 '/' → 抛异常以避免死循环 */ /* 2-D: 若紧跟 '/',抛出异常防止死循环 */
else if (next == '/') { else if (next == '/') {
throw new LexicalException( throw new LexicalException(
"Unexpected '/' after numeric literal", "数字字面量后不允许直接出现 '/'",
line, col line, col
); );
} }
/* 其余字符(运算符、分隔符等)留给后续扫描器处理 */ // 其余情况如分号括号运算符交由其他扫描器处理
} }
/* 3. 返回 NUMBER_LITERAL Token */ /* 3. 返回 NUMBER_LITERAL Token */

View File

@ -28,7 +28,7 @@ public final class SemanticAnalysisReporter {
System.err.println("语义分析发现 " + errors.size() + " 个错误:"); System.err.println("语义分析发现 " + errors.size() + " 个错误:");
errors.forEach(err -> System.err.println(" " + err)); errors.forEach(err -> System.err.println(" " + err));
} else { } else {
// System.out.println("## 语义分析通过,没有发现错误\n"); System.out.println("\n## 语义分析通过,没有发现错误\n");
} }
} }