Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | 46x 517x 517x 517x 517x 651x 651x 5x 5x 656x 405x 118x 146x 315x 315x 258x 258x 11x 11x 11x 33x 763x 752x 752x 564x 159x 139x 139x 139x 9x 130x 159x 78x 16x 16x 16x 13x 13x 483x 483x 322x 322x 161x 152x 152x 725x 725x 15x 15x 322x 322x 1x 1x 321x 321x 321x 321x 5x 126x 20x 126x 126x 126x 13x 13x 7x 2x 165x 163x 163x 150x 54x 96x 85x 85x 74x 52x 165x 165x 1903x 1903x 1866x 165x 36x 36x 517x 517x 517x 517x 159x 517x | /**
* ADR-024 integer conversions: E0868, E0869 and E0891.
*
* #1322. Six rules across `TypeResolver` and `CodeGenerator`, reached through
* three entry points -- a declaration's initializer, an assignment, a cast --
* and two rethrow WRAPPERS that caught the message and prefixed `${line}:${col}`
* onto it. The wrappers are why the assignment fixtures already showed a real
* position while the cast fixtures showed `1:0`: the same rule, smuggling its
* position through the message on one path and not the other.
*
* ## The rule, once
*
* A literal must fit the target's range. A non-literal integer source must not
* be wider than the target, and must agree with it on signedness. That is the
* whole of it, whichever of the three spellings reaches it -- which is why it
* is one analyzer with three listener methods rather than three checks.
*
* ## What is deliberately NOT typed
*
* A lone bit extraction, `large[0, 8]`, is ADR-024's explicit reinterpret --
* the escape hatch the rule tells the author to use. Typing it would make the
* sanctioned form fail the very check it exists to satisfy; codegen's
* declaration path declined for that reason, and this pass declines for all
* three. A composite's integer type is the typer's, which settles it by
* `CompositeType.integerOf` over its value leaves -- category from the first
* integer operand, width from the widest -- the one rule 2.2 sizes its clamp
* helper by (#1668).
*
* ## Two holes codegen had, both closed
*
* A COMPOSITE source was typed on a declaration and not on an assignment, and
* an assignment was checked against the ROOT variable's declared type rather
* than the type the value actually lands in. The second is the sharper one:
* `c.col <- wide` emitted `c.col = wide;`, a u32 truncated into a u8 field with
* no diagnostic, because the lookup found `c` -- a struct -- and skipped.
* Reading the chain to the field is what ADR-036's bounds rule already did,
* so both holes closed by asking the question that was already being asked
* next door -- now the one operand typer's `typeOfTarget` (#1668).
*
* ## A third hole this closes
*
* `u8 narrow <- this.wide;` inside a scope compiled clean, while the identical
* line at top level was rejected -- codegen's text-keyed lookup did not resolve
* the `this.` spelling, so the source read as untyped and untyped never
* rejects. The lexical frames resolve it, so the rule now holds in the scope
* contexts too. Measured against the corpus before relying on it.
*
* ## Typed by the one operand typer (#1668)
*
* Source and target come from `OperandTyper`, so a suffixed literal is its
* suffix's type (`u8 x <- 300u16` narrows), `-w` is `w`'s type, a cast inside
* a composite counts at the type it names, a bit range's const width folds,
* and a C or C++ header's integer -- a source, or the field a value lands in
* -- has its width on this target. Every assignment site is read, `for`
* headers included (#1726).
*/
import { ParserRuleContext, ParseTreeWalker } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import TYPE_WIDTH from "../../types/TYPE_WIDTH";
import invariant from "../../utils/invariant";
import ParserUtils from "../../utils/ParserUtils";
import TypeCheckUtils from "../../utils/TypeCheckUtils";
import OperandTyper from "../../utils/OperandTyper";
import CompositeType from "../../utils/CompositeType";
import AssignmentSiteListener from "./AssignmentSiteListener";
import StructInitializerType from "./helpers/StructInitializerType";
import IIntegerConversionError from "./types/IIntegerConversionError";
import type IAnalysisContext from "./types/IAnalysisContext";
import type IOperandType from "../../types/IOperandType";
import type TAssignmentSite from "./types/TAssignmentSite";
const INTEGER_LITERAL = /^-?(?:\d+|0[xX][0-9a-fA-F]+|0[bB][01]+)$/;
class IntegerConversionListener extends CNextListener {
private readonly found: IIntegerConversionError[] = [];
public constructor(private readonly context: IAnalysisContext) {
super();
}
public errors(): IIntegerConversionError[] {
return this.found;
}
// --- The spellings that reach the one rule -------------------------------
override enterVariableDeclaration = (
ctx: Parser.VariableDeclarationContext,
): void => {
this.checkDeclaration(ctx.type().getText(), ctx.expression());
};
/**
* A `for` header's declaration is a declaration (#1760 second review:
* `for (u8 j <- 300; ...)` and `for (u32 i <- k; ...)` were accepted).
*/
override enterForVarDecl = (ctx: Parser.ForVarDeclContext): void => {
this.checkDeclaration(ctx.type().getText(), ctx.expression());
};
private checkDeclaration(
target: string,
value: Parser.ExpressionContext | null,
): void {
if (!value || !TypeCheckUtils.isInteger(target)) return;
this.check(target, value, "assign", true);
}
// --- E0891 at every other position a value lands in ------------------------
//
// #1760 second review, owner ruling "all positions now": a float reaches an
// integer argument, return, field or element only through a cast, as it
// reaches a declaration or an assignment. Narrowing and sign change at these
// positions are #1618's.
override enterArgumentList = (ctx: Parser.ArgumentListContext): void => {
for (const argument of ctx.expression()) this.checkFloatingAt(argument);
};
override enterReturnStatement = (
ctx: Parser.ReturnStatementContext,
): void => {
this.checkFloatingAt(ctx.expression());
};
override enterFieldInitializer = (
ctx: Parser.FieldInitializerContext,
): void => {
this.checkFloatingAt(ctx.expression());
};
override enterArrayInitializer = (
ctx: Parser.ArrayInitializerContext,
): void => {
this.checkFloatingAt(ctx.expression());
for (const element of ctx.arrayInitializerElement()) {
this.checkFloatingAt(element.expression());
}
};
/** E0891 for a value, at the integer type its position gives it */
private checkFloatingAt(value: Parser.ExpressionContext | null): void {
if (!value) return;
const target = StructInitializerType.valueType(value, this.context);
if (target !== null && TypeCheckUtils.isInteger(target)) {
this.checkFloating(target, value);
}
}
/** An assignment, in a statement or a `for` header (#1726) */
public checkSite(site: TAssignmentSite): void {
// A compound operator is arithmetic at the operands' width, which ADR-044
// governs; only a plain `<-` is a conversion.
if (!site.assignmentOperator().ASSIGN()) return;
const value = site.expression();
const target = site.assignmentTarget();
// A two-expression subscript is a slice or a bit range (ADR-007): a SPAN of
// the buffer, not an element, with rules of its own.
if (target.postfixTargetOp().some((op) => op.expression().length === 2)) {
return;
}
// The type the value actually lands in -- following the chain to the
// field or element, not the root variable's own type. See the class
// comment for what reading the root instead let through.
const targetType =
OperandTyper.typeOfTarget(target, this.context)?.typeName ?? "";
if (!TypeCheckUtils.isInteger(targetType)) return;
this.check(targetType, value, "assign", true);
}
override enterCastExpression = (ctx: Parser.CastExpressionContext): void => {
const target = ctx.type().getText();
if (!TypeCheckUtils.isInteger(target)) return;
// Composites are not typed for a cast: `(u8)(a + b)` is the author saying
// which width they mean.
const source = this.conversionSource(ctx.unaryExpression(), false);
if (source !== null) this.checkConversion(target, source, ctx, "cast");
};
// --- The one rule --------------------------------------------------------
/**
* `typeComposites` is `true` everywhere now, and the parameter survives only
* because a CAST still declines: `(u8)(a + b)` is the author saying which
* width they mean.
*
* It existed to reproduce a divergence codegen had. A composite source
* (`a + b` -- category from the first integer operand, width from the widest)
* was typed on a DECLARATION's initializer and never on an assignment
* statement, so `u8 s <- large + 1;` was rejected while
* `matrix2d[i][j] <- i * 10 + j;` was accepted. #1322 preserved that and
* raised it; the language owner ruled it a bug, and it is closed.
*/
private check(
target: string,
value: Parser.ExpressionContext,
kind: "assign" | "cast",
typeComposites: boolean,
): void {
const text = value.getText().trim();
if (INTEGER_LITERAL.test(text)) {
this.checkLiteral(target, text, value);
return;
}
if (this.checkFloating(target, value)) return;
const source = this.conversionSource(value, typeComposites);
if (source !== null) this.checkConversion(target, source, value, kind);
}
/**
* E0891: a floating value reaches an integer target only through a cast,
* which saturates. #1800, owner ruling 2026-09-28: "this should be a
* compiler error with an explicit cast". The implicit form had been
* accepted, and emitted as C's conversion, which is undefined for NaN and
* for a value past the target's range. Asked of every value leaf, so a
* floating composite or ternary counts. A cast, which is not this path,
* and a float's bit range (ADR-007), which the typer types as an integer,
* do not.
*/
private checkFloating(target: string, value: ParserRuleContext): boolean {
const floating = CompositeType.floatingOf(
OperandTyper.valueLeaves(value, this.context),
);
if (floating === null) return false;
this.report(
value,
"E0891",
`Implicit conversion from floating ${floating} to integer ${target}`,
`Write the conversion as a cast, which saturates: (${target})value (ADR-024)`,
);
return true;
}
private checkLiteral(
target: string,
text: string,
at: ParserRuleContext,
): void {
// BigInt, not parseInt: a u64 bound is past 2^53, where a double stops
// being exact and `0xFFFFFFFFFFFFFFFF` would round into range.
const value = text.startsWith("-") ? -BigInt(text.slice(1)) : BigInt(text);
if (TypeCheckUtils.isUnsigned(target) && value < 0n) {
this.report(
at,
"E0868",
`Negative value ${text} cannot be assigned to unsigned type ${target}`,
`An unsigned type holds no negative values; use a signed type such as i${TYPE_WIDTH[target]}.`,
);
return;
}
const range = TypeCheckUtils.integerRange(target);
invariant(range, `every caller checks that ${target} is an integer`);
const [min, max] = range;
if (value < min || value > max) {
this.report(
at,
"E0868",
`Value ${text} exceeds ${target} range (${min} to ${max})`,
"Widen the target type, or narrow the value.",
);
}
}
private checkConversion(
target: string,
source: string,
at: ParserRuleContext,
kind: "assign" | "cast",
): void {
if (source === target || !TypeCheckUtils.isInteger(source)) return;
const verb = kind === "cast" ? "cast" : "assign";
const subject = kind === "cast" ? "expr" : "value";
const targetWidth = TYPE_WIDTH[target];
if (TYPE_WIDTH[source] > targetWidth) {
this.report(
at,
"E0869",
`Cannot ${verb} ${source} to ${target} (narrowing)`,
`Use bit indexing to say which bits you mean: ${subject}[0, ${targetWidth}]`,
);
return;
}
if (TypeCheckUtils.isSigned(source) !== TypeCheckUtils.isSigned(target)) {
this.report(
at,
"E0869",
`Cannot ${verb} ${source} to ${target} (sign change)`,
`Use bit indexing to reinterpret the bits explicitly: ${subject}[0, ${targetWidth}]`,
);
}
}
// --- What type a source is ------------------------------------------------
/**
* The integer type a source converts from, as a C-Next integer name, or
* null when the rule does not judge it (#1668, the design's ยง5 row):
*
* - a top-level ternary: its branches are what matter, and
* `(val > 0) ? 1 : -1` has literal branches with no declared type (a
* `test-no-warnings` execution fixture asserts it is fine);
* - a lone bit extraction, `large[0, 8]`: ADR-024's explicit reinterpret,
* the form this rule tells the author to use;
* - a composite, `a + b`: its integer type, as the typer settled it -- the
* one rule 2.2 sizes its clamp helper by -- unless `composites` is false,
* as for a cast;
* - anything else: the typer's type, a C or C++ integer included, at its
* width on this target; a suffixed literal is its suffix's type.
*/
private conversionSource(
expr: ParserRuleContext,
composites: boolean,
): string | null {
if (IntegerConversionListener.isTernary(expr)) return null;
const t = OperandTyper.typeOf(expr, this.context);
if (t === null) return null;
if (t.form.kind === "composite") {
return composites && t.bitWidth !== null ? t.typeName : null;
}
if (t.form.kind === "bitRange" || t.form.kind === "bitIndex") return null;
return IntegerConversionListener.integerName(t);
}
/** `u8`/`i32`... for an integer of known width, else null */
private static integerName(t: IOperandType): string | null {
if (t.dimensions.length > 0 || t.bitWidth === null) return null;
if (t.category === "signed") return `i${t.bitWidth}`;
Eif (t.category === "unsigned") return `u${t.bitWidth}`;
return null;
}
/** Whether the expression, past its single-child levels, is a real ternary. */
private static isTernary(expr: ParserRuleContext): boolean {
let node: ParserRuleContext = expr;
while (node.getChildCount() === 1) {
const child = node.getChild(0);
if (!(child instanceof ParserRuleContext)) break;
node = child;
}
return (
node instanceof Parser.TernaryExpressionContext && node.COLON() !== null
);
}
private report(
at: ParserRuleContext,
code: string,
message: string,
helpText: string,
): void {
const { line, column } = ParserUtils.getPosition(at);
this.found.push({ code, line, column, message, helpText });
}
}
class IntegerConversionAnalyzer {
/** #1456: handed in rather than read off shared state. */
constructor(private readonly context: IAnalysisContext) {}
public analyze(tree: Parser.ProgramContext): IIntegerConversionError[] {
const listener = new IntegerConversionListener(this.context);
ParseTreeWalker.DEFAULT.walk(listener, tree);
ParseTreeWalker.DEFAULT.walk(
new AssignmentSiteListener((site) => listener.checkSite(site)),
tree,
);
// Reported in source order, as the one walk these replace did
return listener
.errors()
.sort((a, b) => a.line - b.line || a.column - b.column);
}
}
export default IntegerConversionAnalyzer;
|