Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ under the licensing terms detailed in LICENSE:
* Mopsgamer <79159094+Mopsgamer@users.noreply.github.com>
* EDM115 <github@edm115.dev>
* Weixie Cui <cuiweixie@gmail.com>
* Abdiel Lopez <a.paperprototype@gmail.com>

Portions of this software are derived from third-party works licensed under
the following terms:
Expand Down
5 changes: 5 additions & 0 deletions cli/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,11 @@ export abstract class Transform {
/** Lists all files in a directory. */
listFiles(dirname: string, baseDir: string): (string[] | null) | Promise<string[] | null>;

/** Called after program initialization, before WASM compilation. Transformers should use
* this hook to perform custom validation of AST constructs such as parameter decorators
* and emit their own diagnostics. Decorators remain in the AST unchanged. */
beforeCompile?(program: Program): void | Promise<void>;
Copy link
Copy Markdown
Member

@MaxGraey MaxGraey May 5, 2026

Choose a reason for hiding this comment

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

Also, I haven't seen any indication that it's been integrated into the pipeline and well tested. For instance afterParse:
https://github.com/search?q=repo%3AAssemblyScript%2Fassemblyscript+afterParse&type=code
for beforeCompile need to do the same

Copy link
Copy Markdown
Member

@MaxGraey MaxGraey May 5, 2026

Choose a reason for hiding this comment

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

It's even less clear why we need beforeCompile when we have afterInitialize?

validate?(program: Program): Promise<bool> | bool;

Specialized validate would be appropriate more, as I suggested. Because it could optionally interrupt the compilation flow without trigger exception (soft bail)

Copy link
Copy Markdown
Author

@PaperPrototype PaperPrototype May 5, 2026

Choose a reason for hiding this comment

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

It's even less clear why we need afterInitialize when we have afterInitialize?

I'm gonna assume you meant:

It's even less clear why we need beforeCompile when we have afterInitialize?

I think a preliminary discussion on this might be a good idea before I just jump and and start adding stuff 😅. Maybe a change to the transformer API should be delegated to it's own PR? What do you think? That would allow this PR to focus solely on adding decorators to the AST.

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.

I'm gonna assume you meant

Yeah, copy-paste typo

Maybe a change to the transformer API should be delegated to it's own PR? What do you think?

If this is not a blocker then that’s actually how it should be.


Comment on lines +276 to +280
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.

Transformers should use

This can be cited as an example, not as a requirement. Also, the comment is rather wordy.

/** Called when parsing is complete, before a program is instantiated from the AST. */
afterParse?(parser: Parser): void | Promise<void>;

Expand Down
4 changes: 4 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export default defineConfig([

// FIXME: Tagged template literal tests with invalid escapes
"tests/compiler/templateliteral.ts",

// Decorators on `this` are not allowed typically in TypeScript, but this
// fixture exercises that AS-only syntax and is validated by transform tests.
"tests/transform/parameter-decorators.ts",
]),

js.configs.recommended,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@
"test:browser": "node --enable-source-maps tests/browser",
"test:asconfig": "cd tests/asconfig && npm run test",
"test:transform": "npm run test:transform:esm && npm run test:transform:cjs",
"test:transform:esm": "node bin/asc tests/compiler/empty --transform ./tests/transform/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/simple.js --noEmit",
"test:transform:cjs": "node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/simple.js --noEmit",
"test:transform:esm": "node bin/asc tests/compiler/empty --transform ./tests/transform/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/simple.js --noEmit && node --experimental-strip-types --no-warnings bin/asc tests/transform/parameter-decorators.ts --transform ./tests/transform/remove-parameter-decorators.ts --noEmit",
"test:transform:cjs": "node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/index.js --noEmit && node bin/asc tests/compiler/empty --transform ./tests/transform/cjs/simple.js --noEmit && node bin/asc tests/transform/parameter-decorators.ts --transform ./tests/transform/cjs/remove-parameter-decorators.js --noEmit",
"test:cli": "node tests/cli/options.js",
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
"asbuild:debug": "node bin/asc --config src/asconfig.json --target debug",
Expand Down
13 changes: 10 additions & 3 deletions src/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ export abstract class Node {
parameters: ParameterNode[],
returnType: TypeNode,
explicitThisType: NamedTypeNode | null,
explicitThisDecorators: DecoratorNode[] | null,
isNullable: bool,
range: Range
): FunctionTypeNode {
return new FunctionTypeNode(parameters, returnType, explicitThisType, isNullable, range);
return new FunctionTypeNode(parameters, returnType, explicitThisType, explicitThisDecorators, isNullable, range);
}

static createTupleType(
Expand Down Expand Up @@ -191,9 +192,10 @@ export abstract class Node {
name: IdentifierExpression,
type: TypeNode,
initializer: Expression | null,
decorators: DecoratorNode[] | null,
range: Range
): ParameterNode {
return new ParameterNode(parameterKind, name, type, initializer, range);
return new ParameterNode(parameterKind, name, type, initializer, decorators, range);
}

// special
Expand Down Expand Up @@ -935,6 +937,8 @@ export class FunctionTypeNode extends TypeNode {
public returnType: TypeNode,
/** Explicitly provided this type, if any. */
public explicitThisType: NamedTypeNode | null, // can't be a function
/** Decorators on an explicit `this` parameter, if any. Preserved as transform-only syntax. */
public explicitThisDecorators: DecoratorNode[] | null,
/** Whether nullable or not. */
isNullable: bool,
/** Source range. */
Expand Down Expand Up @@ -997,12 +1001,13 @@ export class ParameterNode extends Node {
public type: TypeNode,
/** Initializer expression, if any. */
public initializer: Expression | null,
/** Decorators, if any. Preserved as transform-only syntax so transforms can rewrite or remove them before validation. */
public decorators: DecoratorNode[] | null,
/** Source range. */
range: Range
) {
super(NodeKind.Parameter, range);
}

/** Implicit field declaration, if applicable. */
implicitFieldDeclaration: FieldDeclaration | null = null;
/** Common flags indicating specific traits. */
Expand Down Expand Up @@ -1696,6 +1701,8 @@ export class Source extends Node {
debugInfoIndex: i32 = -1;
/** Re-exported sources. */
exportPaths: string[] | null = null;
/** Function types with parameter or this-parameter decorators, revisited after transforms for validation. */
decoratedFunctionTypes: FunctionTypeNode[] | null = null;

/** Checks if this source represents native code. */
get isNative(): bool {
Expand Down
20 changes: 17 additions & 3 deletions src/extra/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ export class ASTBuilder {
sb.push(isNullable ? "((" : "(");
let explicitThisType = node.explicitThisType;
if (explicitThisType) {
this.serializeParameterDecorators(node.explicitThisDecorators);
sb.push("this: ");
this.visitTypeNode(explicitThisType);
}
Expand Down Expand Up @@ -1189,6 +1190,7 @@ export class ASTBuilder {
let numParameters = parameters.length;
let explicitThisType = signature.explicitThisType;
if (explicitThisType) {
this.serializeParameterDecorators(signature.explicitThisDecorators);
sb.push("this: ");
this.visitTypeNode(explicitThisType);
}
Expand Down Expand Up @@ -1577,7 +1579,7 @@ export class ASTBuilder {

// other

serializeDecorator(node: DecoratorNode): void {
serializeDecorator(node: DecoratorNode, isInline: bool = false): void {
let sb = this.sb;
sb.push("@");
this.visitNode(node.name);
Expand All @@ -1592,16 +1594,28 @@ export class ASTBuilder {
this.visitNode(args[i]);
}
}
sb.push(")\n");
sb.push(")");
}
if (isInline) {
sb.push(" ");
} else {
sb.push("\n");
indent(sb, this.indentLevel);
}
}

serializeParameterDecorators(decorators: DecoratorNode[] | null): void {
if (decorators) {
for (let i = 0, k = decorators.length; i < k; ++i) {
this.serializeDecorator(decorators[i], true);
}
}
indent(sb, this.indentLevel);
}

serializeParameter(node: ParameterNode): void {
let sb = this.sb;
let kind = node.parameterKind;
this.serializeParameterDecorators(node.decorators);
let implicitFieldDeclaration = node.implicitFieldDeclaration;
if (implicitFieldDeclaration) {
this.serializeAccessModifiers(implicitFieldDeclaration);
Expand Down
Loading