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 | 406x 406x 1297x 1297x 1297x 1297x 1297x 1297x 35x 1262x 115x 115x 1377x 947x 430x 430x 430x 407x 23x 23x 517x 517x 517x 517x 111x 406x 406x 430x 430x 430x 397x 33x 16x 17x 1x 16x 34x 418x 418x 201x 217x 217x 170x 47x 23x | /**
* UndeclaredValueAnalyzer — rejects a bare identifier in a value position that
* denotes nothing this file can see (E0427).
*
* Issue #1353. `#985` closed this hole for a CALL (`E0422`); the value
* reference was never covered, so `u32 v <- notDeclaredAnywhere;` exited 0 and
* emitted `uint32_t v = notDeclaredAnywhere;`. The cause is the same shape as
* #1312's: `TypeValidator.resolveBareIdentifier` returns `string | null` where
* `null` means BOTH "emit it unchanged, it is fine" (a local needing no rename,
* or a known global at file scope) and "no idea what this is", so no caller can
* tell a resolved name from an unresolved one.
*
* Three positions can hold an undeclared name and they are one question asked
* three ways -- "is this name visible here, as kind K?":
*
* | position | owner |
* | -------- | ---------------------------------- |
* | call | `FunctionCallAnalyzer` (E0422) |
* | type | `UndeclaredTypeAnalyzer` (E0426) |
* | value | this analyzer (E0427) |
*
* The shared half is the lookup, not the policy: existence goes through
* `NameExistence` and `ScopeFrameResolver`, while each position keeps its own
* rules. E0422's are substantial and specific to calls -- ADR-030 ordering,
* ADR-040 callable variables, ADR-057 implicit scope calls, stdlib header
* hints -- and folding them in here would delete working behavior across ten
* fixtures rather than remove a duplicate decision.
*
* A call target is therefore skipped outright: E0422 already owns it, and two
* diagnostics for one name is worse than one.
*/
import { ParserRuleContext, ParseTreeWalker, TerminalNode } from "antlr4ng";
import { CNextListener } from "../../PARSE/2-Parse/grammar/CNextListener";
import * as Parser from "../../PARSE/2-Parse/grammar/CNextParser";
import BUILTIN_TYPE_NAMES from "./BUILTIN_TYPE_NAMES";
import ChainRoot from "../../utils/ChainRoot";
import ICodeGenSymbols from "../../types/ICodeGenSymbols";
import IUndeclaredValueError from "./types/IUndeclaredValueError";
import NameExistence from "../../PARSE/3-Declare/NameExistence";
import ParserUtils from "../../utils/ParserUtils";
import REJECTED_KEYWORDS from "./REJECTED_KEYWORDS";
import ScopeUtils from "../../utils/ScopeUtils";
import OperandTyper from "../../utils/OperandTyper";
import SymbolTable from "../../PARSE/3-Declare/SymbolTable";
import TChainRoot from "../../types/TChainRoot";
import type IAnalysisContext from "./types/IAnalysisContext";
class UndeclaredValueListener extends CNextListener {
constructor(private readonly analyzer: UndeclaredValueAnalyzer) {
super();
}
/**
* The READ position.
*
* ADR-016's three spellings read the chain from DIFFERENT offsets, which is
* the whole of `ChainRoot`'s docstring: a bare name IS the primary, while
* `this.x` and `global.x` put the keyword on the primary and the name in the
* first op. Taking the name from the primary alone is why every rooted read
* went unchecked -- `u32 v <- this.gx`, where `gx` is a file-scope global and
* not a member of the enclosing scope, emitted `Scope__gx` at exit 0. That is
* #1582's own defect one grammar rule over, and it is the case the write
* position's fixture calls "the only witness to the split".
*/
override enterPostfixExpression = (
ctx: Parser.PostfixExpressionContext,
): void => {
const primary = ctx.primaryExpression();
Iif (!primary) {
return;
}
const ops = ctx.postfixOp();
const { root, identifier, opsConsumed } = ChainRoot.headOf(primary, ops);
// `name(...)` is a call. E0422 owns undefined calls, with ADR-030/040/057
// rules this analyzer deliberately does not reimplement. The root keyword
// consumes the primary, so the parentheses sit `opsConsumed` further along
// -- the offset travels with the root instead of being re-derived here.
if (
ops.length > opsConsumed &&
ops[opsConsumed].getText().startsWith("(")
) {
return;
}
this.check(identifier, root, ctx);
};
/**
* The WRITE position. `assignmentTarget` is its own grammar rule, not a
* `postfixExpression`, so no target ever reached the listener above and
* `witness <- 5` against a name in no scope transpiled at exit 0 (#1582).
*
* One hook covers all three rules that reference the target -- the assignment
* statement and a `for` loop's init and update clauses -- because they share
* the node, not because the check is repeated for each.
*/
override enterAssignmentTarget = (
ctx: Parser.AssignmentTargetContext,
): void => {
this.check(ctx.IDENTIFIER(), ChainRoot.ofTarget(ctx), ctx);
};
/**
* One name, one question, for both positions.
*
* The two hooks differ only in how they read `(root, identifier)` off their
* node -- the pair `ChainRoot` already separates. Everything after that is
* the same policy, so it is written once: a second copy would be free to
* gain an exempt spelling the other did not, which is the read/write
* divergence #1582 is about.
*
* `identifier` is nullable because the read hook's primary may be a literal
* or a parenthesised expression, and because the generated accessor for an
* assignment target asserts non-null over a `getToken` that can return null.
*/
private check(
identifier: TerminalNode | null,
root: TChainRoot,
ctx: ParserRuleContext,
): void {
if (!identifier) {
return;
}
const name = identifier.getText();
// ADR-026: `break`/`continue` parse as identifiers and are rejected by
// E0703, which names the structured alternative. Reporting them as
// undefined would be true and useless.
//
// That reasoning is the READ position's, and it does not carry to the
// write position: E0703 is raised from `LoopAnalyzer`'s
// `enterPrimaryExpression` alone, so `break <- 5` is exempted here and
// owned by nothing -- it reaches the C compiler as `break = 5;`. The
// exemption stays shared rather than being split, because narrowing it is
// a diagnostic decision for the rule that owns those spellings; tracked as
// #1632 with the reproduction.
Iif (REJECTED_KEYWORDS.has(name) || BUILTIN_TYPE_NAMES.has(name)) {
return;
}
if (this.analyzer.isVisible(name, root, ctx)) {
return;
}
// The caret names the identifier, not the `this`/`global` keyword the
// spelling may start with. `getPosition` takes the shape structurally, so
// the terminal's own token is what carries the position here. For a bare
// name this is the primary's own start token, so no position moves.
const { line, column } = ParserUtils.getPosition({
start: identifier.symbol,
});
this.analyzer.addError(name, line, column);
}
}
class UndeclaredValueAnalyzer {
private readonly errors: IUndeclaredValueError[] = [];
/**
* #1456: what the program declares, handed in rather than read off
* `CodeGenState`. Constructor rather than a parameter on `analyze`, because
* the predicates below are reached from the listener's walk.
*/
constructor(private readonly context: IAnalysisContext) {}
analyze(tree: Parser.ProgramContext): IUndeclaredValueError[] {
this.errors.length = 0;
// Same precondition as E0426, and the value axis needs it MORE: a `#define`
// never reaches the symbol table at all, so `_isKnownForeignName` -- which
// does catch a header typedef -- has nothing to fall back on for a macro.
if (this.context.reachesForeignHeader) {
return this.errors;
}
ParseTreeWalker.DEFAULT.walk(new UndeclaredValueListener(this), tree);
return this.errors;
}
/**
* Whether a name in a VALUE position denotes something this file can see.
*
* ADR-016's root is PART of the question, not a decoration on it: `this.x`
* asks the enclosing scope, `global.x` asks file scope, and only a bare name
* searches outward (#1322 found four copies of that distinction disagreeing,
* three of which emitted broken C at exit 0). A local is Program's lexical
* frames' answer (#1668); a file-scope value -- this file's or an included
* one's -- is `NameExistence.isValueName`'s, and a scope member is
* `isScopeMemberValue`'s, whichever file declared it.
*
* Without the split, `this.gx` where `gx` is a file-scope global -- not a
* member of the enclosing scope -- passes on the bare lookup and emits
* `Scope__gx`, a name nothing declares.
*
* Both POSITIONS ask this identically, which is why there is one predicate
* and not two. #1582 first gave the split to the write position alone, and
* the read position beside it could not see a rooted name at all: `this.gx
* <- 5` was rejected while `u32 v <- this.gx` on the next line emitted
* `Scope__gx` at exit 0. Two predicates that agree by inspection are the
* divergence this analyzer exists to prevent.
*/
isVisible(name: string, root: TChainRoot, at: ParserRuleContext): boolean {
const symbols = this.context.symbols;
const scopePath = OperandTyper.scopePathAt(at, this.context);
if (root === null) {
return (
UndeclaredValueAnalyzer.isDeclaredValue(
name,
at,
scopePath,
this.context,
) || NameExistence.isKnownEnumMember(name, symbols)
);
}
// `global.x` is a file-scope value, this file's or one an `#include`
// brought: the include-filtered predicate answers both.
if (root === "global") {
return NameExistence.isValueName(name, symbols, this.context.symbolTable);
}
// `this.` outside any scope is E0431's to reject, and two diagnostics for
// one name is worse than one.
if (scopePath === "") {
return true;
}
return UndeclaredValueAnalyzer.isScopeMemberValue(
name,
scopePath,
symbols,
this.context.symbolTable,
);
}
/**
* Whether a name is a value declared by the scope at `scopePath`.
*
* Written once because the bare spelling and the `this.` spelling ask it
* identically -- the bare outward walk reaches the enclosing scope, and
* `this.` names it directly. #1582 briefly carried two copies eighty lines
* apart, which is the shape CLAUDE.md forbids: the single source of truth is
* the DECISION, and the term `scopeMembers` is missing (it spans the run
* rather than the include graph, #1494) has to be added in one place.
*/
private static isScopeMemberValue(
name: string,
scopePath: string,
symbols: ICodeGenSymbols,
symbolTable: SymbolTable,
): boolean {
return (
NameExistence.isValueName(
ScopeUtils.qualifyInScope(name, scopePath),
symbols,
symbolTable,
) ||
(symbols.scopeMembers.get(scopePath)?.has(name) ?? false)
);
}
/**
* Whether a bare name denotes a DECLARED value -- a variable, parameter,
* const, function, register or type name this file can see -- as distinct
* from an enum MEMBER, which `isVisible` also admits.
*
* #1322: split out for E0424. An enum member written bare is a name this
* file can see (so it is not undefined), but whether it may stand bare is a
* question about its POSITION, and that rule must first know the name is not
* a variable that merely shares the spelling. One predicate answers both
* analyzers, so "declared" cannot mean two things.
*/
static isDeclaredValue(
name: string,
at: ParserRuleContext,
scopePath: string,
context: IAnalysisContext,
): boolean {
const symbolTable = context.symbolTable;
// A local, parameter or `for` variable in an enclosing lexical frame of
// THIS file, declared before `at` (#1668: Program's frames).
//
// #1398: deliberately the lexical half alone. The full `typeOfName` falls
// back to the run-wide symbol table, which answers "declared anywhere in
// this run" -- so a const declared in a sibling that this file never
// included resolved here and returned visible, and E0427 could not fire
// across a file boundary at all. The cross-file half of the question is
// answered by `NameExistence.isValueName` below, whose `knownVariables`
// term is include-filtered. The fallback itself stays for #1220's
// essential-type analyzers, which want exactly the run-wide answer.
if (
context.program.lexicalDeclarationAt(
context.sourceFile,
name,
ParserUtils.getPosition(at),
) !== null
) {
return true;
}
const symbols = context.symbols;
// A function referenced as a value (ADR-029 function-as-type), a type used
// as the base of `Type.MEMBER`, a register, which is a value at an address
// (ADR-004) and so answers here but NOT in the type position (#1336), and
// -- since #1398 -- a file-scope variable or const from this file or a
// `.cnx` it includes. That last term was briefly written here instead of in
// the predicate, which left the module that owns "is this a visible value"
// with the incomplete answer; see `isValueName`'s comment.
// ADR-111: when a register becomes a type, `isValueName` loses the register
// term. It does not collapse into `isTypeName` -- the variable term stays.
//
// #1430: `CodeGenState.knownFunctions` is deliberately NOT consulted, for
// the reason `NameExistence`'s class comment already gives for
// `callbackTypes`. Codegen fills it and `reset()` clears it, both after the
// analyzers run, so at analysis time it is empty for the first file and
// holds file N-1's function names for every file after. Because it is OR'd
// toward "visible", a stale entry SUPPRESSED E0427: the same program with
// its `#include` lines swapped either diagnosed the undefined name or
// emitted C the compiler rejects at exit 0. It was redundant as well as
// wrong -- `isValueName` reaches `symbols.functionReturnTypes`, the
// per-file view of the same ADR-029 fact, on the identical key. The
// qualified read below goes for the same reason, but on the key-shape
// argument alone: reinstating it reddens no fixture.
//
// #1295 correction: that used to credit "the `scopeMembers` term beside it
// already answers cross-file (#1494)", and `scopeMembers` does NOT cross a
// file boundary -- `git grep -c scopeMembers -- VisibleSymbols.ts` is 0. It
// is absent from the merge accumulator and survives only via the `...base`
// spread, i.e. THIS file's symbols. `knownScopes` and
// `scopeMemberVisibility`, written by the same `processScope`, ARE merged,
// so the three disagree about what "visible" means. What actually answers
// cross-file here is the run-wide `symbolTable` term below.
if (NameExistence.isValueName(name, symbols, symbolTable)) {
return true;
}
return (
scopePath !== "" &&
UndeclaredValueAnalyzer.isScopeMemberValue(
name,
scopePath,
symbols,
symbolTable,
)
);
}
addError(identifier: string, line: number, column: number): void {
this.errors.push({
code: "E0427",
identifier,
line,
column,
message: `'${identifier}' is not defined`,
});
}
}
export default UndeclaredValueAnalyzer;
|