-
Notifications
You must be signed in to change notification settings - Fork 6
Adding a new transformation
A VSL transformation can do a few things:
- Modify the AST
- Process the AST to send information to backend
While a short list, there are very powerful things. Type deduction, memory optimizer, and most features are implemented at the transformation level. Besides parsing and code emission, generally all features are implemented at or by transformers.
This quick overview will show how to create the @deprecated("This is deprecated")
transformer.
Transformers work in passes. For example the first pass in VSL first populates the AST with metadata (so you can find the parents of an AST node etc.). The second pass registers all the identifiers and creates scopes. The third pass type deducts globals and registers fields. The fourth pass performs expression type deduction and registers these variables and types in their parent scope. Which pass should your transformation go in? It depends on what information it needs, optimally run your transformation as early as possible. Check the CompilationGroup#compile method to identify the order of transformation.
To create a transformer create a file (camel case) in the src/vsl/transform/passes
directory, take a look at the naming conventions used and try to have your class fit that. To create a transformer your file should look like:
src/vsl/transform/passes/RegisterAThing.js
:
import Transformation from '../transformation';
import TransformError from '../transformError';
import e from '../../errors';
import t from '../../parser/nodes';
/**
* Clearly explain what this transformation does.
*/
export default class DescribeDeprecatedAnnotation extends Transformation {
constructor() {
super(t.Annotation, "Describe::DeprecatedAnnotation");
}
modify(node: Node, tool: ASTTool) {
/* code */
}
}
in the super()
call the first argument is the node type that you want this to be called on (null
makes it called on all nodes). The second argument is the name that the transformation will appear as when debugging. In the modify
callback is where your code will be
To identify how the AST looks to make your transformer, create a file like test.vsl
(VSL's gitignore excludes all files starting with /test
).
test.vsl
:
@deprecated("This is deprecated")
class MyClass {}
Now take a look at the AST for this:
$ vsl debugsrc -dasts test.vsl
You should see the VSL AST appear:
CodeBlock
└ statements[]
└ ClassStatement
├ name: Identifier; MyClass
├ generics[]
├ superclasses[]
├ statements: CodeBlock
│ └ statements[]
└ annotations[]
└ Annotation
From here we can see our annotation. The ClassStatement
that we want to find is the 2nd parent of the Annotation
node. Now, knowing what the AST looks we can prepare our modify()
callback. Also, to find the name of the annotation parameter, we can take a look at the Annotation node's documentation or you can also just look at it's source. Looking at this we see it's argument is in annotationNode.args[0]
.
In VSL's implementation, a 'scope' is a group of variables. This means a class has a scope called a subscope with all it's fields and methods, it also has a static scope with all of it's static fields and methods. However classes are in scopes. Since VSL variables store additional information that their AST node doesn't provide, VSL has _ScopeItem_s which are objects which represent some value in a scope. For example let x = 1
has x
being ScopeAliasItem
which stores the node that declares x
and other information such as the type of x
and it's binding (constant/variable). The node can also reference its ScopeItem using a .reference
attribute on many declarations.
So it only make sense to have @deprecated
on a declaration, one that is in a scope, so to ensure this now we will modify our callback:
import ScopeItem from '../../scope/scopeItem';
// ...
export default class DescribeDeprecatedAnnotation extends Transformation {
// ...
modify(node: Node, tool: ASTTool) {
const parentNode = tool.nthParent(2); // Get the declaration node
// Check if node can have a `@deprecated` attribute
const targetedDeclaration = parentNode.reference;
if (targetedDeclaration instanceof ScopeItem) {
// TODO: IMPLEMENTATION
} else {
// Throw an error otherwise
throw new TransformError(
`Unexpected @deprecated. The @deprecated can only be used on ` +
`declarations.`,
node // Pass the node that the error occurred at for rich errors
);
}
}
}
Now to implement this we will use two properties of ScopeItem
s:
-
typeDescription
: gives a string of the value it represents, e.g. 'Function' or 'Variable' -
rootId
: gives the name of the value (e.g.let x = 1
returnsx
). -
deprecationStatus
:null
if not deprecated otherwise a string of the deprecation message.
Here's what that looks like:
const targetedDeclarationType = targetedDeclaration.typeDescription;
const targetedDeclarationName = targetedDeclaration.rootId;
const annotationText = node.args?.length > 0 ?
node.args[0] :
`${targetedDeclarationType} ${targetedDeclarationName} is deprecated. ` +
`It's use is not recommended as it will likely be removed in a future release.`;
targetedDeclaration.deprecationStatus = deprecationText;
here we obtain the text by using node.args[0]
is the user provided it (node.args?.length > 0
) otherwise we craft a default message.
Anyway, that's it as far as the transformation goes! Now when the backend receives a ScopeItem
with the deprecationStatus
, it can emit a warning when used. For backend watchers and code emission that'll probably be another wiki page.
- Introduction
- Installation
- VSL By Example
- Usage
- WASM (WebAssembly)
- The Basics
- Your First Program
- Syntax
- Concepts
- Modules
- Advanced Details
- Interop
- VSL Development
- Common Errors