Frontend Package
The frontend transforms TypeScript into IR.
Entry Points
buildModuleDependencyGraph
Main entry for compiling a project:
const result = buildModuleDependencyGraph(entryPoint, {
projectRoot: "/path/to/project",
sourceRoot: "src",
rootNamespace: "MyApp",
typeRoots: ["node_modules/@tsonic/globals"],
});
if (result.ok) {
const { modules, entryModule } = result.value;
}
createProgram
Lower-level TypeScript program creation:
const result = createProgram(filePaths, options);
if (result.ok) {
const program = result.value;
}
Module Resolution
Local Imports
Local imports must have .ts extension:
// Resolved from importing file's directory
"./utils.ts" -> resolved to absolute path
"../models/User.ts" -> resolved relative
Resolution in resolver/path-resolution.ts:
const resolveLocalImport = (
specifier: string,
importingFile: string,
sourceRoot: string
): Result<string, Diagnostic> => {
// Must have .js or .ts extension
if (!specifier.endsWith(".js") && !specifier.endsWith(".ts")) {
return error(createDiagnostic("TSN1001", ...));
}
// Resolve to absolute path
const resolved = path.resolve(path.dirname(importingFile), specifier);
return ok(resolved);
};
.NET Imports
.NET imports map to CLR namespaces:
"@tsonic/dotnet/System.js" -> clrNamespace: "System"
"@tsonic/dotnet/System.IO.js" -> clrNamespace: "System.IO"
Resolution via clrResolver:
const resolveClrImport = (
specifier: string
): Result<ClrResolution, Diagnostic> => {
// Extract namespace from specifier
const namespace = specifier
.replace("@tsonic/dotnet/", "")
.replace(/\.js$/, "")
.replace(/\//g, ".");
return ok({ clrNamespace: namespace, isLocal: false });
};
Validation
Import Validation
validation/imports.ts:
- Verify
.tsextension on local imports - Verify
.jsor.tsextension on local imports - No dynamic imports
- Resolve all import specifiers
- Check module existence
Feature Validation
validation/features.ts:
const unsupportedFeatures = [
"with statement",
"import.meta",
"dynamic import()",
"Promise.then/catch/finally",
];
validation/unsupported-utility-types.ts:
Validates that unsupported utility types are not used:
- TSN7406: Mapped types (Partial, Required, Readonly, Pick, Omit)
- TSN7407: Conditional types (Extract, Exclude, NonNullable, ReturnType)
- TSN7410: Intersection types (A & B)
Export Validation
validation/exports.ts:
- Entry point must export
main() - Exported names must resolve
- Re-exports must be valid
Yield Lowering Pass
validation/yield-lowering-pass.ts:
Transforms generator functions for C# emission:
- Converts
yieldexpressions toyieldStatementIR nodes - Handles
yield*delegation - Transforms
returnin generators togeneratorReturnStatement - Captures TNext values via receive targets
// Input: const x = yield value;
// Output: yieldStatement with receiveTarget for 'x'
Numeric Proof Pass
validation/numeric-proof-pass.ts:
Recovers numeric intent from declarations:
- Detects
int,long,byte, etc. from@tsonic/core - Tracks numeric type through expressions
- Enables clean integer emission without cosmetic casts
// const x: int = 10;
// Proof: x has numeric kind 'int32'
Anonymous Type Lowering Pass
ir/validation/anonymous-type-lowering-pass.ts:
Transforms anonymous object type literals into synthesized nominal classes:
- Scans function signatures, variables, and class members for inline
{ x: T }types - Generates unique class names:
funcName_Return,funcName_paramName - Replaces inline types with references to synthesized classes
- Hoists synthesized classes to module level
// Before: function getPoint(): { x: number; y: number }
// After: function getPoint(): getPoint_Return
// class getPoint_Return { x: number; y: number }
Rest Type Synthesis Pass
ir/validation/rest-type-synthesis-pass.ts:
Handles object rest patterns ({ a, ...rest }) by synthesizing types for rest properties:
- Identifies all object destructuring patterns with rest properties
- Computes remaining members by excluding picked keys from source type
- Generates unique synthetic class names via content hashing
- Creates synthetic class declarations with computed properties
- Attaches shape info to rest properties for emitter use
// Before: const { id, ...rest } = { id: 1, name: "Alice", age: 30 }
// Synthesizes: class __Rest_xyz with { name: string; age: number }
Attribute Collection Pass
ir/validation/attribute-collection-pass.ts:
Collects C# attribute declarations from marker-call API:
- Scans for
A.on(Class).type.add(AttrType, ...args)patterns - Extracts attribute type and constructor arguments
- Attaches attribute metadata to IR class declarations
- Enables C#
[Attribute]generation
// Input: A.on(User).type.add(SerializableAttribute)
// Result: User class marked with [Serializable] in emitted C#
Generic Validation
validation/generics.ts:
Validates generic type usage for C# compatibility:
- TSN7203: Symbol keys in index signatures
- TSN7415: Nullable unions with unconstrained generics
// TSN7415: T | null where T is unconstrained
function getValue<T>(value: T | null): T; // Error
// Fix: Add constraint
function getValue<T extends object>(value: T | null): T; // OK
IR Building
Statement Conversion
ir/converters/statements/:
// TypeScript AST -> IR
const convertStatement = (
node: ts.Statement,
checker: ts.TypeChecker
): IrStatement => {
if (ts.isFunctionDeclaration(node)) {
return convertFunctionDeclaration(node, checker);
}
if (ts.isClassDeclaration(node)) {
return convertClassDeclaration(node, checker);
}
// ... other statements
};
Expression Conversion
ir/converters/expressions/:
const convertExpression = (
node: ts.Expression,
checker: ts.TypeChecker
): IrExpression => {
if (ts.isNumericLiteral(node)) {
return { kind: "literal", value: Number(node.text), raw: node.text };
}
if (ts.isBinaryExpression(node)) {
return convertBinaryExpression(node, checker);
}
// ... other expressions
};
Anonymous Object Synthesis
ir/converters/anonymous-synthesis.ts:
Object literals without explicit type annotations auto-synthesize nominal types:
// Input: const point = { x: 10, y: 20 };
// Synthesizes: class __Anon_File_Line_Col with x, y properties
Eligible patterns: property assignments, shorthand properties, arrow functions. Ineligible patterns: method shorthand, getters/setters.
Type Conversion
ir/type-converter/:
const convertType = (node: ts.TypeNode, checker: ts.TypeChecker): IrType => {
if (ts.isTypeReferenceNode(node)) {
return convertReferenceType(node, checker);
}
if (ts.isArrayTypeNode(node)) {
return {
kind: "arrayType",
elementType: convertType(node.elementType, checker),
};
}
// ... other types
};
ir/type-converter/inference.ts:
Lambda parameter types are contextually inferred from surrounding context:
// numbers.map(n => n * 2)
// 'n' type is inferred from array element type
Uses checker.getContextualType() to extract types from call expressions.
Dependency Graph
Graph Building
program/dependency-graph.ts:
const buildDependencyGraph = (
entryPoint: string,
options: CompilerOptions
): Result<DependencyGraph, Diagnostic[]> => {
const visited = new Set<string>();
const modules: IrModule[] = [];
const visit = (filePath: string) => {
if (visited.has(filePath)) return;
visited.add(filePath);
const module = buildIrModule(filePath, ...);
modules.push(module);
// Visit imports
for (const imp of module.imports) {
if (imp.resolved?.isLocal) {
visit(imp.resolved.absolutePath);
}
}
};
visit(entryPoint);
return ok({ modules, entryModule: modules[0] });
};
Circular Dependency Detection
const detectCircular = (modules: IrModule[]): string[][] => {
// Tarjan's algorithm for SCC detection
// Returns groups of circular dependencies
};
Symbol Table
Symbol Tracking
symbol-table/:
type SymbolEntry = {
name: string;
kind: "variable" | "function" | "class" | "interface" | "type";
type?: IrType;
exported: boolean;
};
type SymbolTable = {
entries: Map<string, SymbolEntry>;
parent?: SymbolTable;
};
Symbol Resolution
const resolveSymbol = (
name: string,
table: SymbolTable
): SymbolEntry | undefined => {
const entry = table.entries.get(name);
if (entry) return entry;
if (table.parent) return resolveSymbol(name, table.parent);
return undefined;
};
Diagnostics
Creating Diagnostics
import { createDiagnostic, addDiagnostic } from "./types/diagnostic.js";
const diagnostic = createDiagnostic(
"TSN1001",
"error",
"Local imports must use .js or .ts extension",
{ file: sourceFile.fileName, line: 10, column: 5, length: 20 },
"Add a .js extension to the import path (recommended)"
);
collector = addDiagnostic(collector, diagnostic);
Diagnostic Codes
| Code | Category | Description |
|---|---|---|
| TSN1001 | Module | Missing file extension |
| TSN1002 | Module | Cannot resolve module |
| TSN2001 | Feature | Unsupported feature |
| TSN3001 | Type | Type error |