@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
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tsonic/emitter",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "C# code generator for Tsonic compiler",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -b",
|
|
16
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
17
|
+
"test": "mocha 'dist/**/*.test.js'",
|
|
18
|
+
"test:watch": "mocha 'dist/**/*.test.js' --watch"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@tsonic/frontend": "0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/chai": "5.2.2",
|
|
25
|
+
"@types/mocha": "10.0.10",
|
|
26
|
+
"@types/node": "20.14.0",
|
|
27
|
+
"chai": "5.3.3",
|
|
28
|
+
"mocha": "11.7.2",
|
|
29
|
+
"yaml": "^2.8.1"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update golden test expected files
|
|
3
|
+
* Run with: npx tsx scripts/update-golden-tests.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { compile, buildIr } from "@tsonic/frontend";
|
|
10
|
+
import { emitCSharpFiles } from "../src/emitter.js";
|
|
11
|
+
import { parseConfigYaml } from "../src/golden-tests/config-parser.js";
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const TESTCASES_DIR = path.join(__dirname, "../testcases");
|
|
16
|
+
|
|
17
|
+
interface TestEntry {
|
|
18
|
+
input: string;
|
|
19
|
+
title: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const walkDir = (dir: string, pathParts: string[] = []): void => {
|
|
23
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
const hasConfig = entries.some((e) => e.name === "config.yaml");
|
|
25
|
+
|
|
26
|
+
if (hasConfig) {
|
|
27
|
+
const configPath = path.join(dir, "config.yaml");
|
|
28
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
29
|
+
const testEntries = parseConfigYaml(configContent);
|
|
30
|
+
|
|
31
|
+
for (const entry of testEntries) {
|
|
32
|
+
const inputPath = path.join(dir, entry.input);
|
|
33
|
+
const baseName = path.basename(entry.input, ".ts");
|
|
34
|
+
const expectedPath = path.join(dir, `${baseName}.cs`);
|
|
35
|
+
|
|
36
|
+
console.log(`Updating: ${pathParts.join("/")}/${baseName}`);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Build namespace from path parts
|
|
40
|
+
const namespaceParts = pathParts.map((part) => part.replace(/-/g, ""));
|
|
41
|
+
const rootNamespace = ["TestCases", ...namespaceParts].join(".");
|
|
42
|
+
const sourceRoot = path.dirname(inputPath);
|
|
43
|
+
|
|
44
|
+
// Compile
|
|
45
|
+
const compileResult = compile([inputPath], {
|
|
46
|
+
sourceRoot,
|
|
47
|
+
rootNamespace,
|
|
48
|
+
useStandardLib: true,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!compileResult.ok) {
|
|
52
|
+
console.error(` ERROR: Compilation failed`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Build IR
|
|
57
|
+
const irResult = buildIr(compileResult.value.program, {
|
|
58
|
+
sourceRoot,
|
|
59
|
+
rootNamespace,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!irResult.ok) {
|
|
63
|
+
console.error(` ERROR: IR build failed`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Emit C#
|
|
68
|
+
const emitResult = emitCSharpFiles(irResult.value, { rootNamespace });
|
|
69
|
+
|
|
70
|
+
if (!emitResult.ok) {
|
|
71
|
+
console.error(` ERROR: Emit failed`);
|
|
72
|
+
for (const err of emitResult.errors) {
|
|
73
|
+
console.error(` ${err.code}: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const csharpFiles = emitResult.files;
|
|
79
|
+
|
|
80
|
+
// Find the generated file
|
|
81
|
+
const className = baseName;
|
|
82
|
+
const generatedKey = Array.from(csharpFiles.keys()).find((key) =>
|
|
83
|
+
key.endsWith(`${className}.cs`)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!generatedKey) {
|
|
87
|
+
console.error(` ERROR: No generated file found`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const fullOutput = csharpFiles.get(generatedKey)!;
|
|
92
|
+
|
|
93
|
+
// Strip the header (first 4 lines: Generated from, Generated at, WARNING, blank)
|
|
94
|
+
const lines = fullOutput.split("\n");
|
|
95
|
+
const bodyStartIndex = lines.findIndex(
|
|
96
|
+
(line, i) => i > 0 && line.startsWith("using")
|
|
97
|
+
);
|
|
98
|
+
const body = lines.slice(bodyStartIndex).join("\n");
|
|
99
|
+
|
|
100
|
+
// Write to expected file
|
|
101
|
+
fs.writeFileSync(expectedPath, body);
|
|
102
|
+
console.log(` OK: ${expectedPath}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error(` ERROR: ${err}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recurse into subdirectories
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
walkDir(path.join(dir, entry.name), [...pathParts, entry.name]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
console.log("Updating golden test expected files...\n");
|
|
118
|
+
walkDir(TESTCASES_DIR);
|
|
119
|
+
console.log("\nDone!");
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter Generator - Generate C# adapters for structural constraints
|
|
3
|
+
* Per spec/15-generics.md §4 - Structural Constraints & Adapters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { IrTypeParameter } from "@tsonic/frontend";
|
|
7
|
+
import { EmitterContext, getIndent, indent } from "./types.js";
|
|
8
|
+
import { emitType } from "./type-emitter.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate adapter interface and wrapper class for a structural constraint
|
|
12
|
+
*
|
|
13
|
+
* Example:
|
|
14
|
+
* Input: T extends { id: number; name: string }
|
|
15
|
+
* Output:
|
|
16
|
+
* interface __Constraint_T {
|
|
17
|
+
* double id { get; }
|
|
18
|
+
* string name { get; }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* sealed class __Wrapper_T : __Constraint_T {
|
|
22
|
+
* public double id { get; set; }
|
|
23
|
+
* public string name { get; set; }
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
export const generateStructuralAdapter = (
|
|
27
|
+
typeParam: IrTypeParameter,
|
|
28
|
+
context: EmitterContext
|
|
29
|
+
): [string, EmitterContext] => {
|
|
30
|
+
if (!typeParam.isStructuralConstraint || !typeParam.structuralMembers) {
|
|
31
|
+
return ["", context];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ind = getIndent(context);
|
|
35
|
+
const bodyInd = getIndent(indent(context));
|
|
36
|
+
const parts: string[] = [];
|
|
37
|
+
let currentContext = context;
|
|
38
|
+
|
|
39
|
+
const interfaceName = `__Constraint_${typeParam.name}`;
|
|
40
|
+
const wrapperName = `__Wrapper_${typeParam.name}`;
|
|
41
|
+
|
|
42
|
+
// Generate interface
|
|
43
|
+
parts.push(`${ind}public interface ${interfaceName}`);
|
|
44
|
+
parts.push(`${ind}{`);
|
|
45
|
+
|
|
46
|
+
for (const member of typeParam.structuralMembers) {
|
|
47
|
+
if (member.kind === "propertySignature") {
|
|
48
|
+
const [memberType, newContext] = emitType(member.type, currentContext);
|
|
49
|
+
currentContext = newContext;
|
|
50
|
+
|
|
51
|
+
const optional = member.isOptional ? "?" : "";
|
|
52
|
+
parts.push(`${bodyInd}${memberType}${optional} ${member.name} { get; }`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parts.push(`${ind}}`);
|
|
57
|
+
parts.push(""); // Blank line
|
|
58
|
+
|
|
59
|
+
// Generate wrapper class
|
|
60
|
+
parts.push(`${ind}public sealed class ${wrapperName} : ${interfaceName}`);
|
|
61
|
+
parts.push(`${ind}{`);
|
|
62
|
+
|
|
63
|
+
for (const member of typeParam.structuralMembers) {
|
|
64
|
+
if (member.kind === "propertySignature") {
|
|
65
|
+
const [memberType, newContext] = emitType(member.type, currentContext);
|
|
66
|
+
currentContext = newContext;
|
|
67
|
+
|
|
68
|
+
const optional = member.isOptional ? "?" : "";
|
|
69
|
+
parts.push(
|
|
70
|
+
`${bodyInd}public ${memberType}${optional} ${member.name} { get; set; }`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
parts.push(`${ind}}`);
|
|
76
|
+
|
|
77
|
+
return [parts.join("\n"), currentContext];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate all structural adapters for a set of type parameters
|
|
82
|
+
*/
|
|
83
|
+
export const generateStructuralAdapters = (
|
|
84
|
+
typeParams: readonly IrTypeParameter[] | undefined,
|
|
85
|
+
context: EmitterContext
|
|
86
|
+
): [string, EmitterContext] => {
|
|
87
|
+
if (!typeParams || typeParams.length === 0) {
|
|
88
|
+
return ["", context];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const adapters: string[] = [];
|
|
92
|
+
let currentContext = context;
|
|
93
|
+
|
|
94
|
+
for (const tp of typeParams) {
|
|
95
|
+
if (tp.isStructuralConstraint && tp.structuralMembers) {
|
|
96
|
+
const [adapterCode, newContext] = generateStructuralAdapter(
|
|
97
|
+
tp,
|
|
98
|
+
currentContext
|
|
99
|
+
);
|
|
100
|
+
if (adapterCode) {
|
|
101
|
+
adapters.push(adapterCode);
|
|
102
|
+
currentContext = newContext;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (adapters.length === 0) {
|
|
108
|
+
return ["", context];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [adapters.join("\n\n"), currentContext];
|
|
112
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for array emission
|
|
3
|
+
* Verifies Tsonic.JSRuntime extension methods usage and JavaScript semantics
|
|
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("Array Emission", () => {
|
|
12
|
+
it("should emit basic array literal with correct type", () => {
|
|
13
|
+
const module: IrModule = {
|
|
14
|
+
kind: "module",
|
|
15
|
+
filePath: "/test/arrays.ts",
|
|
16
|
+
namespace: "Test",
|
|
17
|
+
className: "arrays",
|
|
18
|
+
isStaticContainer: true,
|
|
19
|
+
imports: [],
|
|
20
|
+
exports: [],
|
|
21
|
+
body: [
|
|
22
|
+
{
|
|
23
|
+
kind: "variableDeclaration",
|
|
24
|
+
declarationKind: "const",
|
|
25
|
+
isExported: false,
|
|
26
|
+
declarations: [
|
|
27
|
+
{
|
|
28
|
+
kind: "variableDeclarator",
|
|
29
|
+
name: { kind: "identifierPattern", name: "numbers" },
|
|
30
|
+
type: {
|
|
31
|
+
kind: "referenceType",
|
|
32
|
+
name: "Array",
|
|
33
|
+
typeArguments: [{ kind: "primitiveType", name: "number" }],
|
|
34
|
+
},
|
|
35
|
+
initializer: {
|
|
36
|
+
kind: "array",
|
|
37
|
+
elements: [
|
|
38
|
+
{ kind: "literal", value: 1 },
|
|
39
|
+
{ kind: "literal", value: 2 },
|
|
40
|
+
{ kind: "literal", value: 3 },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const code = emitModule(module);
|
|
50
|
+
|
|
51
|
+
// Integer literals create List<int> - C# infers type from literal values
|
|
52
|
+
// Uses global:: FQN for unambiguous resolution
|
|
53
|
+
expect(code).to.include(
|
|
54
|
+
"new global::System.Collections.Generic.List<int> { 1, 2, 3 }"
|
|
55
|
+
);
|
|
56
|
+
// No using statements - all types use global:: FQN
|
|
57
|
+
expect(code).not.to.include("using System.Collections.Generic");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should emit sparse array with holes", () => {
|
|
61
|
+
const module: IrModule = {
|
|
62
|
+
kind: "module",
|
|
63
|
+
filePath: "/test/sparse.ts",
|
|
64
|
+
namespace: "Test",
|
|
65
|
+
className: "sparse",
|
|
66
|
+
isStaticContainer: true,
|
|
67
|
+
imports: [],
|
|
68
|
+
exports: [],
|
|
69
|
+
body: [
|
|
70
|
+
{
|
|
71
|
+
kind: "variableDeclaration",
|
|
72
|
+
declarationKind: "const",
|
|
73
|
+
isExported: false,
|
|
74
|
+
declarations: [
|
|
75
|
+
{
|
|
76
|
+
kind: "variableDeclarator",
|
|
77
|
+
name: { kind: "identifierPattern", name: "sparse" },
|
|
78
|
+
type: {
|
|
79
|
+
kind: "referenceType",
|
|
80
|
+
name: "Array",
|
|
81
|
+
typeArguments: [{ kind: "primitiveType", name: "number" }],
|
|
82
|
+
},
|
|
83
|
+
initializer: {
|
|
84
|
+
kind: "array",
|
|
85
|
+
elements: [
|
|
86
|
+
{ kind: "literal", value: 1 },
|
|
87
|
+
undefined, // hole
|
|
88
|
+
{ kind: "literal", value: 3 },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const code = emitModule(module);
|
|
98
|
+
|
|
99
|
+
// Should handle sparse array with default - integer literals create List<int>
|
|
100
|
+
expect(code).to.include(
|
|
101
|
+
"new global::System.Collections.Generic.List<int> { 1, default, 3 }"
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should emit array with string elements", () => {
|
|
106
|
+
const module: IrModule = {
|
|
107
|
+
kind: "module",
|
|
108
|
+
filePath: "/test/strings.ts",
|
|
109
|
+
namespace: "Test",
|
|
110
|
+
className: "strings",
|
|
111
|
+
isStaticContainer: true,
|
|
112
|
+
imports: [],
|
|
113
|
+
exports: [],
|
|
114
|
+
body: [
|
|
115
|
+
{
|
|
116
|
+
kind: "variableDeclaration",
|
|
117
|
+
declarationKind: "const",
|
|
118
|
+
isExported: false,
|
|
119
|
+
declarations: [
|
|
120
|
+
{
|
|
121
|
+
kind: "variableDeclarator",
|
|
122
|
+
name: { kind: "identifierPattern", name: "words" },
|
|
123
|
+
type: {
|
|
124
|
+
kind: "referenceType",
|
|
125
|
+
name: "Array",
|
|
126
|
+
typeArguments: [{ kind: "primitiveType", name: "string" }],
|
|
127
|
+
},
|
|
128
|
+
initializer: {
|
|
129
|
+
kind: "array",
|
|
130
|
+
elements: [
|
|
131
|
+
{ kind: "literal", value: "hello" },
|
|
132
|
+
{ kind: "literal", value: "world" },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const code = emitModule(module);
|
|
142
|
+
|
|
143
|
+
// Should use string type parameter
|
|
144
|
+
expect(code).to.include(
|
|
145
|
+
"new global::System.Collections.Generic.List<string>"
|
|
146
|
+
);
|
|
147
|
+
expect(code).to.include('"hello", "world"');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should emit empty array", () => {
|
|
151
|
+
const module: IrModule = {
|
|
152
|
+
kind: "module",
|
|
153
|
+
filePath: "/test/empty.ts",
|
|
154
|
+
namespace: "Test",
|
|
155
|
+
className: "empty",
|
|
156
|
+
isStaticContainer: true,
|
|
157
|
+
imports: [],
|
|
158
|
+
exports: [],
|
|
159
|
+
body: [
|
|
160
|
+
{
|
|
161
|
+
kind: "variableDeclaration",
|
|
162
|
+
declarationKind: "const",
|
|
163
|
+
isExported: false,
|
|
164
|
+
declarations: [
|
|
165
|
+
{
|
|
166
|
+
kind: "variableDeclarator",
|
|
167
|
+
name: { kind: "identifierPattern", name: "empty" },
|
|
168
|
+
type: {
|
|
169
|
+
kind: "referenceType",
|
|
170
|
+
name: "Array",
|
|
171
|
+
typeArguments: [{ kind: "primitiveType", name: "number" }],
|
|
172
|
+
},
|
|
173
|
+
initializer: {
|
|
174
|
+
kind: "array",
|
|
175
|
+
elements: [],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const code = emitModule(module);
|
|
184
|
+
|
|
185
|
+
// Should create empty array
|
|
186
|
+
expect(code).to.include(
|
|
187
|
+
"new global::System.Collections.Generic.List<double>()"
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("should emit array method calls correctly", () => {
|
|
192
|
+
const module: IrModule = {
|
|
193
|
+
kind: "module",
|
|
194
|
+
filePath: "/test/methods.ts",
|
|
195
|
+
namespace: "Test",
|
|
196
|
+
className: "methods",
|
|
197
|
+
isStaticContainer: true,
|
|
198
|
+
imports: [],
|
|
199
|
+
exports: [],
|
|
200
|
+
body: [
|
|
201
|
+
{
|
|
202
|
+
kind: "variableDeclaration",
|
|
203
|
+
declarationKind: "const",
|
|
204
|
+
isExported: false,
|
|
205
|
+
declarations: [
|
|
206
|
+
{
|
|
207
|
+
kind: "variableDeclarator",
|
|
208
|
+
name: { kind: "identifierPattern", name: "arr" },
|
|
209
|
+
initializer: {
|
|
210
|
+
kind: "array",
|
|
211
|
+
elements: [
|
|
212
|
+
{ kind: "literal", value: 1 },
|
|
213
|
+
{ kind: "literal", value: 2 },
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
kind: "expressionStatement",
|
|
221
|
+
expression: {
|
|
222
|
+
kind: "call",
|
|
223
|
+
callee: {
|
|
224
|
+
kind: "memberAccess",
|
|
225
|
+
object: { kind: "identifier", name: "arr" },
|
|
226
|
+
property: "push",
|
|
227
|
+
isComputed: false,
|
|
228
|
+
isOptional: false,
|
|
229
|
+
},
|
|
230
|
+
arguments: [{ kind: "literal", value: 3 }],
|
|
231
|
+
isOptional: false,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const code = emitModule(module);
|
|
238
|
+
|
|
239
|
+
// Should call push method on array instance - C# implicitly converts int to double
|
|
240
|
+
expect(code).to.include("arr.push(3)");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should handle array element access", () => {
|
|
244
|
+
const module: IrModule = {
|
|
245
|
+
kind: "module",
|
|
246
|
+
filePath: "/test/access.ts",
|
|
247
|
+
namespace: "Test",
|
|
248
|
+
className: "access",
|
|
249
|
+
isStaticContainer: true,
|
|
250
|
+
imports: [],
|
|
251
|
+
exports: [],
|
|
252
|
+
body: [
|
|
253
|
+
{
|
|
254
|
+
kind: "variableDeclaration",
|
|
255
|
+
declarationKind: "const",
|
|
256
|
+
isExported: false,
|
|
257
|
+
declarations: [
|
|
258
|
+
{
|
|
259
|
+
kind: "variableDeclarator",
|
|
260
|
+
name: { kind: "identifierPattern", name: "arr" },
|
|
261
|
+
initializer: {
|
|
262
|
+
kind: "array",
|
|
263
|
+
elements: [{ kind: "literal", value: 10 }],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
kind: "variableDeclaration",
|
|
270
|
+
declarationKind: "const",
|
|
271
|
+
isExported: false,
|
|
272
|
+
declarations: [
|
|
273
|
+
{
|
|
274
|
+
kind: "variableDeclarator",
|
|
275
|
+
name: { kind: "identifierPattern", name: "first" },
|
|
276
|
+
initializer: {
|
|
277
|
+
kind: "memberAccess",
|
|
278
|
+
object: {
|
|
279
|
+
kind: "identifier",
|
|
280
|
+
name: "arr",
|
|
281
|
+
inferredType: {
|
|
282
|
+
kind: "arrayType",
|
|
283
|
+
elementType: { kind: "primitiveType", name: "number" },
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
property: { kind: "literal", value: 0 },
|
|
287
|
+
isComputed: true,
|
|
288
|
+
isOptional: false,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const code = emitModule(module);
|
|
297
|
+
|
|
298
|
+
// Should use static helper for array access with global:: prefix
|
|
299
|
+
expect(code).to.include("global::Tsonic.Runtime.Array.get(arr, 0)");
|
|
300
|
+
});
|
|
301
|
+
});
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for the Tsonic emitter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate standard file header for emitted C# files
|
|
7
|
+
*
|
|
8
|
+
* @param filePath - Source TypeScript file path
|
|
9
|
+
* @param options - Header generation options
|
|
10
|
+
* @returns Multi-line header string with trailing newline
|
|
11
|
+
*/
|
|
12
|
+
export const generateFileHeader = (
|
|
13
|
+
filePath: string,
|
|
14
|
+
options: {
|
|
15
|
+
readonly includeTimestamp?: boolean;
|
|
16
|
+
readonly timestamp?: string;
|
|
17
|
+
} = {}
|
|
18
|
+
): string => {
|
|
19
|
+
const lines: string[] = [];
|
|
20
|
+
|
|
21
|
+
lines.push(`// Generated from: ${filePath}`);
|
|
22
|
+
|
|
23
|
+
if (options.includeTimestamp ?? true) {
|
|
24
|
+
const timestamp = options.timestamp ?? new Date().toISOString();
|
|
25
|
+
lines.push(`// Generated at: ${timestamp}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
lines.push("// WARNING: Do not modify this file manually");
|
|
29
|
+
lines.push("");
|
|
30
|
+
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { IrExport } from "@tsonic/frontend";
|
|
6
|
+
import { EmitterContext, getIndent } from "../types.js";
|
|
7
|
+
import { emitExpression } from "../expression-emitter.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Emit an export declaration
|
|
11
|
+
*/
|
|
12
|
+
export const emitExport = (
|
|
13
|
+
exp: IrExport,
|
|
14
|
+
context: EmitterContext
|
|
15
|
+
): [string | null, EmitterContext] => {
|
|
16
|
+
switch (exp.kind) {
|
|
17
|
+
case "named":
|
|
18
|
+
// Named exports are handled by marking declarations as public
|
|
19
|
+
return [null, context];
|
|
20
|
+
|
|
21
|
+
case "default": {
|
|
22
|
+
// Default exports need special handling
|
|
23
|
+
// For MVP, we'll emit a comment
|
|
24
|
+
const [exprFrag, newContext] = emitExpression(exp.expression, context);
|
|
25
|
+
const ind = getIndent(context);
|
|
26
|
+
return [`${ind}// Default export: ${exprFrag.text}`, newContext];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "declaration":
|
|
30
|
+
// Export declarations are already handled in the body
|
|
31
|
+
return [null, context];
|
|
32
|
+
|
|
33
|
+
default:
|
|
34
|
+
return [null, context];
|
|
35
|
+
}
|
|
36
|
+
};
|