Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8ff13cd
chore: update my email
JairusSW Apr 22, 2026
19cacf9
chore(cli): enable `--enable multi-value` flag
JairusSW Apr 22, 2026
4527d65
feat: wire up support for parsing and resolving tuples
JairusSW Apr 22, 2026
c53e659
tests: add tests for tuples
JairusSW Apr 22, 2026
8c480bb
Merge branch 'AssemblyScript:main' into main
JairusSW Apr 22, 2026
6c869a5
tests: organize a bit better and cover more cases
JairusSW Apr 22, 2026
b7dfb8f
feat: add support for named tuple types like `[x: f64, y: f64]`
JairusSW Apr 23, 2026
98fe27f
tests: add tests for named tuple types
JairusSW Apr 23, 2026
39eb3f9
chore(cli): keep multi-value feature as unimplemented in the cli since
JairusSW Apr 23, 2026
e8563bd
fix: tuples should not be parsed if multi-value is disabled
JairusSW Apr 23, 2026
4b2a399
chore: why did i make it readonly lol
JairusSW Apr 23, 2026
5cc7dce
chore: make sure bootstrapping works
JairusSW Apr 23, 2026
5375b24
chore: revert random formatting changes
JairusSW Apr 23, 2026
c8429ce
chore: token check should come before feature
JairusSW Apr 23, 2026
1ca3ad7
chore: update tuple grammer to show named types logically
JairusSW Apr 23, 2026
7c7f000
chore: add options parameter to end of Parser constructor
JairusSW Apr 23, 2026
dda5084
chore: add `Readonly<T>` type helper
JairusSW Apr 23, 2026
5874329
tests: add a few tests for empty tuples and more nullable tests
JairusSW Apr 23, 2026
e5ab232
chore: revert to options workaround
JairusSW Apr 23, 2026
5ef3d89
Update src/parser.ts
JairusSW Apr 23, 2026
d59cbbc
chore: stick with temporary options workaround
JairusSW Apr 23, 2026
301a564
Update src/program.ts
JairusSW Apr 23, 2026
a97c523
Update src/parser.ts
JairusSW Apr 23, 2026
9cd4cb2
chore: fix typo and indenting
JairusSW Apr 23, 2026
f45638a
tests: test each line/col
JairusSW Apr 23, 2026
729cb2d
fix: `readonly` should be ignored for now
JairusSW Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ under the licensing terms detailed in LICENSE:
* Adrien Zinger <zinger.ad@gmail.com>
* Ruixiang Chen <xiang19890319@gmail.com>
* Daniel Salvadori <danaugrs@gmail.com>
* Jairus Tanaka <jairus.v.tanaka@outlook.com>
* Jairus Tanaka <me@jairus.dev>
* CountBleck <Mr.YouKnowWhoIAm@protonmail.com>
* Abdul Rauf <abdulraufmujahid@gmail.com>
* Bach Le <bach@bullno1.com>
Expand Down
32 changes: 32 additions & 0 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const enum NodeKind {
// types
NamedType,
FunctionType,
TupleType,
TypeName,
TypeParameter,
Parameter,
Expand Down Expand Up @@ -161,6 +162,15 @@ export abstract class Node {
return new FunctionTypeNode(parameters, returnType, explicitThisType, isNullable, range);
}

static createTupleType(
elements: TypeNode[],
elementNames: (IdentifierExpression | null)[] | null,
isNullable: bool,
range: Range
): TupleTypeNode {
return new TupleTypeNode(elements, elementNames, isNullable, range);
}

static createOmittedType(
range: Range
): NamedTypeNode {
Expand Down Expand Up @@ -862,6 +872,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 = <TupleTypeNode>changetype<TypeNode>(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);
}
Expand Down Expand Up @@ -928,6 +944,22 @@ export class FunctionTypeNode extends TypeNode {
}
}

/** Represents a tuple type. */
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. */
range: Range
) {
super(NodeKind.TupleType, isNullable, range);
}
}

/** Represents a type parameter. */
export class TypeParameterNode extends Node {
constructor(
Expand Down
36 changes: 36 additions & 0 deletions src/extra/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
TypeNode,
NamedTypeNode,
FunctionTypeNode,
TupleTypeNode,
TypeName,
TypeParameterNode,

Expand Down Expand Up @@ -134,6 +135,10 @@ export class ASTBuilder {
this.visitFunctionTypeNode(<FunctionTypeNode>node);
break;
}
case NodeKind.TupleType: {
this.visitTupleTypeNode(<TupleTypeNode>node);
break;
}
case NodeKind.TypeParameter: {
this.visitTypeParameter(<TypeParameterNode>node);
break;
Expand Down Expand Up @@ -387,6 +392,10 @@ export class ASTBuilder {
this.visitFunctionTypeNode(<FunctionTypeNode>node);
break;
}
case NodeKind.TupleType: {
this.visitTupleTypeNode(<TupleTypeNode>node);
break;
}
default: assert(false);
}
}
Expand Down Expand Up @@ -450,6 +459,33 @@ export class ASTBuilder {
if (isNullable) sb.push(") | null");
}

visitTupleTypeNode(node: TupleTypeNode): void {
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]);
}
}
sb.push("]");
if (node.isNullable) sb.push(" | null");
}

visitTypeParameter(node: TypeParameterNode): void {
this.visitIdentifierExpression(node.name);
let extendsType = node.extendsType;
Expand Down
57 changes: 56 additions & 1 deletion src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import {
CommonFlags,
Feature,
LIBRARY_PREFIX,
PATH_DELIMITER
} from "./common";
Expand Down Expand Up @@ -42,6 +43,7 @@ import {
TypeName,
NamedTypeNode,
FunctionTypeNode,
TupleTypeNode,
ArrowKind,

Expression,
Expand Down Expand Up @@ -90,6 +92,7 @@ import {

mangleInternalPath
} from "./ast";
import { Options } from "./compiler";

/** Represents a dependee. */
class Dependee {
Expand Down Expand Up @@ -118,7 +121,9 @@ export class Parser extends DiagnosticEmitter {
sources: Source[];
/** Current overridden module name. */
currentModuleName: string | null = null;

// TODO: Remove when multi-value feature will enable by default.
/** Compiler options. */
options: Options | null = null;
/** Constructs a new parser. */
constructor(
diagnostics: DiagnosticMessage[] | null = null,
Expand Down Expand Up @@ -563,6 +568,49 @@ 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)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually tuple doesn't and is impossible to rely on mutli-value.

declare function f(): [i32,i32];
function b() {
  let a = f();
  let b = a;
  b[0] = 1;
  assert(a[0] == 1);
}

In this cases, multiple value cannot handle this case since it treat the whole tuple as a value instead of a heap object.

Copy link
Copy Markdown
Contributor Author

@JairusSW JairusSW Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HerrCai0907, I'll add full support for tuples in the future. Tuples will need to exist in two forms:

  1. Tuple becomes multi value when returned from function
  2. When used as a heap type (eg. store<[i32, i32]>(), Array<[i32, i32]>), it becomes a heap type

Then, depending on usage, tuples will be lifted and lowered from multi-value to tuple

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We’ve been discussing this for a long time, and it’s clear that the only way to ensure that tuples are properly converted to multivalues is to place a readonly attribute before them. Otherwise, we’ll have to analyze the control flow for side effects which required custom pass based on binaryen infra.

let elements: TypeNode[] = [];
let elementNames: (IdentifierExpression | null)[] = [];
let hasElementNames = false;
Comment thread
MaxGraey marked this conversation as resolved.
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) {
this.error(
DiagnosticCode._0_expected,
tn.range(tn.pos), "]"
);
}
return null;
}
}
type = Node.createTupleType(elements, hasElementNames ? elementNames : null, false, tn.range(startPos, tn.pos));

// 'void'
} else if (token == Token.Void) {
type = Node.createNamedType(
Expand Down Expand Up @@ -4581,6 +4629,13 @@ function isCircularTypeAlias(name: string, type: TypeNode): bool {
}
break;
}
case NodeKind.TupleType: {
let elements = (<TupleTypeNode>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;
Expand Down
9 changes: 6 additions & 3 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -436,12 +436,15 @@ 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);
this.nativeFile = nativeFile;
this.filesByName.set(nativeFile.internalName, nativeFile);

// TODO: temporary workaround. remove after multi-value support is finished
this.parser.options = this.options;
}

/** Module instance. */
Expand Down
31 changes: 31 additions & 0 deletions src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {

import {
FunctionTypeNode,
TupleTypeNode,
ParameterKind,
TypeNode,
NodeKind,
Expand Down Expand Up @@ -171,6 +172,10 @@ export class Resolver extends DiagnosticEmitter {
resolved = this.resolveFunctionType(<FunctionTypeNode>node, flow, ctxElement, ctxTypes, reportMode);
break;
}
case NodeKind.TupleType: {
resolved = this.resolveTupleType(<TupleTypeNode>node, flow, ctxElement, ctxTypes, reportMode);
break;
}
default: assert(false);
}
node.currentlyResolving = false;
Expand Down Expand Up @@ -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<string,Type> | 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,
Expand Down
9 changes: 9 additions & 0 deletions tests/compiler/tuple-circular.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"asc_flags": [
"--enable", "multi-value"
],
"stderr": [
"TS2456: Type alias 'Loop' circularly references itself.",
"1 parse error(s)"
]
}
1 change: 1 addition & 0 deletions tests/compiler/tuple-circular.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type Loop = [Loop, i32];
9 changes: 9 additions & 0 deletions tests/compiler/tuple-disabled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"asc_flags": [
"--disable", "multi-value"
],
"stderr": [
"TS1110: Type expected.",
"3 parse error(s)"
]
}
3 changes: 3 additions & 0 deletions tests/compiler/tuple-disabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function tupleDisabled(x: [left: i32, right: i32]): void {}
export type tupleTypeDisabled1 = [i32, i32];
export type tupleTypeDisabled2 = [];
37 changes: 37 additions & 0 deletions tests/compiler/tuple-errors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"asc_flags": [
"--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",
"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)"
]
}
Loading