Skip to content

Commit b6031af

Browse files
authored
refactor(transpiler). new architecture with tests (#2713)
1 parent d443f5e commit b6031af

329 files changed

Lines changed: 23636 additions & 8868 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/scheduled_tasks.lock

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/alphatab/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOf
44
import { Logger } from '@coderline/alphatab/Logger';
55
import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput';
66

7+
/**
8+
* @target web
9+
* @internal
10+
*/
711
declare const webkitAudioContext: any;
812

913
/**
10-
* @target
14+
* @target web
1115
* @internal
1216
*/
1317
export class AlphaSynthWebAudioSynthOutputDevice implements ISynthOutputDevice {

packages/csharp/src/AlphaTab.Test/TestPlatform.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ static partial class TestPlatform
1818
var currentDir = new DirectoryInfo(System.Environment.CurrentDirectory);
1919
while (currentDir != null)
2020
{
21-
if (currentDir.GetDirectories(".git").Length == 1)
21+
var dotGit = Path.Combine(currentDir.FullName, ".git");
22+
if (Directory.Exists(dotGit) || File.Exists(dotGit))
2223
{
2324
return Path.Join(currentDir.FullName, "packages", "alphatab");
2425
}

packages/csharp/src/AlphaTab/Collections/Map.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ public Map(IEnumerable<ArrayTuple<TKey, TValue>> entries)
5858
this[entry.V0] = entry.V1;
5959
}
6060
}
61+
62+
public Map(params ArrayTuple<TKey, TValue>[] entries)
63+
{
64+
foreach (var entry in entries)
65+
{
66+
this[entry.V0] = entry.V1;
67+
}
68+
}
69+
6170
public Map(IEnumerable<KeyValuePair<TKey, TValue>> entries)
6271
{
6372
foreach (var entry in entries)

packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,11 @@ internal inline fun <reified T> List<T>.concat(other: Iterable<T>): List<T> {
374374
copy.push(other)
375375
return copy
376376
}
377+
378+
internal inline fun Throwable.cause(): Throwable? {
379+
return this.cause
380+
}
381+
382+
internal inline fun Throwable.stack(): String {
383+
return this.stackTraceToString()
384+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package alphaTab.core.ecmaScript
22

3-
import alphaTab.core.ArrayTuple
3+
import alphaTab.core.IArrayTuple
44

55
public class Record<TKey, TValue> : alphaTab.collections.Map<TKey, TValue> {
66
constructor() : super()
7-
constructor(vararg elements: ArrayTuple<TKey, TValue>) : super(elements.asIterable())
7+
constructor(vararg elements: IArrayTuple<TKey, TValue>) : super(elements.asIterable())
88
}

packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,7 @@ class NotExpector<T>(private val actual: T, private val message: String? = null)
153153
}
154154

155155
class Expector<T>(private val actual: T, private val message: String? = null) {
156-
val not
157-
get() = NotExpector(actual, message)
156+
fun not() = NotExpector(actual, message)
158157

159158
fun equal(expected: Any?, message: String? = null) {
160159
var actualToCheck = actual

packages/transpiler/biome.jsonc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
3+
"root": false,
4+
"extends": "//",
5+
"files": {
6+
"includes": [
7+
"*.ts",
8+
"src/**",
9+
"test/**",
10+
"!test/fixtures/**"
11+
]
12+
},
13+
"formatter": {
14+
"includes": [
15+
"*.ts",
16+
"src/**",
17+
"test/**",
18+
"!test/fixtures/**"
19+
]
20+
},
21+
"linter": {
22+
"includes": [
23+
"*.ts",
24+
"src/**",
25+
"test/**",
26+
"!test/fixtures/**"
27+
]
28+
}
29+
}

packages/transpiler/docs/IR.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Transpiler IR
2+
3+
The intermediate representation (IR) is the data structure the transpiler operates on between parsing TypeScript and emitting C# or Kotlin source.
4+
5+
The canonical definitions live in [`Ir.ts`](Ir.ts). Both the C# and Kotlin targets consume the same IR; per-target differences manifest only in:
6+
7+
- the printer (`CSharpAstPrinter` / `KotlinAstPrinter`),
8+
- the per-target strategy hooks declared by [`../src/csharp/TargetStrategy.ts`](../csharp/TargetStrategy.ts).
9+
10+
Import the IR via `import * as cs from '../src/ir/Ir'`. The `cs` alias is historical (the C# target was implemented first); the namespace is the canonical IR shared by all targets.
11+
12+
## Pipeline
13+
14+
The IR moves through three named stages in every emit:
15+
16+
```
17+
TypeScript source
18+
19+
20+
1. AstTransformer ── per-file walk, produces a raw IR SourceFile per TS root
21+
22+
23+
2. PassPipeline ── named whole-program passes mutate the IR in place
24+
│ (resolve-types, rewrite-visibilities, ...)
25+
26+
3. AstPrinter ── per-file walk, emits .cs/.kt text
27+
```
28+
29+
The transformer is the only stage allowed to allocate new `tsSymbol`-backed nodes from TypeScript. Passes mutate existing nodes (set flags, replace expressions, propagate up the inheritance graph). The printer is read-only over the IR.
30+
31+
## Invariants
32+
33+
The following invariants must hold whenever the IR enters the printer stage:
34+
35+
1. **No `UnresolvedTypeNode`.** Every `TypeNode` reachable from any `SourceFile` must be a concrete kind (`PrimitiveTypeNode`, `ArrayTypeNode`, `MapTypeNode`, `ArrayTupleNode`, `FunctionTypeNode`, `TypeReference`, or a `NamedTypeDeclaration`). The `resolve-types` pass enforces this; retiring `UnresolvedTypeNode` entirely is a planned follow-up.
36+
2. **Every node has a `parent`.** With one documented exception: see "Paren wrapping" below.
37+
3. **Override propagation has run.** After `rewrite-visibilities`, every method or property that overrides a virtual base must have either `isOverride: true` (set by the transformer) or, after the pass, `isVirtual: true` if it is itself an override target. The pass also sets `hasVirtualMembersOrSubClasses` on enclosing types.
38+
4. **Naming conventions applied.** All identifier strings on member-access nodes have already been routed through the target's `toMethodNameCase` / `toPropertyNameCase`; the printer does not re-case.
39+
5. **Smart-cast lowering applied.** The transformer wraps any expression that requires a runtime type narrowing through `SmartCastResolver`. Printers do no type inference of their own.
40+
41+
## Syntax and runtime support reference
42+
43+
A full catalogue of supported constructs, partial support, gaps, and runtime built-in mappings lives in [`SYNTAX.md`](SYNTAX.md). [`LIMITATIONS.md`](LIMITATIONS.md) redirects there.
44+
45+
## Paren wrapping exception
46+
47+
`paren(parent, inner, tsNode)` in [`../csharp/CSharpAstBuilder.ts`](../csharp/CSharpAstBuilder.ts) does **not** rewire `inner.parent`. Several transformer paths (smart-cast walk-up, the `_coerceIntegerBitOp` `nextParent` chain) rely on the inner expression's parent still pointing at its original context to make routing decisions. Code that wraps an expression in parens and then expects to walk up the tree from the inner expression must be aware of this.
48+
49+
## Per-target extension points
50+
51+
The [`TargetStrategy`](../csharp/TargetStrategy.ts) interface enumerates every place the IR's emitted shape can differ between targets:
52+
53+
- naming conventions (4 cases),
54+
- core type name rewriting (`toCoreTypeName`),
55+
- runtime type aliases (`make{Exception,Iterable,Iterator,Generator}Type`),
56+
- module / namespace mapping (`getDefaultUsings`),
57+
- symbol name rewrites (`getNameFromSymbol`, `getClassName`),
58+
- inheritance lookup (`getOverriddenMembers`),
59+
- identifier / module tag (`targetTag`, `alphaSkiaModule`).
60+
61+
Both `CSharpEmitterContext` and `KotlinEmitterContext` implement this interface today (inheritance-based); a planned follow-up converts the relationship to composition (`context` accepts a `TargetStrategy` field).
62+
63+
## Adding a new pass
64+
65+
1. Create a class implementing [`IrPass`](../src/passes/IrPass.ts) under `../src/passes/`.
66+
2. Add it to the pass list of the relevant emitter (`CSharpEmitter.ts` or `KotlinEmitter.ts`).
67+
3. Add a fixture under `test/fixtures/` that exercises the pass's behaviour.
68+
4. Run `npm test` to confirm snapshots still match (byte-identical output is the default; if the pass intentionally changes output, regenerate snapshots with `UPDATE_SNAPSHOTS=1` and document the change).

0 commit comments

Comments
 (0)