c-next 0.1.60 → 0.1.62

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 (106) hide show
  1. package/README.md +8 -0
  2. package/grammar/CNext.g4 +3 -17
  3. package/package.json +1 -1
  4. package/src/cli/serve/ServeCommand.ts +166 -35
  5. package/src/cli/serve/__tests__/ServeCommand.test.ts +227 -3
  6. package/src/lib/__tests__/parseCHeader.mocked.test.ts +145 -0
  7. package/src/lib/__tests__/parseCHeader.test.ts +194 -0
  8. package/src/lib/parseCHeader.ts +114 -0
  9. package/src/lib/types/TSymbolKind.ts +2 -1
  10. package/src/transpiler/Transpiler.ts +676 -618
  11. package/src/transpiler/__tests__/DualCodePaths.test.ts +4 -0
  12. package/src/transpiler/__tests__/Transpiler.coverage.test.ts +1 -98
  13. package/src/transpiler/__tests__/Transpiler.test.ts +3 -3
  14. package/src/transpiler/data/IncludeTreeWalker.ts +1 -1
  15. package/src/transpiler/logic/analysis/FunctionCallAnalyzer.ts +74 -46
  16. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +126 -120
  17. package/src/transpiler/logic/analysis/NullCheckAnalyzer.ts +44 -35
  18. package/src/transpiler/logic/parser/grammar/CNext.interp +1 -3
  19. package/src/transpiler/logic/parser/grammar/CNextListener.ts +0 -22
  20. package/src/transpiler/logic/parser/grammar/CNextParser.ts +665 -1084
  21. package/src/transpiler/logic/parser/grammar/CNextVisitor.ts +0 -14
  22. package/src/transpiler/logic/symbols/CSymbolCollector.ts +23 -16
  23. package/src/transpiler/logic/symbols/CppSymbolCollector.ts +67 -43
  24. package/src/transpiler/logic/symbols/cnext/index.ts +47 -27
  25. package/src/transpiler/output/codegen/CodeGenerator.ts +875 -1647
  26. package/src/transpiler/output/codegen/TypeResolver.ts +56 -32
  27. package/src/transpiler/output/codegen/TypeValidator.ts +24 -75
  28. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +1764 -0
  29. package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +10 -7
  30. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +38 -37
  31. package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +211 -46
  32. package/src/transpiler/output/codegen/analysis/StringLengthCounter.ts +3 -3
  33. package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +191 -238
  34. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +60 -15
  35. package/src/transpiler/output/codegen/assignment/AssignmentContextBuilder.ts +75 -75
  36. package/src/transpiler/output/codegen/assignment/AssignmentKind.ts +3 -0
  37. package/src/transpiler/output/codegen/assignment/IAssignmentContext.ts +3 -0
  38. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +3 -0
  39. package/src/transpiler/output/codegen/assignment/handlers/BitAccessHandlers.ts +42 -0
  40. package/src/transpiler/output/codegen/assignment/handlers/__tests__/BitAccessHandlers.test.ts +76 -2
  41. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +5 -1
  42. package/src/transpiler/output/codegen/generators/__tests__/GeneratorRegistry.test.ts +51 -0
  43. package/src/transpiler/output/codegen/generators/declarationGenerators/RegisterGenerator.ts +11 -24
  44. package/src/transpiler/output/codegen/generators/declarationGenerators/RegisterMacroGenerator.ts +64 -0
  45. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +16 -10
  46. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopedRegisterGenerator.ts +18 -27
  47. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +5 -1
  48. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +1 -17
  49. package/src/transpiler/output/codegen/generators/statements/SwitchGenerator.ts +77 -40
  50. package/src/transpiler/output/codegen/generators/support/IncludeGenerator.ts +93 -47
  51. package/src/transpiler/output/codegen/helpers/ArrayAccessHelper.ts +129 -0
  52. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +23 -18
  53. package/src/transpiler/output/codegen/helpers/AssignmentTargetExtractor.ts +58 -0
  54. package/src/transpiler/output/codegen/helpers/AssignmentValidator.ts +33 -31
  55. package/src/transpiler/output/codegen/helpers/BaseIdentifierBuilder.ts +53 -0
  56. package/src/transpiler/output/codegen/helpers/CastValidator.ts +146 -0
  57. package/src/transpiler/output/codegen/helpers/ChildStatementCollector.ts +116 -0
  58. package/src/transpiler/output/codegen/helpers/LiteralEvaluator.ts +61 -0
  59. package/src/transpiler/output/codegen/helpers/MemberSeparatorResolver.ts +10 -3
  60. package/src/transpiler/output/codegen/helpers/PostfixChainBuilder.ts +101 -0
  61. package/src/transpiler/output/codegen/helpers/SimpleIdentifierResolver.ts +54 -0
  62. package/src/transpiler/output/codegen/helpers/StatementExpressionCollector.ts +117 -0
  63. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +44 -0
  64. package/src/transpiler/output/codegen/helpers/TransitiveModificationPropagator.ts +127 -0
  65. package/src/transpiler/output/codegen/helpers/TypeGenerationHelper.ts +249 -0
  66. package/src/transpiler/output/codegen/helpers/__tests__/ArrayAccessHelper.test.ts +479 -0
  67. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentTargetExtractor.test.ts +105 -0
  68. package/src/transpiler/output/codegen/helpers/__tests__/BaseIdentifierBuilder.test.ts +68 -0
  69. package/src/transpiler/output/codegen/helpers/__tests__/CastValidator.test.ts +233 -0
  70. package/src/transpiler/output/codegen/helpers/__tests__/ChildStatementCollector.test.ts +191 -0
  71. package/src/transpiler/output/codegen/helpers/__tests__/LiteralEvaluator.test.ts +62 -0
  72. package/src/transpiler/output/codegen/helpers/__tests__/MemberSeparatorResolver.test.ts +1 -0
  73. package/src/transpiler/output/codegen/helpers/__tests__/PostfixChainBuilder.test.ts +147 -0
  74. package/src/transpiler/output/codegen/helpers/__tests__/SimpleIdentifierResolver.test.ts +105 -0
  75. package/src/transpiler/output/codegen/helpers/__tests__/StatementExpressionCollector.test.ts +221 -0
  76. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +201 -0
  77. package/src/transpiler/output/codegen/helpers/__tests__/TransitiveModificationPropagator.test.ts +289 -0
  78. package/src/transpiler/output/codegen/helpers/__tests__/TypeGenerationHelper.test.ts +419 -0
  79. package/src/transpiler/output/codegen/types/IArrayAccessDeps.ts +23 -0
  80. package/src/transpiler/output/codegen/types/IArrayAccessInfo.ts +26 -0
  81. package/src/transpiler/output/codegen/types/IBaseIdentifierResult.ts +11 -0
  82. package/src/transpiler/output/codegen/types/IMemberSeparatorDeps.ts +7 -0
  83. package/src/transpiler/output/codegen/types/IPostfixChainDeps.ts +16 -0
  84. package/src/transpiler/output/codegen/types/IPostfixOperation.ts +11 -0
  85. package/src/transpiler/output/codegen/types/ISimpleIdentifierDeps.ts +21 -0
  86. package/src/transpiler/output/codegen/utils/CodegenParserUtils.ts +98 -0
  87. package/src/transpiler/output/codegen/utils/ExpressionUnwrapper.ts +22 -22
  88. package/src/transpiler/output/codegen/utils/__tests__/CodegenParserUtils.test.ts +228 -0
  89. package/src/transpiler/types/IFileResult.ts +0 -4
  90. package/src/transpiler/types/IPipelineFile.ts +27 -0
  91. package/src/transpiler/types/IPipelineInput.ts +23 -0
  92. package/src/transpiler/types/TranspilerState.ts +1 -1
  93. package/src/utils/FormatUtils.ts +28 -2
  94. package/src/utils/MapUtils.ts +25 -0
  95. package/src/utils/PostfixAnalysisUtils.ts +48 -0
  96. package/src/utils/__tests__/FormatUtils.test.ts +42 -0
  97. package/src/utils/__tests__/MapUtils.test.ts +85 -0
  98. package/src/utils/constants/OperatorMappings.ts +19 -0
  99. package/src/lib/__tests__/transpiler.test.ts +0 -303
  100. package/src/lib/transpiler.ts +0 -149
  101. package/src/lib/types/ITranspileOptions.ts +0 -15
  102. package/src/lib/types/ITranspileResult.ts +0 -20
  103. package/src/transpiler/logic/StandaloneContextBuilder.ts +0 -150
  104. package/src/transpiler/logic/__tests__/StandaloneContextBuilder.test.ts +0 -647
  105. package/src/transpiler/types/ITranspileContext.ts +0 -49
  106. package/src/transpiler/types/ITranspileContribution.ts +0 -32
package/README.md CHANGED
@@ -94,6 +94,14 @@ cnext src/ -o build/src --header-out build/include --clean
94
94
  cnext --help
95
95
  ```
96
96
 
97
+ ## VS Code Extension
98
+
99
+ The C-Next VS Code extension provides syntax highlighting, live C preview, IntelliSense, and error diagnostics.
100
+
101
+ **Install from:** [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=jlaustill.vscode-c-next) (coming soon)
102
+
103
+ **Source:** [github.com/jlaustill/vscode-c-next](https://github.com/jlaustill/vscode-c-next)
104
+
97
105
  ## Getting Started with PlatformIO
98
106
 
99
107
  C-Next integrates seamlessly with PlatformIO embedded projects. The transpiler automatically converts `.cnx` files to `.c` before each build.
package/grammar/CNext.g4 CHANGED
@@ -248,14 +248,11 @@ assignmentOperator
248
248
  ;
249
249
 
250
250
  // Assignment target with unified postfix chain approach (Issue #387)
251
- // global/this prefixes use postfixTargetOp* for any combination of member/array access
252
- // Bare identifiers fall back to existing memberAccess/arrayAccess rules for compatibility
251
+ // All patterns use postfixTargetOp* for member/array access chains
253
252
  assignmentTarget
254
253
  : 'global' '.' IDENTIFIER postfixTargetOp* // global.x, global.x[i][j].y, etc.
255
254
  | 'this' '.' IDENTIFIER postfixTargetOp* // this.x, this.x[i].y, etc.
256
- | arrayAccess // arr[i], reg[start, width]
257
- | memberAccess // GPIO7.DR_SET, arr[i].field
258
- | IDENTIFIER // Simple identifier
255
+ | IDENTIFIER postfixTargetOp* // x, arr[i], obj.field, arr[i].field, etc.
259
256
  ;
260
257
 
261
258
  // Unified postfix operation for assignment targets (Issue #387)
@@ -459,18 +456,7 @@ arrayInitializerElement
459
456
  | arrayInitializer // For nested arrays: [[1,2], [3,4]]
460
457
  ;
461
458
 
462
- memberAccess
463
- : IDENTIFIER ('.' IDENTIFIER)+ ('[' expression ']')+ // ADR-036: screen.pixels[0][0]
464
- | IDENTIFIER ('.' IDENTIFIER)+ '[' expression ',' expression ']' // GPIO7.DR[start, width]
465
- | IDENTIFIER ('.' IDENTIFIER)+ // GPIO7.DR_SET
466
- | IDENTIFIER ('[' expression ']')+ ('.' IDENTIFIER)+ // arr[i].field1.field2...
467
- | IDENTIFIER (('[' expression ']') | ('.' IDENTIFIER))+ // arr[i].field[j].member... (any mix)
468
- ;
469
-
470
- arrayAccess
471
- : IDENTIFIER '[' expression ']' // Single element/bit
472
- | IDENTIFIER '[' expression ',' expression ']' // Bit range [start, width]
473
- ;
459
+ // Note: memberAccess and arrayAccess rules removed - unified into assignmentTarget with postfixTargetOp*
474
460
 
475
461
  argumentList
476
462
  : expression (',' expression)*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.60",
3
+ "version": "0.1.62",
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",
@@ -1,20 +1,29 @@
1
1
  /**
2
2
  * ServeCommand
3
3
  * JSON-RPC server for VS Code extension communication
4
+ *
5
+ * Phase 2b (ADR-060): Uses the full Transpiler for transpilation and symbol
6
+ * extraction, enabling include resolution, C++ auto-detection, and cross-file
7
+ * symbol support.
4
8
  */
5
9
 
6
10
  import { createInterface, Interface } from "node:readline";
11
+ import { dirname } from "node:path";
7
12
  import JsonRpcHandler from "./JsonRpcHandler";
8
13
  import IJsonRpcRequest from "./types/IJsonRpcRequest";
9
14
  import IJsonRpcResponse from "./types/IJsonRpcResponse";
10
15
  import ConfigPrinter from "../ConfigPrinter";
11
- import transpile from "../../lib/transpiler";
16
+ import ConfigLoader from "../ConfigLoader";
17
+ import Transpiler from "../../transpiler/Transpiler";
12
18
  import parseWithSymbols from "../../lib/parseWithSymbols";
19
+ import parseCHeader from "../../lib/parseCHeader";
13
20
 
14
21
  /**
15
- * Method handler type
22
+ * Method handler type (async to support Transpiler.transpileSource)
16
23
  */
17
- type MethodHandler = (params?: Record<string, unknown>) => IMethodResult;
24
+ type MethodHandler = (
25
+ params?: Record<string, unknown>,
26
+ ) => Promise<IMethodResult>;
18
27
 
19
28
  /**
20
29
  * Result from a method handler
@@ -26,6 +35,14 @@ interface IMethodResult {
26
35
  errorMessage?: string;
27
36
  }
28
37
 
38
+ /**
39
+ * Result of validating and extracting source parameters.
40
+ */
41
+ interface ISourceParams {
42
+ source: string;
43
+ filePath: string | undefined;
44
+ }
45
+
29
46
  /**
30
47
  * Options for the serve command
31
48
  */
@@ -41,14 +58,23 @@ class ServeCommand {
41
58
  private static shouldShutdown = false;
42
59
  private static readline: Interface | null = null;
43
60
  private static debugMode = false;
61
+ private static transpiler: Transpiler | null = null;
44
62
 
45
63
  /**
46
64
  * Method handlers registry
47
65
  */
48
66
  private static readonly methods: Record<string, MethodHandler> = {
49
67
  getVersion: ServeCommand.handleGetVersion,
50
- transpile: ServeCommand.handleTranspile,
51
- parseSymbols: ServeCommand.handleParseSymbols,
68
+ initialize: ServeCommand.handleInitialize,
69
+ transpile: ServeCommand._withSourceValidation(
70
+ ServeCommand._handleTranspile,
71
+ ),
72
+ parseSymbols: ServeCommand._withSourceValidation(
73
+ ServeCommand._handleParseSymbols,
74
+ ),
75
+ parseCHeader: ServeCommand._withSourceValidation(
76
+ ServeCommand._handleParseCHeader,
77
+ ),
52
78
  shutdown: ServeCommand.handleShutdown,
53
79
  };
54
80
 
@@ -92,6 +118,34 @@ class ServeCommand {
92
118
  }
93
119
  }
94
120
 
121
+ // ========================================================================
122
+ // Parameter Validation Helpers (Issue #707: Reduce code duplication)
123
+ // ========================================================================
124
+
125
+ /**
126
+ * Wrapper that validates source params before calling handler.
127
+ * Eliminates duplicate validation code across handlers.
128
+ */
129
+ private static _withSourceValidation(
130
+ handler: (params: ISourceParams) => Promise<IMethodResult>,
131
+ ): MethodHandler {
132
+ return async (params?: Record<string, unknown>): Promise<IMethodResult> => {
133
+ if (!params || typeof params.source !== "string") {
134
+ return {
135
+ success: false,
136
+ errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
137
+ errorMessage: "Missing required param: source",
138
+ };
139
+ }
140
+ const validated: ISourceParams = {
141
+ source: String(params.source),
142
+ filePath:
143
+ typeof params.filePath === "string" ? params.filePath : undefined,
144
+ };
145
+ return handler(validated);
146
+ };
147
+ }
148
+
95
149
  /**
96
150
  * Handle a single line of input
97
151
  */
@@ -115,20 +169,23 @@ class ServeCommand {
115
169
  const request = parseResult.request!;
116
170
  this.log(`method: ${request.method}`);
117
171
 
118
- // Dispatch to method handler
119
- const response = this.dispatch(request);
120
- this.writeResponse(response);
172
+ // Dispatch to method handler (async)
173
+ this.dispatch(request).then((response) => {
174
+ this.writeResponse(response);
121
175
 
122
- // Handle shutdown after response is written
123
- if (this.shouldShutdown) {
124
- this.readline?.close();
125
- }
176
+ // Handle shutdown after response is written
177
+ if (this.shouldShutdown) {
178
+ this.readline?.close();
179
+ }
180
+ });
126
181
  }
127
182
 
128
183
  /**
129
184
  * Dispatch a request to the appropriate handler
130
185
  */
131
- private static dispatch(request: IJsonRpcRequest): IJsonRpcResponse {
186
+ private static async dispatch(
187
+ request: IJsonRpcRequest,
188
+ ): Promise<IJsonRpcResponse> {
132
189
  const handler = this.methods[request.method];
133
190
 
134
191
  if (!handler) {
@@ -139,7 +196,7 @@ class ServeCommand {
139
196
  );
140
197
  }
141
198
 
142
- const result = handler(request.params);
199
+ const result = await handler(request.params);
143
200
 
144
201
  if (result.success) {
145
202
  return JsonRpcHandler.formatResponse(request.id, result.result);
@@ -162,7 +219,7 @@ class ServeCommand {
162
219
  /**
163
220
  * Handle getVersion method
164
221
  */
165
- private static handleGetVersion(): IMethodResult {
222
+ private static async handleGetVersion(): Promise<IMethodResult> {
166
223
  return {
167
224
  success: true,
168
225
  result: { version: ConfigPrinter.getVersion() },
@@ -170,61 +227,135 @@ class ServeCommand {
170
227
  }
171
228
 
172
229
  /**
173
- * Handle transpile method
230
+ * Handle initialize method
231
+ * Loads project config and creates a Transpiler instance
174
232
  */
175
- private static handleTranspile(
233
+ private static async handleInitialize(
176
234
  params?: Record<string, unknown>,
177
- ): IMethodResult {
178
- if (!params || typeof params.source !== "string") {
235
+ ): Promise<IMethodResult> {
236
+ if (!params || typeof params.workspacePath !== "string") {
179
237
  return {
180
238
  success: false,
181
239
  errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
182
- errorMessage: "Missing required param: source",
240
+ errorMessage: "Missing required param: workspacePath",
183
241
  };
184
242
  }
185
243
 
186
- const result = transpile(params.source);
244
+ const workspacePath = params.workspacePath;
245
+ ServeCommand.log(`initializing with workspace: ${workspacePath}`);
246
+
247
+ const config = ConfigLoader.load(workspacePath);
248
+
249
+ ServeCommand.transpiler = new Transpiler({
250
+ inputs: [],
251
+ includeDirs: config.include ?? [],
252
+ cppRequired: config.cppRequired ?? false,
253
+ target: config.target ?? "",
254
+ debugMode: config.debugMode ?? false,
255
+ noCache: config.noCache ?? false,
256
+ });
257
+
258
+ ServeCommand.log(
259
+ `initialized (cppRequired=${config.cppRequired ?? false}, includeDirs=${(config.include ?? []).length})`,
260
+ );
187
261
 
188
262
  return {
189
263
  success: true,
190
- result: {
191
- success: result.success,
192
- code: result.code,
193
- errors: result.errors,
194
- },
264
+ result: { success: true },
195
265
  };
196
266
  }
197
267
 
198
268
  /**
199
- * Handle parseSymbols method
269
+ * Handle transpile method (called via _withSourceValidation wrapper)
270
+ * Uses full Transpiler for include resolution and C++ auto-detection
200
271
  */
201
- private static handleParseSymbols(
202
- params?: Record<string, unknown>,
203
- ): IMethodResult {
204
- if (!params || typeof params.source !== "string") {
272
+ private static async _handleTranspile(
273
+ params: ISourceParams,
274
+ ): Promise<IMethodResult> {
275
+ if (!ServeCommand.transpiler) {
205
276
  return {
206
277
  success: false,
207
278
  errorCode: JsonRpcHandler.ERROR_INVALID_PARAMS,
208
- errorMessage: "Missing required param: source",
279
+ errorMessage: "Server not initialized. Call initialize first.",
209
280
  };
210
281
  }
211
282
 
212
- const result = parseWithSymbols(params.source);
283
+ const { source, filePath } = params;
284
+
285
+ const options = filePath
286
+ ? { workingDir: dirname(filePath), sourcePath: filePath }
287
+ : undefined;
288
+
289
+ const result = await ServeCommand.transpiler.transpileSource(
290
+ source,
291
+ options,
292
+ );
213
293
 
214
294
  return {
215
295
  success: true,
216
296
  result: {
217
297
  success: result.success,
298
+ code: result.code,
218
299
  errors: result.errors,
219
- symbols: result.symbols,
300
+ cppDetected: ServeCommand.transpiler.isCppDetected(),
220
301
  },
221
302
  };
222
303
  }
223
304
 
305
+ /**
306
+ * Handle parseSymbols method (called via _withSourceValidation wrapper)
307
+ * Runs full transpilation for include/C++ detection, then extracts symbols
308
+ * from the parse tree (preserving "extract symbols even with parse errors" behavior)
309
+ */
310
+ private static async _handleParseSymbols(
311
+ params: ISourceParams,
312
+ ): Promise<IMethodResult> {
313
+ const { source, filePath } = params;
314
+
315
+ // If transpiler is initialized, run transpileSource to trigger header
316
+ // resolution and C++ detection (results are discarded, we just want
317
+ // the side effects on the symbol table)
318
+ if (ServeCommand.transpiler && filePath) {
319
+ try {
320
+ await ServeCommand.transpiler.transpileSource(source, {
321
+ workingDir: dirname(filePath),
322
+ sourcePath: filePath,
323
+ });
324
+ } catch {
325
+ // Ignore transpilation errors - we still extract symbols below
326
+ }
327
+ }
328
+
329
+ // Delegate symbol extraction to parseWithSymbols (shared with WorkspaceIndex)
330
+ const result = parseWithSymbols(source);
331
+
332
+ return {
333
+ success: true,
334
+ result,
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Handle parseCHeader method (called via _withSourceValidation wrapper)
340
+ * Parses C/C++ header files and extracts symbols
341
+ */
342
+ private static async _handleParseCHeader(
343
+ params: ISourceParams,
344
+ ): Promise<IMethodResult> {
345
+ const { source, filePath } = params;
346
+
347
+ const result = parseCHeader(source, filePath);
348
+
349
+ return {
350
+ success: true,
351
+ result,
352
+ };
353
+ }
354
+
224
355
  /**
225
356
  * Handle shutdown method
226
357
  */
227
- private static handleShutdown(): IMethodResult {
358
+ private static async handleShutdown(): Promise<IMethodResult> {
228
359
  ServeCommand.shouldShutdown = true;
229
360
  return {
230
361
  success: true,
@@ -25,7 +25,7 @@ describe("ServeCommand", () => {
25
25
  stdoutWriteSpy.mockRestore();
26
26
  });
27
27
 
28
- // Helper to send a request and capture the response
28
+ // Helper to send a request and capture the async response
29
29
  async function sendRequest(request: object): Promise<object> {
30
30
  const line = JSON.stringify(request);
31
31
  // Access private handleLine method for testing
@@ -34,8 +34,14 @@ describe("ServeCommand", () => {
34
34
  ).handleLine.bind(ServeCommand);
35
35
  handleLine(line);
36
36
 
37
- // Parse the response from stdout.write call
38
- const writeCall = stdoutWriteSpy.mock.calls[0];
37
+ // Wait for async dispatch to complete and write response
38
+ await vi.waitFor(() => {
39
+ expect(stdoutWriteSpy.mock.calls.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ // Parse the response from the latest stdout.write call
43
+ const writeCall =
44
+ stdoutWriteSpy.mock.calls[stdoutWriteSpy.mock.calls.length - 1];
39
45
  if (writeCall) {
40
46
  const responseStr = writeCall[0] as string;
41
47
  return JSON.parse(responseStr.trim());
@@ -54,8 +60,67 @@ describe("ServeCommand", () => {
54
60
  });
55
61
  });
56
62
 
63
+ describe("initialize", () => {
64
+ it("initializes with workspace path", async () => {
65
+ const response = await sendRequest({
66
+ id: 10,
67
+ method: "initialize",
68
+ params: { workspacePath: "/tmp" },
69
+ });
70
+
71
+ expect(response).toMatchObject({
72
+ id: 10,
73
+ result: { success: true },
74
+ });
75
+ });
76
+
77
+ it("returns error for missing workspacePath param", async () => {
78
+ const response = await sendRequest({
79
+ id: 11,
80
+ method: "initialize",
81
+ params: {},
82
+ });
83
+
84
+ expect(response).toMatchObject({
85
+ id: 11,
86
+ error: {
87
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
88
+ message: "Missing required param: workspacePath",
89
+ },
90
+ });
91
+ });
92
+ });
93
+
57
94
  describe("transpile", () => {
95
+ it("returns error when server not initialized", async () => {
96
+ // Reset transpiler to null by accessing private field
97
+ (ServeCommand as unknown as { transpiler: null }).transpiler = null;
98
+ stdoutWriteSpy.mockClear();
99
+
100
+ const response = await sendRequest({
101
+ id: 50,
102
+ method: "transpile",
103
+ params: { source: "u8 x <- 5;" },
104
+ });
105
+
106
+ expect(response).toMatchObject({
107
+ id: 50,
108
+ error: {
109
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
110
+ message: "Server not initialized. Call initialize first.",
111
+ },
112
+ });
113
+ });
114
+
58
115
  it("transpiles valid C-Next source", async () => {
116
+ // Initialize first (required for transpile)
117
+ await sendRequest({
118
+ id: 100,
119
+ method: "initialize",
120
+ params: { workspacePath: "/tmp" },
121
+ });
122
+ stdoutWriteSpy.mockClear();
123
+
59
124
  const response = await sendRequest({
60
125
  id: 2,
61
126
  method: "transpile",
@@ -73,6 +138,14 @@ describe("ServeCommand", () => {
73
138
  });
74
139
 
75
140
  it("returns errors for invalid source", async () => {
141
+ // Initialize first
142
+ await sendRequest({
143
+ id: 101,
144
+ method: "initialize",
145
+ params: { workspacePath: "/tmp" },
146
+ });
147
+ stdoutWriteSpy.mockClear();
148
+
76
149
  const response = await sendRequest({
77
150
  id: 3,
78
151
  method: "transpile",
@@ -139,6 +212,35 @@ describe("ServeCommand", () => {
139
212
  );
140
213
  });
141
214
 
215
+ it("parses symbols with filePath after initialization", async () => {
216
+ // Initialize first
217
+ await sendRequest({
218
+ id: 60,
219
+ method: "initialize",
220
+ params: { workspacePath: "/tmp" },
221
+ });
222
+ stdoutWriteSpy.mockClear();
223
+
224
+ const response = await sendRequest({
225
+ id: 61,
226
+ method: "parseSymbols",
227
+ params: {
228
+ source: "void myFunc() { }",
229
+ filePath: "/tmp/test.cnx",
230
+ },
231
+ });
232
+
233
+ const result = response as {
234
+ id: number;
235
+ result: { success: boolean; symbols: Array<{ name: string }> };
236
+ };
237
+ expect(result.id).toBe(61);
238
+ expect(result.result.success).toBe(true);
239
+ expect(result.result.symbols).toEqual(
240
+ expect.arrayContaining([expect.objectContaining({ name: "myFunc" })]),
241
+ );
242
+ });
243
+
142
244
  it("returns error for missing source param", async () => {
143
245
  const response = await sendRequest({
144
246
  id: 7,
@@ -154,6 +256,95 @@ describe("ServeCommand", () => {
154
256
  },
155
257
  });
156
258
  });
259
+
260
+ it("returns error for missing params", async () => {
261
+ const response = await sendRequest({
262
+ id: 70,
263
+ method: "parseSymbols",
264
+ });
265
+
266
+ expect(response).toMatchObject({
267
+ id: 70,
268
+ error: {
269
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
270
+ message: "Missing required param: source",
271
+ },
272
+ });
273
+ });
274
+ });
275
+
276
+ describe("parseCHeader", () => {
277
+ it("parses symbols from C header source", async () => {
278
+ const response = await sendRequest({
279
+ id: 71,
280
+ method: "parseCHeader",
281
+ params: { source: "int myFunction(void);" },
282
+ });
283
+
284
+ const result = response as {
285
+ id: number;
286
+ result: { success: boolean; symbols: Array<{ name: string }> };
287
+ };
288
+ expect(result.id).toBe(71);
289
+ expect(result.result.success).toBe(true);
290
+ expect(result.result.symbols).toEqual(
291
+ expect.arrayContaining([
292
+ expect.objectContaining({ name: "myFunction" }),
293
+ ]),
294
+ );
295
+ });
296
+
297
+ it("parses struct definitions", async () => {
298
+ const response = await sendRequest({
299
+ id: 72,
300
+ method: "parseCHeader",
301
+ params: {
302
+ source: "typedef struct { int x; int y; } Point;",
303
+ filePath: "/tmp/types.h",
304
+ },
305
+ });
306
+
307
+ const result = response as {
308
+ id: number;
309
+ result: { success: boolean; symbols: Array<{ name: string }> };
310
+ };
311
+ expect(result.id).toBe(72);
312
+ expect(result.result.success).toBe(true);
313
+ expect(result.result.symbols).toEqual(
314
+ expect.arrayContaining([expect.objectContaining({ name: "Point" })]),
315
+ );
316
+ });
317
+
318
+ it("returns error for missing source param", async () => {
319
+ const response = await sendRequest({
320
+ id: 73,
321
+ method: "parseCHeader",
322
+ params: {},
323
+ });
324
+
325
+ expect(response).toMatchObject({
326
+ id: 73,
327
+ error: {
328
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
329
+ message: "Missing required param: source",
330
+ },
331
+ });
332
+ });
333
+
334
+ it("returns error for missing params", async () => {
335
+ const response = await sendRequest({
336
+ id: 74,
337
+ method: "parseCHeader",
338
+ });
339
+
340
+ expect(response).toMatchObject({
341
+ id: 74,
342
+ error: {
343
+ code: JsonRpcHandler.ERROR_INVALID_PARAMS,
344
+ message: "Missing required param: source",
345
+ },
346
+ });
347
+ });
157
348
  });
158
349
 
159
350
  describe("shutdown", () => {
@@ -197,11 +388,44 @@ describe("ServeCommand", () => {
197
388
  handleLine("");
198
389
  handleLine(" ");
199
390
 
391
+ // Wait a tick to ensure any async work would have started
392
+ await new Promise((r) => setTimeout(r, 10));
393
+
200
394
  // No response should be written for empty lines
201
395
  expect(stdoutWriteSpy).not.toHaveBeenCalled();
202
396
  });
203
397
  });
204
398
 
399
+ describe("debug logging", () => {
400
+ it("writes debug messages to stderr when debug mode is enabled", async () => {
401
+ const stderrWriteSpy = vi
402
+ .spyOn(process.stderr, "write")
403
+ .mockImplementation(() => true);
404
+
405
+ // Enable debug mode via private field
406
+ (ServeCommand as unknown as { debugMode: boolean }).debugMode = true;
407
+
408
+ const response = await sendRequest({
409
+ id: 80,
410
+ method: "getVersion",
411
+ });
412
+
413
+ expect(response).toMatchObject({
414
+ id: 80,
415
+ result: { version: expect.any(String) },
416
+ });
417
+
418
+ // Debug logs should have been written to stderr
419
+ expect(stderrWriteSpy).toHaveBeenCalled();
420
+ const debugOutput = stderrWriteSpy.mock.calls.map((c) => c[0]).join("");
421
+ expect(debugOutput).toContain("[serve]");
422
+
423
+ // Clean up
424
+ (ServeCommand as unknown as { debugMode: boolean }).debugMode = false;
425
+ stderrWriteSpy.mockRestore();
426
+ });
427
+ });
428
+
205
429
  describe("invalid JSON", () => {
206
430
  it("returns parse error", async () => {
207
431
  const handleLine = (