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 381 382 383 384 385 386 387 388 | 1593x 1593x 1593x 1593x 1593x 507x 507x 20957x 1317x 14806x 14806x 14806x 14806x 3925x 2x 3923x 3923x 3923x 2x 3921x 14802x 8747x 4536x 1955x 1353x 1084x 269x 269x 20x 4x 16x 16x 4x 12x 5x 7x 2x 5x 1962x 454x 1508x 4536x | /**
* Factory functions and type guards for IScopeSymbol.
*
* Provides utilities for creating and inspecting C-Next scopes.
*/
import type IScopeSymbol from "../transpiler/types/symbols/IScopeSymbol";
import type TVisibility from "../transpiler/types/TVisibility";
import ESourceLanguage from "./types/ESourceLanguage";
import QualifiedCName from "./QualifiedCName";
class ScopeUtils {
// ============================================================================
// Factory Functions
// ============================================================================
/**
* Create the global scope with self-reference parent.
*
* Global scope has:
* - name: "" (empty string)
* - parent: points to itself (self-reference)
* - scope: points to itself (self-reference)
*/
static createGlobalScope(): IScopeSymbol {
// Create a mutable object first to establish self-references
const global: IScopeSymbol = {
kind: "scope",
name: "",
parent: null as unknown as IScopeSymbol, // Temporary, will be set below
scope: null as unknown as IScopeSymbol, // Temporary, will be set below
members: [],
functions: [],
variables: [],
memberVisibility: new Map(),
// #1334: filled by ScopeCollector, one entry per declaring block.
declarationSites: new Set<string>(),
sourceFile: "",
sourceLine: 0,
sourceLanguage: ESourceLanguage.CNext,
isExported: true,
// Patched below: identityOf walks the scope chain, and the chain is not
// complete until the self-references are set.
fullyQualifiedCName: "",
cnxScopedName: "",
};
// Set self-references for global scope
(global as unknown as { parent: IScopeSymbol }).parent = global;
(global as unknown as { scope: IScopeSymbol }).scope = global;
// #1285: computed through the same encoder as every other symbol rather than
// hardcoded, so the global scope cannot become the one symbol whose identity
// was derived a second way. Both resolve to "" -- it has no name and no
// outer scope -- which is what makes a global symbol keep its bare name.
Object.assign(global, ScopeUtils.identityOf(global));
return global;
}
/**
* Create a named scope with the given parent.
*
* Named scopes can be nested (e.g., Outer.Inner).
*/
static createScope(name: string, parent: IScopeSymbol): IScopeSymbol {
const scope: IScopeSymbol = {
kind: "scope",
name,
parent,
scope: parent, // Scope's containing scope is its parent
// #1285: a nested scope's own identity comes from its parent chain, so
// `Inner` inside `Outer` is `Outer__Inner` without any site knowing how
// deep it sits.
...ScopeUtils.identityOf({ name, scope: parent }),
members: [],
functions: [],
variables: [],
memberVisibility: new Map(),
// #1334: filled by ScopeCollector, one entry per declaring block.
declarationSites: new Set<string>(),
sourceFile: "",
sourceLine: 0,
sourceLanguage: ESourceLanguage.CNext,
isExported: true,
};
return scope;
}
// ============================================================================
// Type Guards
// ============================================================================
/**
* Check if a scope is the global scope.
*
* Global scope is identified by:
* - Empty name ("")
* - Self-referential parent (parent === scope)
*/
static isGlobalScope(scope: IScopeSymbol): boolean {
return scope.name === "" && scope.parent === scope;
}
// ============================================================================
// Visibility Utilities
// ============================================================================
/**
* ADR-016: Get the default visibility for a scope member based on its type.
*
* Member-type-aware defaults reduce boilerplate:
* - Functions: public by default (API surface)
* - Variables/types: private by default (internal state)
*
* @param isFunction - Whether the member is a function declaration
* @returns The default visibility for this member type
*/
static getDefaultVisibility(isFunction: boolean): TVisibility {
return isFunction ? "public" : "private";
}
// ============================================================================
// Path Utilities
// ============================================================================
/**
* Get the scope path from outermost to innermost (excluding global scope).
*
* For scope "Outer.Inner", returns ["Outer", "Inner"].
* For global scope, returns [].
*/
static getScopePath(scope: IScopeSymbol): string[] {
const path: string[] = [];
const seen = new Set<IScopeSymbol>();
let current = scope;
while (!ScopeUtils.isGlobalScope(current)) {
// A parent chain that revisits a scope never reaches the global scope.
// Only createGlobalScope() may be its own parent, and only with an empty
// name; anything else self-referential is malformed. Fail loudly: this
// walk runs for every symbol added to the SymbolTable, and looping here
// hangs the whole transpile with no output to diagnose.
if (seen.has(current)) {
throw new Error(
`Malformed scope chain: '${current.name}' is its own ancestor, so it ` +
`never reaches the global scope. Build scopes with ` +
`ScopeUtils.createGlobalScope()/createScope() — only the global ` +
`scope is self-parented, and its name must be empty.`,
);
}
seen.add(current);
path.unshift(current.name);
// A chain that simply ends is the other way this walk fails to terminate
// at the global scope, and it is the shape hand-built scopes actually
// have — the encoder this replaced took `{ name: string }` structurally,
// so an object with no parent was a complete scope to it. Without this the
// next line hands `undefined` to isGlobalScope and the whole transpile
// dies on a bare TypeError, now from inside SymbolTable.addTSymbol, which
// runs for every symbol.
if (!current.parent) {
throw new Error(
`Malformed scope chain: '${current.name}' has no parent, so it never ` +
`reaches the global scope. Build scopes with ` +
`ScopeUtils.createGlobalScope()/createScope().`,
);
}
current = current.parent;
}
return path;
}
// ============================================================================
// Transpiled C Names
// ============================================================================
/**
* Build the transpiled C name for a symbol from its scope chain.
*
* This is the single encoder for symbol identity: `Motor__init` for `init`
* in scope `Motor`, `Outer__Inner__process` for a nested scope, and the bare
* name for a global symbol. ADR-063 makes the result injective, so it is also
* the canonical identity a symbol can be looked up by.
*
* Walks the parent chain rather than reading `scope.name` alone — the latter
* is the leaf name, so it silently drops outer scopes. The two agreed only
* because the grammar does not admit nested scopes today, which is a latent
* divergence rather than a shared decision.
*
* @param symbol Any symbol carrying a bare name and its declaring scope
* @returns The C identifier, e.g. "Motor__init"
*/
static getTranspiledCName(symbol: {
name: string;
scope: IScopeSymbol;
}): string {
return QualifiedCName.fromParts([
...ScopeUtils.getScopePath(symbol.scope),
symbol.name,
]);
}
/**
* Build the SOURCE-language qualified name for a symbol from its scope chain.
*
* `Motor.init`, `Outer.Inner.process`, and the bare name for a global symbol --
* the spelling a C-Next author would recognize. The counterpart to
* getTranspiledCName, which builds the identifier the C compiler sees.
*
* Walks the same parent chain, for the same reason: reading `scope.name` alone
* drops every outer scope.
*/
static getCnxScopedName(symbol: {
name: string;
scope: IScopeSymbol;
}): string {
return QualifiedCName.fromSourceParts([
...ScopeUtils.getScopePath(symbol.scope),
symbol.name,
]);
}
/**
* The C name a bare member of `scope` is emitted under, or the bare name at
* file scope.
*
* The drop-in for `QualifiedCName.fromParts([currentScopeName, name])`, which is the
* leaf-only encoder #1285 exists to remove. Identical at depth one -- a
* top-level scope's leaf name IS its whole chain -- and correct beyond it,
* where the string version dropped every outer component.
*
* Null and the global scope both mean "no qualification": a global symbol
* keeps its bare name, which is what makes `global.x` reachable.
*/
static qualifyInScope(name: string, scope: IScopeSymbol | null): string {
return ScopeUtils.qualifyPathInScope([name], scope);
}
/**
* Qualify a bare type name against a scope, if the scope declares it.
*
* ADR-057: a bare name inside a scope resolves local -> scope -> global. The
* predicate is asked about the QUALIFIED name so that only actual type
* declarations capture it -- a scope function or variable sharing a leaf name
* with a global type must not shadow that type at a type position.
*
* Takes the scope REFERENCE, not its name. The string version this replaces
* joined one level from a leaf, so at depth two it asked about
* `Inner__Config` for a type whose name is `Outer__Inner__Config` and got
* "no" -- silently falling through to the bare name, which is the #1200
* failure shape (#1285).
*
* `isKnownType` is injected rather than read from CodeGenState so this stays
* usable from the symbols layer, which must not depend on codegen.
*/
static qualifyScopeType(
typeName: string,
scope: IScopeSymbol | null,
isKnownType: (qualifiedName: string) => boolean,
): string {
if (!scope || ScopeUtils.isGlobalScope(scope)) {
return typeName;
}
const qualified = ScopeUtils.getTranspiledCName({ name: typeName, scope });
return isKnownType(qualified) ? qualified : typeName;
}
/**
* Resolve an array dimension that names a symbol (an enum count, a macro) to
* the identifier the generated C should use.
*
* Issue #1127: this rule previously lived only in
* HeaderSymbolAdapter.resolveArrayDimension() and served variables only, so a
* struct field carrying `EColor.COUNT` had no way to reach `EColor__COUNT`. It is
* shared so the variable path and the struct-field path apply one rule;
* `isKnownEnum` is injected rather than read from CodeGenState so this stays
* usable from any layer.
*
* #1357: moved here from QualifiedCName, and takes the scope REFERENCE rather
* than its name. It is a scope-aware operation -- three of its four branches
* qualify against the enclosing scope -- so on QualifiedCName it was the last
* API through which a caller holding only a scope NAME could still build a
* one-level qualified name. Qualifying through `qualifyInScope` also walks the
* parent chain, which the name-taking version could not.
*
* @param dim Dimension text as written in the source
* @param scope Enclosing scope, or null/global at file scope
* @param isKnownEnum Does this *qualified* name name an enum?
* @returns The C identifier, or `dim` unchanged when it names nothing
*
* @example resolveDimensionName("EColor.COUNT", global, p) => "EColor__COUNT"
* @example resolveDimensionName("State.COUNT", Motor, p) => "Motor__State__COUNT"
* @example resolveDimensionName("this.State.COUNT", Motor, p) => "Motor__State__COUNT"
* @example resolveDimensionName("global.EColor.COUNT", Motor, p) => "EColor__COUNT"
* @example resolveDimensionName("10", Motor, p) => "10"
*/
static resolveDimensionName(
dim: string,
scope: IScopeSymbol | null,
isKnownEnum: (qualifiedName: string) => boolean,
): string {
if (!dim.includes(QualifiedCName.SOURCE_SEPARATOR)) {
return dim;
}
const parts = dim.split(QualifiedCName.SOURCE_SEPARATOR);
// `global.X.Y` is explicitly global - drop the marker, add no scope prefix
if (parts[0] === "global") {
return QualifiedCName.fromParts(parts.slice(1));
}
// `this.X.Y` is explicitly scope-local - drop the marker, prefix the scope
if (parts[0] === "this") {
return ScopeUtils.qualifyPathInScope(parts.slice(1), scope);
}
// Bare `X.Y` inside a scope resolves scope-first, then global (ADR-057).
// Prefix only when the scope really declares that enum.
if (
scope &&
!ScopeUtils.isGlobalScope(scope) &&
isKnownEnum(ScopeUtils.qualifyInScope(parts[0], scope))
) {
return ScopeUtils.qualifyPathInScope(parts, scope);
}
return QualifiedCName.fromParts(parts);
}
/**
* The C name a multi-part member path takes inside `scope`, or the bare joined
* path at file scope. The one implementation; `qualifyInScope` is the
* single-component spelling of it.
*
* #1385 review: the two used to branch on the same guard and then both build
* `fromParts([...getScopePath(scope), ...])`, which is one decision written
* twice -- in the file whose entire purpose is being the one encoder.
*
* Collapsing them settles a divergence rather than introducing one. The old
* `qualifyInScope` returned a DOTTED name verbatim at file scope but expanded
* it inside a scope, because only the second path reached `fromParts`:
*
* qualifyInScope("a.b", null) -> "a.b" <- did not expand
* qualifyInScope("a.b", Motor) -> "Motor__a__b"
*
* Both now expand. Nothing passes a dotted name today -- every one of the 21
* call sites hands over a bare identifier or an already-split component -- so
* this is behavior-preserving in practice and consistent for the first time.
*/
static qualifyPathInScope(
path: readonly string[],
scope: IScopeSymbol | null,
): string {
if (!scope || ScopeUtils.isGlobalScope(scope)) {
return QualifiedCName.fromParts(path);
}
return QualifiedCName.fromParts([
...ScopeUtils.getScopePath(scope),
...path,
]);
}
/**
* Both qualified names for a symbol, computed together.
*
* Returned as a pair rather than as two separate calls so a construction site
* cannot produce half an identity. Setting one and forgetting the other leaves
* a symbol whose C name and source name disagree about where it lives, which
* is the shape of defect this whole line of work exists to remove.
*
* Spread into every C-Next symbol literal, so both names are a property of the
* symbol from the moment it exists rather than something each consumer
* re-derives from `scope`.
*/
static identityOf(symbol: { name: string; scope: IScopeSymbol }): {
fullyQualifiedCName: string;
cnxScopedName: string;
} {
return {
fullyQualifiedCName: ScopeUtils.getTranspiledCName(symbol),
cnxScopedName: ScopeUtils.getCnxScopedName(symbol),
};
}
}
export default ScopeUtils;
|