From 8ff13cd38c2c0643f1abd7849eec7faff7fc929c Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 14:46:28 -0400 Subject: [PATCH 01/25] chore: update my email --- NOTICE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NOTICE b/NOTICE index 148bc0ca87..da2cb563d4 100644 --- a/NOTICE +++ b/NOTICE @@ -51,7 +51,7 @@ under the licensing terms detailed in LICENSE: * Adrien Zinger * Ruixiang Chen * Daniel Salvadori -* Jairus Tanaka +* Jairus Tanaka * CountBleck * Abdul Rauf * Bach Le From 19cacf91ef5d4d3cce053033dfff8b58f60d76c9 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 14:47:29 -0400 Subject: [PATCH 02/25] chore(cli): enable `--enable multi-value` flag --- cli/options.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/options.json b/cli/options.json index f6776dbe26..a077a3d4c6 100644 --- a/cli/options.json +++ b/cli/options.json @@ -222,12 +222,12 @@ " gc Garbage collection (WIP).", " stringref String reference types.", " relaxed-simd Relaxed SIMD operations.", + " multi-value Multi value types.", "" ], "TODO_doesNothingYet": [ " exception-handling Exception handling.", " tail-calls Tail call operations.", - " multi-value Multi value types.", " memory64 Memory64 operations.", " extended-const Extended const expressions." ], From 4527d65a42e2e78019d3c3af5f27674f7cb82e63 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 14:47:57 -0400 Subject: [PATCH 03/25] feat: wire up support for parsing and resolving tuples --- src/ast.ts | 29 +++++++++++++++++++++++++++++ src/extra/ast.ts | 25 +++++++++++++++++++++++++ src/parser.ts | 42 +++++++++++++++++++++++++++++++++++++++++- src/resolver.ts | 31 +++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/ast.ts b/src/ast.ts index 01d8e9a421..76476e22ba 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -50,6 +50,7 @@ export const enum NodeKind { // types NamedType, FunctionType, + TupleType, TypeName, TypeParameter, Parameter, @@ -161,6 +162,14 @@ export abstract class Node { return new FunctionTypeNode(parameters, returnType, explicitThisType, isNullable, range); } + static createTupleType( + elements: TypeNode[], + isNullable: bool, + range: Range + ): TupleTypeNode { + return new TupleTypeNode(elements, isNullable, range); + } + static createOmittedType( range: Range ): NamedTypeNode { @@ -862,6 +871,12 @@ export abstract class TypeNode extends Node { if (functionTypeNode.returnType.hasGenericComponent(typeParameterNodes)) return true; let explicitThisType = functionTypeNode.explicitThisType; if (explicitThisType && explicitThisType.hasGenericComponent(typeParameterNodes)) return true; + } else if (this.kind == NodeKind.TupleType) { + let tupleTypeNode = changetype(this); + let elements = tupleTypeNode.elements; + for (let i = 0, k = elements.length; i < k; ++i) { + if (elements[i].hasGenericComponent(typeParameterNodes)) return true; + } } else { assert(false); } @@ -928,6 +943,20 @@ export class FunctionTypeNode extends TypeNode { } } +/** Represents a tuple type. */ +export class TupleTypeNode extends TypeNode { + constructor( + /** Tuple elements. */ + public elements: TypeNode[], + /** Whether nullable or not. */ + isNullable: bool, + /** Source range. */ + range: Range + ) { + super(NodeKind.TupleType, isNullable, range); + } +} + /** Represents a type parameter. */ export class TypeParameterNode extends Node { constructor( diff --git a/src/extra/ast.ts b/src/extra/ast.ts index ebe9217f90..f163db37d4 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -15,6 +15,7 @@ import { TypeNode, NamedTypeNode, FunctionTypeNode, + TupleTypeNode, TypeName, TypeParameterNode, @@ -134,6 +135,10 @@ export class ASTBuilder { this.visitFunctionTypeNode(node); break; } + case NodeKind.TupleType: { + this.visitTupleTypeNode(node); + break; + } case NodeKind.TypeParameter: { this.visitTypeParameter(node); break; @@ -387,6 +392,10 @@ export class ASTBuilder { this.visitFunctionTypeNode(node); break; } + case NodeKind.TupleType: { + this.visitTupleTypeNode(node); + break; + } default: assert(false); } } @@ -450,6 +459,22 @@ export class ASTBuilder { if (isNullable) sb.push(") | null"); } + visitTupleTypeNode(node: TupleTypeNode): void { + let sb = this.sb; + sb.push("["); + let elements = node.elements; + let numElements = elements.length; + if (numElements) { + this.visitTypeNode(elements[0]); + for (let i = 1; i < numElements; ++i) { + sb.push(", "); + this.visitTypeNode(elements[i]); + } + } + sb.push("]"); + if (node.isNullable) sb.push(" | null"); + } + visitTypeParameter(node: TypeParameterNode): void { this.visitIdentifierExpression(node.name); let extendsType = node.extendsType; diff --git a/src/parser.ts b/src/parser.ts index 7c69843973..8434a5a866 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -42,6 +42,7 @@ import { TypeName, NamedTypeNode, FunctionTypeNode, + TupleTypeNode, ArrowKind, Expression, @@ -509,8 +510,19 @@ export class Parser extends DiagnosticEmitter { let type: TypeNode; + // 'readonly' Type + if (token == Token.Readonly) { + let innerType = this.parseType(tn, acceptParenthesized, suppressErrors); + if (!innerType) return null; + type = Node.createNamedType( + Node.createSimpleTypeName("Readonly", tn.range(startPos, tn.pos)), + [ innerType ], + false, + tn.range(startPos, tn.pos) + ); + // '(' ... - if (token == Token.OpenParen) { + } else if (token == Token.OpenParen) { // '(' FunctionSignature ')' let isInnerParenthesized = tn.skip(Token.OpenParen); @@ -563,6 +575,27 @@ export class Parser extends DiagnosticEmitter { return null; } + // '[' Type (',' Type)* ']' + } else if (token == Token.OpenBracket) { + let elements: TypeNode[] = []; + if (!tn.skip(Token.CloseBracket)) { + do { + let element = this.parseType(tn, true, suppressErrors); + if (!element) return null; + elements.push(element); + } while (tn.skip(Token.Comma)); + if (!tn.skip(Token.CloseBracket)) { + if (!suppressErrors) { + this.error( + DiagnosticCode._0_expected, + tn.range(tn.pos), "]" + ); + } + return null; + } + } + type = Node.createTupleType(elements, false, tn.range(startPos, tn.pos)); + // 'void' } else if (token == Token.Void) { type = Node.createNamedType( @@ -4581,6 +4614,13 @@ function isCircularTypeAlias(name: string, type: TypeNode): bool { } break; } + case NodeKind.TupleType: { + let elements = (type).elements; + for (let i = 0, k = elements.length; i < k; i++) { + if (isCircularTypeAlias(name, elements[i])) return true; + } + break; + } default: assert(false); } return false; diff --git a/src/resolver.ts b/src/resolver.ts index 2a4df6c3e7..2b3b4d49e8 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -45,6 +45,7 @@ import { import { FunctionTypeNode, + TupleTypeNode, ParameterKind, TypeNode, NodeKind, @@ -171,6 +172,10 @@ export class Resolver extends DiagnosticEmitter { resolved = this.resolveFunctionType(node, flow, ctxElement, ctxTypes, reportMode); break; } + case NodeKind.TupleType: { + resolved = this.resolveTupleType(node, flow, ctxElement, ctxTypes, reportMode); + break; + } default: assert(false); } node.currentlyResolving = false; @@ -452,6 +457,32 @@ export class Resolver extends DiagnosticEmitter { return node.isNullable ? signature.type.asNullable() : signature.type; } + /** Resolves a {@link TupleTypeNode}. */ + private resolveTupleType( + /** The type to resolve. */ + node: TupleTypeNode, + /** The flow */ + flow: Flow | null, + /** Contextual element. */ + ctxElement: Element, + /** Contextual types, i.e. `T`. */ + ctxTypes: Map | null = null, + /** How to proceed with eventual diagnostics. */ + reportMode: ReportMode = ReportMode.Report + ): Type | null { + let elements = node.elements; + for (let i = 0, k = elements.length; i < k; ++i) { + if (!this.resolveType(elements[i], flow, ctxElement, ctxTypes, reportMode)) return null; + } + if (reportMode == ReportMode.Report) { + this.error( + DiagnosticCode.Not_implemented_0, + node.range, "Tuple types" + ); + } + return null; + } + private resolveBuiltinNotNullableType( /** The type to resolve. */ node: NamedTypeNode, From c53e6598b9f78d92f73b802499c48d65d0e53fbb Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 14:48:19 -0400 Subject: [PATCH 04/25] tests: add tests for tuples --- tests/compiler/tuple-circular.json | 6 ++++ tests/compiler/tuple-circular.ts | 1 + tests/compiler/tuple-type-errors.json | 6 ++++ tests/compiler/tuple-type-errors.ts | 37 +++++++++++++++++++++++++ tests/compiler/tuple-type.json | 6 ++++ tests/compiler/tuple-type.ts | 1 + tests/parser/tuple-errors.ts | 3 ++ tests/parser/tuple-errors.ts.fixture.ts | 3 ++ tests/parser/tuple.ts | 16 +++++++++++ tests/parser/tuple.ts.fixture.ts | 36 ++++++++++++++++++++++++ 10 files changed, 115 insertions(+) create mode 100644 tests/compiler/tuple-circular.json create mode 100644 tests/compiler/tuple-circular.ts create mode 100644 tests/compiler/tuple-type-errors.json create mode 100644 tests/compiler/tuple-type-errors.ts create mode 100644 tests/compiler/tuple-type.json create mode 100644 tests/compiler/tuple-type.ts create mode 100644 tests/parser/tuple-errors.ts create mode 100644 tests/parser/tuple-errors.ts.fixture.ts create mode 100644 tests/parser/tuple.ts create mode 100644 tests/parser/tuple.ts.fixture.ts diff --git a/tests/compiler/tuple-circular.json b/tests/compiler/tuple-circular.json new file mode 100644 index 0000000000..91f5d9a68d --- /dev/null +++ b/tests/compiler/tuple-circular.json @@ -0,0 +1,6 @@ +{ + "stderr": [ + "TS2456: Type alias 'Loop' circularly references itself.", + "1 parse error(s)" + ] +} diff --git a/tests/compiler/tuple-circular.ts b/tests/compiler/tuple-circular.ts new file mode 100644 index 0000000000..dd016d7679 --- /dev/null +++ b/tests/compiler/tuple-circular.ts @@ -0,0 +1 @@ +type Loop = [Loop, i32]; diff --git a/tests/compiler/tuple-type-errors.json b/tests/compiler/tuple-type-errors.json new file mode 100644 index 0000000000..06dd8d1b42 --- /dev/null +++ b/tests/compiler/tuple-type-errors.json @@ -0,0 +1,6 @@ +{ + "stderr": [ + "AS100: Not implemented: Tuple types", + "13 compile error(s)" + ] +} diff --git a/tests/compiler/tuple-type-errors.ts b/tests/compiler/tuple-type-errors.ts new file mode 100644 index 0000000000..ae083d9c18 --- /dev/null +++ b/tests/compiler/tuple-type-errors.ts @@ -0,0 +1,37 @@ +export type TupleTypeUnimplemented1 = []; +export type TupleTypeUnimplemented2 = [i32]; +export type TupleTypeUnimplemented3 = [i32, []]; +export type TupleTypeUnimplemented4 = [i32, TupleTypeUnimplemented1]; +export type TupleTypeUnimplemented5 = [string, i32]; +export type TupleTypeUnimplemented6 = [i32[], [i32]]; +export type TupleTypeUnimplemented7 = [i32, i32]; + +export function TupleParamUnimplemented1(x: []): void { } +export function TupleParamUnimplemented2(x: [i32]): void { } +export function TupleParamUnimplemented3(x: [i32, []]): void { } +export function TupleParamUnimplemented4(x: [i32, TupleTypeUnimplemented1]): void { } +export function TupleParamUnimplemented5(x: [string, i32]): void { } +export function TupleParamUnimplemented6(x: [i32[], [i32]]): void { } +export function TupleParamUnimplemented7(x: [i32, i32]): void { } + +export function TupleReturnUnimplemented1(): [] { + return []; +} +export function TupleReturnUnimplemented2(): [i32] { + return [0]; +} +export function TupleReturnUnimplemented3(): [i32, []] { + return [0, []]; +} +export function TupleReturnUnimplemented4(): [i32, TupleTypeUnimplemented1] { + return [0, []]; +} +export function TupleReturnUnimplemented5(): [string, i32] { + return ["foo", 0]; +} +export function TupleReturnUnimplemented6(): [i32[], [i32]] { + return [new Array(), [0]]; +} +export function TupleReturnUnimplemented7(): [i32, i32] { + return [0, 1]; +} diff --git a/tests/compiler/tuple-type.json b/tests/compiler/tuple-type.json new file mode 100644 index 0000000000..d6e0c18067 --- /dev/null +++ b/tests/compiler/tuple-type.json @@ -0,0 +1,6 @@ +{ + "asc_flags": [ + ], + "stderr": [ + ] +} diff --git a/tests/compiler/tuple-type.ts b/tests/compiler/tuple-type.ts new file mode 100644 index 0000000000..8cf62aeb86 --- /dev/null +++ b/tests/compiler/tuple-type.ts @@ -0,0 +1 @@ +// nothing yet just getting the parser working first diff --git a/tests/parser/tuple-errors.ts b/tests/parser/tuple-errors.ts new file mode 100644 index 0000000000..ecfed92541 --- /dev/null +++ b/tests/parser/tuple-errors.ts @@ -0,0 +1,3 @@ +export type Loop = [Loop, i32]; +export type BadTuple = [i32, ]; +export function badReadonly(x: readonly): void {} diff --git a/tests/parser/tuple-errors.ts.fixture.ts b/tests/parser/tuple-errors.ts.fixture.ts new file mode 100644 index 0000000000..d4ae1a5762 --- /dev/null +++ b/tests/parser/tuple-errors.ts.fixture.ts @@ -0,0 +1,3 @@ +// ERROR 2456: "Type alias 'Loop' circularly references itself." in tuple-errors.ts(1,13+4) +// ERROR 1110: "Type expected." in tuple-errors.ts(2,30+1) +// ERROR 1110: "Type expected." in tuple-errors.ts(3,40+1) diff --git a/tests/parser/tuple.ts b/tests/parser/tuple.ts new file mode 100644 index 0000000000..a6947118c8 --- /dev/null +++ b/tests/parser/tuple.ts @@ -0,0 +1,16 @@ +function tuple1(): [i32, i32] { return [1, 2]; } +function tuple2(): [i32, [i32, i32]] { return [1, [2, 3]]; } +function tuple3(): [i32, string] { return [1, "a"]; } +function tuple4(): [Array, i32[]] { return [new Array(), [1, 2]]; } +function tuple5(): [i32] { return [1]; } +function tuple6(): [[i32[]]] { return [[[1, 2]]]; } + +function func1(a: i32, b: i32): [i32, i32] { return [a, b]; } +function func2(x: [i32, i32]): [i32, i32] { return x; } +function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { return x; } +function func4(x: readonly [i32, string]): [void] { return [void(0)]; } +function func5(x: readonly [Array, i32[]]): readonly [i32] { return [x[1].length]; } + +type type1 = [i32, i32]; +type type2 = [i32, [i32, i32]]; +type type3 = readonly [i32, string]; diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts new file mode 100644 index 0000000000..4b7c19d509 --- /dev/null +++ b/tests/parser/tuple.ts.fixture.ts @@ -0,0 +1,36 @@ +function tuple1(): [i32, i32] { + return [1, 2]; +} +function tuple2(): [i32, [i32, i32]] { + return [1, [2, 3]]; +} +function tuple3(): [i32, string] { + return [1, "a"]; +} +function tuple4(): [Array, Array] { + return [new Array(), [1, 2]]; +} +function tuple5(): [i32] { + return [1]; +} +function tuple6(): [[Array]] { + return [[[1, 2]]]; +} +function func1(a: i32, b: i32): [i32, i32] { + return [a, b]; +} +function func2(x: [i32, i32]): [i32, i32] { + return x; +} +function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { + return x; +} +function func4(x: Readonly<[i32, string]>): [void] { + return [void(0)]; +} +function func5(x: Readonly<[Array, Array]>): Readonly<[i32]> { + return [x[1].length]; +} +type type1 = [i32, i32]; +type type2 = [i32, [i32, i32]]; +type type3 = Readonly<[i32, string]>; From 6c869a5781468fdeead47a98c6b891e474327cdf Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 17:45:26 -0400 Subject: [PATCH 05/25] tests: organize a bit better and cover more cases --- tests/compiler/tuple-circular.json | 3 +++ tests/compiler/tuple-errors.json | 9 ++++++++ .../{tuple-type-errors.ts => tuple-errors.ts} | 23 +++++++++++++++++++ tests/compiler/tuple-type-errors.json | 6 ----- tests/compiler/tuple-type.json | 1 + tests/parser.js | 18 ++++++++++++--- tests/parser/tuple.ts | 5 ++++ tests/parser/tuple.ts.fixture.ts | 9 ++++++++ 8 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 tests/compiler/tuple-errors.json rename tests/compiler/{tuple-type-errors.ts => tuple-errors.ts} (74%) delete mode 100644 tests/compiler/tuple-type-errors.json diff --git a/tests/compiler/tuple-circular.json b/tests/compiler/tuple-circular.json index 91f5d9a68d..d196b422ed 100644 --- a/tests/compiler/tuple-circular.json +++ b/tests/compiler/tuple-circular.json @@ -1,4 +1,7 @@ { + "asc_flags": [ + "--enable", "multi-value" + ], "stderr": [ "TS2456: Type alias 'Loop' circularly references itself.", "1 parse error(s)" diff --git a/tests/compiler/tuple-errors.json b/tests/compiler/tuple-errors.json new file mode 100644 index 0000000000..9d5243a9a1 --- /dev/null +++ b/tests/compiler/tuple-errors.json @@ -0,0 +1,9 @@ +{ + "asc_flags": [ + "--enable", "multi-value" + ], + "stderr": [ + "AS100: Not implemented: Tuple types", + "18 compile error(s)" + ] +} diff --git a/tests/compiler/tuple-type-errors.ts b/tests/compiler/tuple-errors.ts similarity index 74% rename from tests/compiler/tuple-type-errors.ts rename to tests/compiler/tuple-errors.ts index ae083d9c18..fc365813ba 100644 --- a/tests/compiler/tuple-type-errors.ts +++ b/tests/compiler/tuple-errors.ts @@ -35,3 +35,26 @@ export function TupleReturnUnimplemented6(): [i32[], [i32]] { export function TupleReturnUnimplemented7(): [i32, i32] { return [0, 1]; } + +type Box = [T, i32]; + +export function TupleGeneric1(x: Box): Box { + return x; +} +export function TupleGeneric2(x: [i32, T]): [i32, T] { + return x; +} + +export function TupleNullable1(x: [i32, i32] | null): [i32, i32] | null { + return x; +} +export function TupleNullable2(x: [] | null): [] | null { + return x; +} + +export function TupleTypeMismatch1(x: [i32, f32]): [f32, i32] { + return x; +} +export function TupleTypeMismatch2(x: [f64, f32]): [f32, f64] { + return x; +} diff --git a/tests/compiler/tuple-type-errors.json b/tests/compiler/tuple-type-errors.json deleted file mode 100644 index 06dd8d1b42..0000000000 --- a/tests/compiler/tuple-type-errors.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "stderr": [ - "AS100: Not implemented: Tuple types", - "13 compile error(s)" - ] -} diff --git a/tests/compiler/tuple-type.json b/tests/compiler/tuple-type.json index d6e0c18067..8a83de2d20 100644 --- a/tests/compiler/tuple-type.json +++ b/tests/compiler/tuple-type.json @@ -1,5 +1,6 @@ { "asc_flags": [ + "--enable", "multi-value" ], "stderr": [ ] diff --git a/tests/parser.js b/tests/parser.js index ec3d89f0d9..687059529a 100644 --- a/tests/parser.js +++ b/tests/parser.js @@ -6,7 +6,7 @@ import { globSync } from "glob"; import { diff } from "../util/text.js"; import { stdoutColors } from "../util/terminal.js"; import * as optionsUtil from "../util/options.js"; -import { Program, Options, ASTBuilder } from "../dist/assemblyscript.js"; +import { Program, Options, ASTBuilder, Feature } from "../dist/assemblyscript.js"; const dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -54,6 +54,11 @@ if (argv.length) { } let failures = 0; +const parserTestFeatures = new Map([ + ["tuple.ts", [Feature.MultiValue]], + ["tuple-more.ts", [Feature.MultiValue]], + ["tuple-errors.ts", [Feature.MultiValue]] +]); for (const filename of tests) { if (filename.charAt(0) == "_" || filename.endsWith(".fixture.ts")) continue; @@ -61,9 +66,16 @@ for (const filename of tests) { console.log(stdoutColors.white("Testing parser/" + filename)); let failed = false; - const program = new Program(new Options()); - const parser = program.parser; + const options = new Options(); const sourceText = fs.readFileSync(basedir + "/" + filename, { encoding: "utf8" }).replace(/\r?\n/g, "\n"); + const features = parserTestFeatures.get(filename); + if (features) { + for (const feature of features) { + options.setFeature(feature); + } + } + const program = new Program(options); + const parser = program.parser; parser.parseFile(sourceText, filename, true); const serializedSourceText = ASTBuilder.build(program.sources[0]); const actual = serializedSourceText + parser.diagnostics.map(diagnostic => "// " + diagnostic +"\n").join(""); diff --git a/tests/parser/tuple.ts b/tests/parser/tuple.ts index a6947118c8..d04ec6eb26 100644 --- a/tests/parser/tuple.ts +++ b/tests/parser/tuple.ts @@ -10,7 +10,12 @@ function func2(x: [i32, i32]): [i32, i32] { return x; } function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { return x; } function func4(x: readonly [i32, string]): [void] { return [void(0)]; } function func5(x: readonly [Array, i32[]]): readonly [i32] { return [x[1].length]; } +function func6(x: [i32, i32] | null): [i32, i32] | null { return x; } +function func7(x: readonly [[i32[]], [string]]): readonly [[i32[]], [string]] { return x; } type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; type type3 = readonly [i32, string]; +type type4 = [i32, i32] | null; +type type5 = [[i32, i32], [i32, i32]]; +type type6 = [Array, T[], T]; diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts index 4b7c19d509..7ab97bb918 100644 --- a/tests/parser/tuple.ts.fixture.ts +++ b/tests/parser/tuple.ts.fixture.ts @@ -31,6 +31,15 @@ function func4(x: Readonly<[i32, string]>): [void] { function func5(x: Readonly<[Array, Array]>): Readonly<[i32]> { return [x[1].length]; } +function func6(x: [i32, i32] | null): [i32, i32] | null { + return x; +} +function func7(x: Readonly<[[Array], [string]]>): Readonly<[[Array], [string]]> { + return x; +} type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; type type3 = Readonly<[i32, string]>; +type type4 = [i32, i32] | null; +type type5 = [[i32, i32], [i32, i32]]; +type type6 = [Array, Array, T]; From b7dfb8fbb90c9b2f7e953edc14755d7746aa8a12 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:15:40 -0400 Subject: [PATCH 06/25] feat: add support for named tuple types like `[x: f64, y: f64]` --- src/ast.ts | 5 ++++- src/extra/ast.ts | 11 +++++++++++ src/parser.ts | 17 ++++++++++++++++- src/program.ts | 6 +++--- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 76476e22ba..da649de549 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -164,10 +164,11 @@ export abstract class Node { static createTupleType( elements: TypeNode[], + elementNames: (IdentifierExpression | null)[] | null, isNullable: bool, range: Range ): TupleTypeNode { - return new TupleTypeNode(elements, isNullable, range); + return new TupleTypeNode(elements, elementNames, isNullable, range); } static createOmittedType( @@ -948,6 +949,8 @@ export class TupleTypeNode extends TypeNode { constructor( /** Tuple elements. */ public elements: TypeNode[], + /** Tuple element names, if any. */ + public elementNames: (IdentifierExpression | null)[] | null, /** Whether nullable or not. */ isNullable: bool, /** Source range. */ diff --git a/src/extra/ast.ts b/src/extra/ast.ts index f163db37d4..699b2e0c71 100644 --- a/src/extra/ast.ts +++ b/src/extra/ast.ts @@ -463,11 +463,22 @@ export class ASTBuilder { let sb = this.sb; sb.push("["); let elements = node.elements; + let elementNames = node.elementNames; let numElements = elements.length; if (numElements) { + let name = elementNames ? elementNames[0] : null; + if (name) { + this.visitIdentifierExpression(name); + sb.push(": "); + } this.visitTypeNode(elements[0]); for (let i = 1; i < numElements; ++i) { sb.push(", "); + name = elementNames ? elementNames[i] : null; + if (name) { + this.visitIdentifierExpression(name); + sb.push(": "); + } this.visitTypeNode(elements[i]); } } diff --git a/src/parser.ts b/src/parser.ts index 8434a5a866..37d57cf257 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -578,11 +578,26 @@ export class Parser extends DiagnosticEmitter { // '[' Type (',' Type)* ']' } else if (token == Token.OpenBracket) { let elements: TypeNode[] = []; + let elementNames: (IdentifierExpression | null)[] = []; + let hasElementNames = false; if (!tn.skip(Token.CloseBracket)) { do { + let elementName: IdentifierExpression | null = null; + let state = tn.mark(); + if (tn.skip(Token.Identifier)) { + let name = tn.readIdentifier(); + let nameRange = tn.range(); + if (tn.skip(Token.Colon)) { + elementName = Node.createIdentifierExpression(name, nameRange); + hasElementNames = true; + } else { + tn.reset(state); + } + } let element = this.parseType(tn, true, suppressErrors); if (!element) return null; elements.push(element); + elementNames.push(elementName); } while (tn.skip(Token.Comma)); if (!tn.skip(Token.CloseBracket)) { if (!suppressErrors) { @@ -594,7 +609,7 @@ export class Parser extends DiagnosticEmitter { return null; } } - type = Node.createTupleType(elements, false, tn.range(startPos, tn.pos)); + type = Node.createTupleType(elements, hasElementNames ? elementNames : null, false, tn.range(startPos, tn.pos)); // 'void' } else if (token == Token.Void) { diff --git a/src/program.ts b/src/program.ts index e5e27ebc59..6e1e28405c 100644 --- a/src/program.ts +++ b/src/program.ts @@ -390,9 +390,9 @@ export namespace OperatorKind { case Token.GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShr; case Token.GreaterThan_GreaterThan_GreaterThan: case Token.GreaterThan_GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShrU; - case Token.Equals_Equals: + case Token.Equals_Equals: case Token.Equals_Equals_Equals: return OperatorKind.Eq; - case Token.Exclamation_Equals: + case Token.Exclamation_Equals: case Token.Exclamation_Equals_Equals: return OperatorKind.Ne; case Token.GreaterThan: return OperatorKind.Gt; case Token.GreaterThan_Equals: return OperatorKind.Ge; @@ -436,7 +436,7 @@ export class Program extends DiagnosticEmitter { diagnostics: DiagnosticMessage[] | null = null ) { super(diagnostics); - this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); + this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); this.parser = new Parser(this.diagnostics, this.sources); this.resolver = new Resolver(this); let nativeFile = new File(this, Source.native); From 98fe27f0ec24a31da694e3c426af3787e0ae756d Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:15:54 -0400 Subject: [PATCH 07/25] tests: add tests for named tuple types --- tests/compiler/tuple-errors.json | 2 +- tests/compiler/tuple-errors.ts | 5 +++++ tests/parser/tuple.ts | 4 ++++ tests/parser/tuple.ts.fixture.ts | 10 ++++++++++ 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/compiler/tuple-errors.json b/tests/compiler/tuple-errors.json index 9d5243a9a1..b4c0f65ed3 100644 --- a/tests/compiler/tuple-errors.json +++ b/tests/compiler/tuple-errors.json @@ -4,6 +4,6 @@ ], "stderr": [ "AS100: Not implemented: Tuple types", - "18 compile error(s)" + "20 compile error(s)" ] } diff --git a/tests/compiler/tuple-errors.ts b/tests/compiler/tuple-errors.ts index fc365813ba..2bab1e6bb2 100644 --- a/tests/compiler/tuple-errors.ts +++ b/tests/compiler/tuple-errors.ts @@ -5,6 +5,7 @@ export type TupleTypeUnimplemented4 = [i32, TupleTypeUnimplemented1]; export type TupleTypeUnimplemented5 = [string, i32]; export type TupleTypeUnimplemented6 = [i32[], [i32]]; export type TupleTypeUnimplemented7 = [i32, i32]; +export type TupleTypeUnimplemented8 = [x: i32, y: i32]; export function TupleParamUnimplemented1(x: []): void { } export function TupleParamUnimplemented2(x: [i32]): void { } @@ -13,6 +14,7 @@ export function TupleParamUnimplemented4(x: [i32, TupleTypeUnimplemented1]): voi export function TupleParamUnimplemented5(x: [string, i32]): void { } export function TupleParamUnimplemented6(x: [i32[], [i32]]): void { } export function TupleParamUnimplemented7(x: [i32, i32]): void { } +export function TupleParamUnimplemented8(x: [left: i32, right: i32]): void { } export function TupleReturnUnimplemented1(): [] { return []; @@ -35,6 +37,9 @@ export function TupleReturnUnimplemented6(): [i32[], [i32]] { export function TupleReturnUnimplemented7(): [i32, i32] { return [0, 1]; } +export function TupleReturnUnimplemented8(): [first: i32, second: i32] { + return [0, 1]; +} type Box = [T, i32]; diff --git a/tests/parser/tuple.ts b/tests/parser/tuple.ts index d04ec6eb26..0cb29747f4 100644 --- a/tests/parser/tuple.ts +++ b/tests/parser/tuple.ts @@ -4,6 +4,8 @@ function tuple3(): [i32, string] { return [1, "a"]; } function tuple4(): [Array, i32[]] { return [new Array(), [1, 2]]; } function tuple5(): [i32] { return [1]; } function tuple6(): [[i32[]]] { return [[[1, 2]]]; } +function tuple7(): [x: i32, y: i32] { return [1, 2]; } +function tuple8(): [head: i32, tail: [lo: i32, hi: i32]] { return [1, [2, 3]]; } function func1(a: i32, b: i32): [i32, i32] { return [a, b]; } function func2(x: [i32, i32]): [i32, i32] { return x; } @@ -12,6 +14,7 @@ function func4(x: readonly [i32, string]): [void] { return [void(0)]; } function func5(x: readonly [Array, i32[]]): readonly [i32] { return [x[1].length]; } function func6(x: [i32, i32] | null): [i32, i32] | null { return x; } function func7(x: readonly [[i32[]], [string]]): readonly [[i32[]], [string]] { return x; } +function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { return x; } type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; @@ -19,3 +22,4 @@ type type3 = readonly [i32, string]; type type4 = [i32, i32] | null; type type5 = [[i32, i32], [i32, i32]]; type type6 = [Array, T[], T]; +type type7 = [start: i32, end: [lo: i32, hi: i32]]; diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts index 7ab97bb918..d385f6b614 100644 --- a/tests/parser/tuple.ts.fixture.ts +++ b/tests/parser/tuple.ts.fixture.ts @@ -16,6 +16,12 @@ function tuple5(): [i32] { function tuple6(): [[Array]] { return [[[1, 2]]]; } +function tuple7(): [x: i32, y: i32] { + return [1, 2]; +} +function tuple8(): [head: i32, tail: [lo: i32, hi: i32]] { + return [1, [2, 3]]; +} function func1(a: i32, b: i32): [i32, i32] { return [a, b]; } @@ -37,9 +43,13 @@ function func6(x: [i32, i32] | null): [i32, i32] | null { function func7(x: Readonly<[[Array], [string]]>): Readonly<[[Array], [string]]> { return x; } +function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { + return x; +} type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; type type3 = Readonly<[i32, string]>; type type4 = [i32, i32] | null; type type5 = [[i32, i32], [i32, i32]]; type type6 = [Array, Array, T]; +type type7 = [start: i32, end: [lo: i32, hi: i32]]; From 39eb3f908b641006582d07419ab491712c61fc89 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:19:20 -0400 Subject: [PATCH 08/25] chore(cli): keep multi-value feature as unimplemented in the cli since it's not ready yet --- cli/options.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/options.json b/cli/options.json index a077a3d4c6..f6776dbe26 100644 --- a/cli/options.json +++ b/cli/options.json @@ -222,12 +222,12 @@ " gc Garbage collection (WIP).", " stringref String reference types.", " relaxed-simd Relaxed SIMD operations.", - " multi-value Multi value types.", "" ], "TODO_doesNothingYet": [ " exception-handling Exception handling.", " tail-calls Tail call operations.", + " multi-value Multi value types.", " memory64 Memory64 operations.", " extended-const Extended const expressions." ], From e8563bd7091e2ee9a89a4d9bc978cbeb7fd8b46a Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:45:46 -0400 Subject: [PATCH 09/25] fix: tuples should not be parsed if multi-value is disabled --- src/parser.ts | 7 +++++-- src/program.ts | 3 +++ tests/compiler/tuple-disabled.json | 9 +++++++++ tests/compiler/tuple-disabled.ts | 3 +++ 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/tuple-disabled.json create mode 100644 tests/compiler/tuple-disabled.ts diff --git a/src/parser.ts b/src/parser.ts index 37d57cf257..a7eb0ac0fd 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -9,6 +9,7 @@ import { CommonFlags, + Feature, LIBRARY_PREFIX, PATH_DELIMITER } from "./common"; @@ -91,6 +92,7 @@ import { mangleInternalPath } from "./ast"; +import { Options } from "./compiler"; /** Represents a dependee. */ class Dependee { @@ -119,7 +121,8 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - + /** Temporary variable so I can disable parsing tuples if multi-variable is disabled */ + readonly options: Options | null = null; /** Constructs a new parser. */ constructor( diagnostics: DiagnosticMessage[] | null = null, @@ -576,7 +579,7 @@ export class Parser extends DiagnosticEmitter { } // '[' Type (',' Type)* ']' - } else if (token == Token.OpenBracket) { + } else if (this.options && this.options.hasFeature(Feature.MultiValue) && token == Token.OpenBracket) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; let hasElementNames = false; diff --git a/src/program.ts b/src/program.ts index 6e1e28405c..5adf1b9aa9 100644 --- a/src/program.ts +++ b/src/program.ts @@ -442,6 +442,9 @@ export class Program extends DiagnosticEmitter { let nativeFile = new File(this, Source.native); this.nativeFile = nativeFile; this.filesByName.set(nativeFile.internalName, nativeFile); + + // temporary fix + this.parser.options = this.options; } /** Module instance. */ diff --git a/tests/compiler/tuple-disabled.json b/tests/compiler/tuple-disabled.json new file mode 100644 index 0000000000..b4e8e8ffe9 --- /dev/null +++ b/tests/compiler/tuple-disabled.json @@ -0,0 +1,9 @@ +{ + "asc_flags": [ + "--disable", "multi-value" + ], + "stderr": [ + "TS1110: Type expected.", + "3 parse error(s)" + ] +} diff --git a/tests/compiler/tuple-disabled.ts b/tests/compiler/tuple-disabled.ts new file mode 100644 index 0000000000..5c93a3e424 --- /dev/null +++ b/tests/compiler/tuple-disabled.ts @@ -0,0 +1,3 @@ +export function tupleDisabled(x: [left: i32, right: i32]): void {} +export type tupleTypeDisabled1 = [i32, i32]; +export type tupleTypeDisabled2 = []; From 4b2a3998dcc759de15c00324568510ce8f972cb0 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:48:04 -0400 Subject: [PATCH 10/25] chore: why did i make it readonly lol --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index a7eb0ac0fd..626332404a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -122,7 +122,7 @@ export class Parser extends DiagnosticEmitter { /** Current overridden module name. */ currentModuleName: string | null = null; /** Temporary variable so I can disable parsing tuples if multi-variable is disabled */ - readonly options: Options | null = null; + options: Options | null = null; /** Constructs a new parser. */ constructor( diagnostics: DiagnosticMessage[] | null = null, From 5cc7dcec474fcd7373944f59c7e1694367895392 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:49:48 -0400 Subject: [PATCH 11/25] chore: make sure bootstrapping works --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 626332404a..ef2f17ae9c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -579,7 +579,7 @@ export class Parser extends DiagnosticEmitter { } // '[' Type (',' Type)* ']' - } else if (this.options && this.options.hasFeature(Feature.MultiValue) && token == Token.OpenBracket) { + } else if (this.options && this.options!.hasFeature(Feature.MultiValue) && token == Token.OpenBracket) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; let hasElementNames = false; From 5375b249cf4e54b8ad934e806079218535b48041 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Wed, 22 Apr 2026 23:57:23 -0400 Subject: [PATCH 12/25] chore: revert random formatting changes --- src/program.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/program.ts b/src/program.ts index 5adf1b9aa9..49fa2f3460 100644 --- a/src/program.ts +++ b/src/program.ts @@ -390,9 +390,9 @@ export namespace OperatorKind { case Token.GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShr; case Token.GreaterThan_GreaterThan_GreaterThan: case Token.GreaterThan_GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShrU; - case Token.Equals_Equals: + case Token.Equals_Equals: case Token.Equals_Equals_Equals: return OperatorKind.Eq; - case Token.Exclamation_Equals: + case Token.Exclamation_Equals: case Token.Exclamation_Equals_Equals: return OperatorKind.Ne; case Token.GreaterThan: return OperatorKind.Gt; case Token.GreaterThan_Equals: return OperatorKind.Ge; @@ -436,7 +436,7 @@ export class Program extends DiagnosticEmitter { diagnostics: DiagnosticMessage[] | null = null ) { super(diagnostics); - this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); + this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); this.parser = new Parser(this.diagnostics, this.sources); this.resolver = new Resolver(this); let nativeFile = new File(this, Source.native); From c8429ce80792b8659f345a32918288e57c8b71e8 Mon Sep 17 00:00:00 2001 From: Jairus Date: Thu, 23 Apr 2026 11:43:06 -0400 Subject: [PATCH 13/25] chore: token check should come before feature Co-authored-by: Max Graey --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index ef2f17ae9c..998e347877 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -579,7 +579,7 @@ export class Parser extends DiagnosticEmitter { } // '[' Type (',' Type)* ']' - } else if (this.options && this.options!.hasFeature(Feature.MultiValue) && token == Token.OpenBracket) { + } else if (token == Token.OpenBracket && this.options && this.options!.hasFeature(Feature.MultiValue)) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; let hasElementNames = false; From 1ca3ad7ebe9a12cc4d3c608dc138d7718d53abcc Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 11:48:13 -0400 Subject: [PATCH 14/25] chore: update tuple grammer to show named types logically --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 998e347877..d6e1c9a5be 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -578,7 +578,7 @@ export class Parser extends DiagnosticEmitter { return null; } - // '[' Type (',' Type)* ']' + // '[' ((Identifier ':')? Type (',' (Identifier ':')? Type)*)? ']' } else if (token == Token.OpenBracket && this.options && this.options!.hasFeature(Feature.MultiValue)) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; From 7c7f000aaf49bc939734e3a3bfa8c19cdd934bfb Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 11:51:42 -0400 Subject: [PATCH 15/25] chore: add options parameter to end of Parser constructor --- src/parser.ts | 10 ++++++---- src/program.ts | 11 ++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index d6e1c9a5be..e2ef29ebef 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,15 +121,17 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - /** Temporary variable so I can disable parsing tuples if multi-variable is disabled */ - options: Options | null = null; + /** Compiler options. */ + options: Options; /** Constructs a new parser. */ constructor( diagnostics: DiagnosticMessage[] | null = null, - sources: Source[] = [] + sources: Source[] = [], + options: Options ) { super(diagnostics); this.sources = sources; + this.options = options; } /** Parses a file and adds its definitions to the program. */ @@ -579,7 +581,7 @@ export class Parser extends DiagnosticEmitter { } // '[' ((Identifier ':')? Type (',' (Identifier ':')? Type)*)? ']' - } else if (token == Token.OpenBracket && this.options && this.options!.hasFeature(Feature.MultiValue)) { + } else if (token == Token.OpenBracket && this.options.hasFeature(Feature.MultiValue)) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; let hasElementNames = false; diff --git a/src/program.ts b/src/program.ts index 49fa2f3460..07455dd37e 100644 --- a/src/program.ts +++ b/src/program.ts @@ -390,9 +390,9 @@ export namespace OperatorKind { case Token.GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShr; case Token.GreaterThan_GreaterThan_GreaterThan: case Token.GreaterThan_GreaterThan_GreaterThan_Equals: return OperatorKind.BitwiseShrU; - case Token.Equals_Equals: + case Token.Equals_Equals: case Token.Equals_Equals_Equals: return OperatorKind.Eq; - case Token.Exclamation_Equals: + case Token.Exclamation_Equals: case Token.Exclamation_Equals_Equals: return OperatorKind.Ne; case Token.GreaterThan: return OperatorKind.Gt; case Token.GreaterThan_Equals: return OperatorKind.Ge; @@ -436,15 +436,12 @@ export class Program extends DiagnosticEmitter { diagnostics: DiagnosticMessage[] | null = null ) { super(diagnostics); - this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); - this.parser = new Parser(this.diagnostics, this.sources); + this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); + this.parser = new Parser(this.diagnostics, this.sources, this.options); this.resolver = new Resolver(this); let nativeFile = new File(this, Source.native); this.nativeFile = nativeFile; this.filesByName.set(nativeFile.internalName, nativeFile); - - // temporary fix - this.parser.options = this.options; } /** Module instance. */ From dda5084e6981ce596c52776b18102e4b71130778 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 12:10:41 -0400 Subject: [PATCH 16/25] chore: add `Readonly` type helper --- src/ast.ts | 7 +++++++ src/parser.ts | 7 +------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index da649de549..2f0e37bc5b 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -177,6 +177,13 @@ export abstract class Node { return new NamedTypeNode(Node.createSimpleTypeName("", range), null, false, range); } + static createReadonlyType( + innerType: TypeNode, + range: Range + ): NamedTypeNode { + return new NamedTypeNode(Node.createSimpleTypeName("Readonly", range), [ innerType ], false, range); + } + static createTypeParameter( name: IdentifierExpression, extendsType: NamedTypeNode | null, diff --git a/src/parser.ts b/src/parser.ts index e2ef29ebef..8a0c85a2f8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -519,12 +519,7 @@ export class Parser extends DiagnosticEmitter { if (token == Token.Readonly) { let innerType = this.parseType(tn, acceptParenthesized, suppressErrors); if (!innerType) return null; - type = Node.createNamedType( - Node.createSimpleTypeName("Readonly", tn.range(startPos, tn.pos)), - [ innerType ], - false, - tn.range(startPos, tn.pos) - ); + type = Node.createReadonlyType(innerType, tn.range(startPos, tn.pos)); // '(' ... } else if (token == Token.OpenParen) { From 5874329493e67f21809e365964d04a5614256de2 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 12:11:04 -0400 Subject: [PATCH 17/25] tests: add a few tests for empty tuples and more nullable tests --- tests/parser/tuple.ts | 7 ++++++- tests/parser/tuple.ts.fixture.ts | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/parser/tuple.ts b/tests/parser/tuple.ts index 0cb29747f4..562bc15093 100644 --- a/tests/parser/tuple.ts +++ b/tests/parser/tuple.ts @@ -1,3 +1,4 @@ +function tuple0(): [] { return []; } function tuple1(): [i32, i32] { return [1, 2]; } function tuple2(): [i32, [i32, i32]] { return [1, [2, 3]]; } function tuple3(): [i32, string] { return [1, "a"]; } @@ -7,7 +8,8 @@ function tuple6(): [[i32[]]] { return [[[1, 2]]]; } function tuple7(): [x: i32, y: i32] { return [1, 2]; } function tuple8(): [head: i32, tail: [lo: i32, hi: i32]] { return [1, [2, 3]]; } -function func1(a: i32, b: i32): [i32, i32] { return [a, b]; } +function func0(x: []): [] { return []; } +function func1(x: i32, y: i32): [i32, i32] { return [y, x]; } function func2(x: [i32, i32]): [i32, i32] { return x; } function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { return x; } function func4(x: readonly [i32, string]): [void] { return [void(0)]; } @@ -15,7 +17,10 @@ function func5(x: readonly [Array, i32[]]): readonly [i32] { return [x[1].l function func6(x: [i32, i32] | null): [i32, i32] | null { return x; } function func7(x: readonly [[i32[]], [string]]): readonly [[i32[]], [string]] { return x; } function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { return x; } +function func9(x: [T, T, i32]): [T] { return x; } +function func10(x: [string | null, i32] | null): [string | null, i32] | null { return x; } +type type0 = []; type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; type type3 = readonly [i32, string]; diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts index d385f6b614..9020ea1b0b 100644 --- a/tests/parser/tuple.ts.fixture.ts +++ b/tests/parser/tuple.ts.fixture.ts @@ -1,3 +1,6 @@ +function tuple0(): [] { + return []; +} function tuple1(): [i32, i32] { return [1, 2]; } @@ -22,8 +25,11 @@ function tuple7(): [x: i32, y: i32] { function tuple8(): [head: i32, tail: [lo: i32, hi: i32]] { return [1, [2, 3]]; } -function func1(a: i32, b: i32): [i32, i32] { - return [a, b]; +function func0(x: []): [] { + return []; +} +function func1(x: i32, y: i32): [i32, i32] { + return [y, x]; } function func2(x: [i32, i32]): [i32, i32] { return x; @@ -46,6 +52,13 @@ function func7(x: Readonly<[[Array], [string]]>): Readonly<[[Array], [ function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { return x; } +function func9(x: [T, T, i32]): [T] { + return x; +} +function func10(x: [string | null, i32] | null): [string | null, i32] | null { + return x; +} +type type0 = []; type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; type type3 = Readonly<[i32, string]>; From e5ab232ce10e891283e903415e976d82c556122f Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 12:18:23 -0400 Subject: [PATCH 18/25] chore: revert to options workaround --- src/parser.ts | 8 +++----- src/program.ts | 5 ++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 8a0c85a2f8..20f6815b44 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,17 +121,15 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - /** Compiler options. */ - options: Options; + /** Compiler options. (workaround) */ + options!: Options; /** Constructs a new parser. */ constructor( diagnostics: DiagnosticMessage[] | null = null, - sources: Source[] = [], - options: Options + sources: Source[] = [] ) { super(diagnostics); this.sources = sources; - this.options = options; } /** Parses a file and adds its definitions to the program. */ diff --git a/src/program.ts b/src/program.ts index 07455dd37e..5adf1b9aa9 100644 --- a/src/program.ts +++ b/src/program.ts @@ -437,11 +437,14 @@ export class Program extends DiagnosticEmitter { ) { super(diagnostics); this.module = Module.create(options.stackSize > 0, options.sizeTypeRef); - this.parser = new Parser(this.diagnostics, this.sources, this.options); + this.parser = new Parser(this.diagnostics, this.sources); this.resolver = new Resolver(this); let nativeFile = new File(this, Source.native); this.nativeFile = nativeFile; this.filesByName.set(nativeFile.internalName, nativeFile); + + // temporary fix + this.parser.options = this.options; } /** Module instance. */ From 5ef3d891c49c93b00028de3ff6a41b92683d8436 Mon Sep 17 00:00:00 2001 From: Jairus Date: Thu, 23 Apr 2026 12:26:19 -0400 Subject: [PATCH 19/25] Update src/parser.ts Co-authored-by: Max Graey --- src/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 20f6815b44..cb648e7520 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,7 +121,7 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - /** Compiler options. (workaround) */ + /** Compiler options. */ options!: Options; /** Constructs a new parser. */ constructor( From d59cbbc12c25f58d4aae04c3bf2d7476750020f0 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 12:50:17 -0400 Subject: [PATCH 20/25] chore: stick with temporary options workaround --- src/parser.ts | 6 +++--- src/program.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index cb648e7520..240c79c64a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,8 +121,8 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - /** Compiler options. */ - options!: Options; + /** Compiler options. (temporary. remove after multi-value/tuple support is finished) */ + options: Options | null = null; /** Constructs a new parser. */ constructor( diagnostics: DiagnosticMessage[] | null = null, @@ -574,7 +574,7 @@ export class Parser extends DiagnosticEmitter { } // '[' ((Identifier ':')? Type (',' (Identifier ':')? Type)*)? ']' - } else if (token == Token.OpenBracket && this.options.hasFeature(Feature.MultiValue)) { + } else if (token == Token.OpenBracket && this.options && this.options!.hasFeature(Feature.MultiValue)) { let elements: TypeNode[] = []; let elementNames: (IdentifierExpression | null)[] = []; let hasElementNames = false; diff --git a/src/program.ts b/src/program.ts index 5adf1b9aa9..e51ad68000 100644 --- a/src/program.ts +++ b/src/program.ts @@ -443,7 +443,7 @@ export class Program extends DiagnosticEmitter { this.nativeFile = nativeFile; this.filesByName.set(nativeFile.internalName, nativeFile); - // temporary fix + // temporary workaround. remove after multi-value support is finished this.parser.options = this.options; } From 301a564885647e5b10628fcab2e86077c66642da Mon Sep 17 00:00:00 2001 From: Jairus Date: Thu, 23 Apr 2026 13:01:40 -0400 Subject: [PATCH 21/25] Update src/program.ts Co-authored-by: Max Graey --- src/program.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/program.ts b/src/program.ts index e51ad68000..6b0365c5f8 100644 --- a/src/program.ts +++ b/src/program.ts @@ -443,7 +443,7 @@ export class Program extends DiagnosticEmitter { this.nativeFile = nativeFile; this.filesByName.set(nativeFile.internalName, nativeFile); - // temporary workaround. remove after multi-value support is finished + // TODO: temporary workaround. remove after multi-value support is finished this.parser.options = this.options; } From a97c523352996dd7178ea394e9486fdc85fe7eb0 Mon Sep 17 00:00:00 2001 From: Jairus Date: Thu, 23 Apr 2026 13:02:24 -0400 Subject: [PATCH 22/25] Update src/parser.ts Co-authored-by: Max Graey --- src/parser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/parser.ts b/src/parser.ts index 240c79c64a..adfc2e4467 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,7 +121,8 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; - /** Compiler options. (temporary. remove after multi-value/tuple support is finished) */ +// TODO: Remove when multi-value feture will enable by default. +/** Compiler options. */ options: Options | null = null; /** Constructs a new parser. */ constructor( From 9cd4cb2d09f810cc1eee2b89840770dbe62869d5 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 13:03:03 -0400 Subject: [PATCH 23/25] chore: fix typo and indenting --- src/parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index adfc2e4467..738eb67e45 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -121,8 +121,8 @@ export class Parser extends DiagnosticEmitter { sources: Source[]; /** Current overridden module name. */ currentModuleName: string | null = null; -// TODO: Remove when multi-value feture will enable by default. -/** Compiler options. */ + // TODO: Remove when multi-value feature will enable by default. + /** Compiler options. */ options: Options | null = null; /** Constructs a new parser. */ constructor( From f45638ae3f8bb364507f269b4128441f32d0d32b Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 16:07:50 -0400 Subject: [PATCH 24/25] tests: test each line/col --- tests/compiler/tuple-errors.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/compiler/tuple-errors.json b/tests/compiler/tuple-errors.json index b4c0f65ed3..bf732842af 100644 --- a/tests/compiler/tuple-errors.json +++ b/tests/compiler/tuple-errors.json @@ -3,7 +3,35 @@ "--enable", "multi-value" ], "stderr": [ + "in tuple-errors.ts(1,13)", + "in tuple-errors.ts(2,13)", + "in tuple-errors.ts(3,13)", + "in tuple-errors.ts(4,13)", + "in tuple-errors.ts(5,13)", + "in tuple-errors.ts(6,13)", + "in tuple-errors.ts(7,13)", + "in tuple-errors.ts(8,13)", "AS100: Not implemented: Tuple types", - "20 compile error(s)" + "in tuple-errors.ts(10,45)", + "in tuple-errors.ts(11,45)", + "in tuple-errors.ts(12,51)", + "in tuple-errors.ts(1,39)", + "in tuple-errors.ts(14,45)", + "in tuple-errors.ts(15,53)", + "in tuple-errors.ts(16,45)", + "in tuple-errors.ts(17,45)", + "in tuple-errors.ts(19,46)", + "in tuple-errors.ts(22,46)", + "in tuple-errors.ts(25,52)", + "in tuple-errors.ts(31,46)", + "in tuple-errors.ts(34,54)", + "in tuple-errors.ts(37,46)", + "in tuple-errors.ts(40,46)", + "in tuple-errors.ts(44,15)", + "in tuple-errors.ts(49,17)", + "in tuple-errors.ts(53,35)", + "in tuple-errors.ts(56,35)", + "in tuple-errors.ts(60,39)", + "in tuple-errors.ts(63,39)" ] } From 729cb2db10ac9b942dcceff484b7bcde58afa9f1 Mon Sep 17 00:00:00 2001 From: Jairus Tanaka Date: Thu, 23 Apr 2026 17:14:40 -0400 Subject: [PATCH 25/25] fix: `readonly` should be ignored for now --- src/ast.ts | 7 ------- src/parser.ts | 15 ++++++++------- tests/parser/tuple.ts.fixture.ts | 8 ++++---- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/ast.ts b/src/ast.ts index 2f0e37bc5b..da649de549 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -177,13 +177,6 @@ export abstract class Node { return new NamedTypeNode(Node.createSimpleTypeName("", range), null, false, range); } - static createReadonlyType( - innerType: TypeNode, - range: Range - ): NamedTypeNode { - return new NamedTypeNode(Node.createSimpleTypeName("Readonly", range), [ innerType ], false, range); - } - static createTypeParameter( name: IdentifierExpression, extendsType: NamedTypeNode | null, diff --git a/src/parser.ts b/src/parser.ts index 738eb67e45..157351cd3a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -514,14 +514,8 @@ export class Parser extends DiagnosticEmitter { let type: TypeNode; - // 'readonly' Type - if (token == Token.Readonly) { - let innerType = this.parseType(tn, acceptParenthesized, suppressErrors); - if (!innerType) return null; - type = Node.createReadonlyType(innerType, tn.range(startPos, tn.pos)); - // '(' ... - } else if (token == Token.OpenParen) { + if (token == Token.OpenParen) { // '(' FunctionSignature ')' let isInnerParenthesized = tn.skip(Token.OpenParen); @@ -574,6 +568,13 @@ export class Parser extends DiagnosticEmitter { return null; } + // 'readonly' Type + } else if (token == Token.Readonly) { + let innerType = this.parseType(tn, acceptParenthesized, suppressErrors); + if (!innerType) return null; + type = innerType; + type.range.start = startPos; + // '[' ((Identifier ':')? Type (',' (Identifier ':')? Type)*)? ']' } else if (token == Token.OpenBracket && this.options && this.options!.hasFeature(Feature.MultiValue)) { let elements: TypeNode[] = []; diff --git a/tests/parser/tuple.ts.fixture.ts b/tests/parser/tuple.ts.fixture.ts index 9020ea1b0b..bf461f898e 100644 --- a/tests/parser/tuple.ts.fixture.ts +++ b/tests/parser/tuple.ts.fixture.ts @@ -37,16 +37,16 @@ function func2(x: [i32, i32]): [i32, i32] { function func3(x: [i32, [i32, i32]], y: i32): [i32, [i32, i32]] { return x; } -function func4(x: Readonly<[i32, string]>): [void] { +function func4(x: [i32, string]): [void] { return [void(0)]; } -function func5(x: Readonly<[Array, Array]>): Readonly<[i32]> { +function func5(x: [Array, Array]): [i32] { return [x[1].length]; } function func6(x: [i32, i32] | null): [i32, i32] | null { return x; } -function func7(x: Readonly<[[Array], [string]]>): Readonly<[[Array], [string]]> { +function func7(x: [[Array], [string]]): [[Array], [string]] { return x; } function func8(x: [left: i32, right: i32]): [first: i32, second: i32] { @@ -61,7 +61,7 @@ function func10(x: [string | null, i32] | null): [string | null, i32] | null { type type0 = []; type type1 = [i32, i32]; type type2 = [i32, [i32, i32]]; -type type3 = Readonly<[i32, string]>; +type type3 = [i32, string]; type type4 = [i32, i32] | null; type type5 = [[i32, i32], [i32, i32]]; type type6 = [Array, Array, T];