@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.
- package/package.json +34 -0
- package/scripts/update-golden-tests.ts +119 -0
- package/src/adapter-generator.ts +112 -0
- package/src/array.test.ts +301 -0
- package/src/constants.ts +32 -0
- package/src/core/exports.ts +36 -0
- package/src/core/imports.test.ts +83 -0
- package/src/core/imports.ts +243 -0
- package/src/core/index.ts +9 -0
- package/src/core/module-emitter/assembly.ts +83 -0
- package/src/core/module-emitter/header.ts +19 -0
- package/src/core/module-emitter/index.ts +17 -0
- package/src/core/module-emitter/namespace.ts +39 -0
- package/src/core/module-emitter/orchestrator.ts +98 -0
- package/src/core/module-emitter/separation.ts +41 -0
- package/src/core/module-emitter/static-container.ts +75 -0
- package/src/core/module-emitter.test.ts +154 -0
- package/src/core/module-emitter.ts +6 -0
- package/src/core/module-map.ts +218 -0
- package/src/core/options.ts +16 -0
- package/src/core/type-params.ts +34 -0
- package/src/emitter-types/context.ts +91 -0
- package/src/emitter-types/core.ts +138 -0
- package/src/emitter-types/csharp-types.ts +42 -0
- package/src/emitter-types/formatting.ts +13 -0
- package/src/emitter-types/fqn.ts +81 -0
- package/src/emitter-types/index.ts +31 -0
- package/src/emitter.ts +107 -0
- package/src/expression-emitter.ts +112 -0
- package/src/expressions/access.ts +104 -0
- package/src/expressions/calls.ts +264 -0
- package/src/expressions/collections.ts +354 -0
- package/src/expressions/functions.ts +71 -0
- package/src/expressions/identifiers.ts +125 -0
- package/src/expressions/index.test.ts +515 -0
- package/src/expressions/index.ts +38 -0
- package/src/expressions/literals.ts +138 -0
- package/src/expressions/operators.ts +211 -0
- package/src/expressions/other.ts +63 -0
- package/src/generator-exchange.ts +120 -0
- package/src/generator.test.ts +193 -0
- package/src/golden-tests/config-parser.ts +67 -0
- package/src/golden-tests/discovery.ts +61 -0
- package/src/golden-tests/index.ts +10 -0
- package/src/golden-tests/registration.ts +26 -0
- package/src/golden-tests/runner.ts +131 -0
- package/src/golden-tests/tree-builder.ts +43 -0
- package/src/golden-tests/types.ts +21 -0
- package/src/golden.test.ts +40 -0
- package/src/hierarchical-bindings.test.ts +258 -0
- package/src/index.ts +14 -0
- package/src/integration.test.ts +303 -0
- package/src/specialization/call-site-rewriting.test.ts +99 -0
- package/src/specialization/collection/expressions.ts +184 -0
- package/src/specialization/collection/index.ts +7 -0
- package/src/specialization/collection/orchestrator.ts +25 -0
- package/src/specialization/collection/statements.ts +91 -0
- package/src/specialization/collection.ts +10 -0
- package/src/specialization/generation.ts +189 -0
- package/src/specialization/generic-classes.test.ts +59 -0
- package/src/specialization/generic-functions.test.ts +292 -0
- package/src/specialization/helpers.ts +39 -0
- package/src/specialization/index.ts +28 -0
- package/src/specialization/interfaces.test.ts +151 -0
- package/src/specialization/naming.ts +34 -0
- package/src/specialization/substitution.ts +186 -0
- package/src/specialization/type-aliases.test.ts +134 -0
- package/src/specialization/types.ts +19 -0
- package/src/specialization-generator.ts +23 -0
- package/src/statement-emitter.ts +117 -0
- package/src/statements/blocks.ts +115 -0
- package/src/statements/classes/helpers.ts +9 -0
- package/src/statements/classes/index.ts +13 -0
- package/src/statements/classes/inline-types.ts +79 -0
- package/src/statements/classes/members/constructors.ts +123 -0
- package/src/statements/classes/members/index.ts +8 -0
- package/src/statements/classes/members/methods.ts +137 -0
- package/src/statements/classes/members/orchestrator.ts +33 -0
- package/src/statements/classes/members/properties.ts +62 -0
- package/src/statements/classes/members.ts +6 -0
- package/src/statements/classes/parameters.ts +69 -0
- package/src/statements/classes/properties.ts +95 -0
- package/src/statements/classes.ts +14 -0
- package/src/statements/control/conditionals.ts +134 -0
- package/src/statements/control/exceptions.ts +59 -0
- package/src/statements/control/index.ts +11 -0
- package/src/statements/control/loops.ts +250 -0
- package/src/statements/control.ts +14 -0
- package/src/statements/declarations/classes.ts +89 -0
- package/src/statements/declarations/enums.ts +32 -0
- package/src/statements/declarations/functions.ts +147 -0
- package/src/statements/declarations/index.ts +10 -0
- package/src/statements/declarations/interfaces.ts +116 -0
- package/src/statements/declarations/structs.test.ts +182 -0
- package/src/statements/declarations/type-aliases.ts +104 -0
- package/src/statements/declarations/variables.ts +159 -0
- package/src/statements/declarations.ts +13 -0
- package/src/statements/index.test.ts +258 -0
- package/src/statements/index.ts +43 -0
- package/src/type-assertion.test.ts +143 -0
- package/src/type-emitter.ts +18 -0
- package/src/types/arrays.ts +21 -0
- package/src/types/dictionaries.ts +52 -0
- package/src/types/emitter.ts +76 -0
- package/src/types/functions.ts +45 -0
- package/src/types/index.test.ts +116 -0
- package/src/types/index.ts +14 -0
- package/src/types/intersections.ts +19 -0
- package/src/types/literals.ts +26 -0
- package/src/types/objects.ts +27 -0
- package/src/types/parameters.test.ts +146 -0
- package/src/types/parameters.ts +95 -0
- package/src/types/primitives.ts +24 -0
- package/src/types/references.ts +187 -0
- package/src/types/unions.test.ts +397 -0
- package/src/types/unions.ts +62 -0
- package/src/types.ts +33 -0
- package/testcases/README.md +213 -0
- package/testcases/arrays/basic/ArrayLiteral.ts +4 -0
- package/testcases/arrays/basic/config.yaml +1 -0
- package/testcases/arrays/destructuring/ArrayDestructure.ts +4 -0
- package/testcases/arrays/destructuring/config.yaml +1 -0
- package/testcases/arrays/methods/ArrayMethods.ts +6 -0
- package/testcases/arrays/methods/config.yaml +1 -0
- package/testcases/arrays/multidimensional/MultiDimensional.ts +10 -0
- package/testcases/arrays/multidimensional/config.yaml +1 -0
- package/testcases/arrays/spread/ArraySpread.ts +3 -0
- package/testcases/arrays/spread/config.yaml +1 -0
- package/testcases/async/basic/AsyncFunction.ts +5 -0
- package/testcases/async/basic/config.yaml +1 -0
- package/testcases/classes/abstract/AbstractClasses.ts +53 -0
- package/testcases/classes/abstract/config.yaml +1 -0
- package/testcases/classes/basic/Person.ts +12 -0
- package/testcases/classes/basic/config.yaml +1 -0
- package/testcases/classes/constructor/User.ts +11 -0
- package/testcases/classes/constructor/config.yaml +1 -0
- package/testcases/classes/field-inference/Counter.ts +11 -0
- package/testcases/classes/field-inference/config.yaml +1 -0
- package/testcases/classes/inheritance/Inheritance.ts +24 -0
- package/testcases/classes/inheritance/config.yaml +1 -0
- package/testcases/classes/static-members/MathHelper.ts +12 -0
- package/testcases/classes/static-members/config.yaml +1 -0
- package/testcases/control-flow/error-handling/ErrorHandling.ts +13 -0
- package/testcases/control-flow/error-handling/config.yaml +1 -0
- package/testcases/control-flow/loops/Loops.ts +21 -0
- package/testcases/control-flow/loops/config.yaml +1 -0
- package/testcases/control-flow/switch/SwitchStatement.ts +15 -0
- package/testcases/control-flow/switch/config.yaml +1 -0
- package/testcases/edge-cases/complex-expressions/ComplexExpressions.ts +10 -0
- package/testcases/edge-cases/complex-expressions/config.yaml +1 -0
- package/testcases/edge-cases/nested-scopes/NestedScopes.ts +10 -0
- package/testcases/edge-cases/nested-scopes/config.yaml +1 -0
- package/testcases/edge-cases/shadowing/Shadowing.ts +16 -0
- package/testcases/edge-cases/shadowing/config.yaml +1 -0
- package/testcases/functions/arrow/ArrowFunction.ts +5 -0
- package/testcases/functions/arrow/config.yaml +1 -0
- package/testcases/functions/basic/Greet.ts +3 -0
- package/testcases/functions/basic/config.yaml +1 -0
- package/testcases/functions/closures/Closures.ts +11 -0
- package/testcases/functions/closures/config.yaml +1 -0
- package/testcases/functions/default-params/DefaultParams.ts +7 -0
- package/testcases/functions/default-params/config.yaml +1 -0
- package/testcases/functions/rest-params/RestParams.ts +7 -0
- package/testcases/functions/rest-params/config.yaml +1 -0
- package/testcases/functions/type-guards/TypeGuards.ts +52 -0
- package/testcases/functions/type-guards/config.yaml +1 -0
- package/testcases/operators/logical/LogicalOperators.ts +11 -0
- package/testcases/operators/logical/config.yaml +1 -0
- package/testcases/operators/nullish-coalescing/NullishCoalescing.ts +7 -0
- package/testcases/operators/nullish-coalescing/config.yaml +1 -0
- package/testcases/operators/optional-chaining/OptionalChaining.ts +15 -0
- package/testcases/operators/optional-chaining/config.yaml +1 -0
- package/testcases/real-world/advanced-generics/advanced-generics.ts +116 -0
- package/testcases/real-world/advanced-generics/config.yaml +1 -0
- package/testcases/real-world/async-ops/async-ops.ts +67 -0
- package/testcases/real-world/async-ops/config.yaml +1 -0
- package/testcases/real-world/business-logic/business-logic.ts +215 -0
- package/testcases/real-world/business-logic/config.yaml +1 -0
- package/testcases/real-world/calculator/calculator.ts +29 -0
- package/testcases/real-world/calculator/config.yaml +1 -0
- package/testcases/real-world/data-structures/config.yaml +1 -0
- package/testcases/real-world/data-structures/data-structures.ts +133 -0
- package/testcases/real-world/functional/config.yaml +1 -0
- package/testcases/real-world/functional/functional.ts +116 -0
- package/testcases/real-world/shapes/config.yaml +1 -0
- package/testcases/real-world/shapes/shapes.ts +87 -0
- package/testcases/real-world/string-utils/config.yaml +1 -0
- package/testcases/real-world/string-utils/string-utils.ts +47 -0
- package/testcases/real-world/todo-list/config.yaml +1 -0
- package/testcases/real-world/todo-list/todo-list.ts +52 -0
- package/testcases/real-world/type-guards/config.yaml +1 -0
- package/testcases/real-world/type-guards/type-guards.ts +71 -0
- package/testcases/structs/basic/Point.ts +9 -0
- package/testcases/structs/basic/config.yaml +1 -0
- package/testcases/types/conditional/ConditionalTypes.ts +35 -0
- package/testcases/types/conditional/config.yaml +1 -0
- package/testcases/types/constants/ModuleConstants.ts +6 -0
- package/testcases/types/constants/config.yaml +1 -0
- package/testcases/types/generics/Generics.ts +15 -0
- package/testcases/types/generics/config.yaml +1 -0
- package/testcases/types/interfaces/Interfaces.ts +14 -0
- package/testcases/types/interfaces/config.yaml +1 -0
- package/testcases/types/mapped/MappedTypes.ts +27 -0
- package/testcases/types/mapped/config.yaml +1 -0
- package/testcases/types/tuples-intersections/TuplesAndIntersections.ts +46 -0
- package/testcases/types/tuples-intersections/config.yaml +1 -0
- package/testcases/types/unions/UnionTypes.ts +11 -0
- package/testcases/types/unions/config.yaml +1 -0
- 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
|
+
};
|