Syntax Trees
The AST classes are responsible for parsing specific parts of schema code into Abstract Syntax Trees (ASTs). Each AST class handles a different type of declaration or construct in the schema language.
The AST classes are responsible for parsing specific parts of schema code into Abstract Syntax Trees (ASTs). Each AST class handles a different type of declaration or construct in the schema language.
import {
SchemaTree,
EnumTree,
ModelTree,
TypeTree,
PropTree,
PluginTree,
UseTree
} from '@stackpress/idea-parser';
- SchemaTree
- EnumTree
- ModelTree
- TypeTree
- PropTree
- PluginTree
- UseTree
- Usage Patterns
- Error Handling
- Integration with Main Functions
- Performance Considerations
- Test-Driven Examples
1. SchemaTree
Parses complete schema files containing multiple declarations.
1.1. Static Methods
The static methods provide convenient ways to configure lexers and parse complete schema files without needing to instantiate the SchemaTree class directly.
1.1.1. Setting Up Schema Definitions
The following example shows how to configure a lexer for schema parsing.
import { SchemaTree, Lexer } from '@stackpress/idea-parser';
const lexer = new Lexer();
SchemaTree.definitions(lexer);
// Lexer now has definitions for all schema constructs:
// enum, prop, type, model, plugin, use keywords and structures
Parameters
| Parameter | Type | Description |
|---|
Returns
The configured lexer instance.
1.1.2. Parsing Complete Schemas
The following example shows how to parse a complete schema file.
import { SchemaTree } from '@stackpress/idea-parser';
const schemaCode = `
plugin "./database" {
provider "postgresql"
}
enum Status {
ACTIVE "Active"
INACTIVE "Inactive"
}
prop Text { type "text" }
model User {
id String @id
name String @field.input(Text)
status Status
}
`;
const ast = SchemaTree.parse(schemaCode);
console.log(ast.type); // 'Program'
console.log(ast.kind); // 'schema'
console.log(ast.body.length); // 4 (plugin, enum, prop, model)
Parameters
| Parameter | Type | Description |
|---|
Returns
A SchemaToken representing the entire parsed schema.
1.2. Methods
The instance methods allow for more granular control over the parsing process, including custom starting positions and reusing configured lexer instances.
1.2.1. Parsing Schema Content
The following example shows how to parse schema content with custom starting position.
import { SchemaTree } from '@stackpress/idea-parser';
const tree = new SchemaTree();
const schemaCode = 'enum Status { ACTIVE "Active" }';
const result = tree.parse(schemaCode, 0);
console.log(result.body[0].kind); // 'enum'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A SchemaToken containing all parsed declarations.
2. EnumTree
Parses enum declarations into AST tokens.
2.1. Static Methods
The static methods provide convenient ways to configure lexers and parse enum declarations without needing to instantiate the EnumTree class directly.
2.1.1. Setting Up Enum Definitions
The following example shows how to configure a lexer for enum parsing.
import { EnumTree, Lexer } from '@stackpress/idea-parser';
const lexer = new Lexer();
EnumTree.definitions(lexer);
// Adds 'EnumWord' token definition for 'enum' keyword
2.1.2. Parsing Enum Declarations
The following example shows how to parse enum declarations based on the test fixtures.
import { EnumTree } from '@stackpress/idea-parser';
const enumCode = `enum Roles {
ADMIN "Admin"
MANAGER "Manager"
USER "User"
}`;
const ast = EnumTree.parse(enumCode);
console.log(ast.kind); // 'enum'
console.log(ast.declarations[0].id.name); // 'Roles'
console.log(ast.declarations[0].init.properties.length); // 3
console.log(ast.declarations[0].init.properties[0].key.name); // 'ADMIN'
console.log(ast.declarations[0].init.properties[0].value.value); // 'Admin'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A DeclarationToken representing the parsed enum.
2.2. Methods
The instance methods allow for more granular control over the enum parsing process, including parsing individual enum structures and properties.
2.2.1. Parsing Enum Structure
The following example shows how to parse the enum structure.
const tree = new EnumTree();
tree._lexer.load('enum Status { ACTIVE "Active" INACTIVE "Inactive" }');
const enumToken = tree.enum();
console.log(enumToken.declarations[0].id.name); // 'Status'
console.log(enumToken.declarations[0].init.properties[0].key.name); // 'ACTIVE'
console.log(enumToken.declarations[0].init.properties[0].value.value); // 'Active'
Returns
A DeclarationToken representing the enum structure.
2.2.2. Parsing Enum Properties
The following example shows how individual enum properties are parsed.
// Inside enum parsing, after opening brace
const property = tree.property();
console.log(property.key.name); // e.g., 'ADMIN'
console.log(property.value.value); // e.g., 'Admin'
Returns
A PropertyToken representing a single enum key-value pair.
3. ModelTree
Parses model declarations (extends TypeTree for shared functionality).
3.1. Static Methods
The static methods provide convenient ways to parse model declarations directly without needing to instantiate the ModelTree class.
3.1.1. Parsing Model Declarations
The following example shows how to parse model declarations based on the test fixtures.
import { ModelTree } from '@stackpress/idea-parser';
const modelCode = `model User @label("User" "Users") {
id String @label("ID") @id @default("nanoid(20)")
username String @label("Username") @searchable @field.input(Text) @is.required
password String @label("Password") @field.password @is.clt(80) @is.cgt(8) @is.required @list.hide @view.hide
role Roles @label("Role") @filterable @field.select @list.text(Uppercase) @view.text(Uppercase)
address Address? @label("Address") @list.hide
age Number @label("Age") @unsigned @filterable @sortable @field.number(Age) @is.gt(0) @is.lt(150)
active Boolean @label("Active") @default(true) @filterable @field.switch @list.yesno @view.yesno
created Date @label("Created") @default("now()") @filterable @sortable @list.date(Pretty)
}`;
const ast = ModelTree.parse(modelCode);
console.log(ast.kind); // 'model'
console.log(ast.mutable); // false (because of '!' modifier)
console.log(ast.declarations[0].id.name); // 'User'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A DeclarationToken representing the parsed model.
3.2. Methods
The instance methods allow for more granular control over the model parsing process, including parsing model structures and their properties.
3.2.1. Parsing Model Structure
The following example shows how to parse the model structure.
const tree = new ModelTree();
tree._lexer.load('model User { id String @id }');
const modelToken = tree.model();
console.log(modelToken.kind); // 'model'
console.log(modelToken.mutable); // false (immutable due to '!')
Returns
A DeclarationToken representing the model structure.
4. TypeTree
Parses type declarations.
4.1. Static Methods
The static methods provide convenient ways to parse type declarations directly without needing to instantiate the TypeTree class.
4.1.1. Parsing Type Declarations
The following example shows how to parse type declarations.
import { TypeTree } from '@stackpress/idea-parser';
const typeCode = `type Address @label("Address" "Addresses") {
street String @field.input @is.required
city String @field.input @is.required
country String @field.select
postal String @field.input
}`;
const ast = TypeTree.parse(typeCode);
console.log(ast.kind); // 'type'
console.log(ast.mutable); // true (mutable by default)
console.log(ast.declarations[0].id.name); // 'Address'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A DeclarationToken representing the parsed type.
4.2. Methods
The instance methods allow for more granular control over the type parsing process, including parsing type structures, properties, and parameters.
4.2.1. Parsing Type Structure
The following example shows how to parse the type structure.
const tree = new TypeTree();
tree._lexer.load('type Address { street String city String }');
const typeToken = tree.type();
console.log(typeToken.kind); // 'type'
console.log(typeToken.mutable); // true (default for types)
Returns
A DeclarationToken representing the type structure.
4.2.2. Parsing Type Properties
The following example shows how type properties (columns) are parsed.
// Inside type parsing
const property = tree.property();
console.log(property.key.name); // e.g., 'street'
console.log(property.value); // Object containing type and attributes
Returns
A PropertyToken representing a type column definition.
4.2.3. Parsing Type Parameters
The following example shows how type parameters are parsed.
// For parsing generic type parameters
const parameter = tree.parameter();
console.log(parameter.key.name); // Parameter name
console.log(parameter.value); // Parameter type/constraint
Returns
A PropertyToken representing a type parameter.
5. PropTree
Parses prop (property configuration) declarations.
5.1. Static Methods
The static methods provide convenient ways to parse prop declarations directly without needing to instantiate the PropTree class.
5.1.1. Parsing Prop Declarations
The following example shows how to parse prop declarations.
import { PropTree } from '@stackpress/idea-parser';
const propCode = `prop EmailInput {
type "email"
format "email"
placeholder "Enter your email"
required true
}`;
const ast = PropTree.parse(propCode);
console.log(ast.kind); // 'prop'
console.log(ast.declarations[0].id.name); // 'EmailInput'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A DeclarationToken representing the parsed prop.
5.2. Methods
The instance methods allow for more granular control over the prop parsing process, including parsing prop structures and their configurations.
5.2.1. Parsing Prop Structure
The following example shows how to parse the prop structure.
const tree = new PropTree();
tree._lexer.load('prop Text { type "text" format "lowercase" }');
const propToken = tree.prop();
console.log(propToken.kind); // 'prop'
console.log(propToken.declarations[0].id.name); // 'Text'
Returns
A DeclarationToken representing the prop configuration.
6. PluginTree
Parses plugin declarations.
6.1. Static Methods
The static methods provide convenient ways to parse plugin declarations directly without needing to instantiate the PluginTree class.
6.1.1. Parsing Plugin Declarations
The following example shows how to parse plugin declarations.
import { PluginTree } from '@stackpress/idea-parser';
const pluginCode = `plugin "./database-plugin" {
provider "postgresql"
url env("DATABASE_URL")
previewFeatures ["fullTextSearch"]
}`;
const ast = PluginTree.parse(pluginCode);
console.log(ast.kind); // 'plugin'
console.log(ast.declarations[0].id.name); // './database-plugin'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
A DeclarationToken representing the parsed plugin.
6.2. Methods
The instance methods allow for more granular control over the plugin parsing process, including parsing plugin structures and their configurations.
6.2.1. Parsing Plugin Structure
The following example shows how to parse the plugin structure.
const tree = new PluginTree();
tree._lexer.load('plugin "./custom" { provider "custom-provider" }');
const pluginToken = tree.plugin();
console.log(pluginToken.kind); // 'plugin'
console.log(pluginToken.declarations[0].id.name); // './custom'
Returns
A DeclarationToken representing the plugin configuration.
7. UseTree
Parses use (import) declarations.
7.1. Static Methods
The static methods provide convenient ways to parse use (import) declarations directly without needing to instantiate the UseTree class.
7.1.1. Parsing Use Declarations
The following example shows how to parse use declarations.
import { UseTree } from '@stackpress/idea-parser';
const useCode = 'use "./shared/types.idea"';
const ast = UseTree.parse(useCode);
console.log(ast.type); // 'ImportDeclaration'
console.log(ast.source.value); // './shared/types.idea'
Parameters
| Parameter | Type | Description |
|---|---|---|
start | number | Starting position in the code (default: 0) |
Returns
An ImportToken representing the parsed use statement.
7.2. Methods
The instance methods allow for more granular control over the use statement parsing process, including parsing import structures and their sources.
7.2.1. Parsing Use Structure
The following example shows how to parse the use structure.
const tree = new UseTree();
tree._lexer.load('use "./another.idea"');
const useToken = tree.use();
console.log(useToken.type); // 'ImportDeclaration'
console.log(useToken.source.value); // './another.idea'
Returns
An ImportToken representing the import statement.
8. Usage Patterns
This section demonstrates common patterns for using the AST classes in real-world scenarios, including parsing individual components, integration with the compiler, and creating custom AST classes.
8.1. Parsing Individual Components
The following examples show how to parse individual schema components using their respective AST classes for targeted parsing operations.
import { EnumTree, ModelTree, TypeTree } from '@stackpress/idea-parser';
// Parse individual enum
const enumAST = EnumTree.parse(`enum Roles {
ADMIN "Admin"
MANAGER "Manager"
USER "User"
}`);
// Parse individual model
const modelAST = ModelTree.parse(`model User {
id String @id
username String @is.required
}`);
// Parse individual type
const typeAST = TypeTree.parse(`type Address {
street String
city String
}`);
8.2. Using with Compiler
The following examples show how AST classes integrate with the Compiler to transform parsed ASTs into usable configuration objects.
import { EnumTree, Compiler } from '@stackpress/idea-parser';
// Parse and compile in one step
const enumAST = EnumTree.parse(`enum Status {
ACTIVE "Active"
INACTIVE "Inactive"
}`);
const [enumName, enumConfig] = Compiler.enum(enumAST);
console.log(enumName); // 'Status'
console.log(enumConfig); // { ACTIVE: 'Active', INACTIVE: 'Inactive' }
8.3. Custom AST Classes
You can extend AbstractTree to create custom parsers:
import { AbstractTree, Lexer } from '@stackpress/idea-parser';
import type { DeclarationToken } from '@stackpress/idea-parser';
class CustomTree extends AbstractTree<DeclarationToken> {
static definitions(lexer: Lexer) {
super.definitions(lexer);
// Add custom token definitions
lexer.define('CustomKeyword', (code, index) => {
// Custom token reader implementation
});
return lexer;
}
static parse(code: string, start = 0) {
return new this().parse(code, start);
}
parse(code: string, start = 0): DeclarationToken {
this._lexer.load(code, start);
return this.customDeclaration();
}
customDeclaration(): DeclarationToken {
// Custom parsing logic
const keyword = this._lexer.expect('CustomKeyword');
// ... more parsing logic
return {
type: 'VariableDeclaration',
kind: 'custom',
start: keyword.start,
end: this._lexer.index,
declarations: [/* ... */]
};
}
}
9. Error Handling
AST classes provide detailed error information when parsing fails:
Syntax Errors
import { SchemaTree, Exception } from '@stackpress/idea-parser';
try {
// Invalid syntax - missing closing brace
SchemaTree.parse('enum Status { ACTIVE "Active"');
} catch (error) {
if (error instanceof Exception) {
console.log('Parse error:', error.message);
console.log('Position:', error.start, '-', error.end);
}
}
Unexpected Tokens
try {
// Invalid - 'enum' keyword expected but found 'model'
EnumTree.parse('model User { id String }');
} catch (error) {
console.log('Expected enum but found model');
}
Empty Input Handling
import { EnumTree } from '@stackpress/idea-parser';
try {
// Empty string will throw an error
EnumTree.parse('');
} catch (error) {
console.log('Error:', error.message); // 'Unexpected end of input'
}
Invalid Identifiers
import { ModelTree } from '@stackpress/idea-parser';
try {
// Invalid - model names must be capitalized
ModelTree.parse('model user { id String }');
} catch (error) {
console.log('Expected CapitalIdentifier but got something else');
}
10. Integration with Main Functions
AST classes are used internally by the main parse and final functions:
// This is what happens internally:
import { SchemaTree, Compiler } from '@stackpress/idea-parser';
export function parse(code: string) {
const ast = SchemaTree.parse(code); // Parse to AST
return Compiler.schema(ast); // Compile to JSON
}
export function final(code: string) {
const ast = SchemaTree.parse(code); // Parse to AST
return Compiler.final(ast); // Compile and clean up
}
11. Performance Considerations
This section covers optimization techniques and best practices for using AST classes efficiently, including lexer reuse and cloning strategies for better performance.
11.1. Lexer Reuse
AST classes can share lexer instances for better performance:
import { Lexer, SchemaTree } from '@stackpress/idea-parser';
// Create and configure lexer once
const lexer = new Lexer();
SchemaTree.definitions(lexer);
// Reuse for multiple parses
const tree = new SchemaTree(lexer);
const result1 = tree.parse(code1);
const result2 = tree.parse(code2);
const result3 = tree.parse(code3);
11.2. Cloning for Backtracking
AST classes use lexer cloning for safe parsing attempts:
// Inside tree parsing methods
const checkpoint = this._lexer.clone();
try {
// Try to parse optional structure
return this.parseOptionalStructure();
} catch (error) {
// Restore lexer state and continue
this._lexer = checkpoint;
return this.parseAlternativeStructure();
}
12. Test-Driven Examples
Based on the test fixtures, here are real-world examples:
Enum with Multiple Values
const enumCode = `enum Roles {
ADMIN "Admin"
MANAGER "Manager"
USER "User"
}`;
const ast = EnumTree.parse(enumCode);
// Produces a complete AST with all three enum values
Complex Model with Attributes
const modelCode = `model User @label("User" "Users") {
id String @label("ID") @id @default("nanoid(20)")
username String @label("Username") @searchable @field.input(Text) @is.required
password String @label("Password") @field.password @is.clt(80) @is.cgt(8) @is.required @list.hide @view.hide
role Roles @label("Role") @filterable @field.select @list.text(Uppercase) @view.text(Uppercase)
address Address? @label("Address") @list.hide
age Number @label("Age") @unsigned @filterable @sortable @field.number(Age) @is.gt(0) @is.lt(150)
balance Number[] @label("Balance") @filterable @sortable @field.number() @list.number() @view.number
active Boolean @label("Active") @default(true) @filterable @field.switch @list.yesno @view.yesno
created Date @label("Created") @default("now()") @filterable @sortable @list.date(Pretty)
updated Date @label("Updated") @default("updated()") @filterable @sortable @list.date(Pretty)
company Company? @label("My Company")
}`;
const ast = ModelTree.parse(modelCode);
// Produces a complete model AST with all columns and attributes
This demonstrates the parser's ability to handle:
- Model mutability (
!modifier) - Attributes (
@label,@id,@default, etc.) - Optional types (
Address?,Company?) - Array types (
Number[]) - Complex attribute parameters (
@field.input(Text),@is.clt(80))