Skip to content

@rcs-lang/ast

The @rcs-lang/ast package provides TypeScript type definitions for working with RCL Abstract Syntax Trees. This guide will help you understand how to use these types effectively in your projects.

Terminal window
npm install @rcs-lang/ast
// Import specific types
import { RclFile, AgentDefinition, FlowDefinition } from '@rcs-lang/ast';
// Import all types under namespace
import * as AST from '@rcs-lang/ast';
// Import utilities
import { walkAST, isMessageDefinition } from '@rcs-lang/ast';

Every AST node has a type property that identifies its kind and a range property that indicates its location in the source code.

import { ASTNode, Range } from '@rcs-lang/ast';
function printNodeInfo(node: ASTNode) {
console.log(`Node type: ${node.type}`);
console.log(`Location: Line ${node.range.start.line}, Column ${node.range.start.character}`);
}

RCL documents follow this structure:

RclFile
├── AgentDefinition
│ ├── ConfigSection (optional)
│ ├── DefaultsSection (optional)
│ └── FlowDefinition[] (one or more)
└── MessagesSection (optional)
└── MessageDefinition[] (zero or more)

For this RCL code:

agent CoffeeBot
displayName: "Coffee Shop Assistant"
flow OrderFlow
start: Welcome
on Welcome
match @reply.text
"coffee" -> OrderCoffee
:default -> Welcome
messages Messages
text Welcome "What can I get you?"

The AST structure would be:

const ast: AST.RclFile = {
type: 'RclFile',
agent: {
type: 'AgentDefinition',
name: 'CoffeeBot',
displayName: 'Coffee Shop Assistant',
flows: [
{
type: 'FlowDefinition',
name: 'OrderFlow',
start: 'Welcome',
states: [
{
type: 'StateDefinition',
name: 'Welcome',
transitions: [/* ... */]
}
]
}
]
},
messages: {
type: 'MessagesSection',
messages: [
{
type: 'TextMessage',
name: 'Welcome',
messageType: 'text',
text: 'What can I get you?'
}
]
}
}
import { AgentDefinition, isAgentDefinition } from '@rcs-lang/ast';
function analyzeAgent(node: ASTNode) {
if (isAgentDefinition(node)) {
console.log(`Agent: ${node.name}`);
console.log(`Display Name: ${node.displayName || 'Not set'}`);
console.log(`Flows: ${node.flows.length}`);
// Check for configuration
if (node.config) {
console.log('Has configuration section');
}
// Check for defaults
if (node.defaults) {
console.log('Has defaults section');
}
}
}
import { FlowDefinition, isFlowDefinition } from '@rcs-lang/ast';
function analyzeFlow(node: ASTNode) {
if (isFlowDefinition(node)) {
console.log(`Flow: ${node.name}`);
console.log(`Start State: ${node.start}`);
console.log(`States: ${node.states.map(s => s.name).join(', ')}`);
}
}
import {
MessageDefinition,
isTextMessage,
isRichCardMessage,
isCarouselMessage
} from '@rcs-lang/ast';
function analyzeMessage(message: MessageDefinition) {
console.log(`Message: ${message.name}`);
if (isTextMessage(message)) {
console.log(`Type: Text`);
console.log(`Content: ${message.text}`);
if (message.suggestions) {
console.log(`Suggestions: ${message.suggestions.length}`);
}
} else if (isRichCardMessage(message)) {
console.log(`Type: Rich Card`);
console.log(`Title: ${message.title}`);
console.log(`Size: ${message.size || 'default'}`);
if (message.description) {
console.log(`Description: ${message.description}`);
}
} else if (isCarouselMessage(message)) {
console.log(`Type: Carousel`);
console.log(`Title: ${message.title}`);
console.log(`Cards: ${message.cards.length}`);
}
}
import { walkAST } from '@rcs-lang/ast';
function findAllIdentifiers(ast: AST.RclFile): string[] {
const identifiers: string[] = [];
walkAST(ast, (node) => {
if (node.type === 'Identifier') {
identifiers.push((node as AST.Identifier).name);
}
});
return identifiers;
}
import { findNode, findAllNodes } from '@rcs-lang/ast';
// Find the first flow
function getFirstFlow(ast: AST.RclFile): AST.FlowDefinition | null {
const flowNode = findNode(ast, (node) => node.type === 'FlowDefinition');
return flowNode as AST.FlowDefinition | null;
}
// Find all text messages
function getAllTextMessages(ast: AST.RclFile): AST.TextMessage[] {
const textNodes = findAllNodes(ast, (node) => node.type === 'TextMessage');
return textNodes as AST.TextMessage[];
}
import { RclFile, isMessageDefinition } from '@rcs-lang/ast';
interface AgentMetadata {
name: string;
displayName?: string;
flowCount: number;
messageCount: number;
messageTypes: string[];
}
function extractMetadata(ast: RclFile): AgentMetadata {
const messageTypes = new Set<string>();
let messageCount = 0;
walkAST(ast, (node) => {
if (isMessageDefinition(node)) {
messageCount++;
messageTypes.add(node.messageType);
}
});
return {
name: ast.agent.name,
displayName: ast.agent.displayName,
flowCount: ast.agent.flows.length,
messageCount,
messageTypes: Array.from(messageTypes)
};
}
import { RCLParser } from '@rcs-lang/parser';
import { RclFile } from '@rcs-lang/ast';
async function parseAndAnalyze(source: string) {
const parser = new RCLParser();
const result = await parser.parse(source);
if (result.success) {
const ast: RclFile = result.data;
const metadata = extractMetadata(ast);
console.log('Agent metadata:', metadata);
}
}

Always use type guards instead of casting:

// ✅ Good: Type-safe with runtime checking
function processNode(node: ASTNode) {
if (isFlowDefinition(node)) {
// TypeScript knows node is FlowDefinition
console.log(node.start);
}
}
// ❌ Bad: Unsafe casting
function processNodeUnsafe(node: ASTNode) {
const flow = node as FlowDefinition; // Could be wrong!
console.log(flow.start); // Runtime error if not a flow
}

Use discriminated unions for complete type coverage:

function processMessage(message: MessageDefinition) {
switch (message.messageType) {
case 'text':
// Handle text message
break;
case 'richCard':
// Handle rich card
break;
case 'carousel':
// Handle carousel
break;
default:
// TypeScript will error if we miss a case
const exhaustive: never = message.messageType;
throw new Error(`Unhandled message type: ${exhaustive}`);
}
}

For detailed API documentation of all interfaces, types, and functions, see the API Reference.