DocsSlangCompiler & Decompiler

Compiler & Decompiler

How compile() builds a live ReactFlow graph from source text, and how decompile() serialises the graph back.

Overview

SlangCompiler.ts and SlangDecompiler.ts form the bi-directional bridge between Slang source text and the ReactFlow node graph. The compiler is incremental — it compares each parsed command against existing nodes and only creates, updates, or removes what has actually changed. The decompiler uses Kahn's topological sort to guarantee that every emitted line refers only to variables declared on earlier lines.

▶ Compile

Source text → SlangCommand[] → ReactFlow nodes + edges → Dagre layout → executeGraph()

◀ Decompile

ReactFlow nodes (topological order) → Slang lines + config commands → source text

compile()

Signature & Return Type

export interface CompileResult {
  success: boolean;
  /** Maps variable names declared in the script to created node IDs */
  nodeMap: Map<string, string>;
}

export function compile(source: string): CompileResult

Compilation Pipeline

1

Reset & Set Batch Mode

slangLogger.info("Compiling…") is called. setBatchMode(true) is invoked on GraphCommands so that all node-create operations are queued rather than triggering re-renders on each step.

2

Parse

parse(source) from SlangParser tokenises every non-comment line into a SlangCommand[]. On parse failure the error is logged, batch mode is exited, and CompileResult { success: false } is returned.

3

Execute each command

For every SlangCommand, executeCommand() is called. It validates the namespace pairing, looks up the CommandDef, resolves input references via nodeMap and findNodeByLabel(), coerces parameters, and either creates a new node or updates an existing one. Config/action commands (def.action is set) are executed as side-effects with no node change.

4

Stale node removal

If no errors occurred, every node whose ID is not present in nodeMap.values() is treated as stale and deleted via deleteNodes(staleIds). This ensures that nodes removed from the script are also removed from the graph.

5

Post-compile layout

setBatchMode(false) releases queued updates. After a 100 ms tick, applyDagreLayout() re-arranges all nodes, fitGraphView() adjusts the viewport, and executeGraph() runs the strategy.

Node Creation vs. Update Logic

executeCommand() calls findNodeByLabel(varName) to check if a node with the same label already exists in the graph.

Node does NOT exist → CREATE

  • • Merges getDefaultNodeData(nodeType) with overrides and label: varName
  • • Positions node in a 4-column grid at x = 80 + col*280, y = 80 + row*120
  • • Calls createNode() then pushNode() so subsequent lines can resolve the ID immediately
  • • Connects each resolved input via connect(sourceId, nodeId, targetHandle)

Node DOES exist → UPDATE

  • • Compares each override value against existingNode.data to detect parameter changes
  • • Calls updateNodeData(nodeId, overrides) only when params changed
  • • Calls reconcileConnections() to add / remove edges as needed
  • • Returns 'updated', 'unchanged', or 'created' to the count reporter

paramGroups Resolution

Commands like TAKEPROFIT and STOPLOSS use a paramGroups discriminator. The compiler reads the first non-input argument as the discriminator string, then selects the matching group to determine which additional params are expected. This allows the same command to accept a variable number of parameters depending on its mode.

// paramGroups for TAKEPROFIT:
// discriminator: 'tpType'
// groups: {
//   percentage: ['takeProfitPercent'],
//   fixed:      ['fixedAmount'],
//   pips:       ['pips', 'pipSize'],
//   money:      ['moneyTarget', 'positionSize'],
// }

tp = rm.TAKEPROFIT(s, "percentage", 2.5)
// → tpType = "percentage", takeProfitPercent = 2.5 (only 1 extra param needed)

tp = rm.TAKEPROFIT(s, "pips", 50, 0.0001)
// → tpType = "pips", pips = 50, pipSize = 0.0001 (2 extra params needed)

Slang Globals

A module-level Map<string, string | number> stores values set by config commands during compilation. It is cleared by resetCompiler().

FunctionDescription
setSlangGlobal(name, value)Stores a key-value pair in the global map. Called by SetWindowSize and other config commands.
getSlangGlobal(name)Retrieves a previously stored global value.
resetCompiler()Clears all globals and calls slangLogger.clear(). Called before each fresh compile.

decompile()

Signature

export function decompile(opts?: { excludeConfig?: boolean }): string

Pass excludeConfig: true to omit ch.* and cg.* lines — useful when saving a strategy to the library without embedding display settings.

Decompilation Pipeline

1

Build reverse map

buildReverseMap() iterates every registered command name via listCommands(), calls getCommandDef() for each, and builds a Map<nodeType, { funcName, def }>. This lets the decompiler look up a node's function name directly from its ReactFlow type string.

2

Build adjacency & in-degree maps

Two maps are constructed from the edge list: adj (source → [{targetId, targetHandle}]) for forward traversal, and incomingByHandle (nodeId → [{sourceId, targetHandle}]) for ordering input edges during emission. inDegree tracks how many incoming edges each node has.

3

Topological sort (Kahn's algorithm)

The queue is seeded with all root nodes (zero in-degree) via findRoots(). Each dequeued node's out-neighbours have their in-degree decremented; those that reach zero are added to the queue. This guarantees every emitted variable refers only to already-declared variables.

4

Emit lines

For each ordered node, incoming edges are sorted by targetHandle to preserve positional input order. Parameter values are read from node.data (falling back to node.data.resultData for action nodes). paramGroups-aware nodes emit only the discriminator and its group's params. Unknown node types emit a // [skip] comment.

5

Emit config commands

Unless excludeConfig is set, a blank line followed by cg.SetWindowSize(n) is appended using the current window range. Then, for each chart that has assigned nodes, a ch.AddToChart(index, …refs) line is emitted (charts sorted by index ascending).

Cycle / Disconnected Node Handling

After the topological pass, any node not yet visited (cycles or truly disconnected nodes) is appended to the ordered list unchanged. This ensures every node in the graph produces a line in the output, even if topological ordering is not strictly achievable.

SlangParser — Core Tokeniser & Registry

The parser is intentionally simple and has no knowledge of graphs or the DOM. Its only jobs are: tokenise text into commands, maintain a global command registry, and provide a coerceParam() helper.

SlangCommand Type

interface SlangCommand {
  varName:    string;     // variable name (left-hand side); '' for standalone calls
  namespace?: string;     // optional prefix, e.g. "sc", "ta"
  funcName:   string;     // function name, e.g. "SMA"
  args:       string[];   // raw argument tokens (quotes already stripped)
  line:       number;     // 1-based source line number (for error messages)
}

CommandDef Type

interface CommandDef {
  nodeType:        string;                          // ReactFlow node type string
  inputs?:         number;                          // leading args that are input node refs
  inputChannels?:  { name: string; type: string }[];// named OHLC / signal input edges
  inputArguments?: { name: string; type: string }[];// parameter definitions (name + type)
  outputChannels?: { name: string; type: string }[];// named output data channels
  variadicInputs?: true;                            // params come first; rest are input refs
  params?:         string[];                        // derived from inputArguments (auto-set)
  paramTypes?:     ('string' | 'number')[];         // derived from inputArguments (auto-set)
  paramGroups?: {
    discriminator: string;                          // param name that selects the active group
    groups:        Record<string, string[]>;        // discriminator value → param names
  };
  action?: (                                        // if set: no node; run as side-effect
    params:         Record<string, string | number>,
    varName:        string,
    resolvedInputs: string[],
  ) => void;
}

Public API

FunctionDescription
parse(source)Splits source by newlines and parses each non-comment line. Returns SlangCommand[]. Throws on syntax errors.
parseLine(line, lineNum)Parses a single line. Returns null for blanks/comments, throws on invalid syntax.
registerCommands(defs)Merges a Record<string, CommandDef> into the global COMMANDS registry. Normalises each def (derives params/paramTypes from inputArguments, sets inputs from inputChannels.length).
getCommandDef(name)Returns the CommandDef for a function name, or undefined.
listCommands()Returns all registered command names, sorted alphabetically.
getCommandDefs()Returns the full COMMANDS registry object.
coerceParam(value, expectedType, paramName)Converts a raw string token to string or number. Throws a human-readable error on type mismatch.

SlangStore — In-Memory Code & Script State

SlangStore.ts provides two independent pub/sub channels in a single module-level singleton, allowing the editor, toolbar, and any other component to stay in sync without prop-drilling.

Code channel

getSlangCode()

Returns the current script text string.

setSlangCode(code)

Updates the in-memory code and notifies all listeners.

onSlangCodeChange(listener)

Subscribes to code changes. Returns an unsubscribe function.

Current script channel

getCurrentSlang()

Returns CurrentSlang | null (id + name of the loaded library entry).

setCurrentSlang(slang)

Sets the currently loaded script metadata and notifies listeners.

onCurrentSlangChange(listener)

Subscribes to script-load changes. Returns an unsubscribe function.

SlangLogger

A class-based singleton (slangLogger) accumulates compilation and runtime messages. The editor subscribes to it to render the live log panel.

LogEntry

interface LogEntry {
  type:      'info' | 'error' | 'success';
  message:   string;
  timestamp: Date;
}

Methods

info(message)Pushes an info-level entry.
success(message)Pushes a success-level entry (green in UI).
error(message)Pushes an error-level entry (red in UI).
subscribe(listener)Registers a callback for every new entry. Returns unsubscribe fn.
getEntries()Returns a readonly snapshot of all accumulated entries.
clear()Empties the log. Called by resetCompiler().

Error format

Compilation errors include the 1-based line number: "L3: Input \"fast\" is not defined — declare it before this line". This message is displayed directly in the log panel and is suitable for the user to read.