!49 enhancement: 优化数字字面量溢出的错误检测与提示

Merge pull request !49 from Luke/bugfix/int-literal-range-diagnostic
This commit is contained in:
Luke 2025-07-30 16:29:58 +00:00 committed by Gitee
commit 15cd43a7d5
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
5 changed files with 356 additions and 69 deletions

11
.run/Bug3.run.xml Normal file
View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Bug3" type="Application" factoryName="Application" folderName="BugFarm">
<option name="ALTERNATIVE_JRE_PATH" value="graalvm-ce-23" />
<option name="MAIN_CLASS_NAME" value="org.jcnc.snow.cli.SnowCLI" />
<module name="Snow" />
<option name="PROGRAM_PARAMETERS" value="compile run -d playground/BugFarm/Bug3 -o target/Bug3 --debug" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View File

@ -1,8 +1,7 @@
module: Main
import: os
function: main
parameter:
return_type: int
return_type: void
body:
loop:
init:
@ -23,23 +22,12 @@ module: Main
step:
inter_j = inter_j + 1
body:
end body
end loop
end body
end loop
return 0
end body
end function
function: print
parameter:
declare i1: int
return_type: void
body:
syscall("PRINT",i1)
end body
end function
end module

View File

@ -0,0 +1,14 @@
module: Main
import: os
function: main
return_type: void
body:
// 合法
declare b1: byte = 127b
declare s1: short = 32767s
declare i1: int = 2147483647
declare l1: long = 9223372036854775807L
end body
end function
end module

View File

@ -0,0 +1,11 @@
module: os
import: os
function: print
parameter:
declare i1: int
return_type: void
body:
syscall("PRINT",i1)
end body
end function
end module

View File

@ -5,50 +5,324 @@ import org.jcnc.snow.compiler.parser.ast.NumberLiteralNode;
import org.jcnc.snow.compiler.semantic.analyzers.base.ExpressionAnalyzer;
import org.jcnc.snow.compiler.semantic.core.Context;
import org.jcnc.snow.compiler.semantic.core.ModuleInfo;
import org.jcnc.snow.compiler.semantic.error.SemanticError;
import org.jcnc.snow.compiler.semantic.symbol.SymbolTable;
import org.jcnc.snow.compiler.semantic.type.BuiltinType;
import org.jcnc.snow.compiler.semantic.type.Type;
import static org.jcnc.snow.compiler.semantic.type.BuiltinType.*;
/**
* {@code NumberLiteralAnalyzer} 用于对数字字面量表达式进行语义分析并推断其精确类型
* <p>
* 类型判定逻辑如下:
* {@code NumberLiteralAnalyzer}
*
* <p>职责
* <ul>
* <li>对数字字面量表达式进行语义分析并推断其精确类型</li>
* <li>按照推断类型进行范围校验</li>
* <li>发生越界时给出智能的错误提示与合理建议</li>
* </ul>
*
* <p>类型推断规则</p>
* <ol>
* <li>首先检查字面量末尾是否带有类型后缀不区分大小写:
* <ul>
* <li>{@code b}: byte 类型{@link BuiltinType#BYTE}</li>
* <li>{@code s}: short 类型{@link BuiltinType#SHORT}</li>
* <li>{@code l}: long 类型{@link BuiltinType#LONG}</li>
* <li>{@code f}: float 类型{@link BuiltinType#FLOAT}</li>
* <li>{@code d}: double 类型{@link BuiltinType#DOUBLE}</li>
* </ul>
* </li>
* <li>若无后缀:
* <ul>
* <li>只要文本中包含 {@code '.'} {@code e/E}即判为 double 类型</li>
* <li>否则默认判为 int 类型</li>
* </ul>
* </li>
* <li>若字面量以类型后缀(b/s/l/f大小写均可)结尾则按后缀直接推断目标类型</li>
* <ul>
* <li>b byte</li>
* <li>s short</li>
* <li>l long</li>
* <li>f float</li>
* </ul>
* <li>若无后缀且文本包含小数点或科学计数法('.' 'e/E')则推断为 double(浮点默认 double不支持 d/D 后缀)</li>
* <li>否则推断为 int</li>
* </ol>
* 本分析器不处理溢出非法格式等诊断仅做类型推断
*
* <p>智能错误提示策略</p>
* <ul>
* <li>整数
* <ul>
* <li>int 放不下但 long 能放下 直接建议 long</li>
* <li> long 也放不下 一次性提示超出 int/long 可表示范围</li>
* </ul>
* </li>
* <li>浮点
* <ul>
* <li>float 放不下但 double 能放下 直接建议 double(无需 d 后缀)</li>
* <li> double 也放不下 一次性提示超出 float/double 可表示范围</li>
* </ul>
* </li>
* </ul>
*/
public class NumberLiteralAnalyzer implements ExpressionAnalyzer<NumberLiteralNode> {
/**
* 对数字字面量进行语义分析推断其类型
* <p>
* 分析流程:
* <ol>
* <li>若字面量以后缀结尾直接按后缀映射类型</li>
* <li>否则若含有小数点或科学计数法标记则为 double否则为 int</li>
* </ol>
* 根据字面量后缀和文本内容推断类型
*
* @param ctx 当前语义分析上下文可用于记录诊断信息等当前未使用
* @param mi 当前模块信息未使用
* @param fn 当前函数节点未使用
* @param locals 当前作用域的符号表未使用
* @param expr 数字字面量表达式节点
* @return 对应的 {@link BuiltinType} INTDOUBLE
* @param hasSuffix 是否存在类型后缀( b/s/l/f 有效)
* @param suffix 后缀字符(已转小写)
* @param digits 去掉下划线与后缀后的数字主体(可能含 . e/E)
* @return 推断类型(byte / short / int / long / float / double 之一)
*/
private static Type inferType(boolean hasSuffix, char suffix, String digits) {
if (hasSuffix) {
// 仅支持 b/s/l/f不支持 d/D(浮点默认 double)
return switch (suffix) {
case 'b' -> BYTE;
case 's' -> SHORT;
case 'l' -> BuiltinType.LONG;
case 'f' -> BuiltinType.FLOAT;
default -> INT; // 其他后缀当作无效 int 处理(如需严格可改为抛/非法后缀)
};
}
// 无后缀包含小数点或 e/E double否则 int
if (looksLikeFloat(digits)) {
return BuiltinType.DOUBLE; // 浮点默认 double
}
return INT;
}
/**
* 做范围校验发生越界时写入智能的错误与建议
*
* @param ctx 语义上下文(承载错误列表与日志)
* @param node 当前数字字面量节点
* @param inferred 推断类型
* @param digits 规整后的数字主体(去下划线去后缀"123.""123.0")
*/
private static void validateRange(Context ctx,
NumberLiteralNode node,
Type inferred,
String digits) {
try {
// 整数类型不允许出现浮点形式 //
if (inferred == BYTE) {
if (looksLikeFloat(digits)) throw new NumberFormatException(digits);
Byte.parseByte(digits);
} else if (inferred == SHORT) {
if (looksLikeFloat(digits)) throw new NumberFormatException(digits);
Short.parseShort(digits);
} else if (inferred == INT) {
if (looksLikeFloat(digits)) throw new NumberFormatException(digits);
Integer.parseInt(digits);
} else if (inferred == BuiltinType.LONG) {
if (looksLikeFloat(digits)) throw new NumberFormatException(digits);
Long.parseLong(digits);
}
// 浮点类型解析 + /下溢判断 //
else if (inferred == BuiltinType.FLOAT) {
float v = Float.parseFloat(digits);
// 上溢Infinity下溢解析为 0.0 但文本并非全零
if (Float.isInfinite(v) || (v == 0.0f && isTextualZero(digits))) {
throw new NumberFormatException("float overflow/underflow: " + digits);
}
} else if (inferred == BuiltinType.DOUBLE) {
double v = Double.parseDouble(digits);
if (Double.isInfinite(v) || (v == 0.0 && isTextualZero(digits))) {
throw new NumberFormatException("double overflow/underflow: " + digits);
}
}
} catch (NumberFormatException ex) {
// 智能的错误描述与建议(header 使用 digits避免带后缀)
String msg = getSmartSuggestionOneShot(digits, inferred);
ctx.getErrors().add(new SemanticError(node, msg));
ctx.log("错误: " + msg);
}
}
/**
* 生成智能的错误提示与建议
*
* <p>策略</p>
* <ul>
* <li>BYTE/SHORT/INT若能放进更大整型直接建议否则一次性提示已超出 int/long 范围</li>
* <li>LONG直接提示超出 long 可表示范围</li>
* <li>FLOAT double 能放下 建议 double否则一次性提示超出 float/double 可表示范围</li>
* <li>DOUBLE直接提示超出 double 可表示范围</li>
* </ul>
*
* @param digits 去后缀后的数字主体(用于 header 与建议示例)
* @param inferred 推断类型
* @return 完整错误消息(含建议)
*/
private static String getSmartSuggestionOneShot(String digits, Type inferred) {
String header;
String suggestion;
switch (inferred) {
case BYTE -> {
long v;
try {
v = Long.parseLong(digits);
} catch (NumberFormatException e) {
// long 都放不下智能
header = composeHeader(digits, "超出 byte/short/int/long 可表示范围。");
return header;
}
if (v >= Short.MIN_VALUE && v <= Short.MAX_VALUE) {
header = composeHeader(digits, "超出 byte 可表示范围。");
suggestion = "建议将变量类型声明为 short并在数字末尾添加 's'(如 " + digits + "s)。";
} else if (v >= Integer.MIN_VALUE && v <= Integer.MAX_VALUE) {
header = composeHeader(digits, "超出 byte 可表示范围。");
suggestion = "建议将变量类型声明为 int(如 " + digits + ")。";
} else {
header = composeHeader(digits, "超出 byte 可表示范围。");
suggestion = "建议将变量类型声明为 long并在数字末尾添加 'L'(如 " + digits + "L)。";
}
return appendSuggestion(header, suggestion);
}
case SHORT -> {
long v;
try {
v = Long.parseLong(digits);
} catch (NumberFormatException e) {
header = composeHeader(digits, "超出 short/int/long 可表示范围。");
return header;
}
if (v >= Integer.MIN_VALUE && v <= Integer.MAX_VALUE) {
header = composeHeader(digits, "超出 short 可表示范围。");
suggestion = "建议将变量类型声明为 int(如 " + digits + ")。";
} else {
header = composeHeader(digits, "超出 short 可表示范围。");
suggestion = "建议将变量类型声明为 long并在数字末尾添加 'L'(如 " + digits + "L)。";
}
return appendSuggestion(header, suggestion);
}
case INT -> {
try {
// 尝试解析为 long若成功说明能进 long
Long.parseLong(digits);
} catch (NumberFormatException e) {
// long 都放不下智能
header = composeHeader(digits, "超出 int/long 可表示范围。");
return header;
}
// 能进 long直接建议 long
header = composeHeader(digits, "超出 int 可表示范围。");
suggestion = "建议将变量类型声明为 long并在数字末尾添加 'L'(如 " + digits + "L)。";
return appendSuggestion(header, suggestion);
}
case LONG -> {
// 已明确处于 long 分支且越界智能
header = composeHeader(digits, "超出 long 可表示范围。");
return header;
}
case FLOAT -> {
// float 放不下尝试 double若能放下则直接建议 double否则智能提示 float/double 都不行
boolean fitsDouble = fitsDouble(digits);
if (fitsDouble) {
header = composeHeader(digits, "超出 float 可表示范围。");
suggestion = "建议将变量类型声明为 double(如 " + digits + ")。"; // double 默认无需 d 后缀
return appendSuggestion(header, suggestion);
} else {
header = composeHeader(digits, "超出 float/double 可表示范围。");
return header;
}
}
case DOUBLE -> {
header = composeHeader(digits, "超出 double 可表示范围。");
return header;
}
default -> {
header = composeHeader(digits, "超出 " + inferred + " 可表示范围。");
return header;
}
}
}
/**
* 生成越界错误头部统一使用数字主体而非原始 raw(避免带后缀引起误导)
*
* @param digits 去后缀后的数字主体
* @param tail 错误尾部描述(超出 int 可表示范围)
*/
private static String composeHeader(String digits, String tail) {
return "数值字面量越界: \"" + digits + "\" " + tail;
}
/**
* 在头部后拼接建议文本(若存在)
*
* @param header 错误头部
* @param suggestion 建议文本(可能为 null)
*/
private static String appendSuggestion(String header, String suggestion) {
return suggestion == null ? header : header + " " + suggestion;
}
/**
* 文本层面判断是否看起来是浮点字面量
* 只要包含 '.' 'e/E'即视为浮点
*/
private static boolean looksLikeFloat(String s) {
return s.indexOf('.') >= 0 || s.indexOf('e') >= 0 || s.indexOf('E') >= 0;
}
/**
* 文本判断是否为零(不解析纯文本)
* 在遇到 e/E 之前若出现任意 '1'..'9'视为非零否则视为零
* 用于识别文本非零但解析结果为 0.0的下溢场景
* <p>
*
* "0.0" true
* "000" true
* "1e-9999" false(e 前有 '1'若解析为 0.0 则视为下溢)
* "0e-9999" true
*/
private static boolean isTextualZero(String s) {
if (s == null || s.isEmpty()) return false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == 'e' || c == 'E') break; // 指数部分不参与是否为零的判断
if (c >= '1' && c <= '9') return true;
}
return false;
}
/**
* 判断 double 是否能正常表示(不溢出也非文本非零但解析为 0.0的下溢)
*
* @param digits 去后缀后的数字主体
*/
private static boolean fitsDouble(String digits) {
try {
double d = Double.parseDouble(digits);
return !Double.isInfinite(d) && !(d == 0.0 && isTextualZero(digits));
} catch (NumberFormatException e) {
return false;
}
}
/**
* 规整数字串
* 仅移除末尾的类型后缀( b/s/l/f大小写均可不含 d/D)
*
* @param s 原始字面量字符串
* @return 规整后的数字主体
*/
private static String normalizeDigits(String s) {
if (s == null) return "";
String t = s.trim();
if (t.isEmpty()) return t;
// 仅移除末尾的类型后缀(b/s/l/f大小写均可)
char last = t.charAt(t.length() - 1);
if ("bBsSfFlL".indexOf(last) >= 0) {
t = t.substring(0, t.length() - 1).trim();
}
return t;
}
/**
* 入口对数字字面量进行语义分析
* <p>
* 分步
* <ol>
* <li>读取原始文本 raw</li>
* <li>识别是否带后缀( b/s/l/f)</li>
* <li>规整数字主体 digits(去下划线去后缀补小数点零)</li>
* <li>按规则推断目标类型</li>
* <li>做范围校验越界时记录智能的错误与建议</li>
* <li>返回推断类型</li>
* </ol>
*/
@Override
public Type analyze(Context ctx,
@ -57,41 +331,30 @@ public class NumberLiteralAnalyzer implements ExpressionAnalyzer<NumberLiteralNo
SymbolTable locals,
NumberLiteralNode expr) {
// 获取字面量原始文本 "123", "3.14", "2f"
// 1) 原始文本( "123", "3.14", "2f", "1_000_000L", "1e300")
String raw = expr.value();
if (raw == null || raw.isEmpty()) {
// 理论上不应为空兜底返回 int 类型
return BuiltinType.INT;
return INT; // 空文本回退为 int(按需可改为错误)
}
// 获取最后一个字符判断是否为类型后缀b/s/l/f/d忽略大小写
// 2) 是否带后缀( b/s/l/f不支持 d/D)
char lastChar = raw.charAt(raw.length() - 1);
char suffix = Character.toLowerCase(lastChar);
boolean hasSuffix = switch (suffix) {
case 'b', 's', 'l', 'f', 'd' -> true;
case 'b', 's', 'l', 'f' -> true;
default -> false;
};
// 若有后缀 digits 为去除后缀的数字部分否则为原文本
String digits = hasSuffix ? raw.substring(0, raw.length() - 1) : raw;
// 3) 规整数字主体
String digitsNormalized = normalizeDigits(raw);
// 1. 若有后缀直接返回对应类型
if (hasSuffix) {
return switch (suffix) {
case 'b' -> BuiltinType.BYTE;
case 's' -> BuiltinType.SHORT;
case 'l' -> BuiltinType.LONG;
case 'f' -> BuiltinType.FLOAT;
case 'd' -> BuiltinType.DOUBLE;
default -> BuiltinType.INT; // 理论上不会到这里
};
}
// 4) 推断类型
Type inferred = inferType(hasSuffix, suffix, digitsNormalized);
// 2. 无后缀根据文本是否含小数点或科学计数法e/E判断类型
if (digits.indexOf('.') >= 0 || digits.indexOf('e') >= 0 || digits.indexOf('E') >= 0) {
return BuiltinType.DOUBLE; // 有小数/科学计数默认 double 类型
}
// 5) 范围校验(发生越界则收集智能的错误与建议)
validateRange(ctx, expr, inferred, digitsNormalized);
return BuiltinType.INT; // 否则为纯整数默认 int 类型
return inferred;
}
}