c-next 0.1.68 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/package.json +1 -1
  2. package/src/transpiler/output/codegen/CodeGenerator.ts +125 -135
  3. package/src/transpiler/output/codegen/TypeValidator.ts +2 -2
  4. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +2 -2
  5. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +29 -1
  6. package/src/transpiler/output/codegen/__tests__/TypeValidator.resolution.test.ts +1 -3
  7. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +3 -1
  8. package/src/transpiler/output/codegen/assignment/AssignmentContextBuilder.ts +49 -0
  9. package/src/transpiler/output/codegen/assignment/IAssignmentContext.ts +15 -0
  10. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +7 -0
  11. package/src/transpiler/output/codegen/assignment/handlers/ArrayHandlers.ts +24 -17
  12. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +16 -5
  13. package/src/transpiler/output/codegen/assignment/handlers/__tests__/AccessPatternHandlers.test.ts +9 -1
  14. package/src/transpiler/output/codegen/assignment/handlers/__tests__/ArrayHandlers.test.ts +18 -1
  15. package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitAccessHandlers.test.ts +9 -1
  16. package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitmapHandlers.test.ts +9 -1
  17. package/src/transpiler/output/codegen/assignment/handlers/__tests__/RegisterHandlers.test.ts +10 -1
  18. package/src/transpiler/output/codegen/assignment/handlers/__tests__/SpecialHandlers.test.ts +9 -1
  19. package/src/transpiler/output/codegen/assignment/handlers/__tests__/StringHandlers.test.ts +9 -1
  20. package/src/transpiler/output/codegen/helpers/MemberSeparatorResolver.ts +6 -1
  21. package/src/transpiler/state/CodeGenState.ts +35 -1
  22. package/src/transpiler/state/__tests__/CodeGenState.test.ts +5 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
@@ -220,41 +220,6 @@ const DEFAULT_TARGET: TargetCapabilities = {
220
220
  hasBasepri: false,
221
221
  };
222
222
 
223
- /**
224
- * ADR-044: Assignment context for overflow behavior tracking
225
- */
226
- interface AssignmentContext {
227
- targetName: string | null;
228
- targetType: string | null;
229
- overflowBehavior: TOverflowBehavior;
230
- }
231
-
232
- /**
233
- * Context for tracking current scope during code generation
234
- */
235
- interface GeneratorContext {
236
- currentScope: string | null; // ADR-016: renamed from currentNamespace
237
- currentFunctionName: string | null; // Issue #269: track current function for pass-by-value lookup
238
- currentFunctionReturnType: string | null; // Issue #477: track return type for enum inference
239
- indentLevel: number;
240
- scopeMembers: Map<string, Set<string>>; // scope -> member names (ADR-016)
241
- currentParameters: Map<string, TParameterInfo>; // ADR-006: track params for pointer semantics
242
- // Issue #558: modifiedParameters removed - now uses analysis-phase results from CodeGenState.modifiedParameters
243
- localArrays: Set<string>; // ADR-006: track local array variables (no & needed)
244
- localVariables: Set<string>; // ADR-016: track local variables (allowed as bare identifiers)
245
- floatBitShadows: Set<string>; // Track declared shadow variables for float bit indexing
246
- floatShadowCurrent: Set<string>; // Track which shadows have current value (skip redundant memcpy reads)
247
- inFunctionBody: boolean; // ADR-016: track if we're inside a function body
248
- typeRegistry: Map<string, TTypeInfo>; // Track variable types for bit access and .length
249
- expectedType: string | null; // For inferred struct initializers
250
- mainArgsName: string | null; // Track the args parameter name for main() translation
251
- assignmentContext: AssignmentContext; // ADR-044: Track current assignment for overflow
252
- lastArrayInitCount: number; // ADR-035: Track element count for size inference
253
- lastArrayFillValue: string | undefined; // ADR-035: Track fill-all value
254
- lengthCache: Map<string, string> | null; // Cache: variable name -> temp variable name for strlen optimization
255
- targetCapabilities: TargetCapabilities; // ADR-049: Target platform for atomic code generation
256
- }
257
-
258
223
  /**
259
224
  * Code Generator - Transpiles C-Next to C
260
225
  *
@@ -269,42 +234,6 @@ export default class CodeGenerator implements IOrchestrator {
269
234
  ["f64", "0.0"],
270
235
  ]);
271
236
 
272
- private context: GeneratorContext =
273
- CodeGenerator.createDefaultContext(DEFAULT_TARGET);
274
-
275
- /**
276
- * Create a fresh GeneratorContext with default values.
277
- */
278
- private static createDefaultContext(
279
- targetCapabilities: TargetCapabilities,
280
- ): GeneratorContext {
281
- return {
282
- currentScope: null,
283
- currentFunctionName: null,
284
- currentFunctionReturnType: null,
285
- indentLevel: 0,
286
- scopeMembers: new Map(),
287
- currentParameters: new Map(),
288
- localArrays: new Set(),
289
- localVariables: new Set(),
290
- floatBitShadows: new Set(),
291
- floatShadowCurrent: new Set(),
292
- inFunctionBody: false,
293
- typeRegistry: new Map(),
294
- expectedType: null,
295
- mainArgsName: null,
296
- assignmentContext: {
297
- targetName: null,
298
- targetType: null,
299
- overflowBehavior: "clamp",
300
- },
301
- lastArrayInitCount: 0,
302
- lastArrayFillValue: undefined,
303
- lengthCache: null,
304
- targetCapabilities,
305
- };
306
- }
307
-
308
237
  /** Token stream for comment extraction (ADR-043) */
309
238
  private tokenStream: CommonTokenStream | null = null;
310
239
 
@@ -485,7 +414,7 @@ export default class CodeGenerator implements IOrchestrator {
485
414
  expectedType: CodeGenState.expectedType,
486
415
  selfIncludeAdded: CodeGenState.selfIncludeAdded, // Issue #369
487
416
  // Issue #644: Postfix expression state
488
- scopeMembers: CodeGenState.scopeMembers,
417
+ scopeMembers: CodeGenState.getAllScopeMembers(),
489
418
  mainArgsName: CodeGenState.mainArgsName,
490
419
  floatBitShadows: CodeGenState.floatBitShadows,
491
420
  floatShadowCurrent: CodeGenState.floatShadowCurrent,
@@ -628,7 +557,7 @@ export default class CodeGenerator implements IOrchestrator {
628
557
  resolveIdentifier(identifier: string): string {
629
558
  // Check current scope first (inner scope shadows outer)
630
559
  if (CodeGenState.currentScope) {
631
- const members = CodeGenState.scopeMembers.get(CodeGenState.currentScope);
560
+ const members = CodeGenState.getScopeMembers(CodeGenState.currentScope);
632
561
  if (members?.has(identifier)) {
633
562
  return `${CodeGenState.currentScope}_${identifier}`;
634
563
  }
@@ -1148,10 +1077,34 @@ export default class CodeGenerator implements IOrchestrator {
1148
1077
  );
1149
1078
  }
1150
1079
 
1080
+ // Issue #779: Resolve bare scope member identifiers before postfix chain processing
1081
+ // This ensures scope members get their prefix even with array/member access.
1082
+ // Skip parameters - they don't need scope resolution and shouldn't be dereferenced
1083
+ // when used with array indexing (buf[idx] is valid C for pointer params).
1084
+ // Also skip known registers - they should be handled by the postfix chain builder
1085
+ // to enable proper register validation (requiring global. when shadowed).
1086
+ let resolvedIdentifier = identifier ?? "";
1087
+ if (!hasGlobal && !hasThis && identifier) {
1088
+ const isParameter = CodeGenState.currentParameters.has(identifier);
1089
+ const isLocalVariable = CodeGenState.localVariables.has(identifier);
1090
+ const isKnownRegister =
1091
+ CodeGenState.symbols?.knownRegisters.has(identifier);
1092
+ if (!isParameter && !isLocalVariable && !isKnownRegister) {
1093
+ const resolved = TypeValidator.resolveBareIdentifier(
1094
+ identifier,
1095
+ false, // not local
1096
+ (name: string) => this.isKnownStruct(name),
1097
+ );
1098
+ if (resolved !== null) {
1099
+ resolvedIdentifier = resolved;
1100
+ }
1101
+ }
1102
+ }
1103
+
1151
1104
  // SonarCloud S3776: Use BaseIdentifierBuilder for base identifier
1152
1105
  const safeIdentifier = identifier ?? "";
1153
1106
  const { result: baseResult, firstId } = BaseIdentifierBuilder.build(
1154
- safeIdentifier,
1107
+ hasGlobal || hasThis ? safeIdentifier : resolvedIdentifier,
1155
1108
  hasGlobal,
1156
1109
  hasThis,
1157
1110
  CodeGenState.currentScope,
@@ -1546,36 +1499,24 @@ export default class CodeGenerator implements IOrchestrator {
1546
1499
  return `\ntypedef ${callbackInfo.returnType} (*${callbackInfo.typedefName})(${paramList});\n`;
1547
1500
  }
1548
1501
 
1549
- /**
1550
- * Issue #268: Store unmodified parameters for a function.
1551
- * Maps function name -> Set of parameter names that were NOT modified.
1552
- * Used by Pipeline to update symbol info before header generation.
1553
- */
1554
- private readonly functionUnmodifiedParams: Map<string, Set<string>> =
1555
- new Map();
1556
-
1557
1502
  /**
1558
1503
  * Issue #268: Get unmodified parameters info for all functions.
1559
1504
  * Returns map of function name -> Set of unmodified parameter names.
1505
+ * Computed on-demand from functionSignatures and modifiedParameters.
1560
1506
  */
1561
1507
  getFunctionUnmodifiedParams(): ReadonlyMap<string, Set<string>> {
1562
- return this.functionUnmodifiedParams;
1508
+ return CodeGenState.getUnmodifiedParameters();
1563
1509
  }
1564
1510
 
1565
1511
  /**
1566
1512
  * Issue #268: Update symbol parameters with auto-const info.
1567
- * Issue #558: Now uses analysis-phase results for modification tracking.
1568
- */
1569
- updateFunctionParamsAutoConst(functionName: string): void {
1570
- // Collect unmodified parameters for this function using analysis results
1571
- const unmodifiedParams = new Set<string>();
1572
- const modifiedSet = CodeGenState.modifiedParameters.get(functionName);
1573
- for (const [paramName] of CodeGenState.currentParameters) {
1574
- if (!modifiedSet?.has(paramName)) {
1575
- unmodifiedParams.add(paramName);
1576
- }
1577
- }
1578
- this.functionUnmodifiedParams.set(functionName, unmodifiedParams);
1513
+ * Now a no-op - unmodified params are computed on-demand from CodeGenState.
1514
+ * Kept for IOrchestrator interface compatibility.
1515
+ */
1516
+ updateFunctionParamsAutoConst(_functionName: string): void {
1517
+ // No-op: Unmodified parameters are now computed on-demand from
1518
+ // CodeGenState.functionSignatures and CodeGenState.modifiedParameters
1519
+ // via CodeGenState.getUnmodifiedParameters().
1579
1520
  }
1580
1521
 
1581
1522
  /**
@@ -1773,23 +1714,16 @@ export default class CodeGenerator implements IOrchestrator {
1773
1714
  * Returns true if the callee modifies that parameter (should not have const).
1774
1715
  */
1775
1716
  isCalleeParameterModified(funcName: string, paramIndex: number): boolean {
1776
- const unmodifiedParams = this.functionUnmodifiedParams.get(funcName);
1777
- if (!unmodifiedParams) {
1778
- // Callee not yet processed - conservatively return false (assume unmodified)
1779
- // This means we won't mark our param as modified, which may cause a C compiler error
1780
- // if the callee actually modifies the param. The C compiler will catch this.
1781
- return false;
1782
- }
1783
-
1784
1717
  // Get the parameter name at the given index from the function signature
1785
1718
  const sig = CodeGenState.functionSignatures.get(funcName);
1786
1719
  if (!sig || paramIndex >= sig.parameters.length) {
1720
+ // Callee not yet processed - conservatively return false (assume unmodified)
1787
1721
  return false;
1788
1722
  }
1789
1723
 
1790
1724
  const paramName = sig.parameters[paramIndex].name;
1791
- // If the param is NOT in the unmodified set, it was modified
1792
- return !unmodifiedParams.has(paramName);
1725
+ // Check directly if the parameter is in the modified set
1726
+ return CodeGenState.isParameterModified(funcName, paramName);
1793
1727
  }
1794
1728
 
1795
1729
  /**
@@ -2047,7 +1981,14 @@ export default class CodeGenerator implements IOrchestrator {
2047
1981
  }
2048
1982
 
2049
1983
  /**
2050
- * Validate register access from inside a scope requires global. prefix
1984
+ * Validate register access from inside a scope requires global. prefix.
1985
+ *
1986
+ * Issue #779: Use ambiguity-aware validation - only require global. when
1987
+ * the register name is ACTUALLY shadowed by a local or scope member.
1988
+ *
1989
+ * Exceptions (no global. required):
1990
+ * 1. Scoped registers defined within the current scope
1991
+ * 2. Unambiguous access - no local/scope member with the same name
2051
1992
  */
2052
1993
  private _validateRegisterAccess(
2053
1994
  registerName: string,
@@ -2056,6 +1997,27 @@ export default class CodeGenerator implements IOrchestrator {
2056
1997
  ): void {
2057
1998
  // Only validate when inside a scope and accessing without global. prefix
2058
1999
  if (CodeGenState.currentScope && !hasGlobal) {
2000
+ // Check if this is a scoped register (defined within the current scope)
2001
+ // The registerName may already be the fully qualified name (e.g., "GPIO_PORTA")
2002
+ // if accessed as PORTA from inside scope GPIO
2003
+ const scopePrefix = `${CodeGenState.currentScope}_`;
2004
+ if (registerName.startsWith(scopePrefix)) {
2005
+ // This is a scoped register - allow bare access
2006
+ return;
2007
+ }
2008
+
2009
+ // Issue #779: Ambiguity-aware validation
2010
+ // Only require global. if the register name is shadowed by:
2011
+ // 1. A local variable in the current function
2012
+ // 2. A member of the current scope
2013
+ const isShadowedByLocal = CodeGenState.localVariables.has(registerName);
2014
+ const isShadowedByScope = CodeGenState.isCurrentScopeMember(registerName);
2015
+
2016
+ if (!isShadowedByLocal && !isShadowedByScope) {
2017
+ // Unambiguous - allow bare access
2018
+ return;
2019
+ }
2020
+
2059
2021
  throw new Error(
2060
2022
  `Error: Use 'global.${registerName}.${memberName}' to access register '${registerName}' ` +
2061
2023
  `from inside scope '${CodeGenState.currentScope}'`,
@@ -2190,30 +2152,11 @@ export default class CodeGenerator implements IOrchestrator {
2190
2152
  * Reset all generator state for a fresh generation pass.
2191
2153
  */
2192
2154
  private resetGeneratorState(targetCapabilities: TargetCapabilities): void {
2193
- // Reset global state first
2155
+ // Reset global state (CodeGenState.reset() handles all field initialization)
2194
2156
  CodeGenState.reset(targetCapabilities);
2195
2157
 
2196
2158
  // Set generator reference for handlers to use
2197
2159
  CodeGenState.generator = this;
2198
-
2199
- // Reset local context (will gradually migrate to CodeGenState)
2200
- this.context = CodeGenerator.createDefaultContext(targetCapabilities);
2201
-
2202
- CodeGenState.knownFunctions = new Set();
2203
- CodeGenState.functionSignatures = new Map();
2204
- CodeGenState.callbackTypes = new Map();
2205
- CodeGenState.callbackFieldTypes = new Map();
2206
- CodeGenState.usedClampOps = new Set();
2207
- CodeGenState.usedSafeDivOps = new Set();
2208
- CodeGenState.needsStdint = false;
2209
- CodeGenState.needsStdbool = false;
2210
- CodeGenState.needsString = false;
2211
- CodeGenState.needsFloatStaticAssert = false;
2212
- CodeGenState.needsISR = false;
2213
- CodeGenState.needsCMSIS = false;
2214
- CodeGenState.needsLimits = false;
2215
- CodeGenState.needsIrqWrappers = false;
2216
- CodeGenState.selfIncludeAdded = false;
2217
2160
  }
2218
2161
 
2219
2162
  /**
@@ -2224,7 +2167,7 @@ export default class CodeGenerator implements IOrchestrator {
2224
2167
 
2225
2168
  // Copy symbol data to CodeGenState.scopeMembers
2226
2169
  for (const [scopeName, members] of symbols.scopeMembers) {
2227
- CodeGenState.scopeMembers.set(scopeName, new Set(members));
2170
+ CodeGenState.setScopeMembers(scopeName, new Set(members));
2228
2171
  }
2229
2172
 
2230
2173
  // Issue #461: Initialize constValues from symbol table
@@ -2412,13 +2355,7 @@ export default class CodeGenerator implements IOrchestrator {
2412
2355
  }
2413
2356
 
2414
2357
  if (CodeGenState.needsIrqWrappers) {
2415
- output.push(
2416
- "// ADR-050: IRQ wrappers to avoid macro collisions with platform headers",
2417
- "static inline void __cnx_disable_irq(void) { __disable_irq(); }",
2418
- "static inline uint32_t __cnx_get_PRIMASK(void) { return __get_PRIMASK(); }",
2419
- "static inline void __cnx_set_PRIMASK(uint32_t mask) { __set_PRIMASK(mask); }",
2420
- "",
2421
- );
2358
+ output.push(...this.generateIrqWrappers());
2422
2359
  }
2423
2360
 
2424
2361
  if (CodeGenState.needsISR) {
@@ -4525,7 +4462,7 @@ export default class CodeGenerator implements IOrchestrator {
4525
4462
 
4526
4463
  // Scope member - may need prefixing
4527
4464
  if (CodeGenState.currentScope) {
4528
- const members = CodeGenState.scopeMembers.get(CodeGenState.currentScope);
4465
+ const members = CodeGenState.getScopeMembers(CodeGenState.currentScope);
4529
4466
  if (members?.has(id)) {
4530
4467
  const scopedName = `${CodeGenState.currentScope}_${id}`;
4531
4468
  return CppModeHelper.maybeAddressOf(scopedName);
@@ -6120,6 +6057,10 @@ export default class CodeGenerator implements IOrchestrator {
6120
6057
  const assignCtx = buildAssignmentContext(ctx, {
6121
6058
  typeRegistry: CodeGenState.typeRegistry,
6122
6059
  generateExpression: () => value,
6060
+ generateAssignmentTarget: (targetCtx) =>
6061
+ this.generateAssignmentTarget(targetCtx),
6062
+ isKnownRegister: (name) => CodeGenState.symbols!.knownRegisters.has(name),
6063
+ currentScope: CodeGenState.currentScope,
6123
6064
  });
6124
6065
  // ADR-109: Handlers access CodeGenState directly, no deps needed
6125
6066
  const assignmentKind = AssignmentClassifier.classify(assignCtx);
@@ -6608,6 +6549,55 @@ export default class CodeGenerator implements IOrchestrator {
6608
6549
  );
6609
6550
  }
6610
6551
 
6552
+ /**
6553
+ * Generate platform-portable IRQ wrappers for critical sections (ADR-050, Issue #778)
6554
+ *
6555
+ * Generates code that works on:
6556
+ * - ARM platforms (bare-metal or Arduino): Uses inline assembly for PRIMASK access
6557
+ * - AVR Arduino: Uses SREG save/restore pattern
6558
+ * - Other platforms: Falls back to CMSIS intrinsics
6559
+ *
6560
+ * This avoids dependencies on CMSIS headers which may not be available on all platforms
6561
+ * (e.g., Teensy 4.x via Arduino.h doesn't expose __get_PRIMASK/__set_PRIMASK).
6562
+ */
6563
+ private generateIrqWrappers(): string[] {
6564
+ return [
6565
+ "// ADR-050: Platform-portable IRQ wrappers for critical sections",
6566
+ "#if defined(__arm__) || defined(__ARM_ARCH)",
6567
+ "// ARM platforms (including ARM Arduino like Teensy 4.x, Due, Zero)",
6568
+ "// Provide inline assembly PRIMASK access to avoid CMSIS header dependencies",
6569
+ "__attribute__((always_inline)) static inline uint32_t __cnx_get_PRIMASK(void) {",
6570
+ " uint32_t result;",
6571
+ ' __asm volatile ("MRS %0, primask" : "=r" (result));',
6572
+ " return result;",
6573
+ "}",
6574
+ "__attribute__((always_inline)) static inline void __cnx_set_PRIMASK(uint32_t mask) {",
6575
+ ' __asm volatile ("MSR primask, %0" :: "r" (mask) : "memory");',
6576
+ "}",
6577
+ "#if defined(ARDUINO)",
6578
+ "static inline void __cnx_disable_irq(void) { noInterrupts(); }",
6579
+ "#else",
6580
+ "__attribute__((always_inline)) static inline void __cnx_disable_irq(void) {",
6581
+ ' __asm volatile ("cpsid i" ::: "memory");',
6582
+ "}",
6583
+ "#endif",
6584
+ "#elif defined(__AVR__)",
6585
+ "// AVR Arduino: use SREG for interrupt state",
6586
+ "// Note: Uses PRIMASK naming for API consistency across platforms (AVR has no PRIMASK)",
6587
+ "// Returns uint8_t which is implicitly widened to uint32_t at call sites - this is intentional",
6588
+ "static inline uint8_t __cnx_get_PRIMASK(void) { return SREG; }",
6589
+ "static inline void __cnx_set_PRIMASK(uint8_t mask) { SREG = mask; }",
6590
+ "static inline void __cnx_disable_irq(void) { cli(); }",
6591
+ "#else",
6592
+ "// Fallback: assume CMSIS is available",
6593
+ "static inline void __cnx_disable_irq(void) { __disable_irq(); }",
6594
+ "static inline uint32_t __cnx_get_PRIMASK(void) { return __get_PRIMASK(); }",
6595
+ "static inline void __cnx_set_PRIMASK(uint32_t mask) { __set_PRIMASK(mask); }",
6596
+ "#endif",
6597
+ "",
6598
+ ];
6599
+ }
6600
+
6611
6601
  /**
6612
6602
  * Mark a clamp operation as used (will trigger helper generation)
6613
6603
  */
@@ -332,7 +332,7 @@ class TypeValidator {
332
332
  return;
333
333
  }
334
334
 
335
- const scopeMembers = CodeGenState.scopeMembers.get(currentScope);
335
+ const scopeMembers = CodeGenState.getScopeMembers(currentScope);
336
336
  if (scopeMembers?.has(identifier)) {
337
337
  throw new Error(
338
338
  `Error: Use 'this.${identifier}' to access scope member '${identifier}' inside scope '${currentScope}'`,
@@ -410,7 +410,7 @@ class TypeValidator {
410
410
  identifier: string,
411
411
  currentScope: string,
412
412
  ): string | null {
413
- const scopeMembers = CodeGenState.scopeMembers.get(currentScope);
413
+ const scopeMembers = CodeGenState.getScopeMembers(currentScope);
414
414
  if (scopeMembers?.has(identifier)) {
415
415
  return `${currentScope}_${identifier}`;
416
416
  }
@@ -242,7 +242,7 @@ describe("CodeGenerator Coverage Tests", () => {
242
242
 
243
243
  // Manually set up scope context to test the resolution path
244
244
  CodeGenState.currentScope = "Motor";
245
- CodeGenState.scopeMembers.set("Motor", new Set(["speed", "setSpeed"]));
245
+ CodeGenState.setScopeMembers("Motor", new Set(["speed", "setSpeed"]));
246
246
 
247
247
  // Now resolve should return prefixed name (line 633)
248
248
  const resolved = generator.resolveIdentifier("speed");
@@ -253,7 +253,7 @@ describe("CodeGenerator Coverage Tests", () => {
253
253
  const { generator } = setupGenerator("u32 globalVar; void main() {}");
254
254
 
255
255
  CodeGenState.currentScope = "Motor";
256
- CodeGenState.scopeMembers.set("Motor", new Set(["speed"]));
256
+ CodeGenState.setScopeMembers("Motor", new Set(["speed"]));
257
257
 
258
258
  // globalVar is not in Motor scope members
259
259
  const resolved = generator.resolveIdentifier("globalVar");
@@ -7318,12 +7318,15 @@ describe("CodeGenerator", () => {
7318
7318
  expect(code).toContain("cfg.enabled = true");
7319
7319
  });
7320
7320
 
7321
- it("should throw when writing register from inside scope without global prefix", () => {
7321
+ it("should throw when writing register from inside scope when SHADOWED without global prefix", () => {
7322
+ // Issue #779: Ambiguity-aware validation - only require global. when shadowed
7322
7323
  const source = `
7323
7324
  register GPIO @ 0x40000000 {
7324
7325
  DR: u32 rw @ 0x00,
7325
7326
  }
7326
7327
  scope Motor {
7328
+ // Shadow the register name with a scope member
7329
+ u32 GPIO <- 0;
7327
7330
  public void init() {
7328
7331
  GPIO.DR <- 0xFF;
7329
7332
  }
@@ -7342,6 +7345,31 @@ describe("CodeGenerator", () => {
7342
7345
  ).toThrow("Use 'global.GPIO.DR' to access register");
7343
7346
  });
7344
7347
 
7348
+ it("should allow bare register access from inside scope when NOT shadowed", () => {
7349
+ // Issue #779: Ambiguity-aware validation - allow unambiguous access
7350
+ const source = `
7351
+ register GPIO @ 0x40000000 {
7352
+ DR: u32 rw @ 0x00,
7353
+ }
7354
+ scope Motor {
7355
+ public void init() {
7356
+ GPIO.DR <- 0xFF;
7357
+ }
7358
+ }
7359
+ `;
7360
+ const { tree, tokenStream } = CNextSourceParser.parse(source);
7361
+ const generator = new CodeGenerator();
7362
+ const tSymbols = CNextResolver.resolve(tree, "test.cnx");
7363
+ const symbols = TSymbolInfoAdapter.convert(tSymbols);
7364
+ const code = generator.generate(tree, tokenStream, {
7365
+ symbolInfo: symbols,
7366
+ sourcePath: "test.cnx",
7367
+ });
7368
+
7369
+ // Should generate GPIO_DR without error since GPIO is unambiguous
7370
+ expect(code).toContain("GPIO_DR = 0xFF");
7371
+ });
7372
+
7345
7373
  it("should generate scope member write from outside any scope", () => {
7346
7374
  const source = `
7347
7375
  scope Timer {
@@ -38,9 +38,7 @@ describe("TypeValidator.resolveBareIdentifier", () => {
38
38
 
39
39
  beforeEach(() => {
40
40
  CodeGenState.reset();
41
- CodeGenState.scopeMembers = new Map([
42
- ["Motor", new Set(["speed", "maxSpeed"])],
43
- ]);
41
+ CodeGenState.setScopeMembers("Motor", new Set(["speed", "maxSpeed"]));
44
42
  CodeGenState.typeRegistry.set("globalCounter", {
45
43
  baseType: "u32",
46
44
  bitWidth: 32,
@@ -89,7 +89,9 @@ function setupState(options: SetupStateOptions = {}): void {
89
89
  CodeGenState.currentScope = options.currentScope;
90
90
  }
91
91
  if (options.scopeMembers) {
92
- CodeGenState.scopeMembers = options.scopeMembers;
92
+ for (const [scope, members] of options.scopeMembers) {
93
+ CodeGenState.setScopeMembers(scope, members);
94
+ }
93
95
  }
94
96
  if (options.currentParameters) {
95
97
  CodeGenState.currentParameters = options.currentParameters;
@@ -18,6 +18,15 @@ interface IContextBuilderDeps {
18
18
 
19
19
  /** Generate C expression for a value */
20
20
  generateExpression(ctx: Parser.ExpressionContext): string;
21
+
22
+ /** Generate fully-resolved assignment target with scope prefixes */
23
+ generateAssignmentTarget(ctx: Parser.AssignmentTargetContext): string;
24
+
25
+ /** Check if an identifier is a known register (for skipping resolution) */
26
+ isKnownRegister(name: string): boolean;
27
+
28
+ /** Current scope name (for scoped register detection) */
29
+ currentScope: string | null;
21
30
  }
22
31
 
23
32
  /**
@@ -32,6 +41,37 @@ interface ITargetExtraction {
32
41
  lastSubscriptExprCount: number;
33
42
  }
34
43
 
44
+ /**
45
+ * Extract the base identifier from a resolved target string.
46
+ * Removes subscripts ([...]) from the resolved target.
47
+ * Examples:
48
+ * "ArrayBug_data[0]" -> "ArrayBug_data"
49
+ * "matrix[i][j]" -> "matrix"
50
+ * "item.field" -> "item"
51
+ * "Motor_speed" -> "Motor_speed"
52
+ */
53
+ function extractResolvedBaseIdentifier(resolvedTarget: string): string {
54
+ // Assumes resolvedTarget always starts with a valid identifier (not empty or starting with [ . ->)
55
+ // as generated by generateAssignmentTarget()
56
+ const bracketIndex = resolvedTarget.indexOf("[");
57
+ const dotIndex = resolvedTarget.indexOf(".");
58
+ const arrowIndex = resolvedTarget.indexOf("->");
59
+
60
+ // Find the earliest terminator
61
+ let endIndex = resolvedTarget.length;
62
+ if (bracketIndex !== -1 && bracketIndex < endIndex) {
63
+ endIndex = bracketIndex;
64
+ }
65
+ if (dotIndex !== -1 && dotIndex < endIndex) {
66
+ endIndex = dotIndex;
67
+ }
68
+ if (arrowIndex !== -1 && arrowIndex < endIndex) {
69
+ endIndex = arrowIndex;
70
+ }
71
+
72
+ return resolvedTarget.substring(0, endIndex);
73
+ }
74
+
35
75
  /**
36
76
  * Extract base identifier from assignment target.
37
77
  * With unified grammar, all patterns use IDENTIFIER postfixTargetOp*.
@@ -100,6 +140,13 @@ function buildAssignmentContext(
100
140
  // Generate value expression
101
141
  const generatedValue = deps.generateExpression(valueCtx);
102
142
 
143
+ // Generate fully-resolved target (with scope prefixes)
144
+ const resolvedTarget = deps.generateAssignmentTarget(targetCtx);
145
+
146
+ // Extract resolved base identifier for type lookups
147
+ // Removes subscripts ([...]) and member access (. or ->) from the end
148
+ const resolvedBaseIdentifier = extractResolvedBaseIdentifier(resolvedTarget);
149
+
103
150
  // Extract target info
104
151
  const hasGlobal = targetCtx.GLOBAL() !== null;
105
152
  const hasThis = targetCtx.THIS() !== null;
@@ -152,6 +199,8 @@ function buildAssignmentContext(
152
199
  cOp,
153
200
  isCompound,
154
201
  generatedValue,
202
+ resolvedTarget,
203
+ resolvedBaseIdentifier,
155
204
  firstIdTypeInfo,
156
205
  memberAccessDepth,
157
206
  subscriptDepth,
@@ -67,6 +67,21 @@ interface IAssignmentContext {
67
67
  /** Generated C expression for the value (right-hand side) */
68
68
  readonly generatedValue: string;
69
69
 
70
+ /**
71
+ * Fully-resolved assignment target with scope prefixes applied.
72
+ * Use this instead of raw identifiers to ensure proper scope resolution.
73
+ * Example: "data" inside scope ArrayBug -> "ArrayBug_data"
74
+ */
75
+ readonly resolvedTarget: string;
76
+
77
+ /**
78
+ * Resolved base identifier for type lookups.
79
+ * Extracted from resolvedTarget by removing subscripts and member access.
80
+ * Example: "ArrayBug_data[0]" -> "ArrayBug_data"
81
+ * Use this for CodeGenState.typeRegistry lookups instead of identifiers[0].
82
+ */
83
+ readonly resolvedBaseIdentifier: string;
84
+
70
85
  // === Type info (looked up from registry) ===
71
86
 
72
87
  /** First identifier's type info, if found */
@@ -15,6 +15,11 @@ import TTypeInfo from "../../types/TTypeInfo";
15
15
  function createMockContext(
16
16
  overrides: Partial<IAssignmentContext> = {},
17
17
  ): IAssignmentContext {
18
+ // Compute resolvedBaseIdentifier from resolvedTarget if not explicitly provided
19
+ const resolvedTarget = overrides.resolvedTarget ?? "x";
20
+ const resolvedBaseIdentifier =
21
+ overrides.resolvedBaseIdentifier ?? resolvedTarget.split(/[[.]/)[0];
22
+
18
23
  return {
19
24
  statementCtx: {} as IAssignmentContext["statementCtx"],
20
25
  targetCtx: {} as IAssignmentContext["targetCtx"],
@@ -31,6 +36,8 @@ function createMockContext(
31
36
  cOp: "=",
32
37
  isCompound: false,
33
38
  generatedValue: "5",
39
+ resolvedTarget,
40
+ resolvedBaseIdentifier,
34
41
  firstIdTypeInfo: null,
35
42
  memberAccessDepth: 0,
36
43
  subscriptDepth: 0,
@@ -20,26 +20,30 @@ function gen(): ICodeGenApi {
20
20
 
21
21
  /**
22
22
  * Handle simple array element: arr[i] <- value
23
+ *
24
+ * Uses resolvedTarget which includes scope prefix and subscript,
25
+ * e.g., "data[0]" inside scope ArrayBug -> "ArrayBug_data[0]"
23
26
  */
24
27
  function handleArrayElement(ctx: IAssignmentContext): string {
25
- const name = ctx.identifiers[0];
26
- const index = gen().generateExpression(ctx.subscripts[0]);
27
-
28
- return `${name}[${index}] ${ctx.cOp} ${ctx.generatedValue};`;
28
+ return `${ctx.resolvedTarget} ${ctx.cOp} ${ctx.generatedValue};`;
29
29
  }
30
30
 
31
31
  /**
32
32
  * Handle multi-dimensional array element: matrix[i][j] <- value
33
+ *
34
+ * Uses resolvedTarget which includes scope prefix and all subscripts.
35
+ * Uses resolvedBaseIdentifier for type lookups to support scoped arrays.
33
36
  */
34
37
  function handleMultiDimArrayElement(ctx: IAssignmentContext): string {
35
- const name = ctx.identifiers[0];
36
- const typeInfo = CodeGenState.typeRegistry.get(name);
38
+ // Use resolvedBaseIdentifier for type lookup (includes scope prefix)
39
+ // e.g., "ArrayBug_data" instead of "data"
40
+ const typeInfo = CodeGenState.typeRegistry.get(ctx.resolvedBaseIdentifier);
37
41
 
38
42
  // ADR-036: Compile-time bounds checking for constant indices
39
43
  if (typeInfo?.arrayDimensions) {
40
44
  const line = ctx.subscripts[0]?.start?.line ?? 0;
41
45
  TypeValidator.checkArrayBounds(
42
- name,
46
+ ctx.resolvedBaseIdentifier,
43
47
  [...typeInfo.arrayDimensions],
44
48
  [...ctx.subscripts],
45
49
  line,
@@ -47,11 +51,7 @@ function handleMultiDimArrayElement(ctx: IAssignmentContext): string {
47
51
  );
48
52
  }
49
53
 
50
- const indices = ctx.subscripts
51
- .map((e) => gen().generateExpression(e))
52
- .join("][");
53
-
54
- return `${name}[${indices}] ${ctx.cOp} ${ctx.generatedValue};`;
54
+ return `${ctx.resolvedTarget} ${ctx.cOp} ${ctx.generatedValue};`;
55
55
  }
56
56
 
57
57
  /**
@@ -69,7 +69,8 @@ function handleArraySlice(ctx: IAssignmentContext): string {
69
69
  );
70
70
  }
71
71
 
72
- const name = ctx.identifiers[0];
72
+ // Use resolvedBaseIdentifier for type lookup (includes scope prefix)
73
+ const name = ctx.resolvedBaseIdentifier;
73
74
  const typeInfo = CodeGenState.typeRegistry.get(name);
74
75
 
75
76
  // Get line number for error messages
@@ -77,10 +78,12 @@ function handleArraySlice(ctx: IAssignmentContext): string {
77
78
 
78
79
  // Validate 1D array only
79
80
  if (typeInfo?.arrayDimensions && typeInfo.arrayDimensions.length > 1) {
81
+ // Use raw identifier in error message for clarity
82
+ const rawName = ctx.identifiers[0];
80
83
  throw new Error(
81
84
  `${line}:0 Error: Slice assignment is only valid on one-dimensional arrays. ` +
82
- `'${name}' has ${typeInfo.arrayDimensions.length} dimensions. ` +
83
- `Access the innermost dimension first (e.g., ${name}[index][offset, length]).`,
85
+ `'${rawName}' has ${typeInfo.arrayDimensions.length} dimensions. ` +
86
+ `Access the innermost dimension first (e.g., ${rawName}[index][offset, length]).`,
84
87
  );
85
88
  }
86
89
 
@@ -109,17 +112,21 @@ function handleArraySlice(ctx: IAssignmentContext): string {
109
112
  } else if (typeInfo?.arrayDimensions?.[0]) {
110
113
  capacity = typeInfo.arrayDimensions[0];
111
114
  } else {
115
+ // Use raw identifier in error message for clarity
116
+ const rawName = ctx.identifiers[0];
112
117
  throw new Error(
113
- `${line}:0 Error: Cannot determine buffer size for '${name}' at compile time.`,
118
+ `${line}:0 Error: Cannot determine buffer size for '${rawName}' at compile time.`,
114
119
  );
115
120
  }
116
121
 
117
122
  // Bounds validation
118
123
  if (offsetValue + lengthValue > capacity) {
124
+ // Use raw identifier in error message for clarity
125
+ const rawName = ctx.identifiers[0];
119
126
  throw new Error(
120
127
  `${line}:0 Error: Slice assignment out of bounds: ` +
121
128
  `offset(${offsetValue}) + length(${lengthValue}) = ${offsetValue + lengthValue} ` +
122
- `exceeds buffer capacity(${capacity}) for '${name}'.`,
129
+ `exceeds buffer capacity(${capacity}) for '${rawName}'.`,
123
130
  );
124
131
  }
125
132
 
@@ -33,11 +33,14 @@ function validateNotCompound(ctx: IAssignmentContext): void {
33
33
  /**
34
34
  * Handle single bit on integer variable: flags[3] <- true
35
35
  * Also handles float bit indexing: f32Var[3] <- true
36
+ * Uses resolvedBaseIdentifier for proper scope prefix support.
36
37
  */
37
38
  function handleIntegerBit(ctx: IAssignmentContext): string {
38
39
  validateNotCompound(ctx);
39
40
 
40
- const name = ctx.identifiers[0];
41
+ // Use resolvedBaseIdentifier for type lookup and code generation
42
+ // e.g., "ArrayBug_flags" instead of "flags"
43
+ const name = ctx.resolvedBaseIdentifier;
41
44
  const bitIndex = gen().generateExpression(ctx.subscripts[0]);
42
45
  const typeInfo = CodeGenState.typeRegistry.get(name);
43
46
 
@@ -67,11 +70,13 @@ function handleIntegerBit(ctx: IAssignmentContext): string {
67
70
  /**
68
71
  * Handle bit range on integer variable: flags[0, 3] <- 5
69
72
  * Also handles float bit range: f32Var[0, 8] <- 0xFF
73
+ * Uses resolvedBaseIdentifier for proper scope prefix support.
70
74
  */
71
75
  function handleIntegerBitRange(ctx: IAssignmentContext): string {
72
76
  validateNotCompound(ctx);
73
77
 
74
- const name = ctx.identifiers[0];
78
+ // Use resolvedBaseIdentifier for type lookup and code generation
79
+ const name = ctx.resolvedBaseIdentifier;
75
80
  const start = gen().generateExpression(ctx.subscripts[0]);
76
81
  const width = gen().generateExpression(ctx.subscripts[1]);
77
82
  const typeInfo = CodeGenState.typeRegistry.get(name);
@@ -126,15 +131,19 @@ function handleStructMemberBit(ctx: IAssignmentContext): string {
126
131
 
127
132
  /**
128
133
  * Handle bit on multi-dimensional array element: matrix[i][j][FIELD_BIT] <- false
134
+ * Uses resolvedBaseIdentifier for proper scope prefix support.
129
135
  */
130
136
  function handleArrayElementBit(ctx: IAssignmentContext): string {
131
137
  validateNotCompound(ctx);
132
138
 
133
- const arrayName = ctx.identifiers[0];
139
+ // Use resolvedBaseIdentifier for type lookup and code generation
140
+ const arrayName = ctx.resolvedBaseIdentifier;
134
141
  const typeInfo = CodeGenState.typeRegistry.get(arrayName);
135
142
 
136
143
  if (!typeInfo?.arrayDimensions) {
137
- throw new Error(`Error: ${arrayName} is not an array`);
144
+ // Use raw identifier in error message for clarity
145
+ const rawName = ctx.identifiers[0];
146
+ throw new Error(`Error: ${rawName} is not an array`);
138
147
  }
139
148
 
140
149
  const numDims = typeInfo.arrayDimensions.length;
@@ -160,12 +169,14 @@ function handleArrayElementBit(ctx: IAssignmentContext): string {
160
169
  *
161
170
  * The target is a chain like array[idx].member or struct.field with a
162
171
  * bit range subscript [start, width] at the end.
172
+ * Uses resolvedBaseIdentifier for proper scope prefix support.
163
173
  */
164
174
  function handleStructChainBitRange(ctx: IAssignmentContext): string {
165
175
  validateNotCompound(ctx);
166
176
 
167
177
  // Build the base target from postfixOps, excluding the last one (the bit range)
168
- const baseId = ctx.identifiers[0];
178
+ // Use resolvedBaseIdentifier for the base to include scope prefix
179
+ const baseId = ctx.resolvedBaseIdentifier;
169
180
  const opsBeforeLast = ctx.postfixOps.slice(0, -1);
170
181
 
171
182
  let baseTarget = baseId;
@@ -16,8 +16,14 @@ import HandlerTestUtils from "./handlerTestUtils";
16
16
  function createMockContext(
17
17
  overrides: Partial<IAssignmentContext> = {},
18
18
  ): IAssignmentContext {
19
+ // Default resolved values based on first identifier
20
+ const identifiers = overrides.identifiers ?? ["Counter", "value"];
21
+ const resolvedTarget = overrides.resolvedTarget ?? `${identifiers.join("_")}`;
22
+ const resolvedBaseIdentifier =
23
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
24
+
19
25
  return {
20
- identifiers: ["Counter", "value"],
26
+ identifiers,
21
27
  subscripts: [],
22
28
  isCompound: false,
23
29
  cnextOp: "<-",
@@ -34,6 +40,8 @@ function createMockContext(
34
40
  isSimpleIdentifier: false,
35
41
  isSimpleThisAccess: false,
36
42
  isSimpleGlobalAccess: false,
43
+ resolvedTarget,
44
+ resolvedBaseIdentifier,
37
45
  ...overrides,
38
46
  } as IAssignmentContext;
39
47
  }
@@ -28,8 +28,14 @@ import HandlerTestUtils from "./handlerTestUtils";
28
28
  function createMockContext(
29
29
  overrides: Partial<IAssignmentContext> = {},
30
30
  ): IAssignmentContext {
31
+ // Default resolved values based on first identifier
32
+ const identifiers = overrides.identifiers ?? ["arr"];
33
+ const resolvedTarget = overrides.resolvedTarget ?? `${identifiers[0]}[i]`;
34
+ const resolvedBaseIdentifier =
35
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
36
+
31
37
  return {
32
- identifiers: ["arr"],
38
+ identifiers,
33
39
  subscripts: [{ mockValue: "i", start: { line: 1 } } as never],
34
40
  isCompound: false,
35
41
  cnextOp: "<-",
@@ -46,6 +52,8 @@ function createMockContext(
46
52
  isSimpleIdentifier: false,
47
53
  isSimpleThisAccess: false,
48
54
  isSimpleGlobalAccess: false,
55
+ resolvedTarget,
56
+ resolvedBaseIdentifier,
49
57
  ...overrides,
50
58
  } as IAssignmentContext;
51
59
  }
@@ -96,6 +104,7 @@ describe("ArrayHandlers", () => {
96
104
  isCompound: true,
97
105
  cnextOp: "+<-",
98
106
  cOp: "+=",
107
+ resolvedTarget: "arr[0]",
99
108
  });
100
109
 
101
110
  const result = getHandler()!(ctx);
@@ -109,6 +118,8 @@ describe("ArrayHandlers", () => {
109
118
  });
110
119
  const ctx = createMockContext({
111
120
  identifiers: ["buffer"],
121
+ resolvedTarget: "buffer[idx]",
122
+ resolvedBaseIdentifier: "buffer",
112
123
  });
113
124
 
114
125
  const result = getHandler()!(ctx);
@@ -137,6 +148,8 @@ describe("ArrayHandlers", () => {
137
148
  { mockValue: "j", start: { line: 1 } } as never,
138
149
  ],
139
150
  subscriptDepth: 2,
151
+ resolvedTarget: "matrix[i][j]",
152
+ resolvedBaseIdentifier: "matrix",
140
153
  });
141
154
 
142
155
  const result = getHandler()!(ctx);
@@ -160,6 +173,8 @@ describe("ArrayHandlers", () => {
160
173
  { mockValue: "z", start: { line: 1 } } as never,
161
174
  ],
162
175
  subscriptDepth: 3,
176
+ resolvedTarget: "cube[x][y][z]",
177
+ resolvedBaseIdentifier: "cube",
163
178
  });
164
179
 
165
180
  const result = getHandler()!(ctx);
@@ -212,6 +227,8 @@ describe("ArrayHandlers", () => {
212
227
  ],
213
228
  isCompound: true,
214
229
  cOp: "-=",
230
+ resolvedTarget: "grid[0][1]",
231
+ resolvedBaseIdentifier: "grid",
215
232
  });
216
233
 
217
234
  const result = getHandler()!(ctx);
@@ -16,8 +16,14 @@ import HandlerTestUtils from "./handlerTestUtils";
16
16
  function createMockContext(
17
17
  overrides: Partial<IAssignmentContext> = {},
18
18
  ): IAssignmentContext {
19
+ // Default resolved values based on first identifier
20
+ const identifiers = overrides.identifiers ?? ["flags"];
21
+ const resolvedTarget = overrides.resolvedTarget ?? `${identifiers[0]}[3]`;
22
+ const resolvedBaseIdentifier =
23
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
24
+
19
25
  return {
20
- identifiers: ["flags"],
26
+ identifiers,
21
27
  subscripts: [{ mockValue: "3" } as never],
22
28
  isCompound: false,
23
29
  cnextOp: "<-",
@@ -34,6 +40,8 @@ function createMockContext(
34
40
  isSimpleIdentifier: false,
35
41
  isSimpleThisAccess: false,
36
42
  isSimpleGlobalAccess: false,
43
+ resolvedTarget,
44
+ resolvedBaseIdentifier,
37
45
  ...overrides,
38
46
  } as IAssignmentContext;
39
47
  }
@@ -25,8 +25,14 @@ import HandlerTestUtils from "./handlerTestUtils";
25
25
  function createMockContext(
26
26
  overrides: Partial<IAssignmentContext> = {},
27
27
  ): IAssignmentContext {
28
+ // Default resolved values based on first identifier
29
+ const identifiers = overrides.identifiers ?? ["flags", "Running"];
30
+ const resolvedTarget = overrides.resolvedTarget ?? identifiers[0];
31
+ const resolvedBaseIdentifier =
32
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
33
+
28
34
  return {
29
- identifiers: ["flags", "Running"],
35
+ identifiers,
30
36
  subscripts: [],
31
37
  isCompound: false,
32
38
  cnextOp: "<-",
@@ -44,6 +50,8 @@ function createMockContext(
44
50
  isSimpleIdentifier: false,
45
51
  isSimpleThisAccess: false,
46
52
  isSimpleGlobalAccess: false,
53
+ resolvedTarget,
54
+ resolvedBaseIdentifier,
47
55
  ...overrides,
48
56
  } as IAssignmentContext;
49
57
  }
@@ -16,8 +16,15 @@ import HandlerTestUtils from "./handlerTestUtils";
16
16
  function createMockContext(
17
17
  overrides: Partial<IAssignmentContext> = {},
18
18
  ): IAssignmentContext {
19
+ // Default resolved values based on first identifier
20
+ const identifiers = overrides.identifiers ?? ["GPIO7", "DR_SET"];
21
+ const resolvedTarget =
22
+ overrides.resolvedTarget ?? `${identifiers.join("_")}[LED_BIT]`;
23
+ const resolvedBaseIdentifier =
24
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
25
+
19
26
  return {
20
- identifiers: ["GPIO7", "DR_SET"],
27
+ identifiers,
21
28
  subscripts: [{ mockValue: "LED_BIT" } as never],
22
29
  isCompound: false,
23
30
  cnextOp: "<-",
@@ -34,6 +41,8 @@ function createMockContext(
34
41
  isSimpleIdentifier: false,
35
42
  isSimpleThisAccess: false,
36
43
  isSimpleGlobalAccess: false,
44
+ resolvedTarget,
45
+ resolvedBaseIdentifier,
37
46
  ...overrides,
38
47
  } as IAssignmentContext;
39
48
  }
@@ -16,8 +16,14 @@ import HandlerTestUtils from "./handlerTestUtils";
16
16
  function createMockContext(
17
17
  overrides: Partial<IAssignmentContext> = {},
18
18
  ): IAssignmentContext {
19
+ // Default resolved values based on first identifier
20
+ const identifiers = overrides.identifiers ?? ["counter"];
21
+ const resolvedTarget = overrides.resolvedTarget ?? identifiers[0];
22
+ const resolvedBaseIdentifier =
23
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
24
+
19
25
  return {
20
- identifiers: ["counter"],
26
+ identifiers,
21
27
  subscripts: [],
22
28
  isCompound: true,
23
29
  cnextOp: "+<-",
@@ -34,6 +40,8 @@ function createMockContext(
34
40
  isSimpleIdentifier: true,
35
41
  isSimpleThisAccess: false,
36
42
  isSimpleGlobalAccess: false,
43
+ resolvedTarget,
44
+ resolvedBaseIdentifier,
37
45
  ...overrides,
38
46
  } as IAssignmentContext;
39
47
  }
@@ -16,14 +16,22 @@ import HandlerTestUtils from "./handlerTestUtils";
16
16
  function createMockContext(
17
17
  overrides: Partial<IAssignmentContext> = {},
18
18
  ): IAssignmentContext {
19
+ // Default resolved values based on first identifier
20
+ const identifiers = overrides.identifiers ?? ["testVar"];
21
+ const resolvedTarget = overrides.resolvedTarget ?? identifiers[0];
22
+ const resolvedBaseIdentifier =
23
+ overrides.resolvedBaseIdentifier ?? identifiers[0];
24
+
19
25
  return {
20
- identifiers: ["testVar"],
26
+ identifiers,
21
27
  subscripts: [],
22
28
  isCompound: false,
23
29
  cnextOp: "<-",
24
30
  cOp: "=",
25
31
  generatedValue: '"hello"',
26
32
  targetCtx: {} as never,
33
+ resolvedTarget,
34
+ resolvedBaseIdentifier,
27
35
  ...overrides,
28
36
  } as IAssignmentContext;
29
37
  }
@@ -89,7 +89,12 @@ class MemberSeparatorResolver {
89
89
  // Scope member access: Sensor.buffer -> Sensor_buffer
90
90
  // Works with or without global. prefix (both are valid syntax)
91
91
  if (deps.isKnownScope(identifierChain[0])) {
92
- deps.validateCrossScopeVisibility(identifierChain[0], memberName);
92
+ // Issue #779: Skip cross-scope validation for scoped register access
93
+ // Board.GPIO where Board_GPIO is a known register is valid
94
+ const scopedRegisterName = `${identifierChain[0]}_${memberName}`;
95
+ if (!deps.isKnownRegister(scopedRegisterName)) {
96
+ deps.validateCrossScopeVisibility(identifierChain[0], memberName);
97
+ }
93
98
  return "_";
94
99
  }
95
100
 
@@ -187,7 +187,7 @@ export default class CodeGenState {
187
187
  static localArrays: Set<string> = new Set();
188
188
 
189
189
  /** Scope member names: scope -> Set of member names */
190
- static scopeMembers: Map<string, Set<string>> = new Map();
190
+ private static scopeMembers: Map<string, Set<string>> = new Map();
191
191
 
192
192
  /** Float bit indexing: declared shadow variables */
193
193
  static floatBitShadows: Set<string> = new Set();
@@ -463,6 +463,26 @@ export default class CodeGenState {
463
463
  return this.modifiedParameters.get(funcName)?.has(paramName) ?? false;
464
464
  }
465
465
 
466
+ /**
467
+ * Compute unmodified parameters for all functions on-demand.
468
+ * Returns a map of function name -> Set of parameter names NOT modified.
469
+ * Computed from functionSignatures and modifiedParameters (no cached state).
470
+ */
471
+ static getUnmodifiedParameters(): Map<string, Set<string>> {
472
+ const result = new Map<string, Set<string>>();
473
+ for (const [funcName, signature] of this.functionSignatures) {
474
+ const modifiedSet = this.modifiedParameters.get(funcName);
475
+ const unmodified = new Set<string>();
476
+ for (const param of signature.parameters) {
477
+ if (!modifiedSet?.has(param.name)) {
478
+ unmodified.add(param.name);
479
+ }
480
+ }
481
+ result.set(funcName, unmodified);
482
+ }
483
+ return result;
484
+ }
485
+
466
486
  /**
467
487
  * Check if a parameter should pass by value.
468
488
  */
@@ -542,6 +562,20 @@ export default class CodeGenState {
542
562
  return this.scopeMembers.get(scopeName);
543
563
  }
544
564
 
565
+ /**
566
+ * Set members of a scope.
567
+ */
568
+ static setScopeMembers(scopeName: string, members: Set<string>): void {
569
+ this.scopeMembers.set(scopeName, members);
570
+ }
571
+
572
+ /**
573
+ * Get all scope members (for IGeneratorState).
574
+ */
575
+ static getAllScopeMembers(): ReadonlyMap<string, ReadonlySet<string>> {
576
+ return this.scopeMembers;
577
+ }
578
+
545
579
  /**
546
580
  * Check if an identifier is a member of the current scope.
547
581
  */
@@ -110,7 +110,7 @@ describe("CodeGenState", () => {
110
110
 
111
111
  it("getScopeMembers returns members for known scope", () => {
112
112
  const members = new Set(["member1", "member2"]);
113
- CodeGenState.scopeMembers.set("TestScope", members);
113
+ CodeGenState.setScopeMembers("TestScope", members);
114
114
 
115
115
  expect(CodeGenState.getScopeMembers("TestScope")).toBe(members);
116
116
  });
@@ -122,14 +122,14 @@ describe("CodeGenState", () => {
122
122
 
123
123
  it("isCurrentScopeMember returns false for non-member", () => {
124
124
  CodeGenState.currentScope = "TestScope";
125
- CodeGenState.scopeMembers.set("TestScope", new Set(["member1"]));
125
+ CodeGenState.setScopeMembers("TestScope", new Set(["member1"]));
126
126
 
127
127
  expect(CodeGenState.isCurrentScopeMember("nonMember")).toBe(false);
128
128
  });
129
129
 
130
130
  it("isCurrentScopeMember returns true for member", () => {
131
131
  CodeGenState.currentScope = "TestScope";
132
- CodeGenState.scopeMembers.set("TestScope", new Set(["member1"]));
132
+ CodeGenState.setScopeMembers("TestScope", new Set(["member1"]));
133
133
 
134
134
  expect(CodeGenState.isCurrentScopeMember("member1")).toBe(true);
135
135
  });
@@ -143,14 +143,14 @@ describe("CodeGenState", () => {
143
143
 
144
144
  it("returns identifier unchanged when not a scope member", () => {
145
145
  CodeGenState.currentScope = "TestScope";
146
- CodeGenState.scopeMembers.set("TestScope", new Set(["member1"]));
146
+ CodeGenState.setScopeMembers("TestScope", new Set(["member1"]));
147
147
 
148
148
  expect(CodeGenState.resolveIdentifier("varName")).toBe("varName");
149
149
  });
150
150
 
151
151
  it("returns scoped name for scope member", () => {
152
152
  CodeGenState.currentScope = "TestScope";
153
- CodeGenState.scopeMembers.set("TestScope", new Set(["member1"]));
153
+ CodeGenState.setScopeMembers("TestScope", new Set(["member1"]));
154
154
 
155
155
  expect(CodeGenState.resolveIdentifier("member1")).toBe(
156
156
  "TestScope_member1",