c-next 0.1.9 → 0.1.10

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.
package/README.md CHANGED
@@ -303,36 +303,38 @@ GPIO7.DR_SET[LED_BIT] <- true; // Generates: GPIO7_DR_SET = (1 << LED_BIT);
303
303
 
304
304
  ### Slice Assignment for Memory Operations
305
305
 
306
- Multi-byte copying with bounds-checked `memcpy` generation:
306
+ Multi-byte copying with compile-time validated `memcpy` generation (Issue #234):
307
307
 
308
308
  ```cnx
309
309
  u8 buffer[256];
310
310
  u32 magic <- 0x12345678;
311
- u8 length <- 4;
312
- u32 offset <- 0;
313
311
 
314
- // Copy 4 bytes from value into buffer at offset
315
- buffer[offset, length] <- magic;
312
+ // Copy 4 bytes from value into buffer at offset 0
313
+ buffer[0, 4] <- magic;
314
+
315
+ // Named offsets using const variables
316
+ const u32 HEADER_OFFSET <- 0;
317
+ const u32 DATA_OFFSET <- 8;
318
+ buffer[HEADER_OFFSET, 4] <- magic;
319
+ buffer[DATA_OFFSET, 8] <- timestamp;
316
320
  ```
317
321
 
318
- Transpiles to runtime bounds-checked code:
322
+ Transpiles to direct memcpy (bounds validated at compile time):
319
323
 
320
324
  ```c
321
325
  uint8_t buffer[256] = {0};
322
326
  uint32_t magic = 0x12345678;
323
- uint8_t length = 4;
324
- uint32_t offset = 0;
325
327
 
326
- if (offset + length <= sizeof(buffer)) {
327
- memcpy(&buffer[offset], &magic, length);
328
- }
328
+ memcpy(&buffer[0], &magic, 4);
329
+ memcpy(&buffer[8], &timestamp, 8);
329
330
  ```
330
331
 
331
332
  **Key Features:**
332
333
 
333
- - Runtime bounds checking prevents buffer overflows
334
- - Enables binary serialization and protocol implementation
335
- - Works with struct fields: `buffer[offset, len] <- config.magic`
334
+ - **Compile-time bounds checking** prevents buffer overflows at compile time
335
+ - Offset and length must be compile-time constants (literals or `const` variables)
336
+ - Silent runtime failures are now compile-time errors
337
+ - Works with struct fields: `buffer[0, 4] <- config.magic`
336
338
  - Distinct from bit operations: array slices use `memcpy`, scalar bit ranges use bit manipulation
337
339
 
338
340
  ### Scopes (ADR-016)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "coverage:grammar:console": "tsx scripts/grammar-coverage.ts console",
38
38
  "unit": "vitest run",
39
39
  "unit:watch": "vitest",
40
- "test:all": "npm run unit && npm test"
40
+ "test:all": "npm run unit && npm run test:q"
41
41
  },
42
42
  "keywords": [
43
43
  "c",
@@ -1131,6 +1131,20 @@ export default class CodeGenerator implements IOrchestrator {
1131
1131
  output.push(" */");
1132
1132
  output.push("");
1133
1133
 
1134
+ // Issue #230: Self-include for extern "C" linkage
1135
+ // When file has public symbols and headers are being generated,
1136
+ // include own header to ensure proper C linkage
1137
+ if (
1138
+ options?.generateHeaders &&
1139
+ this.symbols!.hasPublicSymbols() &&
1140
+ this.sourcePath
1141
+ ) {
1142
+ const basename = this.sourcePath.replace(/^.*[\\/]/, "");
1143
+ const headerName = basename.replace(/\.cnx$|\.cnext$/, ".h");
1144
+ output.push(`#include "${headerName}"`);
1145
+ output.push("");
1146
+ }
1147
+
1134
1148
  // Pass through #include directives from source
1135
1149
  // C-Next does NOT hardcode any libraries - all includes must be explicit
1136
1150
  // ADR-043: Comments before first include become file-level comments
@@ -5603,9 +5617,43 @@ export default class CodeGenerator implements IOrchestrator {
5603
5617
  if (isActualArray || isISRType) {
5604
5618
  // Check for slice assignment: array[offset, length] <- value
5605
5619
  if (exprs.length === 2) {
5606
- // Slice assignment - generate memcpy with bounds checking
5607
- const offset = this._generateExpression(exprs[0]);
5608
- const length = this._generateExpression(exprs[1]);
5620
+ // Issue #234: Slice assignment requires compile-time constant offset and length
5621
+ // to ensure bounds safety at compile time, not runtime
5622
+ const offsetValue = this._tryEvaluateConstant(exprs[0]);
5623
+ const lengthValue = this._tryEvaluateConstant(exprs[1]);
5624
+
5625
+ const line = exprs[0].start?.line ?? arrayAccessCtx.start?.line ?? 0;
5626
+
5627
+ // Issue #234: Reject slice assignment on multi-dimensional arrays
5628
+ // Slice assignment is only valid on 1D arrays (the innermost dimension)
5629
+ // For multi-dimensional arrays like board[4][8], use board[row][offset, length]
5630
+ // (Note: grammar currently doesn't support this - tracked as future work)
5631
+ if (
5632
+ typeInfo?.arrayDimensions &&
5633
+ typeInfo.arrayDimensions.length > 1
5634
+ ) {
5635
+ throw new Error(
5636
+ `${line}:0 Error: Slice assignment is only valid on one-dimensional arrays. ` +
5637
+ `'${name}' has ${typeInfo.arrayDimensions.length} dimensions. ` +
5638
+ `Access the innermost dimension first (e.g., ${name}[index][offset, length]).`,
5639
+ );
5640
+ }
5641
+
5642
+ // Validate offset is compile-time constant
5643
+ if (offsetValue === undefined) {
5644
+ throw new Error(
5645
+ `${line}:0 Error: Slice assignment offset must be a compile-time constant. ` +
5646
+ `Runtime offsets are not allowed to ensure bounds safety.`,
5647
+ );
5648
+ }
5649
+
5650
+ // Validate length is compile-time constant
5651
+ if (lengthValue === undefined) {
5652
+ throw new Error(
5653
+ `${line}:0 Error: Slice assignment length must be a compile-time constant. ` +
5654
+ `Runtime lengths are not allowed to ensure bounds safety.`,
5655
+ );
5656
+ }
5609
5657
 
5610
5658
  // Compound operators not supported for slice assignment
5611
5659
  if (cOp !== "=") {
@@ -5614,22 +5662,50 @@ export default class CodeGenerator implements IOrchestrator {
5614
5662
  );
5615
5663
  }
5616
5664
 
5617
- // Set flag to include string.h for memcpy
5618
- this.needsString = true;
5619
-
5620
- // Generate bounds-checked memcpy
5621
- // if (offset + length <= sizeof(buffer)) { memcpy(&buffer[offset], &value, length); }
5622
- // Issue #213: For string parameters, use capacity for bounds checking
5623
- // since sizeof(char*) gives pointer size, not buffer size
5665
+ // Determine buffer capacity for compile-time bounds check
5666
+ let capacity: number;
5624
5667
  if (
5625
5668
  typeInfo?.isString &&
5626
5669
  typeInfo.stringCapacity &&
5627
5670
  !typeInfo.isArray
5628
5671
  ) {
5629
- const capacity = typeInfo.stringCapacity + 1;
5630
- return `if (${offset} + ${length} <= ${capacity}) { memcpy(&${name}[${offset}], &${value}, ${length}); }`;
5672
+ capacity = typeInfo.stringCapacity + 1;
5673
+ } else if (typeInfo?.arrayDimensions && typeInfo.arrayDimensions[0]) {
5674
+ capacity = typeInfo.arrayDimensions[0];
5675
+ } else {
5676
+ // Can't determine capacity at compile time - this shouldn't happen
5677
+ // for properly tracked arrays, but fall back to error
5678
+ throw new Error(
5679
+ `${line}:0 Error: Cannot determine buffer size for '${name}' at compile time.`,
5680
+ );
5631
5681
  }
5632
- return `if (${offset} + ${length} <= sizeof(${name})) { memcpy(&${name}[${offset}], &${value}, ${length}); }`;
5682
+
5683
+ // Issue #234: Compile-time bounds validation
5684
+ if (offsetValue + lengthValue > capacity) {
5685
+ throw new Error(
5686
+ `${line}:0 Error: Slice assignment out of bounds: ` +
5687
+ `offset(${offsetValue}) + length(${lengthValue}) = ${offsetValue + lengthValue} ` +
5688
+ `exceeds buffer capacity(${capacity}) for '${name}'.`,
5689
+ );
5690
+ }
5691
+
5692
+ if (offsetValue < 0) {
5693
+ throw new Error(
5694
+ `${line}:0 Error: Slice assignment offset cannot be negative: ${offsetValue}`,
5695
+ );
5696
+ }
5697
+
5698
+ if (lengthValue <= 0) {
5699
+ throw new Error(
5700
+ `${line}:0 Error: Slice assignment length must be positive: ${lengthValue}`,
5701
+ );
5702
+ }
5703
+
5704
+ // Set flag to include string.h for memcpy
5705
+ this.needsString = true;
5706
+
5707
+ // Generate memcpy without runtime bounds check (already validated at compile time)
5708
+ return `memcpy(&${name}[${offsetValue}], &${value}, ${lengthValue});`;
5633
5709
  }
5634
5710
 
5635
5711
  // Normal array element assignment (single index)
@@ -5,6 +5,7 @@
5
5
  * Issue #60: Extracted from CodeGenerator for better separation of concerns.
6
6
  */
7
7
 
8
+ import { ParserRuleContext } from "antlr4ng";
8
9
  import * as Parser from "../parser/grammar/CNextParser";
9
10
  import BITMAP_SIZE from "./types/BITMAP_SIZE";
10
11
  import BITMAP_BACKING_TYPE from "./types/BITMAP_BACKING_TYPE";
@@ -58,6 +59,16 @@ class SymbolCollector {
58
59
  // For scope context during collection (used by getTypeName)
59
60
  private _collectingScope: string | null = null;
60
61
 
62
+ // Issue #232: Track which scope variables are used in which functions
63
+ // Maps "ScopeName_varName" -> Set of function names that use it
64
+ private _scopeVariableUsage: Map<string, Set<string>> = new Map();
65
+
66
+ // Store function declarations for usage analysis
67
+ private _scopeFunctionBodies: Map<
68
+ string,
69
+ Parser.FunctionDeclarationContext[]
70
+ > = new Map();
71
+
61
72
  // Readonly public accessors
62
73
  get knownScopes(): ReadonlySet<string> {
63
74
  return this._knownScopes;
@@ -143,6 +154,30 @@ class SymbolCollector {
143
154
  return this._registerMemberCTypes;
144
155
  }
145
156
 
157
+ // Issue #232: Getter for scope variable usage analysis
158
+ get scopeVariableUsage(): ReadonlyMap<string, Set<string>> {
159
+ return this._scopeVariableUsage;
160
+ }
161
+
162
+ /**
163
+ * Issue #232: Check if a scope variable is used in only one function.
164
+ * Returns the function name if single-function, null otherwise.
165
+ */
166
+ getSingleFunctionForVariable(
167
+ scopeName: string,
168
+ varName: string,
169
+ ): string | null {
170
+ const fullVarName = `${scopeName}_${varName}`;
171
+ const usedIn = this._scopeVariableUsage.get(fullVarName);
172
+
173
+ if (!usedIn || usedIn.size !== 1) {
174
+ return null;
175
+ }
176
+
177
+ // Extract the single element from the Set (we know it exists since size === 1)
178
+ return [...usedIn][0];
179
+ }
180
+
146
181
  /**
147
182
  * Create a new SymbolCollector and collect all symbols from the parse tree.
148
183
  * @param tree The C-Next program parse tree
@@ -259,6 +294,12 @@ class SymbolCollector {
259
294
  const funcName = funcDecl.IDENTIFIER().getText();
260
295
  members.add(funcName);
261
296
  memberVisibility.set(funcName, visibility);
297
+
298
+ // Issue #232: Store function for usage analysis
299
+ if (!this._scopeFunctionBodies.has(name)) {
300
+ this._scopeFunctionBodies.set(name, []);
301
+ }
302
+ this._scopeFunctionBodies.get(name)!.push(funcDecl);
262
303
  }
263
304
 
264
305
  // ADR-017: Collect enums declared inside scopes
@@ -322,10 +363,144 @@ class SymbolCollector {
322
363
  this._scopeMembers.set(name, members);
323
364
  this._scopeMemberVisibility.set(name, memberVisibility);
324
365
 
366
+ // Issue #232: Analyze which functions use which scope variables
367
+ this.analyzeScopeVariableUsage(name, members);
368
+
325
369
  // Clear collecting scope
326
370
  this._collectingScope = null;
327
371
  }
328
372
 
373
+ /**
374
+ * Issue #232: Analyze which scope variables are used in which functions.
375
+ * Issue #233: Also track which variables are WRITTEN to (for reset injection).
376
+ * This enables making single-function variables local instead of file-scope.
377
+ */
378
+ private analyzeScopeVariableUsage(
379
+ scopeName: string,
380
+ members: Set<string>,
381
+ ): void {
382
+ const functions = this._scopeFunctionBodies.get(scopeName);
383
+ if (!functions) return;
384
+
385
+ // Collect all variable names (non-function members)
386
+ const variableNames = new Set<string>();
387
+ for (const member of members) {
388
+ // Check if this is a function by seeing if we have it in the function list
389
+ const isFuncMember = functions.some(
390
+ (f) => f.IDENTIFIER().getText() === member,
391
+ );
392
+ if (!isFuncMember) {
393
+ variableNames.add(member);
394
+ }
395
+ }
396
+
397
+ // For each function, find which variables it references and writes
398
+ for (const funcDecl of functions) {
399
+ const funcName = funcDecl.IDENTIFIER().getText();
400
+ const block = funcDecl.block();
401
+ if (!block) continue;
402
+
403
+ // Find all this.varName references in the function body (reads + writes)
404
+ const usedVars = this.findScopedMemberReferences(block, variableNames);
405
+
406
+ // Record usage
407
+ for (const varName of usedVars) {
408
+ const fullVarName = `${scopeName}_${varName}`;
409
+ if (!this._scopeVariableUsage.has(fullVarName)) {
410
+ this._scopeVariableUsage.set(fullVarName, new Set());
411
+ }
412
+ this._scopeVariableUsage.get(fullVarName)!.add(funcName);
413
+ }
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Issue #232: Recursively find all this.varName references in an AST node.
419
+ * Returns a set of variable names that are referenced.
420
+ *
421
+ * `this.temp` parses as PostfixExpressionContext with:
422
+ * - primaryExpression: 'this'
423
+ * - postfixOp[0]: '.temp'
424
+ */
425
+ private findScopedMemberReferences(
426
+ node: ParserRuleContext,
427
+ variableNames: Set<string>,
428
+ ): Set<string> {
429
+ const found = new Set<string>();
430
+
431
+ // Check if this node is a PostfixExpression with 'this' primary
432
+ if (node instanceof Parser.PostfixExpressionContext) {
433
+ const primary = node.primaryExpression();
434
+ if (primary && primary.getText() === "this") {
435
+ // Get the first postfixOp - it should be .memberName
436
+ const ops = node.postfixOp();
437
+ if (ops.length > 0) {
438
+ const firstOp = ops[0];
439
+ const identifier = firstOp.IDENTIFIER();
440
+ if (identifier) {
441
+ const memberName = identifier.getText();
442
+ if (variableNames.has(memberName)) {
443
+ found.add(memberName);
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+
450
+ // Also check thisAccess, thisMemberAccess, thisArrayAccess in lvalue context
451
+ if (node instanceof Parser.ThisAccessContext) {
452
+ const identifier = node.IDENTIFIER();
453
+ if (identifier) {
454
+ const memberName = identifier.getText();
455
+ if (variableNames.has(memberName)) {
456
+ found.add(memberName);
457
+ }
458
+ }
459
+ }
460
+
461
+ if (node instanceof Parser.ThisMemberAccessContext) {
462
+ // First IDENTIFIER after 'this.'
463
+ const identifiers = node.IDENTIFIER();
464
+ if (identifiers.length > 0) {
465
+ const firstMember = identifiers[0].getText();
466
+ if (variableNames.has(firstMember)) {
467
+ found.add(firstMember);
468
+ }
469
+ }
470
+ }
471
+
472
+ if (node instanceof Parser.ThisArrayAccessContext) {
473
+ // First IDENTIFIER after 'this.'
474
+ const identifiers = node.IDENTIFIER();
475
+ if (identifiers.length > 0) {
476
+ const firstMember = identifiers[0].getText();
477
+ if (variableNames.has(firstMember)) {
478
+ found.add(firstMember);
479
+ }
480
+ }
481
+ }
482
+
483
+ // Recursively check all children using .children property.
484
+ // Type assertion needed because antlr4ng's ParserRuleContext doesn't expose
485
+ // .children in its public type definitions, but it exists at runtime.
486
+ const children = (node as { children?: unknown[] }).children;
487
+ if (children) {
488
+ for (const child of children) {
489
+ if (child instanceof ParserRuleContext) {
490
+ const childFound = this.findScopedMemberReferences(
491
+ child,
492
+ variableNames,
493
+ );
494
+ for (const v of childFound) {
495
+ found.add(v);
496
+ }
497
+ }
498
+ }
499
+ }
500
+
501
+ return found;
502
+ }
503
+
329
504
  /**
330
505
  * Collect struct declarations and track field types
331
506
  */
@@ -539,6 +714,21 @@ class SymbolCollector {
539
714
  }
540
715
  return value;
541
716
  }
717
+
718
+ /**
719
+ * Issue #230: Check if any scope members are public (exported)
720
+ * Used to determine if a self-include header is needed for extern "C" linkage
721
+ */
722
+ hasPublicSymbols(): boolean {
723
+ for (const [, visibilityMap] of this._scopeMemberVisibility) {
724
+ for (const [, visibility] of visibilityMap) {
725
+ if (visibility === "public") {
726
+ return true;
727
+ }
728
+ }
729
+ }
730
+ return false;
731
+ }
542
732
  }
543
733
 
544
734
  export default SymbolCollector;
@@ -91,6 +91,23 @@ interface ISymbolInfo {
91
91
 
92
92
  /** Register member C types: "reg_member" -> C type (e.g., "uint32_t") */
93
93
  readonly registerMemberCTypes: ReadonlyMap<string, string>;
94
+
95
+ // === Issue #232: Scope Variable Usage Analysis ===
96
+
97
+ /**
98
+ * Scope variable usage: "Scope_varName" -> Set of function names that use it.
99
+ * Used to determine if a variable is single-function (can be local).
100
+ */
101
+ readonly scopeVariableUsage: ReadonlyMap<string, ReadonlySet<string>>;
102
+
103
+ /**
104
+ * Check if a scope variable is used in only one function.
105
+ * Returns the function name if single-function, null otherwise.
106
+ */
107
+ getSingleFunctionForVariable(
108
+ scopeName: string,
109
+ varName: string,
110
+ ): string | null;
94
111
  }
95
112
 
96
113
  export default ISymbolInfo;
@@ -30,6 +30,15 @@ import generateScopedRegister from "./ScopedRegisterGenerator";
30
30
  * - Visibility control (private -> static, public -> extern)
31
31
  * - Organization without runtime overhead
32
32
  */
33
+ /**
34
+ * Issue #232: Information about a single-function variable
35
+ */
36
+ interface ISingleFunctionVar {
37
+ varDecl: Parser.VariableDeclarationContext;
38
+ targetFunction: string;
39
+ isPrivate: boolean;
40
+ }
41
+
33
42
  const generateScope: TGeneratorFn<Parser.ScopeDeclarationContext> = (
34
43
  node: Parser.ScopeDeclarationContext,
35
44
  input: IGeneratorInput,
@@ -44,6 +53,10 @@ const generateScope: TGeneratorFn<Parser.ScopeDeclarationContext> = (
44
53
  const lines: string[] = [];
45
54
  lines.push(`/* Scope: ${name} */`);
46
55
 
56
+ // Issue #232: First pass - identify single-function variables
57
+ // Maps functionName -> list of variable declarations that are local to it
58
+ const localVarsForFunction = new Map<string, ISingleFunctionVar[]>();
59
+
47
60
  for (const member of node.scopeMember()) {
48
61
  const visibility = member.visibilityModifier()?.getText() || "private";
49
62
  const isPrivate = visibility === "private";
@@ -51,8 +64,30 @@ const generateScope: TGeneratorFn<Parser.ScopeDeclarationContext> = (
51
64
  // Handle variable declarations
52
65
  if (member.variableDeclaration()) {
53
66
  const varDecl = member.variableDeclaration()!;
54
- const type = orchestrator.generateType(varDecl.type());
55
67
  const varName = varDecl.IDENTIFIER().getText();
68
+
69
+ // Issue #232: Check if this PRIVATE variable is used in only one function
70
+ // PUBLIC variables must stay file-scope because they can be accessed from outside
71
+ const targetFunc = isPrivate
72
+ ? input.symbols?.getSingleFunctionForVariable(name, varName)
73
+ : null;
74
+
75
+ if (targetFunc) {
76
+ // Single-function private variable - emit as local, not file-scope
77
+ if (!localVarsForFunction.has(targetFunc)) {
78
+ localVarsForFunction.set(targetFunc, []);
79
+ }
80
+ localVarsForFunction.get(targetFunc)!.push({
81
+ varDecl,
82
+ targetFunction: targetFunc,
83
+ isPrivate,
84
+ });
85
+ // Don't emit file-scope declaration
86
+ continue;
87
+ }
88
+
89
+ // Multi-function or unused variable - emit at file scope (original behavior)
90
+ const type = orchestrator.generateType(varDecl.type());
56
91
  const fullName = `${name}_${varName}`;
57
92
  const prefix = isPrivate ? "static " : "";
58
93
 
@@ -99,7 +134,53 @@ const generateScope: TGeneratorFn<Parser.ScopeDeclarationContext> = (
99
134
  ? orchestrator.generateParameterList(funcDecl.parameterList()!)
100
135
  : "void";
101
136
 
102
- const body = orchestrator.generateBlock(funcDecl.block());
137
+ let body = orchestrator.generateBlock(funcDecl.block());
138
+
139
+ // Issue #232: Inject local variable declarations for single-function vars
140
+ const localVars = localVarsForFunction.get(funcName);
141
+ if (localVars && localVars.length > 0) {
142
+ // Generate local variable declarations
143
+ const localDecls: string[] = [];
144
+ for (const { varDecl } of localVars) {
145
+ const varType = orchestrator.generateType(varDecl.type());
146
+ const varName = varDecl.IDENTIFIER().getText();
147
+ const varFullName = `${name}_${varName}`;
148
+
149
+ const arrayDims = varDecl.arrayDimension();
150
+ const isArray = arrayDims.length > 0;
151
+ let decl = ` ${varType} ${varFullName}`;
152
+ if (isArray) {
153
+ decl += orchestrator.generateArrayDimensions(arrayDims);
154
+ }
155
+ // ADR-045: Add string capacity dimension for string arrays
156
+ if (varDecl.type().stringType()) {
157
+ const stringCtx = varDecl.type().stringType()!;
158
+ const intLiteral = stringCtx.INTEGER_LITERAL();
159
+ if (intLiteral) {
160
+ const capacity = parseInt(intLiteral.getText(), 10);
161
+ decl += `[${capacity + 1}]`;
162
+ }
163
+ }
164
+ if (varDecl.expression()) {
165
+ decl += ` = ${orchestrator.generateExpression(varDecl.expression()!)}`;
166
+ } else {
167
+ decl += ` = ${orchestrator.getZeroInitializer(varDecl.type(), isArray)}`;
168
+ }
169
+ localDecls.push(decl + ";");
170
+ }
171
+
172
+ // Inject after opening brace: { -> {\n local_var = init;
173
+ // Body format is typically "{\n ...statements...\n}"
174
+ const openBraceIdx = body.indexOf("{");
175
+ if (openBraceIdx !== -1) {
176
+ const afterBrace = body.substring(openBraceIdx + 1);
177
+ body =
178
+ body.substring(0, openBraceIdx + 1) +
179
+ "\n" +
180
+ localDecls.join("\n") +
181
+ afterBrace;
182
+ }
183
+ }
103
184
 
104
185
  // ADR-016: Exit function body context
105
186
  orchestrator.exitFunctionBody();
@@ -8,6 +8,8 @@
8
8
  * - Bitwise: |, ^, &
9
9
  * - Shift: <<, >> with validation
10
10
  * - Arithmetic: +, -, *, /, %
11
+ *
12
+ * Issue #235: Includes constant folding for compile-time constant expressions.
11
13
  */
12
14
  import * as Parser from "../../../parser/grammar/CNextParser";
13
15
  import IGeneratorOutput from "../IGeneratorOutput";
@@ -16,6 +18,89 @@ import IGeneratorInput from "../IGeneratorInput";
16
18
  import IGeneratorState from "../IGeneratorState";
17
19
  import IOrchestrator from "../IOrchestrator";
18
20
 
21
+ /**
22
+ * Issue #235: Try to parse a string as a numeric constant.
23
+ * Returns the numeric value if it's a simple integer literal, undefined otherwise.
24
+ * Handles decimal, hex (0x), and binary (0b) formats.
25
+ */
26
+ const tryParseNumericLiteral = (code: string): number | undefined => {
27
+ const trimmed = code.trim();
28
+
29
+ // Decimal integer (including negative)
30
+ if (/^-?\d+$/.test(trimmed)) {
31
+ return parseInt(trimmed, 10);
32
+ }
33
+
34
+ // Hex literal
35
+ if (/^0[xX][0-9a-fA-F]+$/.test(trimmed)) {
36
+ return parseInt(trimmed, 16);
37
+ }
38
+
39
+ // Binary literal
40
+ if (/^0[bB][01]+$/.test(trimmed)) {
41
+ return parseInt(trimmed.substring(2), 2);
42
+ }
43
+
44
+ return undefined;
45
+ };
46
+
47
+ /**
48
+ * Issue #235: Evaluate a constant arithmetic expression.
49
+ * Returns the result if all operands are numeric and evaluation succeeds,
50
+ * undefined otherwise (falls back to non-folded code).
51
+ */
52
+ const tryFoldConstants = (
53
+ operandCodes: string[],
54
+ operators: string[],
55
+ ): number | undefined => {
56
+ // Parse all operands as numbers
57
+ const values = operandCodes.map(tryParseNumericLiteral);
58
+
59
+ // If any operand is not a constant, we can't fold
60
+ if (values.some((v) => v === undefined)) {
61
+ return undefined;
62
+ }
63
+
64
+ // Evaluate left-to-right with the operators
65
+ let result = values[0] as number;
66
+ for (let i = 0; i < operators.length; i++) {
67
+ const op = operators[i];
68
+ const rightValue = values[i + 1] as number;
69
+
70
+ switch (op) {
71
+ case "*":
72
+ result = result * rightValue;
73
+ break;
74
+ case "/":
75
+ // Avoid division by zero - let C compiler handle it
76
+ if (rightValue === 0) {
77
+ return undefined;
78
+ }
79
+ // Integer division (truncate toward zero like C)
80
+ result = Math.trunc(result / rightValue);
81
+ break;
82
+ case "%":
83
+ // Avoid modulo by zero
84
+ if (rightValue === 0) {
85
+ return undefined;
86
+ }
87
+ result = result % rightValue;
88
+ break;
89
+ case "+":
90
+ result = result + rightValue;
91
+ break;
92
+ case "-":
93
+ result = result - rightValue;
94
+ break;
95
+ default:
96
+ // Unknown operator - don't fold
97
+ return undefined;
98
+ }
99
+ }
100
+
101
+ return result;
102
+ };
103
+
19
104
  /**
20
105
  * Generate C code for an OR expression (lowest precedence binary op).
21
106
  */
@@ -320,6 +405,7 @@ const generateShiftExpr = (
320
405
 
321
406
  /**
322
407
  * Generate C code for an additive expression.
408
+ * Issue #235: Includes constant folding for compile-time constant expressions.
323
409
  */
324
410
  const generateAdditiveExpr = (
325
411
  node: Parser.AdditiveExpressionContext,
@@ -336,25 +422,25 @@ const generateAdditiveExpr = (
336
422
 
337
423
  // Issue #152: Extract operators in order from parse tree children
338
424
  const operators = orchestrator.getOperatorsFromChildren(node);
339
- const firstResult = generateMultiplicativeExpr(
340
- exprs[0],
341
- input,
342
- state,
343
- orchestrator,
425
+
426
+ // Generate code for all operands
427
+ const operandResults = exprs.map((expr) =>
428
+ generateMultiplicativeExpr(expr, input, state, orchestrator),
344
429
  );
345
- effects.push(...firstResult.effects);
346
- let result = firstResult.code;
430
+ const operandCodes = operandResults.map((r) => r.code);
431
+ operandResults.forEach((r) => effects.push(...r.effects));
347
432
 
348
- for (let i = 1; i < exprs.length; i++) {
433
+ // Issue #235: Try constant folding for compile-time constant expressions
434
+ const foldedResult = tryFoldConstants(operandCodes, operators);
435
+ if (foldedResult !== undefined) {
436
+ return { code: String(foldedResult), effects };
437
+ }
438
+
439
+ // Fall back to standard code generation
440
+ let result = operandCodes[0];
441
+ for (let i = 1; i < operandCodes.length; i++) {
349
442
  const op = operators[i - 1] || "+";
350
- const exprResult = generateMultiplicativeExpr(
351
- exprs[i],
352
- input,
353
- state,
354
- orchestrator,
355
- );
356
- effects.push(...exprResult.effects);
357
- result += ` ${op} ${exprResult.code}`;
443
+ result += ` ${op} ${operandCodes[i]}`;
358
444
  }
359
445
 
360
446
  return { code: result, effects };
@@ -363,6 +449,7 @@ const generateAdditiveExpr = (
363
449
  /**
364
450
  * Generate C code for a multiplicative expression.
365
451
  * This is the bottom of the binary chain - delegates to unary via orchestrator.
452
+ * Issue #235: Includes constant folding for compile-time constant expressions.
366
453
  */
367
454
  const generateMultiplicativeExpr = (
368
455
  node: Parser.MultiplicativeExpressionContext,
@@ -380,11 +467,23 @@ const generateMultiplicativeExpr = (
380
467
 
381
468
  // Issue #152: Extract operators in order from parse tree children
382
469
  const operators = orchestrator.getOperatorsFromChildren(node);
383
- let result = orchestrator.generateUnaryExpr(exprs[0]);
384
470
 
385
- for (let i = 1; i < exprs.length; i++) {
471
+ // Generate code for all operands
472
+ const operandCodes = exprs.map((expr) =>
473
+ orchestrator.generateUnaryExpr(expr),
474
+ );
475
+
476
+ // Issue #235: Try constant folding for compile-time constant expressions
477
+ const foldedResult = tryFoldConstants(operandCodes, operators);
478
+ if (foldedResult !== undefined) {
479
+ return { code: String(foldedResult), effects: [] };
480
+ }
481
+
482
+ // Fall back to standard code generation
483
+ let result = operandCodes[0];
484
+ for (let i = 1; i < operandCodes.length; i++) {
386
485
  const op = operators[i - 1] || "*";
387
- result += ` ${op} ${orchestrator.generateUnaryExpr(exprs[i])}`;
486
+ result += ` ${op} ${operandCodes[i]}`;
388
487
  }
389
488
 
390
489
  return { code: result, effects: [] };
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Helper function generators for overflow-safe arithmetic and safe division.
3
3
  * Extracted from CodeGenerator.ts as part of ADR-053 A5.
4
+ *
5
+ * Portability note: Uses __builtin_add_overflow, __builtin_sub_overflow, and
6
+ * __builtin_mul_overflow intrinsics (GCC 5+, Clang 3.4+). C-Next targets embedded
7
+ * systems using arm-none-eabi-gcc, so these are available. MSVC is not supported.
4
8
  */
5
9
  import TYPE_MAP from "../../types/TYPE_MAP";
6
10
  import WIDER_TYPE_MAP from "../../types/WIDER_TYPE_MAP";
@@ -33,10 +37,13 @@ function generateSingleHelper(
33
37
  switch (operation) {
34
38
  case "add":
35
39
  if (isUnsigned) {
36
- // Unsigned addition: use wider type for b to prevent truncation (Issue #94)
40
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
41
+ // Issue #94: Wide operand check prevents truncation issues
37
42
  return `static inline ${cType} cnx_clamp_add_${cnxType}(${cType} a, ${widerType} b) {
38
- if (b > ${maxValue} - a) return ${maxValue};
39
- return a + (${cType})b;
43
+ if (b > (${widerType})(${maxValue} - a)) return ${maxValue};
44
+ ${cType} result;
45
+ if (__builtin_add_overflow(a, (${cType})b, &result)) return ${maxValue};
46
+ return result;
40
47
  }`;
41
48
  } else if (useWiderArithmetic) {
42
49
  // Signed addition: compute in wider type, then clamp (Issue #94)
@@ -58,11 +65,14 @@ function generateSingleHelper(
58
65
 
59
66
  case "sub":
60
67
  if (isUnsigned) {
61
- // Unsigned subtraction: use wider type for b to prevent truncation (Issue #94)
62
- // Cast a to wider type for comparison to handle b > type max
68
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
69
+ // Issue #94: Wide operand check prevents truncation issues
70
+ // Use > (not >=) since b == a produces valid result 0 via the builtin path
63
71
  return `static inline ${cType} cnx_clamp_sub_${cnxType}(${cType} a, ${widerType} b) {
64
- if (b >= (${widerType})a) return 0;
65
- return a - (${cType})b;
72
+ if (b > (${widerType})a) return 0;
73
+ ${cType} result;
74
+ if (__builtin_sub_overflow(a, (${cType})b, &result)) return 0;
75
+ return result;
66
76
  }`;
67
77
  } else if (useWiderArithmetic) {
68
78
  // Signed subtraction: compute in wider type, then clamp (Issue #94)
@@ -83,10 +93,13 @@ function generateSingleHelper(
83
93
 
84
94
  case "mul":
85
95
  if (isUnsigned) {
86
- // Unsigned multiplication: use wider type for b to prevent truncation (Issue #94)
96
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
97
+ // Issue #94: Wide operand check prevents truncation issues
87
98
  return `static inline ${cType} cnx_clamp_mul_${cnxType}(${cType} a, ${widerType} b) {
88
99
  if (b != 0 && a > ${maxValue} / b) return ${maxValue};
89
- return a * (${cType})b;
100
+ ${cType} result;
101
+ if (__builtin_mul_overflow(a, (${cType})b, &result)) return ${maxValue};
102
+ return result;
90
103
  }`;
91
104
  } else if (useWiderArithmetic) {
92
105
  // Signed multiplication: compute in wider type, then clamp (Issue #94)
@@ -144,13 +157,19 @@ function generateDebugHelper(
144
157
  switch (operation) {
145
158
  case "add":
146
159
  if (isUnsigned) {
147
- // Use wider type for b to prevent truncation (Issue #94)
160
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
161
+ // Issue #94: Wide operand check prevents truncation issues
148
162
  return `static inline ${cType} cnx_clamp_add_${cnxType}(${cType} a, ${widerType} b) {
149
- if (b > ${maxValue} - a) {
163
+ if (b > (${widerType})(${maxValue} - a)) {
150
164
  fprintf(stderr, "PANIC: Integer overflow in ${cnxType} ${opName}\\n");
151
165
  abort();
152
166
  }
153
- return a + (${cType})b;
167
+ ${cType} result;
168
+ if (__builtin_add_overflow(a, (${cType})b, &result)) {
169
+ fprintf(stderr, "PANIC: Integer overflow in ${cnxType} ${opName}\\n");
170
+ abort();
171
+ }
172
+ return result;
154
173
  }`;
155
174
  } else if (useWiderArithmetic) {
156
175
  // Signed addition: compute in wider type, check bounds (Issue #94)
@@ -175,13 +194,20 @@ function generateDebugHelper(
175
194
 
176
195
  case "sub":
177
196
  if (isUnsigned) {
178
- // Use wider type for b to prevent truncation (Issue #94)
197
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
198
+ // Issue #94: Wide operand check prevents truncation issues
199
+ // Use > (not >=) since b == a produces valid result 0 via the builtin path
179
200
  return `static inline ${cType} cnx_clamp_sub_${cnxType}(${cType} a, ${widerType} b) {
180
- if (b >= (${widerType})a) {
201
+ if (b > (${widerType})a) {
181
202
  fprintf(stderr, "PANIC: Integer underflow in ${cnxType} ${opName}\\n");
182
203
  abort();
183
204
  }
184
- return a - (${cType})b;
205
+ ${cType} result;
206
+ if (__builtin_sub_overflow(a, (${cType})b, &result)) {
207
+ fprintf(stderr, "PANIC: Integer underflow in ${cnxType} ${opName}\\n");
208
+ abort();
209
+ }
210
+ return result;
185
211
  }`;
186
212
  } else if (useWiderArithmetic) {
187
213
  // Signed subtraction: compute in wider type, check bounds (Issue #94)
@@ -206,13 +232,19 @@ function generateDebugHelper(
206
232
 
207
233
  case "mul":
208
234
  if (isUnsigned) {
209
- // Use wider type for b to prevent truncation (Issue #94)
235
+ // Issue #231: Hybrid approach - check wide operand first, then use builtin
236
+ // Issue #94: Wide operand check prevents truncation issues
210
237
  return `static inline ${cType} cnx_clamp_mul_${cnxType}(${cType} a, ${widerType} b) {
211
238
  if (b != 0 && a > ${maxValue} / b) {
212
239
  fprintf(stderr, "PANIC: Integer overflow in ${cnxType} ${opName}\\n");
213
240
  abort();
214
241
  }
215
- return a * (${cType})b;
242
+ ${cType} result;
243
+ if (__builtin_mul_overflow(a, (${cType})b, &result)) {
244
+ fprintf(stderr, "PANIC: Integer overflow in ${cnxType} ${opName}\\n");
245
+ abort();
246
+ }
247
+ return result;
216
248
  }`;
217
249
  } else if (useWiderArithmetic) {
218
250
  // Signed multiplication: compute in wider type, check bounds (Issue #94)
@@ -8,6 +8,11 @@ interface ICodeGeneratorOptions {
8
8
  target?: string;
9
9
  /** ADR-010: Source file path for validating includes */
10
10
  sourcePath?: string;
11
+ /**
12
+ * Issue #230: When true, emit self-include for extern "C" linkage.
13
+ * Only set this when headers will actually be generated alongside the implementation.
14
+ */
15
+ generateHeaders?: boolean;
11
16
  }
12
17
 
13
18
  export default ICodeGeneratorOptions;
@@ -565,6 +565,8 @@ class Pipeline {
565
565
  {
566
566
  debugMode: this.config.debugMode,
567
567
  target: this.config.target,
568
+ sourcePath: file.path, // Issue #230: For self-include header generation
569
+ generateHeaders: this.config.generateHeaders, // Issue #230: Enable self-include when headers are generated
568
570
  },
569
571
  );
570
572
 
@@ -724,6 +726,7 @@ class Pipeline {
724
726
  workingDir?: string; // For resolving relative #includes
725
727
  includeDirs?: string[]; // Additional include paths
726
728
  sourcePath?: string; // Optional source path for error messages
729
+ generateHeaders?: boolean; // Issue #230: Enable self-include for extern "C" tests
727
730
  },
728
731
  ): Promise<IFileResult> {
729
732
  const workingDir = options?.workingDir ?? process.cwd();
@@ -868,6 +871,8 @@ class Pipeline {
868
871
  {
869
872
  debugMode: this.config.debugMode,
870
873
  target: this.config.target,
874
+ sourcePath, // Issue #230: For self-include header generation
875
+ generateHeaders: options?.generateHeaders, // Issue #230: Enable self-include for extern "C" tests
871
876
  },
872
877
  );
873
878