diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock deleted file mode 100644 index a17d52712..000000000 --- a/.claude/scheduled_tasks.lock +++ /dev/null @@ -1 +0,0 @@ -{"sessionId":"ddc5edd3-51f2-4690-ada7-4273b04ae0c6","pid":2972655,"procStart":"43393170","acquiredAt":1778849010756} \ No newline at end of file diff --git a/packages/alphatab/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts b/packages/alphatab/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts index c509256cf..69cb50e41 100644 --- a/packages/alphatab/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts +++ b/packages/alphatab/src/platform/javascript/AlphaSynthWebAudioOutputBase.ts @@ -4,10 +4,14 @@ import { EventEmitter, EventEmitterOfT, type IEventEmitter, type IEventEmitterOf import { Logger } from '@coderline/alphatab/Logger'; import type { ISynthOutput, ISynthOutputDevice } from '@coderline/alphatab/synth/ISynthOutput'; +/** + * @target web + * @internal + */ declare const webkitAudioContext: any; /** - * @target + * @target web * @internal */ export class AlphaSynthWebAudioSynthOutputDevice implements ISynthOutputDevice { diff --git a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs index 1115b5acd..9f3723529 100644 --- a/packages/csharp/src/AlphaTab.Test/TestPlatform.cs +++ b/packages/csharp/src/AlphaTab.Test/TestPlatform.cs @@ -18,7 +18,8 @@ static partial class TestPlatform var currentDir = new DirectoryInfo(System.Environment.CurrentDirectory); while (currentDir != null) { - if (currentDir.GetDirectories(".git").Length == 1) + var dotGit = Path.Combine(currentDir.FullName, ".git"); + if (Directory.Exists(dotGit) || File.Exists(dotGit)) { return Path.Join(currentDir.FullName, "packages", "alphatab"); } diff --git a/packages/csharp/src/AlphaTab/Collections/Map.cs b/packages/csharp/src/AlphaTab/Collections/Map.cs index 30bf4d50b..13ed0af9b 100644 --- a/packages/csharp/src/AlphaTab/Collections/Map.cs +++ b/packages/csharp/src/AlphaTab/Collections/Map.cs @@ -58,6 +58,15 @@ public Map(IEnumerable> entries) this[entry.V0] = entry.V1; } } + + public Map(params ArrayTuple[] entries) + { + foreach (var entry in entries) + { + this[entry.V0] = entry.V1; + } + } + public Map(IEnumerable> entries) { foreach (var entry in entries) diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt index 679418d08..763569473 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/Globals.kt @@ -374,3 +374,11 @@ internal inline fun List.concat(other: Iterable): List { copy.push(other) return copy } + +internal inline fun Throwable.cause(): Throwable? { + return this.cause +} + +internal inline fun Throwable.stack(): String { + return this.stackTraceToString() +} diff --git a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Record.kt b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Record.kt index 9f6d96200..f62dc53e1 100644 --- a/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Record.kt +++ b/packages/kotlin/src/android/src/main/java/alphaTab/core/ecmaScript/Record.kt @@ -1,8 +1,8 @@ package alphaTab.core.ecmaScript -import alphaTab.core.ArrayTuple +import alphaTab.core.IArrayTuple public class Record : alphaTab.collections.Map { constructor() : super() - constructor(vararg elements: ArrayTuple) : super(elements.asIterable()) + constructor(vararg elements: IArrayTuple) : super(elements.asIterable()) } diff --git a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt index 17972d74e..3453ebb15 100644 --- a/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt +++ b/packages/kotlin/src/android/src/test/java/alphaTab/core/TestGlobals.kt @@ -153,8 +153,7 @@ class NotExpector(private val actual: T, private val message: String? = null) } class Expector(private val actual: T, private val message: String? = null) { - val not - get() = NotExpector(actual, message) + fun not() = NotExpector(actual, message) fun equal(expected: Any?, message: String? = null) { var actualToCheck = actual diff --git a/packages/transpiler/biome.jsonc b/packages/transpiler/biome.jsonc new file mode 100644 index 000000000..e7843a406 --- /dev/null +++ b/packages/transpiler/biome.jsonc @@ -0,0 +1,29 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "root": false, + "extends": "//", + "files": { + "includes": [ + "*.ts", + "src/**", + "test/**", + "!test/fixtures/**" + ] + }, + "formatter": { + "includes": [ + "*.ts", + "src/**", + "test/**", + "!test/fixtures/**" + ] + }, + "linter": { + "includes": [ + "*.ts", + "src/**", + "test/**", + "!test/fixtures/**" + ] + } +} diff --git a/packages/transpiler/docs/IR.md b/packages/transpiler/docs/IR.md new file mode 100644 index 000000000..625ef53b8 --- /dev/null +++ b/packages/transpiler/docs/IR.md @@ -0,0 +1,68 @@ +# Transpiler IR + +The intermediate representation (IR) is the data structure the transpiler operates on between parsing TypeScript and emitting C# or Kotlin source. + +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: + +- the printer (`CSharpAstPrinter` / `KotlinAstPrinter`), +- the per-target strategy hooks declared by [`../src/csharp/TargetStrategy.ts`](../csharp/TargetStrategy.ts). + +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. + +## Pipeline + +The IR moves through three named stages in every emit: + +``` +TypeScript source + │ + ▼ +1. AstTransformer ── per-file walk, produces a raw IR SourceFile per TS root + │ + ▼ +2. PassPipeline ── named whole-program passes mutate the IR in place + │ (resolve-types, rewrite-visibilities, ...) + ▼ +3. AstPrinter ── per-file walk, emits .cs/.kt text +``` + +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. + +## Invariants + +The following invariants must hold whenever the IR enters the printer stage: + +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. +2. **Every node has a `parent`.** With one documented exception: see "Paren wrapping" below. +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. +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. +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. + +## Syntax and runtime support reference + +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. + +## Paren wrapping exception + +`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. + +## Per-target extension points + +The [`TargetStrategy`](../csharp/TargetStrategy.ts) interface enumerates every place the IR's emitted shape can differ between targets: + +- naming conventions (4 cases), +- core type name rewriting (`toCoreTypeName`), +- runtime type aliases (`make{Exception,Iterable,Iterator,Generator}Type`), +- module / namespace mapping (`getDefaultUsings`), +- symbol name rewrites (`getNameFromSymbol`, `getClassName`), +- inheritance lookup (`getOverriddenMembers`), +- identifier / module tag (`targetTag`, `alphaSkiaModule`). + +Both `CSharpEmitterContext` and `KotlinEmitterContext` implement this interface today (inheritance-based); a planned follow-up converts the relationship to composition (`context` accepts a `TargetStrategy` field). + +## Adding a new pass + +1. Create a class implementing [`IrPass`](../src/passes/IrPass.ts) under `../src/passes/`. +2. Add it to the pass list of the relevant emitter (`CSharpEmitter.ts` or `KotlinEmitter.ts`). +3. Add a fixture under `test/fixtures/` that exercises the pass's behaviour. +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). diff --git a/packages/transpiler/docs/SYNTAX.md b/packages/transpiler/docs/SYNTAX.md new file mode 100644 index 000000000..18aaa1d17 --- /dev/null +++ b/packages/transpiler/docs/SYNTAX.md @@ -0,0 +1,223 @@ +# Transpiler syntax and runtime support reference + +Cross-check this document before writing TypeScript that will be transpiled to C# and/or Kotlin. + +**Status values:** `✅ yes` · `⚠️ partial` · `❌ no` + +--- + +## Top-level declarations + +| Construct | Status | Notes | +|-----------|--------|-------| +| `class` (concrete, abstract) | ✅ yes | Fields, methods, constructors, `extends`, `implements`; generic type parameters with optional constraints | +| `interface` | ✅ yes | Extends multiple interfaces; generic type parameters | +| `enum` (numeric) | ✅ yes | Members may have explicit numeric initialisers | +| `enum` (string-initialised members) | ❌ no | Transformer passes string initialisers through unguarded → invalid C#/Kotlin. Use numeric enums only. | +| `type` alias — function shape | ✅ yes | Emitted as a delegate | +| `type` alias — `T \| null` / `T \| undefined` | ✅ yes | Emitted as nullable `T` | +| `type` alias — `@discriminated` union | ✅ yes | Base interface + one class per member with discriminator field | +| `type` alias — union of distinct non-null types | ⚠️ partial | Collapses to `object` with warning. Workaround: common base class/interface or `@discriminated`. | +| `type` alias — intersection (`T & U`) | ⚠️ partial | `T & {}` (NonNullable) works; multiple distinct operands → `object` | +| `type` alias — inline type literal (`{ a: number }`) | ❌ no | `Unsupported internal type of kind TypeLiteral`. Workaround: named class/interface. | +| `import` declaration | ✅ yes | Consumed for symbol resolution; not emitted | +| `export default class/interface/enum` | ✅ yes | Becomes the namespace member; `@public` JsDoc overrides visibility | +| Named export in the same file | ✅ yes | Added as sibling in the same namespace | +| Global variable declaration | ❌ no | `Global statements in modules are not yet supported`. Workaround: wrap in a static class. | +| Global function declaration | ❌ no | Same error as global variable. Workaround: static method on a class. | +| Namespace / module merging | ❌ no | Workaround: flat file-per-class layout | + +--- + +## Class members + +| Construct | Status | Notes | +|-----------|--------|-------| +| Property (`public`/`protected`/`private`, `readonly`, `static`, initialiser) | ✅ yes | `readonly` honoured; `static readonly` → `const` candidate | +| Property accessor pair (`get` + `set`) | ✅ yes | C# property / Kotlin `var` | +| Get-only accessor | ✅ yes | C# computed property / Kotlin `val` | +| Method (`public`/`protected`/`private`, `static`, `abstract`, `override`) | ✅ yes | Generic type parameters supported | +| Constructor | ✅ yes | Parameters with optional defaults; calls `super()` | +| `async` method | ✅ yes | `Task` / Kotlin `suspend fun` | +| Generator method (`function*` / `yield`) | ✅ yes | `IEnumerator` / Kotlin `Iterator` | +| Generator `TReturn` type parameter | ⚠️ partial | `yield` works; return type collapses to `IEnumerator` | +| `@partial` tag | ✅ yes | `partial class` in C# / `Partials` companion in Kotlin | +| `@target csharp` / `@target kotlin` / `/*@target web*/` | ✅ yes | Conditionally skips emit on the non-matching target | +| `@delegated` tag | ✅ yes | Member body delegated to a per-target hand-written partial | +| `@lateinit` tag (Kotlin only) | ✅ yes | Property emitted as `lateinit var` | + +--- + +## Statements + +| Construct | Status | Notes | +|-----------|--------|-------| +| Block `{ }` | ✅ yes | | +| `var` declaration | ✅ yes | Maps to mutable binding | +| `let` declaration | ✅ yes | Maps to mutable binding | +| `const` declaration | ✅ yes | Maps to mutable binding; does not produce a `readonly` member | +| Array destructuring in variable declaration | ✅ yes | `const [a, b] = expr` → tuple deconstruct | +| Object destructuring in variable declaration | ❌ no | Workaround: temporary variable + explicit property access | +| Rest/spread destructuring | ❌ no | Workaround: manual decomposition. Spread in call args (`...args`) is supported. | +| `if` / `else` | ✅ yes | | +| `do…while` loop | ✅ yes | | +| `while` loop | ✅ yes | | +| `for` loop (C-style) | ✅ yes | | +| `for…of` loop | ✅ yes | Arrays and iterables; initialiser may be a new variable or existing variable | +| `for…of` with destructuring initialiser | ❌ no | Workaround: destructure manually inside the loop body | +| `for…in` loop | ✅ yes | Over object keys; emitted as `foreach` over keys | +| `switch` / `case` / `default` | ✅ yes | Implicit fallthrough preserved (may require explicit `goto case` in C#) | +| `break` (unlabelled) | ✅ yes | | +| `continue` (unlabelled) | ✅ yes | | +| `break