c-next 0.2.16 → 0.2.17

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 (56) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +1347 -414
  3. package/dist/index.js.map +3 -3
  4. package/grammar/CNext.g4 +4 -0
  5. package/package.json +5 -1
  6. package/src/transpiler/Transpiler.ts +90 -22
  7. package/src/transpiler/__tests__/DualCodePaths.test.ts +1 -1
  8. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +57 -10
  9. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +186 -14
  10. package/src/transpiler/logic/analysis/SignedShiftAnalyzer.ts +124 -12
  11. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +200 -0
  12. package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +386 -1
  13. package/src/transpiler/logic/analysis/__tests__/SignedShiftAnalyzer.test.ts +211 -0
  14. package/src/transpiler/logic/parser/grammar/CNext.interp +1 -1
  15. package/src/transpiler/logic/parser/grammar/CNextParser.ts +154 -86
  16. package/src/transpiler/logic/symbols/SymbolTable.ts +17 -2
  17. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +6 -4
  18. package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +15 -2
  19. package/src/transpiler/logic/symbols/cnext/utils/TypeUtils.ts +64 -50
  20. package/src/transpiler/output/codegen/CodeGenerator.ts +151 -94
  21. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +165 -17
  22. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +150 -34
  23. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
  24. package/src/transpiler/output/codegen/__tests__/TypeValidator.test.ts +11 -0
  25. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +26 -7
  26. package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/ScopeGenerator.test.ts +86 -0
  27. package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +43 -24
  28. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +48 -43
  29. package/src/transpiler/output/codegen/generators/expressions/ExpressionGenerator.ts +9 -2
  30. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +44 -0
  31. package/src/transpiler/output/codegen/generators/expressions/__tests__/ExpressionGenerator.test.ts +82 -1
  32. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +17 -3
  33. package/src/transpiler/output/codegen/helpers/ParameterSignatureBuilder.ts +17 -4
  34. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +227 -32
  35. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +0 -21
  36. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +60 -39
  37. package/src/transpiler/output/codegen/helpers/TypeRegistrationEngine.ts +170 -36
  38. package/src/transpiler/output/codegen/helpers/VariableDeclHelper.ts +37 -39
  39. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +117 -0
  40. package/src/transpiler/output/codegen/helpers/__tests__/ParameterSignatureBuilder.test.ts +94 -2
  41. package/src/transpiler/output/codegen/helpers/__tests__/StringDeclHelper.test.ts +268 -1
  42. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +0 -64
  43. package/src/transpiler/output/codegen/helpers/__tests__/TypeRegistrationEngine.test.ts +101 -0
  44. package/src/transpiler/output/codegen/helpers/__tests__/VariableDeclHelper.test.ts +29 -5
  45. package/src/transpiler/output/codegen/types/ICallbackTypeInfo.ts +2 -1
  46. package/src/transpiler/output/codegen/types/IParameterInput.ts +7 -0
  47. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +8 -0
  48. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +75 -0
  49. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +280 -0
  50. package/src/transpiler/output/headers/generators/IHeaderTypeInput.ts +13 -0
  51. package/src/transpiler/output/headers/generators/generateStructHeader.ts +48 -28
  52. package/src/transpiler/state/CodeGenState.ts +71 -6
  53. package/src/transpiler/state/__tests__/CodeGenState.test.ts +253 -11
  54. package/src/utils/LiteralUtils.ts +23 -0
  55. package/src/utils/__tests__/LiteralUtils.test.ts +101 -0
  56. package/src/utils/types/IParameterSymbol.ts +1 -0
package/grammar/CNext.g4 CHANGED
@@ -538,10 +538,14 @@ stringType
538
538
 
539
539
  // C-Next style array type: dimensions in type position
540
540
  // Supports: u8[8], u8[4][4], u8[] (unbounded), string<32>[5]
541
+ // Also supports scoped/qualified types: this.Type[4], Scope.Type[4], global.Type[4]
541
542
  arrayType
542
543
  : primitiveType arrayTypeDimension+
543
544
  | userType arrayTypeDimension+
544
545
  | stringType arrayTypeDimension+
546
+ | scopedType arrayTypeDimension+ // this.Type[4] - scoped type array
547
+ | qualifiedType arrayTypeDimension+ // Scope.Type[4] - qualified type array
548
+ | globalType arrayTypeDimension+ // global.Type[4] - global type array
545
549
  ;
546
550
 
547
551
  arrayTypeDimension
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
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",
@@ -24,6 +24,10 @@
24
24
  "test:update": "tsx scripts/test.ts --update",
25
25
  "integration:transpile": "tsx scripts/test.ts --transpile-only",
26
26
  "integration:execute": "tsx scripts/test.ts --execute-only",
27
+ "test:bugs": "tsx scripts/test.ts -- bugs",
28
+ "test:bugs:q": "tsx scripts/test.ts -- bugs -q",
29
+ "integration:bugs:transpile": "tsx scripts/test.ts -- bugs --transpile-only",
30
+ "integration:bugs:execute": "tsx scripts/test.ts -- bugs --execute-only",
27
31
  "analyze": "./scripts/static-analysis.sh",
28
32
  "clean": "rm -rf dist src/transpiler/logic/parser/grammar src/transpiler/logic/parser/c/grammar src/transpiler/logic/parser/cpp/grammar",
29
33
  "prettier:check": "prettier --check .",
@@ -1410,8 +1410,15 @@ class Transpiler {
1410
1410
  CodeGenState.symbolTable,
1411
1411
  );
1412
1412
 
1413
+ // ADR-029: Convert callback types to header format
1414
+ const callbackTypesForHeader = this._buildCallbackTypesForHeader();
1415
+
1413
1416
  const typeInputWithSymbolTable = typeInput
1414
- ? { ...typeInput, symbolTable: CodeGenState.symbolTable }
1417
+ ? {
1418
+ ...typeInput,
1419
+ symbolTable: CodeGenState.symbolTable,
1420
+ callbackTypes: callbackTypesForHeader,
1421
+ }
1415
1422
  : undefined;
1416
1423
 
1417
1424
  const unmodifiedParams = this.codeGenerator.getFunctionUnmodifiedParams();
@@ -1437,6 +1444,52 @@ class Transpiler {
1437
1444
  );
1438
1445
  }
1439
1446
 
1447
+ /**
1448
+ * ADR-029: Build callback types for header generation.
1449
+ * Only includes callbacks that are actually used as struct field types.
1450
+ * Converts CodeGenState.callbackTypes to the format expected by IHeaderTypeInput.
1451
+ */
1452
+ private _buildCallbackTypesForHeader(): ReadonlyMap<
1453
+ string,
1454
+ {
1455
+ typedefName: string;
1456
+ returnType: string;
1457
+ parameters: ReadonlyArray<{ type: string; isStruct: boolean }>;
1458
+ }
1459
+ > {
1460
+ const result = new Map<
1461
+ string,
1462
+ {
1463
+ typedefName: string;
1464
+ returnType: string;
1465
+ parameters: ReadonlyArray<{ type: string; isStruct: boolean }>;
1466
+ }
1467
+ >();
1468
+
1469
+ // Collect callback function names that are actually used as struct field types
1470
+ const usedCallbackTypes = new Set<string>();
1471
+ for (const [, funcName] of CodeGenState.callbackFieldTypes) {
1472
+ usedCallbackTypes.add(funcName);
1473
+ }
1474
+
1475
+ // Only include callbacks that are used as struct field types
1476
+ for (const funcName of usedCallbackTypes) {
1477
+ const cbInfo = CodeGenState.callbackTypes.get(funcName);
1478
+ if (cbInfo) {
1479
+ result.set(funcName, {
1480
+ typedefName: cbInfo.typedefName,
1481
+ returnType: cbInfo.returnType,
1482
+ parameters: cbInfo.parameters.map((p) => ({
1483
+ type: p.type,
1484
+ isStruct: p.isStruct,
1485
+ })),
1486
+ });
1487
+ }
1488
+ }
1489
+
1490
+ return result;
1491
+ }
1492
+
1440
1493
  /**
1441
1494
  * Collect external enum sources from included C-Next files.
1442
1495
  */
@@ -1509,6 +1562,9 @@ class Transpiler {
1509
1562
 
1510
1563
  // Issue #914: For callback-compatible functions, bake pointer/const overrides
1511
1564
  // onto each parameter. Skip auto-const (matches CodeGenerator path).
1565
+ // Note: isOpaqueHandle is not set here because callback params get their
1566
+ // pointer/const semantics from the typedef signature via isCallbackPointer/
1567
+ // isCallbackConst, which take precedence over opaque handling in the builder.
1512
1568
  if (callbackTypedefType) {
1513
1569
  const updatedParams = TypedefParamParser.resolveCallbackParams(
1514
1570
  headerSymbol.parameters,
@@ -1517,29 +1573,41 @@ class Transpiler {
1517
1573
  return { ...headerSymbol, parameters: updatedParams };
1518
1574
  }
1519
1575
 
1520
- // Apply auto-const to non-callback function parameters
1576
+ // Apply auto-const and resolve opaque type info for non-callback function parameters
1521
1577
  const unmodified = unmodifiedParams.get(headerSymbol.name);
1522
- if (unmodified) {
1523
- const updatedParams = headerSymbol.parameters.map((param) => {
1524
- const isPointerParam =
1525
- !param.isConst &&
1526
- !param.isArray &&
1527
- param.type !== "f32" &&
1528
- param.type !== "f64" &&
1529
- param.type !== "ISR" &&
1530
- !knownEnums.has(param.type ?? "");
1531
- const isArrayParam = param.isArray && !param.isConst;
1532
-
1533
- if ((isPointerParam || isArrayParam) && unmodified.has(param.name)) {
1534
- return { ...param, isAutoConst: true };
1535
- }
1536
- return param;
1537
- });
1538
-
1539
- return { ...headerSymbol, parameters: updatedParams };
1540
- }
1578
+ const updatedParams = headerSymbol.parameters.map((param) => {
1579
+ // Issue #995: Resolve opaque type info ONCE onto the symbol.
1580
+ // This is the single source of truth for both body (.c/.cpp) and header (.h/.hpp).
1581
+ const isOpaque = CodeGenState.isOpaqueType(param.type ?? "");
1582
+
1583
+ // ADR-006: Only non-array pointer params get auto-const.
1584
+ // Arrays are pass-by-reference and mutable by default - auto-const would
1585
+ // break compatibility with C APIs expecting mutable pointers (issue #986).
1586
+ // Note: isAutoConst may be set here, but ParameterSignatureBuilder will
1587
+ // suppress it for opaque handles (Issue #995) — single source of truth.
1588
+ const isPointerParam =
1589
+ !param.isConst &&
1590
+ !param.isArray &&
1591
+ param.type !== "f32" &&
1592
+ param.type !== "f64" &&
1593
+ param.type !== "ISR" &&
1594
+ !knownEnums.has(param.type ?? "");
1595
+
1596
+ const shouldAutoConst =
1597
+ unmodified && isPointerParam && unmodified.has(param.name);
1598
+
1599
+ // Return updated param with resolved flags
1600
+ if (shouldAutoConst || isOpaque) {
1601
+ return {
1602
+ ...param,
1603
+ isAutoConst: shouldAutoConst || undefined,
1604
+ isOpaqueHandle: isOpaque || undefined,
1605
+ };
1606
+ }
1607
+ return param;
1608
+ });
1541
1609
 
1542
- return headerSymbol;
1610
+ return { ...headerSymbol, parameters: updatedParams };
1543
1611
  });
1544
1612
  }
1545
1613
 
@@ -110,7 +110,7 @@ void main() {
110
110
 
111
111
  it("produces identical code for const arrays with inferred size", async () => {
112
112
  const source = `
113
- const u8 VALUES[] <- [1, 2, 3, 4, 5];
113
+ const u8[] VALUES <- [1, 2, 3, 4, 5];
114
114
 
115
115
  void main() {
116
116
  u8 x <- VALUES[0];
@@ -315,7 +315,10 @@ class FunctionCallListener extends CNextListener {
315
315
  if (!baseName) return;
316
316
 
317
317
  // Walk through postfix ops to find the call and resolve the name
318
- const { resolvedName, foundCall } = this.resolveCallTarget(ops, baseName);
318
+ const { resolvedName, foundCall, isGlobalCall } = this.resolveCallTarget(
319
+ ops,
320
+ baseName,
321
+ );
319
322
  if (!foundCall) return;
320
323
 
321
324
  // Check if the function is defined
@@ -325,6 +328,7 @@ class FunctionCallListener extends CNextListener {
325
328
  line,
326
329
  column,
327
330
  this.currentScope,
331
+ isGlobalCall,
328
332
  );
329
333
  };
330
334
 
@@ -340,6 +344,9 @@ class FunctionCallListener extends CNextListener {
340
344
  if (primary.THIS()) {
341
345
  return "this";
342
346
  }
347
+ if (primary.GLOBAL()) {
348
+ return "global";
349
+ }
343
350
  return null;
344
351
  }
345
352
 
@@ -351,15 +358,21 @@ class FunctionCallListener extends CNextListener {
351
358
  private resolveCallTarget(
352
359
  ops: Parser.PostfixOpContext[],
353
360
  baseName: string,
354
- ): { resolvedName: string; foundCall: boolean } {
361
+ ): { resolvedName: string; foundCall: boolean; isGlobalCall: boolean } {
355
362
  let resolvedName = baseName;
363
+ let isGlobalCall = baseName === "global";
356
364
 
357
365
  for (const op of ops) {
358
366
  // Member access: check if it's Scope.member or this.member pattern
359
367
  if (op.IDENTIFIER()) {
360
368
  const resolved = this.resolveMemberAccess(resolvedName, op);
361
369
  if (resolved === null) {
362
- return { resolvedName, foundCall: false };
370
+ return { resolvedName, foundCall: false, isGlobalCall };
371
+ }
372
+ // If resolution went through a known scope, this is a scope
373
+ // method call, not a global function lookup
374
+ if (isGlobalCall && this.analyzer.isScope(resolvedName)) {
375
+ isGlobalCall = false;
363
376
  }
364
377
  resolvedName = resolved;
365
378
  continue;
@@ -369,12 +382,12 @@ class FunctionCallListener extends CNextListener {
369
382
  if (op.argumentList() || op.getChildCount() === 2) {
370
383
  const text = op.getText();
371
384
  if (text.startsWith("(")) {
372
- return { resolvedName, foundCall: true };
385
+ return { resolvedName, foundCall: true, isGlobalCall };
373
386
  }
374
387
  }
375
388
  }
376
389
 
377
- return { resolvedName, foundCall: false };
390
+ return { resolvedName, foundCall: false, isGlobalCall };
378
391
  }
379
392
 
380
393
  /**
@@ -391,6 +404,11 @@ class FunctionCallListener extends CNextListener {
391
404
  return `${this.currentScope}_${memberName}`;
392
405
  }
393
406
 
407
+ // Issue #985: Handle global.member -> member (strip global prefix)
408
+ if (resolvedName === "global") {
409
+ return memberName;
410
+ }
411
+
394
412
  // Check if base is a known scope
395
413
  if (this.analyzer.isScope(resolvedName)) {
396
414
  return `${resolvedName}_${memberName}`;
@@ -934,12 +952,14 @@ class FunctionCallAnalyzer {
934
952
  * @param line Source line number
935
953
  * @param column Source column number
936
954
  * @param currentScope The current scope name (if inside a scope)
955
+ * @param isGlobalCall Whether the call used global. prefix
937
956
  */
938
957
  public checkFunctionCall(
939
958
  name: string,
940
959
  line: number,
941
960
  column: number,
942
961
  currentScope: string | null,
962
+ isGlobalCall: boolean = false,
943
963
  ): void {
944
964
  // Check for self-recursion (MISRA C:2012 Rule 17.2)
945
965
  if (this.currentFunctionName && name === this.currentFunctionName) {
@@ -981,7 +1001,8 @@ class FunctionCallAnalyzer {
981
1001
  // ADR-057: Allow implicit scope function calls without this. prefix
982
1002
  // Check if this is an unqualified call to a scope function
983
1003
  // e.g., calling helper() instead of this.helper() inside a scope
984
- if (currentScope) {
1004
+ // Skip for global. calls — global. explicitly means global scope
1005
+ if (currentScope && !isGlobalCall) {
985
1006
  const qualifiedName = `${currentScope}_${name}`;
986
1007
  if (this.definedFunctions.has(qualifiedName)) {
987
1008
  return; // OK - implicit resolution will handle it
@@ -989,11 +1010,15 @@ class FunctionCallAnalyzer {
989
1010
  }
990
1011
 
991
1012
  // Not defined - report error with optional hint
1013
+ // If the function is local (defined later in this file), it's a
1014
+ // define-before-use error regardless of global. prefix
1015
+ const isLocalFunction = this.allLocalFunctions.has(name);
992
1016
  const header = this.findStdlibHeader(name);
993
- let message = `function '${name}' called before definition`;
994
- if (header) {
995
- message += `; hint: '${name}' is available from ${header} — try global.${name}()`;
996
- }
1017
+ const message = this.buildUndefinedFunctionMessage(
1018
+ name,
1019
+ header,
1020
+ isGlobalCall && !isLocalFunction,
1021
+ );
997
1022
 
998
1023
  this.errors.push({
999
1024
  code: "E0422",
@@ -1004,6 +1029,28 @@ class FunctionCallAnalyzer {
1004
1029
  });
1005
1030
  }
1006
1031
 
1032
+ /**
1033
+ * Issue #985: Build error message for undefined function calls.
1034
+ * Adjusts hint based on whether the call used global. prefix.
1035
+ */
1036
+ private buildUndefinedFunctionMessage(
1037
+ name: string,
1038
+ header: string | null,
1039
+ isGlobalCall: boolean,
1040
+ ): string {
1041
+ if (isGlobalCall && header) {
1042
+ return `'${name}' is not declared in any included header; add #include <${header}>`;
1043
+ }
1044
+ if (isGlobalCall) {
1045
+ return `'${name}' is not declared in any included header`;
1046
+ }
1047
+ let message = `function '${name}' called before definition`;
1048
+ if (header) {
1049
+ message += `; hint: '${name}' is available from ${header} — try global.${name}()`;
1050
+ }
1051
+ return message;
1052
+ }
1053
+
1007
1054
  /**
1008
1055
  * Check if a function is defined externally (from included files)
1009
1056
  * This includes C/C++ headers AND C-Next includes.
@@ -157,10 +157,32 @@ class InitializationListener extends CNextListener {
157
157
  return;
158
158
  }
159
159
 
160
- // Simple variable assignment: x <- value (no postfix ops)
160
+ // Resolve the assignment target ONCE (single source of truth)
161
+ const target = this._resolveAssignmentTarget(baseId, postfixOps);
162
+
163
+ // Issue #1012: Compound assignment reads the LHS before writing.
164
+ // Check if the assignment operator is a compound operator (not simple `<-`).
165
+ const isCompoundAssignment = ctx.assignmentOperator().ASSIGN() === null;
166
+ if (isCompoundAssignment) {
167
+ const { line, column } = ParserUtils.getPosition(ctx);
168
+ this.analyzer.checkRead(target.varName, line, column, target.fieldName);
169
+ }
170
+
171
+ // Record the assignment to the resolved target
172
+ this.analyzer.recordAssignment(target.varName, target.fieldName);
173
+ };
174
+
175
+ /**
176
+ * Resolve an assignment target to its variable name and optional field name.
177
+ * Single classification path used by both read checks and assignment recording.
178
+ */
179
+ private _resolveAssignmentTarget(
180
+ baseId: string,
181
+ postfixOps: Parser.PostfixTargetOpContext[],
182
+ ): { varName: string; fieldName?: string } {
183
+ // Simple variable: x <- value (no postfix ops)
161
184
  if (postfixOps.length === 0) {
162
- this.analyzer.recordAssignment(baseId);
163
- return;
185
+ return { varName: baseId };
164
186
  }
165
187
 
166
188
  // Analyze postfix operations
@@ -168,15 +190,13 @@ class InitializationListener extends CNextListener {
168
190
 
169
191
  // Member access: p.x <- value (struct field)
170
192
  if (identifiers.length >= 2 && !hasSubscript) {
171
- const varName = identifiers[0];
172
- const fieldName = identifiers[1];
173
- this.analyzer.recordAssignment(varName, fieldName);
174
- } else {
175
- // Array access or mixed: arr[i] <- value or arr[i].field <- value
176
- // Consider the array/base as a whole initialized
177
- this.analyzer.recordAssignment(baseId);
193
+ return { varName: identifiers[0], fieldName: identifiers[1] };
178
194
  }
179
- };
195
+
196
+ // Array access or mixed: arr[i] <- value or arr[i].field <- value
197
+ // Consider the array/base as a whole
198
+ return { varName: baseId };
199
+ }
180
200
 
181
201
  // ========================================================================
182
202
  // Function Call Arguments (ADR-006: pass-by-reference may initialize)
@@ -526,14 +546,159 @@ class InitializationAnalyzer {
526
546
 
527
547
  /**
528
548
  * Process scope member variable declarations (ADR-016)
549
+ * Issue #1019: Scope members require explicit initialization like locals
529
550
  */
530
551
  private _processScopeMembers(decl: Parser.DeclarationContext): void {
531
552
  const scopeDecl = decl.scopeDeclaration();
532
553
  if (!scopeDecl) return;
533
554
 
534
555
  const scopeName = scopeDecl.IDENTIFIER().getText();
556
+
557
+ // Phase 1: Find all members assigned in any scope function
558
+ const assignedMembers = this._findAssignedScopeMembers(scopeDecl);
559
+
560
+ // Phase 2: Process each member with assignment info
561
+ for (const member of scopeDecl.scopeMember()) {
562
+ this._processScopeMemberVariable(member, scopeName, assignedMembers);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Scan all functions in a scope to find which members are assigned.
568
+ * Issue #1019: A member assigned in ANY function is considered initialized
569
+ * for reads in other functions within the same scope.
570
+ */
571
+ private _findAssignedScopeMembers(
572
+ scopeDecl: Parser.ScopeDeclarationContext,
573
+ ): Set<string> {
574
+ const assigned = new Set<string>();
575
+
535
576
  for (const member of scopeDecl.scopeMember()) {
536
- this._processScopeMemberVariable(member, scopeName);
577
+ const funcDecl = member.functionDeclaration();
578
+ if (!funcDecl) continue;
579
+
580
+ const body = funcDecl.block();
581
+ if (!body) continue;
582
+
583
+ // Scan the function body for assignments to scope members
584
+ this._collectAssignmentsInBlock(body, assigned);
585
+ }
586
+
587
+ return assigned;
588
+ }
589
+
590
+ /**
591
+ * Recursively collect variable names that are assigned in a block.
592
+ * Looks for assignment statements targeting bare identifiers or this.member.
593
+ */
594
+ private _collectAssignmentsInBlock(
595
+ block: Parser.BlockContext,
596
+ assigned: Set<string>,
597
+ ): void {
598
+ for (const stmt of block.statement()) {
599
+ this._collectAssignmentsInStatement(stmt, assigned);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Collect assignments from a single statement, recursing into nested blocks.
605
+ */
606
+ private _collectAssignmentsInStatement(
607
+ stmt: Parser.StatementContext,
608
+ assigned: Set<string>,
609
+ ): void {
610
+ this._collectDirectAssignment(stmt, assigned);
611
+ this._collectFromControlFlow(stmt, assigned);
612
+ this._collectFromSwitch(stmt, assigned);
613
+ this._collectFromBlock(stmt, assigned);
614
+ }
615
+
616
+ /**
617
+ * Collect assignment from the statement itself (if it's an assignment).
618
+ */
619
+ private _collectDirectAssignment(
620
+ stmt: Parser.StatementContext,
621
+ assigned: Set<string>,
622
+ ): void {
623
+ const assignStmt = stmt.assignmentStatement();
624
+ if (!assignStmt) return;
625
+
626
+ const target = assignStmt.assignmentTarget();
627
+ if (!target) return;
628
+
629
+ // Both bare identifier and this.member use the same IDENTIFIER token
630
+ const id = target.IDENTIFIER()?.getText();
631
+ if (id) {
632
+ assigned.add(id);
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Recurse into control flow statements (if, while, do-while, for).
638
+ */
639
+ private _collectFromControlFlow(
640
+ stmt: Parser.StatementContext,
641
+ assigned: Set<string>,
642
+ ): void {
643
+ // if statement
644
+ const ifStmt = stmt.ifStatement();
645
+ if (ifStmt) {
646
+ for (const childStmt of ifStmt.statement()) {
647
+ this._collectAssignmentsInStatement(childStmt, assigned);
648
+ }
649
+ }
650
+
651
+ // while statement
652
+ const whileBody = stmt.whileStatement()?.statement();
653
+ if (whileBody) {
654
+ this._collectAssignmentsInStatement(whileBody, assigned);
655
+ }
656
+
657
+ // do-while statement (Issue #1019 review feedback)
658
+ const doWhileBody = stmt.doWhileStatement()?.block();
659
+ if (doWhileBody) {
660
+ this._collectAssignmentsInBlock(doWhileBody, assigned);
661
+ }
662
+
663
+ // for statement
664
+ const forBody = stmt.forStatement()?.statement();
665
+ if (forBody) {
666
+ this._collectAssignmentsInStatement(forBody, assigned);
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Recurse into switch statement cases.
672
+ */
673
+ private _collectFromSwitch(
674
+ stmt: Parser.StatementContext,
675
+ assigned: Set<string>,
676
+ ): void {
677
+ const switchStmt = stmt.switchStatement();
678
+ if (!switchStmt) return;
679
+
680
+ for (const switchCase of switchStmt.switchCase()) {
681
+ const caseBlock = switchCase.block();
682
+ if (caseBlock) {
683
+ this._collectAssignmentsInBlock(caseBlock, assigned);
684
+ }
685
+ }
686
+ const defaultBlock = switchStmt.defaultCase()?.block();
687
+ if (defaultBlock) {
688
+ this._collectAssignmentsInBlock(defaultBlock, assigned);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Recurse into standalone block statement.
694
+ */
695
+ private _collectFromBlock(
696
+ stmt: Parser.StatementContext,
697
+ assigned: Set<string>,
698
+ ): void {
699
+ const block = stmt.block();
700
+ if (block) {
701
+ this._collectAssignmentsInBlock(block, assigned);
537
702
  }
538
703
  }
539
704
 
@@ -543,6 +708,7 @@ class InitializationAnalyzer {
543
708
  private _processScopeMemberVariable(
544
709
  member: Parser.ScopeMemberContext,
545
710
  scopeName: string,
711
+ assignedMembers: Set<string>,
546
712
  ): void {
547
713
  const memberVar = member.variableDeclaration();
548
714
  if (!memberVar) return;
@@ -552,9 +718,15 @@ class InitializationAnalyzer {
552
718
  const { line, column } = ParserUtils.getPosition(memberVar);
553
719
  const typeName = this._extractUserTypeName(memberVar.type());
554
720
 
721
+ // Issue #1019: Scope members are initialized if they have an inline
722
+ // initializer OR are assigned in any function within the scope
723
+ const hasInlineInit = memberVar.expression() !== null;
724
+ const isAssignedInScope = assignedMembers.has(varName);
725
+ const hasInitializer = hasInlineInit || isAssignedInScope;
726
+
555
727
  // Register with both raw name and transpiled C name for scope resolution
556
- this.declareVariable(varName, line, column, true, typeName);
557
- this.declareVariable(fullName, line, column, true, typeName);
728
+ this.declareVariable(varName, line, column, hasInitializer, typeName);
729
+ this.declareVariable(fullName, line, column, hasInitializer, typeName);
558
730
  }
559
731
 
560
732
  /**