AST-Based Chunking
Explains AST-based code chunking using tree-sitter parsers, with language configs, metadata, and fallback behavior.
What this file does
Explains AST-based code chunking using tree-sitter parsers, with language configs, metadata, and fallback behavior.
When to use it
- Building a code search or RAG system that needs semantic chunks
- Replacing naive line- or character-based code splitting
- Adding AST-aware chunking to a retrieval pipeline
- Understanding how CodeRAG's chunking algorithm works
Assumes this stack
AST-Based Chunking
CodeRAG uses Abstract Syntax Tree (AST) parsing to split code into semantic chunks rather than arbitrary character or line-based splits. This produces more meaningful search units.
How Tree-Sitter Parsers Work
CodeRAG uses Synth parsers (built on tree-sitter) to parse code into AST nodes. Tree-sitter is a parser generator that creates fast, incremental parsers for programming languages.
Key concepts:
- Nodes: AST nodes represent code constructs (functions, classes, expressions, etc.)
- Spans: Each node has a span with start/end positions (line, column, offset)
- Types: Node types identify the construct (e.g.,
FunctionDeclaration,ClassDeclaration) - Tree structure: Nodes form a hierarchical tree with parent/child relationships
Example AST:
// Code:
function hello(name: string) {
return `Hello, ${name}`
}
// Simplified AST:
{
type: "FunctionDeclaration",
span: { start: { line: 0, column: 0 }, end: { line: 2, column: 1 } },
children: [
{ type: "Identifier", value: "hello" },
{ type: "Parameters", children: [...] },
{ type: "BlockStatement", children: [...] }
]
}
Synth parser interface:
interface SynthParser {
parseAsync: (source: string, options?: Record<string, unknown>) => Promise<Tree>
}
interface Tree {
meta: { language: string; source: string }
root: NodeId
nodes: BaseNode[]
}
All Synth parsers use async parsing because they're WASM-based (v0.3.x).
Semantic Boundaries
Semantic boundaries define where code should be split. CodeRAG chunks code at boundaries defined in the language configuration.
Common boundaries by language:
TypeScript/JavaScript:
FunctionDeclarationClassDeclarationInterfaceDeclarationTypeAliasDeclarationExportNamedDeclarationExportDefaultDeclaration
Python:
FunctionDefAsyncFunctionDefClassDefModule
Go:
FuncDeclMethodDeclTypeSpecGenDecl
Rust:
function_itemimpl_itemstruct_itemenum_itemtrait_item
Why semantic boundaries?
Semantic chunking ensures each chunk is a complete, meaningful unit:
// Good: Function-level chunking
// Chunk 1:
export function calculateBM25(tf: number, idf: number, docLen: number, avgDocLen: number): number {
const k1 = 1.2
const b = 0.75
const numerator = tf * (k1 + 1)
const denominator = tf + k1 * (1 - b + b * docLen / avgDocLen)
return idf * (numerator / denominator)
}
// Bad: Character-based chunking at 100 chars
// Chunk 1: "export function calculateBM25(tf: number, idf: number, docLen: number, avgDocLen: number): number {"
// Chunk 2: "const k1 = 1.2\n const b = 0.75\n const numerator = tf * (k1 + 1)\n const denominator = tf + k"
// Result: Incomplete, nonsensical chunks
Language-Specific Configurations
Each language has a configuration defining its parser, boundaries, and context types.
Configuration structure:
interface LanguageConfig {
parser: string // NPM package name
extensions: readonly string[] // File extensions
boundaries: readonly string[] // AST node types for chunking
contextTypes?: readonly string[] // Context to preserve (imports, types)
embedded?: EmbeddedLanguageConfig[] // Embedded languages
parserOptions?: Record<string, unknown>
}
Example: TypeScript configuration:
typescript: {
parser: '@sylphx/synth-js',
extensions: ['.ts', '.mts', '.cts'],
boundaries: [
'FunctionDeclaration',
'ClassDeclaration',
'InterfaceDeclaration',
'TypeAliasDeclaration',
'EnumDeclaration',
'MethodDefinition',
'ExportNamedDeclaration',
'ExportDefaultDeclaration',
],
contextTypes: ['ImportDeclaration', 'TypeAliasDeclaration', 'InterfaceDeclaration'],
parserOptions: { sourceType: 'module' },
}
Context preservation:
Context types (imports, type definitions) can be prepended to each chunk for better understanding:
// With preserveContext: true
// Chunk 1 (with context):
import { User } from './types'
export function getUser(id: string): User {
return database.findById(id)
}
// Without context, the chunk would start at "export function..."
Chunk Metadata
Each chunk includes metadata for precise navigation and filtering.
ChunkResult interface:
interface ChunkResult {
readonly content: string // Chunk source code
readonly type: string // AST node type
readonly startLine: number // 1-indexed
readonly endLine: number // 1-indexed
readonly metadata: Record<string, unknown>
}
Example chunk:
{
content: "export function parseQuery(query: string): string[] {\n return query.toLowerCase().split(/\\s+/)\n}",
type: "FunctionDeclaration",
startLine: 5,
endLine: 7,
metadata: {
name: "parseQuery",
exported: true
}
}
Metadata uses:
- Search results: Display which function/class matched
- Navigation: Jump to exact line in editor
- Filtering: Search only specific node types (e.g., only functions)
- Ranking: Boost certain types (e.g., exported functions)
Chunking Process
Step-by-step chunking algorithm:
- Detect language: Determine language from file extension
- Load parser: Get Synth parser for the language
- Parse AST: Parse source code into syntax tree
- Extract chunks: Traverse tree, extract nodes at semantic boundaries
- Merge small chunks: Combine small chunks below minChunkSize
- Split large chunks: Recursively split chunks exceeding maxChunkSize
- Add metadata: Attach type, line numbers, and other metadata
API usage:
import { chunkCodeByAST } from '@sylphx/coderag'
const chunks = await chunkCodeByAST(
sourceCode,
'example.ts',
{
maxChunkSize: 1000, // Max chars per chunk
minChunkSize: 100, // Min chars per chunk
preserveContext: true, // Include imports/types
nodeTypes: undefined, // Chunk all boundaries
parseEmbedded: true // Parse code blocks in markdown
}
)
for (const chunk of chunks) {
console.log(`${chunk.type} (lines ${chunk.startLine}-${chunk.endLine})`)
console.log(chunk.content)
}
Fallback behavior:
If AST parsing fails (unknown language, syntax error), CodeRAG falls back to character-based chunking:
// Fallback chunk
{
content: "... raw text ...",
type: "text",
startLine: 0,
endLine: 0,
metadata: { fallback: true, reason: "no-semantic-boundaries" }
}
Supported Languages
CodeRAG supports 15+ languages through Synth parsers:
Tier 1 (Full AST support):
- JavaScript/TypeScript/JSX/TSX
- Python
- Go
- Java
- C/C++
- Rust
Tier 2 (Markup/Config):
- Markdown
- HTML
- XML
- JSON
- YAML
- TOML
- INI
Tier 3 (Specialized):
- Protobuf
See languages.md for full details.
What's inside
7 sections covering AST parsing, semantic boundaries, language configs, chunk metadata, algorithm, API usage, and supported languages
Change this for your project
- Replace
@sylphx/synth-jswith your own parser package name - Replace
@sylphx/coderagwith your own chunking library - Replace
/Users/kyle/coderag/docs/guide/languages.mdwith your own docs path
Where it goes
Keep it in your repository where the agent or team that needs it will read it.
Worth borrowing
- Using AST node types as chunk boundaries instead of arbitrary character counts
- Preserving context (imports, type definitions) in each chunk for better retrieval
- Falling back to character-based chunking when AST parsing fails
Related Documents
基于命题分块以增强RAG
Implements proposition chunking for RAG: decomposes documents into atomic facts, evaluates quality, and compares retrieval against standard chunking.
TileMap Chunk Manager
Divides a large tilemap into fixed-size chunks, loading and rendering only those visible to reduce memory and draw calls by over 99%.
Message Chunking for MCP stdio Transport
Explains macOS 64KB pipe write limit and provides a chunking layer for MCP stdio transport that splits and reassembles large messages transparently.
🤖 n8n AI Agent Mastery Course 2025
Serves as the main README for an n8n course repository, listing 10+ AI agent workflow projects with links to YouTube tutorials and Docker setup instructions.