@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,264 @@
1
+ /**
2
+ * Call and new expression emitters
3
+ */
4
+
5
+ import { IrExpression } from "@tsonic/frontend";
6
+ import { EmitterContext, CSharpFragment } from "../types.js";
7
+ import { emitExpression } from "../expression-emitter.js";
8
+ import { emitTypeArguments, generateSpecializedName } from "./identifiers.js";
9
+
10
+ /**
11
+ * Ref/out/in parameter handling:
12
+ * The frontend extracts parameter passing modes from resolved signatures
13
+ * and attaches them to IrCallExpression.argumentPassing array.
14
+ * The emitter reads this array and prefixes arguments with ref/out/in keywords.
15
+ */
16
+
17
+ /**
18
+ * Check if an expression is an lvalue (can be passed by reference)
19
+ * Only identifiers and member accesses are lvalues in C#
20
+ */
21
+ const isLValue = (expr: IrExpression): boolean => {
22
+ return expr.kind === "identifier" || expr.kind === "memberAccess";
23
+ };
24
+
25
+ /**
26
+ * Check if a call expression needs an explicit cast because the inferred type
27
+ * differs from the C# return type. This handles cases like Math.floor() which
28
+ * returns double in C# but is cast to int in TypeScript via `as int`.
29
+ */
30
+ const needsIntCast = (
31
+ expr: Extract<IrExpression, { kind: "call" }>,
32
+ calleeName: string
33
+ ): boolean => {
34
+ // Check if the inferred type is int (a reference type from @tsonic/types)
35
+ const inferredType = expr.inferredType;
36
+ if (
37
+ !inferredType ||
38
+ inferredType.kind !== "referenceType" ||
39
+ inferredType.name !== "int"
40
+ ) {
41
+ return false;
42
+ }
43
+
44
+ // Check if this is a Math method that returns double
45
+ const mathMethodsReturningDouble = [
46
+ "Math.floor",
47
+ "Math.ceil",
48
+ "Math.round",
49
+ "Math.abs",
50
+ "Math.pow",
51
+ "Math.sqrt",
52
+ "Math.min",
53
+ "Math.max",
54
+ "Tsonic.JSRuntime.Math.floor",
55
+ "Tsonic.JSRuntime.Math.ceil",
56
+ "Tsonic.JSRuntime.Math.round",
57
+ ];
58
+
59
+ return mathMethodsReturningDouble.some(
60
+ (m) => calleeName === m || calleeName.endsWith(`.${m.split(".").pop()}`)
61
+ );
62
+ };
63
+
64
+ /**
65
+ * Emit a function call expression
66
+ */
67
+ export const emitCall = (
68
+ expr: Extract<IrExpression, { kind: "call" }>,
69
+ context: EmitterContext
70
+ ): [CSharpFragment, EmitterContext] => {
71
+ // Type-aware method call rewriting
72
+ // Use inferredType to determine if we need to rewrite as static helper
73
+ if (
74
+ expr.callee.kind === "memberAccess" &&
75
+ typeof expr.callee.property === "string"
76
+ ) {
77
+ const methodName = expr.callee.property;
78
+ const objectType = expr.callee.object.inferredType;
79
+
80
+ // Rewrite based on type:
81
+ // - string → Tsonic.JSRuntime.String.method()
82
+ // - number → Tsonic.JSRuntime.Number.method()
83
+ // - Array<T> → Tsonic.JSRuntime.Array.method() (extension methods on List<T>)
84
+ // - Other custom types → Keep as instance method
85
+
86
+ const isStringType =
87
+ objectType?.kind === "primitiveType" && objectType.name === "string";
88
+ const isNumberType =
89
+ objectType?.kind === "primitiveType" && objectType.name === "number";
90
+ const isArrayType = objectType?.kind === "arrayType";
91
+
92
+ const shouldRewriteAsStatic = isStringType || isNumberType || isArrayType;
93
+
94
+ // Only rewrite to JSRuntime in "js" mode
95
+ // In "dotnet" mode, there is no JS emulation - use .NET APIs directly
96
+ const runtime = context.options.runtime ?? "js";
97
+ if (shouldRewriteAsStatic && runtime === "js") {
98
+ // Runtime mode "js": Use Tsonic.JSRuntime
99
+ // Determine which runtime class based on type
100
+ let runtimeClass: string;
101
+ if (isStringType) {
102
+ runtimeClass = "String";
103
+ } else if (isNumberType) {
104
+ runtimeClass = "Number";
105
+ } else {
106
+ runtimeClass = "Array";
107
+ }
108
+
109
+ // Rewrite: obj.method(args) → global::Tsonic.JSRuntime.{Class}.method(obj, args)
110
+ const [objectFrag, objContext] = emitExpression(
111
+ expr.callee.object,
112
+ context
113
+ );
114
+ let currentContext = objContext;
115
+
116
+ const args: string[] = [objectFrag.text]; // Object becomes first argument
117
+ for (let i = 0; i < expr.arguments.length; i++) {
118
+ const arg = expr.arguments[i];
119
+ if (!arg) continue; // Skip undefined (shouldn't happen in valid IR)
120
+ if (arg.kind === "spread") {
121
+ const [spreadFrag, ctx] = emitExpression(
122
+ arg.expression,
123
+ currentContext
124
+ );
125
+ args.push(`params ${spreadFrag.text}`);
126
+ currentContext = ctx;
127
+ } else {
128
+ const [argFrag, ctx] = emitExpression(arg, currentContext);
129
+ // Check if this argument needs ref/out/in prefix
130
+ // Note: argumentPassing[i] corresponds to method parameter i+1 (first param is the object)
131
+ // Only add prefix if argument is an lvalue (identifier or member access)
132
+ const passingMode = expr.argumentPassing?.[i + 1];
133
+ const prefix =
134
+ passingMode && passingMode !== "value" && isLValue(arg)
135
+ ? `${passingMode} `
136
+ : "";
137
+ args.push(`${prefix}${argFrag.text}`);
138
+ currentContext = ctx;
139
+ }
140
+ }
141
+
142
+ const text = `global::Tsonic.JSRuntime.${runtimeClass}.${methodName}(${args.join(", ")})`;
143
+ return [{ text }, currentContext];
144
+ }
145
+ }
146
+
147
+ // Regular function call
148
+ const [calleeFrag, newContext] = emitExpression(expr.callee, context);
149
+ let currentContext = newContext;
150
+
151
+ // Handle generic type arguments
152
+ let typeArgsStr = "";
153
+ let finalCalleeName = calleeFrag.text;
154
+
155
+ if (expr.typeArguments && expr.typeArguments.length > 0) {
156
+ if (expr.requiresSpecialization) {
157
+ // Monomorphisation: Generate specialized method name
158
+ // e.g., process<string> → process__string
159
+ const [specializedName, specContext] = generateSpecializedName(
160
+ calleeFrag.text,
161
+ expr.typeArguments,
162
+ currentContext
163
+ );
164
+ finalCalleeName = specializedName;
165
+ currentContext = specContext;
166
+ } else {
167
+ // Emit explicit type arguments for generic call
168
+ // e.g., identity<string>(value)
169
+ const [typeArgs, typeContext] = emitTypeArguments(
170
+ expr.typeArguments,
171
+ currentContext
172
+ );
173
+ typeArgsStr = typeArgs;
174
+ currentContext = typeContext;
175
+ }
176
+ }
177
+
178
+ const args: string[] = [];
179
+ for (let i = 0; i < expr.arguments.length; i++) {
180
+ const arg = expr.arguments[i];
181
+ if (!arg) continue; // Skip undefined (shouldn't happen in valid IR)
182
+ if (arg.kind === "spread") {
183
+ // Spread in function call
184
+ const [spreadFrag, ctx] = emitExpression(arg.expression, currentContext);
185
+ args.push(`params ${spreadFrag.text}`);
186
+ currentContext = ctx;
187
+ } else {
188
+ const [argFrag, ctx] = emitExpression(arg, currentContext);
189
+ // Check if this argument needs ref/out/in prefix
190
+ // Only add prefix if argument is an lvalue (identifier or member access)
191
+ const passingMode = expr.argumentPassing?.[i];
192
+ const prefix =
193
+ passingMode && passingMode !== "value" && isLValue(arg)
194
+ ? `${passingMode} `
195
+ : "";
196
+ args.push(`${prefix}${argFrag.text}`);
197
+ currentContext = ctx;
198
+ }
199
+ }
200
+
201
+ const callOp = expr.isOptional ? "?." : "";
202
+ const callText = `${finalCalleeName}${typeArgsStr}${callOp}(${args.join(", ")})`;
203
+
204
+ // Add cast if needed (e.g., Math.floor returning double but asserted as int)
205
+ const text = needsIntCast(expr, finalCalleeName)
206
+ ? `(int)${callText}`
207
+ : callText;
208
+
209
+ return [{ text }, currentContext];
210
+ };
211
+
212
+ /**
213
+ * Emit a new expression
214
+ */
215
+ export const emitNew = (
216
+ expr: Extract<IrExpression, { kind: "new" }>,
217
+ context: EmitterContext
218
+ ): [CSharpFragment, EmitterContext] => {
219
+ const [calleeFrag, newContext] = emitExpression(expr.callee, context);
220
+ let currentContext = newContext;
221
+
222
+ // Handle generic type arguments
223
+ let typeArgsStr = "";
224
+ let finalClassName = calleeFrag.text;
225
+
226
+ if (expr.typeArguments && expr.typeArguments.length > 0) {
227
+ if (expr.requiresSpecialization) {
228
+ // Monomorphisation: Generate specialized class name
229
+ // e.g., new Box<string>() → new Box__string()
230
+ const [specializedName, specContext] = generateSpecializedName(
231
+ calleeFrag.text,
232
+ expr.typeArguments,
233
+ currentContext
234
+ );
235
+ finalClassName = specializedName;
236
+ currentContext = specContext;
237
+ } else {
238
+ // Emit explicit type arguments for generic constructor
239
+ // e.g., new Box<string>(value)
240
+ const [typeArgs, typeContext] = emitTypeArguments(
241
+ expr.typeArguments,
242
+ currentContext
243
+ );
244
+ typeArgsStr = typeArgs;
245
+ currentContext = typeContext;
246
+ }
247
+ }
248
+
249
+ const args: string[] = [];
250
+ for (const arg of expr.arguments) {
251
+ if (arg.kind === "spread") {
252
+ const [spreadFrag, ctx] = emitExpression(arg.expression, currentContext);
253
+ args.push(`params ${spreadFrag.text}`);
254
+ currentContext = ctx;
255
+ } else {
256
+ const [argFrag, ctx] = emitExpression(arg, currentContext);
257
+ args.push(argFrag.text);
258
+ currentContext = ctx;
259
+ }
260
+ }
261
+
262
+ const text = `new ${finalClassName}${typeArgsStr}(${args.join(", ")})`;
263
+ return [{ text }, currentContext];
264
+ };
@@ -0,0 +1,354 @@
1
+ /**
2
+ * Collection expression emitters (arrays and objects)
3
+ */
4
+
5
+ import { IrExpression, IrType } from "@tsonic/frontend";
6
+ import { EmitterContext, CSharpFragment } from "../types.js";
7
+ import { emitType } from "../type-emitter.js";
8
+ import { emitExpression } from "../expression-emitter.js";
9
+
10
+ /**
11
+ * Escape a string for use in a C# string literal.
12
+ * Handles backslashes, quotes, newlines, carriage returns, and tabs.
13
+ */
14
+ const escapeCSharpString = (str: string): string =>
15
+ str
16
+ .replace(/\\/g, "\\\\")
17
+ .replace(/"/g, '\\"')
18
+ .replace(/\n/g, "\\n")
19
+ .replace(/\r/g, "\\r")
20
+ .replace(/\t/g, "\\t");
21
+
22
+ /**
23
+ * Emit an array literal
24
+ */
25
+ export const emitArray = (
26
+ expr: Extract<IrExpression, { kind: "array" }>,
27
+ context: EmitterContext,
28
+ expectedType?: IrType
29
+ ): [CSharpFragment, EmitterContext] => {
30
+ let currentContext = context;
31
+ const elements: string[] = [];
32
+
33
+ // Determine element type from expected type or inferred type
34
+ let elementType = "object";
35
+
36
+ // Check if all elements are literals to infer element type
37
+ const definedElements = expr.elements.filter(
38
+ (el): el is IrExpression => el !== undefined
39
+ );
40
+
41
+ if (definedElements.length > 0) {
42
+ const allLiterals = definedElements.every((el) => el.kind === "literal");
43
+
44
+ if (allLiterals) {
45
+ const literals = definedElements as Extract<
46
+ IrExpression,
47
+ { kind: "literal" }
48
+ >[];
49
+
50
+ // Check if all are numbers
51
+ const allNumbers = literals.every((lit) => typeof lit.value === "number");
52
+
53
+ if (allNumbers) {
54
+ // TypeScript `number` is always `double` in C#
55
+ // Even if all literals are integers, use double for TS semantics
56
+ elementType = "double";
57
+ }
58
+ // Check if all are strings
59
+ else if (literals.every((lit) => typeof lit.value === "string")) {
60
+ elementType = "string";
61
+ }
62
+ // Check if all are booleans
63
+ else if (literals.every((lit) => typeof lit.value === "boolean")) {
64
+ elementType = "bool";
65
+ }
66
+ }
67
+ }
68
+
69
+ // Use expected/inferred type if available (takes precedence over literal inference)
70
+ if (expectedType) {
71
+ if (expectedType.kind === "arrayType") {
72
+ const [elemTypeStr, newContext] = emitType(
73
+ expectedType.elementType,
74
+ currentContext
75
+ );
76
+ elementType = elemTypeStr;
77
+ currentContext = newContext;
78
+ } else if (
79
+ expectedType.kind === "referenceType" &&
80
+ expectedType.name === "Array" &&
81
+ expectedType.typeArguments &&
82
+ expectedType.typeArguments.length > 0
83
+ ) {
84
+ const firstArg = expectedType.typeArguments[0];
85
+ if (firstArg) {
86
+ const [elemTypeStr, newContext] = emitType(firstArg, currentContext);
87
+ elementType = elemTypeStr;
88
+ currentContext = newContext;
89
+ }
90
+ }
91
+ }
92
+ // If no expectedType, try to use the inferredType from the expression
93
+ else if (expr.inferredType && expr.inferredType.kind === "arrayType") {
94
+ // Only use inferredType if we didn't already determine the type from literals
95
+ if (elementType === "object") {
96
+ const [elemTypeStr, newContext] = emitType(
97
+ expr.inferredType.elementType,
98
+ currentContext
99
+ );
100
+ elementType = elemTypeStr;
101
+ currentContext = newContext;
102
+ }
103
+ }
104
+
105
+ // Check if array contains only spread elements (e.g., [...arr1, ...arr2])
106
+ const allSpreads = expr.elements.every(
107
+ (el) => el !== undefined && el.kind === "spread"
108
+ );
109
+
110
+ if (allSpreads && expr.elements.length > 0) {
111
+ // Emit as chained Enumerable.Concat calls using explicit static invocation
112
+ // Note: Concat returns IEnumerable<T>, so wrap in Enumerable.ToList() at the end
113
+ const spreadElements = expr.elements.filter(
114
+ (el): el is Extract<IrExpression, { kind: "spread" }> =>
115
+ el !== undefined && el.kind === "spread"
116
+ );
117
+
118
+ const firstSpread = spreadElements[0];
119
+ if (!firstSpread) {
120
+ // Should never happen due to allSpreads check, but satisfy TypeScript
121
+ return [
122
+ { text: "new global::System.Collections.Generic.List<object>()" },
123
+ currentContext,
124
+ ];
125
+ }
126
+
127
+ const [firstFrag, firstContext] = emitExpression(
128
+ firstSpread.expression,
129
+ currentContext
130
+ );
131
+ currentContext = firstContext;
132
+
133
+ let result = firstFrag.text;
134
+ for (let i = 1; i < spreadElements.length; i++) {
135
+ const spread = spreadElements[i];
136
+ if (spread) {
137
+ const [spreadFrag, newContext] = emitExpression(
138
+ spread.expression,
139
+ currentContext
140
+ );
141
+ // Use explicit static call for extension method
142
+ result = `global::System.Linq.Enumerable.Concat(${result}, ${spreadFrag.text})`;
143
+ currentContext = newContext;
144
+ }
145
+ }
146
+
147
+ // Wrap in Enumerable.ToList() using explicit static call
148
+ return [
149
+ { text: `global::System.Linq.Enumerable.ToList(${result})` },
150
+ currentContext,
151
+ ];
152
+ }
153
+
154
+ // Regular array or mixed spreads/elements
155
+ for (const element of expr.elements) {
156
+ if (element === undefined) {
157
+ // Sparse array hole - fill with default value
158
+ elements.push("default");
159
+ } else if (element.kind === "spread") {
160
+ // Spread mixed with other elements - not yet supported
161
+ elements.push("/* ...spread */");
162
+ } else {
163
+ const [elemFrag, newContext] = emitExpression(element, currentContext);
164
+ elements.push(elemFrag.text);
165
+ currentContext = newContext;
166
+ }
167
+ }
168
+
169
+ // Use constructor syntax for empty arrays, initializer syntax for non-empty
170
+ const text =
171
+ elements.length === 0
172
+ ? `new global::System.Collections.Generic.List<${elementType}>()`
173
+ : `new global::System.Collections.Generic.List<${elementType}> { ${elements.join(", ")} }`;
174
+
175
+ return [{ text }, currentContext];
176
+ };
177
+
178
+ /**
179
+ * Emit an object literal
180
+ *
181
+ * Handles two cases based on contextual type:
182
+ * 1. Dictionary type (Record<K,V> or index signature) → Dictionary<string, T> initializer
183
+ * 2. Nominal type (interface, class) → new TypeName { prop = value, ... }
184
+ *
185
+ * Anonymous object types should be caught by validation (TSN7403) before reaching here.
186
+ */
187
+ export const emitObject = (
188
+ expr: Extract<IrExpression, { kind: "object" }>,
189
+ context: EmitterContext
190
+ ): [CSharpFragment, EmitterContext] => {
191
+ let currentContext = context;
192
+
193
+ // Check if contextual type is a dictionary type
194
+ if (expr.contextualType?.kind === "dictionaryType") {
195
+ return emitDictionaryLiteral(expr, currentContext);
196
+ }
197
+
198
+ // Regular object literal with nominal type
199
+ const properties: string[] = [];
200
+
201
+ for (const prop of expr.properties) {
202
+ if (prop.kind === "spread") {
203
+ // Spread in object literal - needs special handling
204
+ properties.push("/* ...spread */");
205
+ } else {
206
+ const key = typeof prop.key === "string" ? prop.key : "/* computed */";
207
+ const [valueFrag, newContext] = emitExpression(
208
+ prop.value,
209
+ currentContext
210
+ );
211
+ properties.push(`${key} = ${valueFrag.text}`);
212
+ currentContext = newContext;
213
+ }
214
+ }
215
+
216
+ // Check for contextual type (from return type, variable annotation, etc.)
217
+ // If present, emit `new TypeName { ... }` instead of anonymous `new { ... }`
218
+ const [typeName, finalContext] = resolveContextualType(
219
+ expr.contextualType,
220
+ currentContext
221
+ );
222
+
223
+ if (!typeName) {
224
+ // ICE: Validation (TSN7403) should have caught anonymous object literals
225
+ throw new Error(
226
+ "ICE: Object literal without contextual type reached emitter - validation missed TSN7403"
227
+ );
228
+ }
229
+
230
+ const text = `new ${typeName} { ${properties.join(", ")} }`;
231
+ return [{ text }, finalContext];
232
+ };
233
+
234
+ /**
235
+ * Emit a dictionary literal using C# collection initializer syntax.
236
+ *
237
+ * Input: const d: Record<string, number> = { a: 1, b: 2 };
238
+ * Output: new Dictionary<string, double> { ["a"] = 1.0, ["b"] = 2.0 }
239
+ */
240
+ const emitDictionaryLiteral = (
241
+ expr: Extract<IrExpression, { kind: "object" }>,
242
+ context: EmitterContext
243
+ ): [CSharpFragment, EmitterContext] => {
244
+ let currentContext = context;
245
+
246
+ const dictType = expr.contextualType as Extract<
247
+ IrType,
248
+ { kind: "dictionaryType" }
249
+ >;
250
+
251
+ // Get key and value type strings
252
+ const [keyTypeStr, ctx1] = emitDictKeyType(dictType.keyType, currentContext);
253
+ const [valueTypeStr, ctx2] = emitType(dictType.valueType, ctx1);
254
+ currentContext = ctx2;
255
+
256
+ // Emit dictionary entries
257
+ const entries: string[] = [];
258
+
259
+ for (const prop of expr.properties) {
260
+ if (prop.kind === "spread") {
261
+ // Spread in dictionary literal - not supported
262
+ throw new Error("ICE: Spread in dictionary literal not supported");
263
+ } else {
264
+ // Key must be a string literal for dictionary initialization
265
+ if (typeof prop.key !== "string") {
266
+ throw new Error(
267
+ "ICE: Computed property key in dictionary literal - validation gap"
268
+ );
269
+ }
270
+
271
+ const [valueFrag, newContext] = emitExpression(
272
+ prop.value,
273
+ currentContext
274
+ );
275
+ entries.push(`["${escapeCSharpString(prop.key)}"] = ${valueFrag.text}`);
276
+ currentContext = newContext;
277
+ }
278
+ }
279
+
280
+ const text =
281
+ entries.length === 0
282
+ ? `new global::System.Collections.Generic.Dictionary<${keyTypeStr}, ${valueTypeStr}>()`
283
+ : `new global::System.Collections.Generic.Dictionary<${keyTypeStr}, ${valueTypeStr}> { ${entries.join(", ")} }`;
284
+
285
+ return [{ text }, currentContext];
286
+ };
287
+
288
+ /**
289
+ * Emit dictionary key type.
290
+ * TS dictionaries only support string keys (enforced by TSN7413).
291
+ */
292
+ const emitDictKeyType = (
293
+ keyType: IrType,
294
+ context: EmitterContext
295
+ ): [string, EmitterContext] => {
296
+ if (keyType.kind === "primitiveType" && keyType.name === "string") {
297
+ return ["string", context];
298
+ }
299
+
300
+ // ICE: Only string keys allowed (enforced by TSN7413)
301
+ throw new Error(
302
+ `ICE: Non-string dictionary key type reached emitter - validation missed TSN7413. Got: ${keyType.kind}`
303
+ );
304
+ };
305
+
306
+ /**
307
+ * Resolve contextual type to C# type string.
308
+ * Uses emitType to properly handle generic type arguments.
309
+ * For imported types, qualifies using importBindings.
310
+ */
311
+ const resolveContextualType = (
312
+ contextualType: IrType | undefined,
313
+ context: EmitterContext
314
+ ): [string | undefined, EmitterContext] => {
315
+ if (!contextualType) {
316
+ return [undefined, context];
317
+ }
318
+
319
+ // For reference types, check if imported and qualify if needed
320
+ if (contextualType.kind === "referenceType") {
321
+ const typeName = contextualType.name;
322
+ const importBinding = context.importBindings?.get(typeName);
323
+
324
+ if (importBinding && importBinding.kind === "type") {
325
+ // Imported type - use qualified name from binding
326
+ // Emit type arguments if present
327
+ if (
328
+ contextualType.typeArguments &&
329
+ contextualType.typeArguments.length > 0
330
+ ) {
331
+ let currentContext = context;
332
+ const typeArgStrs: string[] = [];
333
+ for (const typeArg of contextualType.typeArguments) {
334
+ const [typeArgStr, newContext] = emitType(typeArg, currentContext);
335
+ typeArgStrs.push(typeArgStr);
336
+ currentContext = newContext;
337
+ }
338
+ return [
339
+ `${importBinding.clrName}<${typeArgStrs.join(", ")}>`,
340
+ currentContext,
341
+ ];
342
+ }
343
+ return [importBinding.clrName, context];
344
+ }
345
+
346
+ // Local type - use emitType to handle type arguments
347
+ const [typeStr, newContext] = emitType(contextualType, context);
348
+ return [typeStr, newContext];
349
+ }
350
+
351
+ // For other types, use standard emitType
352
+ const [typeStr, newContext] = emitType(contextualType, context);
353
+ return [typeStr, newContext];
354
+ };
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Function expression emitters (function expressions and arrow functions)
3
+ */
4
+
5
+ import { IrExpression } from "@tsonic/frontend";
6
+ import { EmitterContext, CSharpFragment, indent } from "../types.js";
7
+ import { emitExpression } from "../expression-emitter.js";
8
+ import { emitStatement } from "../statement-emitter.js";
9
+
10
+ /**
11
+ * Emit a function expression (placeholder for now)
12
+ */
13
+ export const emitFunctionExpression = (
14
+ _expr: Extract<IrExpression, { kind: "functionExpression" }>,
15
+ context: EmitterContext
16
+ ): [CSharpFragment, EmitterContext] => {
17
+ // Function expressions need to be converted to lambdas or delegates
18
+ // For MVP, we'll emit a placeholder
19
+ const text = "/* function expression */";
20
+ return [{ text }, context];
21
+ };
22
+
23
+ /**
24
+ * Emit an arrow function as C# lambda
25
+ */
26
+ export const emitArrowFunction = (
27
+ expr: Extract<IrExpression, { kind: "arrowFunction" }>,
28
+ context: EmitterContext
29
+ ): [CSharpFragment, EmitterContext] => {
30
+ // Convert arrow function to C# lambda
31
+ let currentContext = context;
32
+ const paramNames: string[] = [];
33
+
34
+ for (const param of expr.parameters) {
35
+ if (param.pattern.kind === "identifierPattern") {
36
+ paramNames.push(param.pattern.name);
37
+ } else {
38
+ paramNames.push("_");
39
+ }
40
+ }
41
+
42
+ // Handle body
43
+ let bodyText: string;
44
+ if (typeof expr.body === "object" && "kind" in expr.body) {
45
+ if (expr.body.kind === "blockStatement") {
46
+ // For block statements in lambdas:
47
+ // - In static contexts (field initializers), indent the block one level
48
+ // - In non-static contexts (local variables), keep at same level
49
+ const blockContext = currentContext.isStatic
50
+ ? indent(currentContext)
51
+ : currentContext;
52
+ const [blockCode, _newContext] = emitStatement(expr.body, blockContext);
53
+
54
+ const params = paramNames.join(", ");
55
+ // The block code has proper indentation, just prepend the lambda signature
56
+ const text = `(${params}) =>\n${blockCode}`;
57
+
58
+ return [{ text }, currentContext];
59
+ } else {
60
+ const [bodyFrag, newContext] = emitExpression(expr.body, currentContext);
61
+ currentContext = newContext;
62
+ bodyText = bodyFrag.text;
63
+ }
64
+ } else {
65
+ bodyText = "/* unknown body */";
66
+ }
67
+
68
+ const params = paramNames.join(", ");
69
+ const text = `(${params}) => ${bodyText}`;
70
+ return [{ text }, currentContext];
71
+ };