Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/laurci/poc-comp-plugins
POC. Simple plugin-based meta-programming platform on top of Typescript
https://github.com/laurci/poc-comp-plugins
Last synced: 30 days ago
JSON representation
POC. Simple plugin-based meta-programming platform on top of Typescript
- Host: GitHub
- URL: https://github.com/laurci/poc-comp-plugins
- Owner: laurci
- Created: 2022-07-02T20:56:34.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2022-08-16T10:49:41.000Z (over 2 years ago)
- Last Synced: 2024-10-30T06:27:15.618Z (3 months ago)
- Language: TypeScript
- Size: 81.1 KB
- Stars: 3
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# comp-plugins POC
## Running:
1. `yarn` to install dependencies
2. `yarn dev` to run the script## The what
The script creates a new typescript compiler instance (program, checker and language service) and loads the project at `./sample`. It then proceeds to transform the sample with the configured plugins (it creates a transformer factory for each plugin). The plugins then proceed to modify the AST and produce changes where needed. The output is written to disk and a node process starts running `index.js`.
The output of the script is split into the compile time logs (including full dumps of the output files) and the runtime logs. (look after the `attempting to run` log)
### The line plugin
This is the most basic plugin. It brings c++'s `__line` into typescript :) It basically replaces any uses of the `__line` identifier with a number literal representing the line it is located on.
**NOT VALID ANYMORE** Now it wraps the number literal in an invocation to `__compute_line` function generated in a fake source file and imported when the plugin finds any usage of `__line`.
### The macro plugin
This plugin implements macros on a very basic level. A macro is just a call expression that contains a double non-null expression before it's identifier `test!!(...)`. This is just one way to do this. You could also completely ignore this and just look for a specific set of identifiers. There are 2 macros implemented:
- `magic!!()`: it gets replaced with the number literal 42
- `log!!(...any[])`: it gets replaced with a call to `console.log`. this example shows that you could have 0 cost abstractions like using different log providers depending on configuration options. you could also completely remove calls depending on the `LOG_LEVEL` config.### The callsite plugin
This plugins looks for usages of the `CallArgumentText` and `CallPosition` types in the parameters of functions. It then binds references to those parameters and replaces the call expression with a modified version that provides the proper values for the random. A bit hard to grasp, so here's an example:
```typescript
// if you have this signature
function assert(condition: unknown, argText?: CallArgumentText, position?: CallPosition): asserts condition;// and you invoke it like so
assert(a === 42);// it will be converted to the following call
assert(a == 42, "a == 42", {line: 42, col: 69, file: "some random absolute path"});// so the runtime can make sense of it and use it for stuff
function assert(condition: unknown, argText?: CallArgumentText, position?: CallPosition): asserts condition {
if (!argText || !position) throw new Error("Invalid call to assert.");if (!condition) {
throw new AssertionError(argText, position.line, position.col, position.file);
}
}
```### The auto register plugin
This plugin looks for exported class declarations that end with `Service` ex `RandomService` and automatically creates a registry that contains references to all of them. Those references can then be used to create instances of the services. The output of this plugin is a generated file that must be imported.
### The derive plugin
Oh boy, where do I even start with this. This plugin uses both a compiler plugin and a language service plugin to achieve it's function. It's basically the same mechanism as Rust's derive macros.
Let's say you have to add a functionality to convert any class to a binary stream. You can do this by creating a `Serializable` abstract class that has an abstract `toByteArray` method. The class that extends `Serializable` must implement the `toByteArray` method, thus conforming to our specification. But what if this task can be automated. Spoiler, you can. If a class extends `derive(Serializable)` the derive compiler plugin will reach for your method implementation to generate the code inside the `toByteArray` method. The compile time implementation gets access to the AST of both the target class and the derivable class. Having access to this information it can then generate the code to read all the properties of the instance and pack them into a byte stream. Other than the startup code, there **ISN'T** any performance penalty. Everything is done at compile time. This is called a 0 cost abstraction.
Before looking into the `Serializable` example code, I highly recommend to check `Greet` first. It's a simple example of a derive plugin. `WelcomeMessage` in `sample/test.ts` derives from it. The implementation is in `sample/meta/greet.ts`.
Take a look at the implementation of `WelcomeMessage` in `sample/test.ts`. The implementation of `toByteArray` is generated by `sample/meta/serializable.ts`. The output JS for `WelcomeMessage` looks like this:
```javascript
class WelcomeMessage extends class {
constructor() {
this._Greet = new (class extends Greet {
hello() {
console.log("comptime hello from WelcomeMessage");
}
})(this, WelcomeMessage);
this._Serializable = new (class extends Serializable {
toByteArray() {
var _buff = new ArrayBuffer(7),
_data = new DataView(_buff);
_data.setUint8(0, this.instance.d);
_data.setUint16(1, this.instance.e);
_data.setUint32(3, this.instance.f);
return new Uint8Array(_buff);
}
})(this, WelcomeMessage);
}
} {
constructor() {
super(...arguments);
this.d = 4;
this.e = 5;
this.f = 6;
}
hello() {
return this._Greet.hello();
}
toByteArray() {
return this._Serializable.toByteArray();
}
}
```The current implementation only serializes unsigned int types. This can be extended to support any type of property.
The way the derive plugin works is a bit complicated, but it shows the power of the approach. You can literally build the language you want on top of Typescript.
The first part is the compiler plugin that replaces the calls to `extends derive(...)` with class implementations of each of the derivable classes. It also provides public methods that proxy the calls to the generated implementations. You can check it in `src/plugins/derive/compiler.ts`.
The second part is the language service plugin that really just helps to eliminate some of the errors created by not implementing abstract members of extended classes and providing different types to `extends derive(...)` basically allowing for multiple inheritance. It's implemented in `src/plugins/derive/language-service.ts`.
## Language service plugins
**DOCS WIP**
## The why
Provide a solid meta-programming platform for Typescript. Help people to remove the runtime bloat and allow for easy to implement zero-cost abstractions. For details see my suggestion in the Deepkit Discord server [here](https://discord.com/channels/759513055117180999/956486537208528937/992438187634987068).
## To do:
- [x] `before` and `after` hooks for plugins
- [x] allowing plugins to append statements to the source file without replacing existing statements
- [x] allowing plugins to create fake source files to generate arbitrary code in it and refer to it from the real source files (should also be able to emit them)
- [x] hack the language service and provide cool editor features to plugins
- [x] add watch mode
- [x] **NOT IN SCOPE ANYMORE** write a few more example plugins (any suggestions appreciated) and simplify the plugins API along the way## Making sense of this codebase
Well... there are no docs, this is just a POC :) So go exploring! Start with `src/bin.ts` and `sample/index.ts`.