@tsonic/emitter 0.0.1

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 (209) hide show
  1. package/package.json +34 -0
  2. package/scripts/update-golden-tests.ts +119 -0
  3. package/src/adapter-generator.ts +112 -0
  4. package/src/array.test.ts +301 -0
  5. package/src/constants.ts +32 -0
  6. package/src/core/exports.ts +36 -0
  7. package/src/core/imports.test.ts +83 -0
  8. package/src/core/imports.ts +243 -0
  9. package/src/core/index.ts +9 -0
  10. package/src/core/module-emitter/assembly.ts +83 -0
  11. package/src/core/module-emitter/header.ts +19 -0
  12. package/src/core/module-emitter/index.ts +17 -0
  13. package/src/core/module-emitter/namespace.ts +39 -0
  14. package/src/core/module-emitter/orchestrator.ts +98 -0
  15. package/src/core/module-emitter/separation.ts +41 -0
  16. package/src/core/module-emitter/static-container.ts +75 -0
  17. package/src/core/module-emitter.test.ts +154 -0
  18. package/src/core/module-emitter.ts +6 -0
  19. package/src/core/module-map.ts +218 -0
  20. package/src/core/options.ts +16 -0
  21. package/src/core/type-params.ts +34 -0
  22. package/src/emitter-types/context.ts +91 -0
  23. package/src/emitter-types/core.ts +138 -0
  24. package/src/emitter-types/csharp-types.ts +42 -0
  25. package/src/emitter-types/formatting.ts +13 -0
  26. package/src/emitter-types/fqn.ts +81 -0
  27. package/src/emitter-types/index.ts +31 -0
  28. package/src/emitter.ts +107 -0
  29. package/src/expression-emitter.ts +112 -0
  30. package/src/expressions/access.ts +104 -0
  31. package/src/expressions/calls.ts +264 -0
  32. package/src/expressions/collections.ts +354 -0
  33. package/src/expressions/functions.ts +71 -0
  34. package/src/expressions/identifiers.ts +125 -0
  35. package/src/expressions/index.test.ts +515 -0
  36. package/src/expressions/index.ts +38 -0
  37. package/src/expressions/literals.ts +138 -0
  38. package/src/expressions/operators.ts +211 -0
  39. package/src/expressions/other.ts +63 -0
  40. package/src/generator-exchange.ts +120 -0
  41. package/src/generator.test.ts +193 -0
  42. package/src/golden-tests/config-parser.ts +67 -0
  43. package/src/golden-tests/discovery.ts +61 -0
  44. package/src/golden-tests/index.ts +10 -0
  45. package/src/golden-tests/registration.ts +26 -0
  46. package/src/golden-tests/runner.ts +131 -0
  47. package/src/golden-tests/tree-builder.ts +43 -0
  48. package/src/golden-tests/types.ts +21 -0
  49. package/src/golden.test.ts +40 -0
  50. package/src/hierarchical-bindings.test.ts +258 -0
  51. package/src/index.ts +14 -0
  52. package/src/integration.test.ts +303 -0
  53. package/src/specialization/call-site-rewriting.test.ts +99 -0
  54. package/src/specialization/collection/expressions.ts +184 -0
  55. package/src/specialization/collection/index.ts +7 -0
  56. package/src/specialization/collection/orchestrator.ts +25 -0
  57. package/src/specialization/collection/statements.ts +91 -0
  58. package/src/specialization/collection.ts +10 -0
  59. package/src/specialization/generation.ts +189 -0
  60. package/src/specialization/generic-classes.test.ts +59 -0
  61. package/src/specialization/generic-functions.test.ts +292 -0
  62. package/src/specialization/helpers.ts +39 -0
  63. package/src/specialization/index.ts +28 -0
  64. package/src/specialization/interfaces.test.ts +151 -0
  65. package/src/specialization/naming.ts +34 -0
  66. package/src/specialization/substitution.ts +186 -0
  67. package/src/specialization/type-aliases.test.ts +134 -0
  68. package/src/specialization/types.ts +19 -0
  69. package/src/specialization-generator.ts +23 -0
  70. package/src/statement-emitter.ts +117 -0
  71. package/src/statements/blocks.ts +115 -0
  72. package/src/statements/classes/helpers.ts +9 -0
  73. package/src/statements/classes/index.ts +13 -0
  74. package/src/statements/classes/inline-types.ts +79 -0
  75. package/src/statements/classes/members/constructors.ts +123 -0
  76. package/src/statements/classes/members/index.ts +8 -0
  77. package/src/statements/classes/members/methods.ts +137 -0
  78. package/src/statements/classes/members/orchestrator.ts +33 -0
  79. package/src/statements/classes/members/properties.ts +62 -0
  80. package/src/statements/classes/members.ts +6 -0
  81. package/src/statements/classes/parameters.ts +69 -0
  82. package/src/statements/classes/properties.ts +95 -0
  83. package/src/statements/classes.ts +14 -0
  84. package/src/statements/control/conditionals.ts +134 -0
  85. package/src/statements/control/exceptions.ts +59 -0
  86. package/src/statements/control/index.ts +11 -0
  87. package/src/statements/control/loops.ts +250 -0
  88. package/src/statements/control.ts +14 -0
  89. package/src/statements/declarations/classes.ts +89 -0
  90. package/src/statements/declarations/enums.ts +32 -0
  91. package/src/statements/declarations/functions.ts +147 -0
  92. package/src/statements/declarations/index.ts +10 -0
  93. package/src/statements/declarations/interfaces.ts +116 -0
  94. package/src/statements/declarations/structs.test.ts +182 -0
  95. package/src/statements/declarations/type-aliases.ts +104 -0
  96. package/src/statements/declarations/variables.ts +159 -0
  97. package/src/statements/declarations.ts +13 -0
  98. package/src/statements/index.test.ts +258 -0
  99. package/src/statements/index.ts +43 -0
  100. package/src/type-assertion.test.ts +143 -0
  101. package/src/type-emitter.ts +18 -0
  102. package/src/types/arrays.ts +21 -0
  103. package/src/types/dictionaries.ts +52 -0
  104. package/src/types/emitter.ts +76 -0
  105. package/src/types/functions.ts +45 -0
  106. package/src/types/index.test.ts +116 -0
  107. package/src/types/index.ts +14 -0
  108. package/src/types/intersections.ts +19 -0
  109. package/src/types/literals.ts +26 -0
  110. package/src/types/objects.ts +27 -0
  111. package/src/types/parameters.test.ts +146 -0
  112. package/src/types/parameters.ts +95 -0
  113. package/src/types/primitives.ts +24 -0
  114. package/src/types/references.ts +187 -0
  115. package/src/types/unions.test.ts +397 -0
  116. package/src/types/unions.ts +62 -0
  117. package/src/types.ts +33 -0
  118. package/testcases/README.md +213 -0
  119. package/testcases/arrays/basic/ArrayLiteral.ts +4 -0
  120. package/testcases/arrays/basic/config.yaml +1 -0
  121. package/testcases/arrays/destructuring/ArrayDestructure.ts +4 -0
  122. package/testcases/arrays/destructuring/config.yaml +1 -0
  123. package/testcases/arrays/methods/ArrayMethods.ts +6 -0
  124. package/testcases/arrays/methods/config.yaml +1 -0
  125. package/testcases/arrays/multidimensional/MultiDimensional.ts +10 -0
  126. package/testcases/arrays/multidimensional/config.yaml +1 -0
  127. package/testcases/arrays/spread/ArraySpread.ts +3 -0
  128. package/testcases/arrays/spread/config.yaml +1 -0
  129. package/testcases/async/basic/AsyncFunction.ts +5 -0
  130. package/testcases/async/basic/config.yaml +1 -0
  131. package/testcases/classes/abstract/AbstractClasses.ts +53 -0
  132. package/testcases/classes/abstract/config.yaml +1 -0
  133. package/testcases/classes/basic/Person.ts +12 -0
  134. package/testcases/classes/basic/config.yaml +1 -0
  135. package/testcases/classes/constructor/User.ts +11 -0
  136. package/testcases/classes/constructor/config.yaml +1 -0
  137. package/testcases/classes/field-inference/Counter.ts +11 -0
  138. package/testcases/classes/field-inference/config.yaml +1 -0
  139. package/testcases/classes/inheritance/Inheritance.ts +24 -0
  140. package/testcases/classes/inheritance/config.yaml +1 -0
  141. package/testcases/classes/static-members/MathHelper.ts +12 -0
  142. package/testcases/classes/static-members/config.yaml +1 -0
  143. package/testcases/control-flow/error-handling/ErrorHandling.ts +13 -0
  144. package/testcases/control-flow/error-handling/config.yaml +1 -0
  145. package/testcases/control-flow/loops/Loops.ts +21 -0
  146. package/testcases/control-flow/loops/config.yaml +1 -0
  147. package/testcases/control-flow/switch/SwitchStatement.ts +15 -0
  148. package/testcases/control-flow/switch/config.yaml +1 -0
  149. package/testcases/edge-cases/complex-expressions/ComplexExpressions.ts +10 -0
  150. package/testcases/edge-cases/complex-expressions/config.yaml +1 -0
  151. package/testcases/edge-cases/nested-scopes/NestedScopes.ts +10 -0
  152. package/testcases/edge-cases/nested-scopes/config.yaml +1 -0
  153. package/testcases/edge-cases/shadowing/Shadowing.ts +16 -0
  154. package/testcases/edge-cases/shadowing/config.yaml +1 -0
  155. package/testcases/functions/arrow/ArrowFunction.ts +5 -0
  156. package/testcases/functions/arrow/config.yaml +1 -0
  157. package/testcases/functions/basic/Greet.ts +3 -0
  158. package/testcases/functions/basic/config.yaml +1 -0
  159. package/testcases/functions/closures/Closures.ts +11 -0
  160. package/testcases/functions/closures/config.yaml +1 -0
  161. package/testcases/functions/default-params/DefaultParams.ts +7 -0
  162. package/testcases/functions/default-params/config.yaml +1 -0
  163. package/testcases/functions/rest-params/RestParams.ts +7 -0
  164. package/testcases/functions/rest-params/config.yaml +1 -0
  165. package/testcases/functions/type-guards/TypeGuards.ts +52 -0
  166. package/testcases/functions/type-guards/config.yaml +1 -0
  167. package/testcases/operators/logical/LogicalOperators.ts +11 -0
  168. package/testcases/operators/logical/config.yaml +1 -0
  169. package/testcases/operators/nullish-coalescing/NullishCoalescing.ts +7 -0
  170. package/testcases/operators/nullish-coalescing/config.yaml +1 -0
  171. package/testcases/operators/optional-chaining/OptionalChaining.ts +15 -0
  172. package/testcases/operators/optional-chaining/config.yaml +1 -0
  173. package/testcases/real-world/advanced-generics/advanced-generics.ts +116 -0
  174. package/testcases/real-world/advanced-generics/config.yaml +1 -0
  175. package/testcases/real-world/async-ops/async-ops.ts +67 -0
  176. package/testcases/real-world/async-ops/config.yaml +1 -0
  177. package/testcases/real-world/business-logic/business-logic.ts +215 -0
  178. package/testcases/real-world/business-logic/config.yaml +1 -0
  179. package/testcases/real-world/calculator/calculator.ts +29 -0
  180. package/testcases/real-world/calculator/config.yaml +1 -0
  181. package/testcases/real-world/data-structures/config.yaml +1 -0
  182. package/testcases/real-world/data-structures/data-structures.ts +133 -0
  183. package/testcases/real-world/functional/config.yaml +1 -0
  184. package/testcases/real-world/functional/functional.ts +116 -0
  185. package/testcases/real-world/shapes/config.yaml +1 -0
  186. package/testcases/real-world/shapes/shapes.ts +87 -0
  187. package/testcases/real-world/string-utils/config.yaml +1 -0
  188. package/testcases/real-world/string-utils/string-utils.ts +47 -0
  189. package/testcases/real-world/todo-list/config.yaml +1 -0
  190. package/testcases/real-world/todo-list/todo-list.ts +52 -0
  191. package/testcases/real-world/type-guards/config.yaml +1 -0
  192. package/testcases/real-world/type-guards/type-guards.ts +71 -0
  193. package/testcases/structs/basic/Point.ts +9 -0
  194. package/testcases/structs/basic/config.yaml +1 -0
  195. package/testcases/types/conditional/ConditionalTypes.ts +35 -0
  196. package/testcases/types/conditional/config.yaml +1 -0
  197. package/testcases/types/constants/ModuleConstants.ts +6 -0
  198. package/testcases/types/constants/config.yaml +1 -0
  199. package/testcases/types/generics/Generics.ts +15 -0
  200. package/testcases/types/generics/config.yaml +1 -0
  201. package/testcases/types/interfaces/Interfaces.ts +14 -0
  202. package/testcases/types/interfaces/config.yaml +1 -0
  203. package/testcases/types/mapped/MappedTypes.ts +27 -0
  204. package/testcases/types/mapped/config.yaml +1 -0
  205. package/testcases/types/tuples-intersections/TuplesAndIntersections.ts +46 -0
  206. package/testcases/types/tuples-intersections/config.yaml +1 -0
  207. package/testcases/types/unions/UnionTypes.ts +11 -0
  208. package/testcases/types/unions/config.yaml +1 -0
  209. package/tsconfig.json +14 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Tests for generator emission
3
+ * Per spec/13-generators.md
4
+ */
5
+
6
+ import { describe, it } from "mocha";
7
+ import { expect } from "chai";
8
+ import { emitModule } from "./emitter.js";
9
+ import { IrModule } from "@tsonic/frontend";
10
+
11
+ describe("Generator Emission", () => {
12
+ it("should generate exchange class for simple generator", () => {
13
+ const module: IrModule = {
14
+ kind: "module",
15
+ filePath: "/test/counter.ts",
16
+ namespace: "Test",
17
+ className: "counter",
18
+ isStaticContainer: true,
19
+ imports: [],
20
+ exports: [],
21
+ body: [
22
+ {
23
+ kind: "functionDeclaration",
24
+ name: "counter",
25
+ parameters: [],
26
+ returnType: {
27
+ kind: "referenceType",
28
+ name: "Generator",
29
+ typeArguments: [
30
+ { kind: "primitiveType", name: "number" },
31
+ { kind: "primitiveType", name: "undefined" },
32
+ { kind: "primitiveType", name: "undefined" },
33
+ ],
34
+ },
35
+ body: {
36
+ kind: "blockStatement",
37
+ statements: [
38
+ {
39
+ kind: "variableDeclaration",
40
+ declarationKind: "let",
41
+ isExported: false,
42
+ declarations: [
43
+ {
44
+ kind: "variableDeclarator",
45
+ name: { kind: "identifierPattern", name: "i" },
46
+ type: { kind: "primitiveType", name: "number" },
47
+ initializer: { kind: "literal", value: 0 },
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ kind: "whileStatement",
53
+ condition: { kind: "literal", value: true },
54
+ body: {
55
+ kind: "blockStatement",
56
+ statements: [
57
+ {
58
+ kind: "expressionStatement",
59
+ expression: {
60
+ kind: "yield",
61
+ expression: { kind: "identifier", name: "i" },
62
+ delegate: false,
63
+ },
64
+ },
65
+ {
66
+ kind: "expressionStatement",
67
+ expression: {
68
+ kind: "assignment",
69
+ operator: "+=",
70
+ left: { kind: "identifier", name: "i" },
71
+ right: { kind: "literal", value: 1 },
72
+ },
73
+ },
74
+ ],
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ isAsync: false,
80
+ isGenerator: true,
81
+ isExported: true,
82
+ },
83
+ ],
84
+ };
85
+
86
+ const code = emitModule(module);
87
+
88
+ // Should contain exchange class
89
+ expect(code).to.include("public sealed class counter_exchange");
90
+ expect(code).to.include("public object? Input { get; set; }");
91
+ expect(code).to.include("public double Output { get; set; }");
92
+
93
+ // Should have IEnumerable return type
94
+ expect(code).to.include(
95
+ "public static IEnumerable<counter_exchange> counter()"
96
+ );
97
+
98
+ // Should use System.Collections.Generic
99
+ expect(code).to.include("using System.Collections.Generic");
100
+
101
+ // Should initialize exchange variable
102
+ expect(code).to.include("var exchange = new counter_exchange()");
103
+
104
+ // Should emit yield with exchange object pattern
105
+ expect(code).to.include("exchange.Output = i");
106
+ expect(code).to.include("yield return exchange");
107
+ });
108
+
109
+ it("should handle async generator", () => {
110
+ const module: IrModule = {
111
+ kind: "module",
112
+ filePath: "/test/asyncCounter.ts",
113
+ namespace: "Test",
114
+ className: "asyncCounter",
115
+ isStaticContainer: true,
116
+ imports: [],
117
+ exports: [],
118
+ body: [
119
+ {
120
+ kind: "functionDeclaration",
121
+ name: "asyncCounter",
122
+ parameters: [],
123
+ returnType: {
124
+ kind: "referenceType",
125
+ name: "AsyncGenerator",
126
+ typeArguments: [
127
+ { kind: "primitiveType", name: "number" },
128
+ { kind: "primitiveType", name: "undefined" },
129
+ { kind: "primitiveType", name: "undefined" },
130
+ ],
131
+ },
132
+ body: {
133
+ kind: "blockStatement",
134
+ statements: [
135
+ {
136
+ kind: "variableDeclaration",
137
+ declarationKind: "let",
138
+ isExported: false,
139
+ declarations: [
140
+ {
141
+ kind: "variableDeclarator",
142
+ name: { kind: "identifierPattern", name: "i" },
143
+ type: { kind: "primitiveType", name: "number" },
144
+ initializer: { kind: "literal", value: 0 },
145
+ },
146
+ ],
147
+ },
148
+ {
149
+ kind: "whileStatement",
150
+ condition: { kind: "literal", value: true },
151
+ body: {
152
+ kind: "blockStatement",
153
+ statements: [
154
+ {
155
+ kind: "expressionStatement",
156
+ expression: {
157
+ kind: "yield",
158
+ expression: { kind: "identifier", name: "i" },
159
+ delegate: false,
160
+ },
161
+ },
162
+ {
163
+ kind: "expressionStatement",
164
+ expression: {
165
+ kind: "assignment",
166
+ operator: "+=",
167
+ left: { kind: "identifier", name: "i" },
168
+ right: { kind: "literal", value: 1 },
169
+ },
170
+ },
171
+ ],
172
+ },
173
+ },
174
+ ],
175
+ },
176
+ isAsync: true,
177
+ isGenerator: true,
178
+ isExported: true,
179
+ },
180
+ ],
181
+ };
182
+
183
+ const code = emitModule(module);
184
+
185
+ // Should contain exchange class
186
+ expect(code).to.include("public sealed class asyncCounter_exchange");
187
+
188
+ // Should have IAsyncEnumerable return type with async
189
+ expect(code).to.include(
190
+ "public static async IAsyncEnumerable<asyncCounter_exchange> asyncCounter()"
191
+ );
192
+ });
193
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Config.yaml parser for golden tests
3
+ */
4
+
5
+ import YAML from "yaml";
6
+ import { TestEntry } from "./types.js";
7
+
8
+ /**
9
+ * Parse config.yaml and extract test entries
10
+ */
11
+ export const parseConfigYaml = (yamlContent: string): readonly TestEntry[] => {
12
+ const parsed = YAML.parse(yamlContent);
13
+
14
+ if (!Array.isArray(parsed)) {
15
+ throw new Error("config.yaml must be an array of test entries");
16
+ }
17
+
18
+ const entries: TestEntry[] = [];
19
+
20
+ for (const item of parsed) {
21
+ if (typeof item === "object" && item !== null) {
22
+ // Check if it's the simple YAML format: { "File.ts": "title" }
23
+ const keys = Object.keys(item);
24
+
25
+ if (keys.length === 1 && keys[0] && keys[0].endsWith(".ts")) {
26
+ // Simple format parsed as object
27
+ const input = keys[0];
28
+ const title = item[input];
29
+
30
+ if (typeof title !== "string") {
31
+ throw new Error(`Title must be a string for ${input}`);
32
+ }
33
+
34
+ entries.push({ input, title });
35
+ } else if (item.input && item.title) {
36
+ // Explicit format: { input: "File.ts", title: "..." }
37
+ const input = item.input;
38
+ const title = item.title;
39
+
40
+ if (typeof input !== "string" || typeof title !== "string") {
41
+ throw new Error(
42
+ "Each test entry must have 'input' and 'title' fields"
43
+ );
44
+ }
45
+
46
+ entries.push({ input, title });
47
+ } else {
48
+ throw new Error(`Invalid test entry: ${JSON.stringify(item)}`);
49
+ }
50
+ } else if (typeof item === "string") {
51
+ // Quoted string format: "File.ts: title here"
52
+ const match = item.match(/^(\S+\.ts):\s*(.+)$/);
53
+ if (!match || !match[1] || !match[2]) {
54
+ throw new Error(`Invalid test entry format: ${item}`);
55
+ }
56
+
57
+ entries.push({
58
+ input: match[1],
59
+ title: match[2].trim(),
60
+ });
61
+ } else {
62
+ throw new Error(`Invalid test entry: ${JSON.stringify(item)}`);
63
+ }
64
+ }
65
+
66
+ return entries;
67
+ };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Test scenario discovery
3
+ */
4
+
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { Scenario } from "./types.js";
8
+ import { parseConfigYaml } from "./config-parser.js";
9
+
10
+ /**
11
+ * Discover all test scenarios by walking the testcases directory (synchronous)
12
+ */
13
+ export const discoverScenarios = (baseDir: string): readonly Scenario[] => {
14
+ const scenarios: Scenario[] = [];
15
+
16
+ const walk = (dir: string, pathParts: string[]): void => {
17
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
18
+
19
+ // Check if this directory contains config.yaml
20
+ const hasConfig = entries.some((e) => e.name === "config.yaml");
21
+
22
+ if (hasConfig) {
23
+ // This is a test directory - read config
24
+ const configPath = path.join(dir, "config.yaml");
25
+ const configContent = fs.readFileSync(configPath, "utf-8");
26
+ const testEntries = parseConfigYaml(configContent);
27
+
28
+ // Create scenarios for each test entry
29
+ for (const entry of testEntries) {
30
+ const inputPath = path.join(dir, entry.input);
31
+ const baseName = path.basename(entry.input, ".ts");
32
+ const expectedPath = path.join(dir, `${baseName}.cs`);
33
+
34
+ // Verify files exist
35
+ if (!fs.existsSync(inputPath)) {
36
+ throw new Error(`Input file not found: ${inputPath}`);
37
+ }
38
+ if (!fs.existsSync(expectedPath)) {
39
+ throw new Error(`Expected file not found: ${expectedPath}`);
40
+ }
41
+
42
+ scenarios.push({
43
+ pathParts,
44
+ title: entry.title,
45
+ inputPath,
46
+ expectedPath,
47
+ });
48
+ }
49
+ }
50
+
51
+ // Recurse into subdirectories
52
+ for (const entry of entries) {
53
+ if (entry.isDirectory()) {
54
+ walk(path.join(dir, entry.name), [...pathParts, entry.name]);
55
+ }
56
+ }
57
+ };
58
+
59
+ walk(baseDir, []);
60
+ return scenarios;
61
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Golden test harness - Public API
3
+ */
4
+
5
+ export type { TestEntry, Scenario, DescribeNode } from "./types.js";
6
+ export { parseConfigYaml } from "./config-parser.js";
7
+ export { discoverScenarios } from "./discovery.js";
8
+ export { buildDescribeTree } from "./tree-builder.js";
9
+ export { normalizeCs, runScenario } from "./runner.js";
10
+ export { registerNode } from "./registration.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Test registration for Mocha
3
+ */
4
+
5
+ import { describe, it } from "mocha";
6
+ import { DescribeNode } from "./types.js";
7
+ import { runScenario } from "./runner.js";
8
+
9
+ /**
10
+ * Register describe blocks recursively
11
+ */
12
+ export const registerNode = (node: DescribeNode): void => {
13
+ describe(node.name, () => {
14
+ // Register child describe blocks
15
+ for (const child of node.children.values()) {
16
+ registerNode(child);
17
+ }
18
+
19
+ // Register tests at this level
20
+ for (const scenario of node.tests) {
21
+ it(scenario.title, async () => {
22
+ await runScenario(scenario);
23
+ });
24
+ }
25
+ });
26
+ };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Test scenario runner
3
+ */
4
+
5
+ import { expect } from "chai";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { compile, buildIr } from "@tsonic/frontend";
9
+ import { emitCSharpFiles } from "../emitter.js";
10
+ import { generateFileHeader } from "../constants.js";
11
+ import { Scenario } from "./types.js";
12
+
13
+ /**
14
+ * Normalize C# output for comparison
15
+ */
16
+ export const normalizeCs = (code: string): string => {
17
+ return (
18
+ code
19
+ .trim()
20
+ // Normalize line endings
21
+ .replace(/\r\n/g, "\n")
22
+ // Remove trailing whitespace
23
+ .replace(/\s+$/gm, "")
24
+ // Normalize timestamp line (make comparison timestamp-agnostic)
25
+ .replace(/\/\/ Generated at: .+/, "// Generated at: TIMESTAMP")
26
+ // Normalize file path to just filename (strip directory path)
27
+ .replace(/\/\/ Generated from: .+\/([^/]+)$/, "// Generated from: $1")
28
+ );
29
+ };
30
+
31
+ /**
32
+ * Run a single test scenario
33
+ */
34
+ export const runScenario = async (scenario: Scenario): Promise<void> => {
35
+ // Read expected output (without header)
36
+ const expectedCsBody = fs.readFileSync(scenario.expectedPath, "utf-8");
37
+
38
+ // Determine source root (parent of input file)
39
+ const sourceRoot = path.dirname(scenario.inputPath);
40
+
41
+ // Build namespace from path parts (case-preserved, hyphens stripped per spec)
42
+ // e.g., ['types', 'interfaces'] → 'TestCases.types.interfaces'
43
+ // Note: pathParts contains directory path only (no filename), so no slicing needed
44
+ const namespaceParts = scenario.pathParts.map((part) =>
45
+ part.replace(/-/g, "")
46
+ ); // Strip hyphens
47
+ const rootNamespace = ["TestCases", ...namespaceParts].join(".");
48
+
49
+ // Step 1: Compile TypeScript → Program
50
+ // Use standard lib for golden tests (they don't have BCL bindings)
51
+ const compileResult = compile([scenario.inputPath], {
52
+ sourceRoot,
53
+ rootNamespace,
54
+ useStandardLib: true,
55
+ });
56
+
57
+ if (!compileResult.ok) {
58
+ // Show diagnostics if compilation failed
59
+ const errors = compileResult.error.diagnostics
60
+ .map((d) => `${d.code}: ${d.message}`)
61
+ .join("\n");
62
+ throw new Error(`Compilation failed:\n${errors}`);
63
+ }
64
+
65
+ // Step 2: Build IR from Program
66
+ const irResult = buildIr(compileResult.value.program, {
67
+ sourceRoot,
68
+ rootNamespace,
69
+ });
70
+
71
+ if (!irResult.ok) {
72
+ const errors = irResult.error
73
+ .map((d) => `${d.code}: ${d.message}`)
74
+ .join("\n");
75
+ throw new Error(`IR build failed:\n${errors}`);
76
+ }
77
+
78
+ // Step 3: Emit IR → C#
79
+ // Note: Don't set entryPointPath - golden tests are NOT entry points
80
+ const emitResult = emitCSharpFiles(irResult.value, {
81
+ rootNamespace,
82
+ });
83
+
84
+ if (!emitResult.ok) {
85
+ const errors = emitResult.errors
86
+ .map((d) => `${d.code}: ${d.message}`)
87
+ .join("\n");
88
+ throw new Error(`Emit failed:\n${errors}`);
89
+ }
90
+
91
+ const csharpFiles = emitResult.files;
92
+
93
+ // Find the generated file for our input
94
+ // The key should be the class name derived from the input file
95
+ const className = path.basename(scenario.inputPath, ".ts");
96
+ const generatedKey = Array.from(csharpFiles.keys()).find((key) =>
97
+ key.endsWith(`${className}.cs`)
98
+ );
99
+
100
+ if (!generatedKey) {
101
+ throw new Error(
102
+ `Could not find generated C# file for ${scenario.inputPath}. Available: ${Array.from(csharpFiles.keys()).join(", ")}`
103
+ );
104
+ }
105
+
106
+ const actualCs = csharpFiles.get(generatedKey);
107
+ if (!actualCs) {
108
+ throw new Error(
109
+ `Generated file key exists but content is missing: ${generatedKey}`
110
+ );
111
+ }
112
+
113
+ // Generate expected header using shared constant (with TIMESTAMP placeholder)
114
+ // Use just the filename for comparison (actual files may have full paths)
115
+ const fileName = path.basename(scenario.inputPath);
116
+ const expectedHeader = generateFileHeader(fileName, {
117
+ timestamp: "TIMESTAMP",
118
+ });
119
+
120
+ // Combine header with expected body
121
+ const expectedCs = expectedHeader + expectedCsBody;
122
+
123
+ // Normalize and compare
124
+ const normalizedActual = normalizeCs(actualCs);
125
+ const normalizedExpected = normalizeCs(expectedCs);
126
+
127
+ expect(normalizedActual).to.equal(
128
+ normalizedExpected,
129
+ `C# output mismatch for ${scenario.pathParts.join("/")}`
130
+ );
131
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Build describe tree structure for nested tests
3
+ */
4
+
5
+ import { Scenario, DescribeNode } from "./types.js";
6
+
7
+ /**
8
+ * Build a tree structure for nested describe blocks
9
+ */
10
+ export const buildDescribeTree = (
11
+ scenarios: readonly Scenario[]
12
+ ): DescribeNode | null => {
13
+ if (scenarios.length === 0) return null;
14
+
15
+ const root: DescribeNode = {
16
+ name: "Golden Tests",
17
+ children: new Map(),
18
+ tests: [],
19
+ };
20
+
21
+ for (const scenario of scenarios) {
22
+ let current = root;
23
+
24
+ // Navigate/create tree nodes for each path part
25
+ for (const part of scenario.pathParts) {
26
+ let node = current.children.get(part);
27
+ if (!node) {
28
+ node = {
29
+ name: part,
30
+ children: new Map(),
31
+ tests: [],
32
+ };
33
+ current.children.set(part, node);
34
+ }
35
+ current = node;
36
+ }
37
+
38
+ // Add test to the leaf node
39
+ current.tests.push(scenario);
40
+ }
41
+
42
+ return root;
43
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Golden test types
3
+ */
4
+
5
+ export type TestEntry = {
6
+ readonly input: string;
7
+ readonly title: string;
8
+ };
9
+
10
+ export type Scenario = {
11
+ readonly pathParts: readonly string[];
12
+ readonly title: string;
13
+ readonly inputPath: string;
14
+ readonly expectedPath: string;
15
+ };
16
+
17
+ export type DescribeNode = {
18
+ readonly name: string;
19
+ readonly children: Map<string, DescribeNode>;
20
+ tests: Scenario[];
21
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Golden Test Harness for Tsonic Emitter
3
+ *
4
+ * Automatically discovers and runs test cases from testcases/ directory.
5
+ * Each directory with config.yaml defines tests:
6
+ * - config.yaml: List of tests (input.ts → expected output)
7
+ * - FileName.ts: TypeScript source
8
+ * - FileName.cs: Expected C# output
9
+ */
10
+
11
+ import * as path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import {
14
+ discoverScenarios,
15
+ buildDescribeTree,
16
+ registerNode,
17
+ } from "./golden-tests/index.js";
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = path.dirname(__filename);
21
+ const TESTCASES_DIR = path.join(__dirname, "../testcases");
22
+
23
+ /**
24
+ * Main test suite setup (synchronous discovery for Mocha compatibility)
25
+ */
26
+ try {
27
+ const scenarios = discoverScenarios(TESTCASES_DIR);
28
+
29
+ if (scenarios.length === 0) {
30
+ console.warn("⚠️ No golden test cases found in testcases/");
31
+ } else {
32
+ const tree = buildDescribeTree(scenarios);
33
+ if (tree) {
34
+ registerNode(tree);
35
+ }
36
+ }
37
+ } catch (error) {
38
+ console.error("❌ Failed to setup golden tests:", error);
39
+ throw error;
40
+ }