Skip to content

2graphic/sinap-typescript-loader

Repository files navigation

For more details on sinap plugins see sinap-core.

Implementing a plugin in TypeScript

Here is an example DFA interpreter if you want to follow along.

Getting Started with an Example Plugin

ExamplePlugin.zip

Two files are required for a sinap plugin written in typescript: package.json and any entry point typescript file; in this example, we call our entry point file interpreter.ts.

Plugin Metadata

The package.json file is modeled after the npm package.json standard. It requires the fields name, main, and sinap. The sinap field requires a kind, plugin-file, and loader. Other fields in package.json are optional.

package.json

{
  "name": "Example Plugin",
  "version": "1.0.0",
  "description": "Your description here",
  "main": "interpreter.ts",
  "sinap": {
    "kind": [
      "Examples",
      "Example1"
    ],
    "plugin-file": "interpreter.ts",
    "loader": "typescript"
  }
}
name
Name of the plugin.
version
Plugin version.
description
Brief description of the plugin.
main
Package entry point.
sinap
Sinap plugin meta information.
kind
Plugin kind. The list represents a hierarchy with the first element being the general category of the plugin, followed by more specific subcategories, and ending with the specific kind of the plugin. E.g. ["Formal Languages", "Finite Automaton", "Deterministic"].
plugin-file
Interpreter code for the plugin, which should also include types for nodes and edges.
loader
The plugin loader which defines what language the plugin code is written in. In this case, typescript.

Interpreter Code

The interpreter.ts file is the entry point for the plugin. It defines types for nodes and edges, and implements a start and step function; a State type must also be defined in this file. For the full code of interpreter.ts, go here.

Type definitions

All plugins must export the following types: Nodes, Edges, Graph, and State. Nodes and Edges can either be classes or unions of classes, Graph and State must be classes. Each of these classes can contain whatever fields they like. If they define certain special fields, they'll be constrained to match what sinap expects the fields to be.

Nodes may specify a parents and/or a children field. It must be of type T[] where T is any subtype of Edges (including Unions of various edge types). If it is more specific than all the edge types, the IDE will prevent edges of non-matching types from being attached to the node.

Similarly, Edges may specify a source and/or destination field of type T where T is any subtype of Nodes. The IDE will likewise prevent invalid edges from being made.

The Graph class can specify nodes and/or edges fields which must be of type Nodes[] and Edges[] respectively. These will be populated with all the nodes and edges in the graph.

The State class contains all the information about the state of execution during interpretation. For example, if the graph is modelling a state machine, then each state would represtent a single transition from one node to the next.

Example:

// Represents a Node in Sinap
// Any fields added will show up in the properties panel
// in SinapIDE
export class Node {
    // If you add a `parents` or `children` field your node
    // they will be populated with the incomming or outgoing
    // edges.
    // If you have several kinds of edges, and pick a more
    // specific kind for the type here, then that criterion
    // will be considered when creating edges in the IDE
    // children: Edge[];
}

// Same for edges
export class Edge {
    // `source` and `destination` are magic like `parents` of
    // Nodes. Any constraints will also be respected
    // destination: Node;
}

export class Graph {
    // Like Nodes and Egdes
    // cannot add restrictions on the type of `nodes` and `edges`
    nodes: Nodes[];
}

// tell sinap what all the node/edge types are
export type Nodes = Node;
export type Edges = Edge;

// Represents a single step of execution. All exectution state 
// should be accessable from here
export class State {
    constructor(
        readonly message: string;
    ) {
        this.message = "Example Message";
    }
}

Start and Step

The interpreter must implement two functions, start and step.

Their signatures must be:

start(graph: Graph, arg1: T, arg2: U, ...): State | V
step(state: State): State | V

where, in addition to graph, start can take any number of arguments of any type. When the program is run, start will be called once, and step will be called on the resulting state (if start returns a State) until the return value is not a State. The States will be saved, as used by the IDE to support stepping forward and backward for debugging. The real return value (an arbitrary type denoted V above) will be displayed as the result of the computation.

Example:

// called to begin interpreting a graph. 
export function start(input: Graph, startNode: Node): State | boolean {
    return new State();
}

// called repeatedly until it returns something other than a 
// State object
export function step(current: State): State | boolean {
    return false;
}

interpreter.ts

// Represents a Node in Sinap
// Any fields added will show up in the properties panel
// in SinapIDE
export class Node {
    // If you add a `parents` or `children` field your node
    // they will be populated with the incomming or outgoing
    // edges.
    // If you have several kinds of edges, and pick a more
    // specific kind for the type here, then that criterion
    // will be considered when creating edges in the IDE
    // children: Edge[];
}

// Same for edges
export class Edge {
    // `source` and `destination` are magic like `parents` of
    // Nodes. Any constraints will also be respected
    // destination: Node;
}

export class Graph {
    // Like Nodes and Egdes
    // cannot add restrictions on the type of `nodes` and `edges`
    nodes: Nodes[];
}

// tell sinap what all the node/edge types are
export type Nodes = Node;
export type Edges = Edge;

// Represents a single step of execution. All exectution state 
// should be accessable from here
export class State {
    constructor(
        readonly message: string;
    ) {
        this.message = "Example Message";
    }
}

// called to begin interpreting a graph. 
export function start(input: Graph, startNode: Node): State | boolean {
    return new State();
}

// called repeatedly until it returns something other than a 
// State object
export function step(current: State): State | boolean {
    return false;
}