Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

- Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it.

### Added

- CodeGraph now indexes **Interv** (`.iv`) — functions, algebraic data types with constructors, `import` module edges, and call edges.

## [1.6.0] - 2026-08-26

### Highlights
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ CodeGraph's parsing engine is a **native Rust kernel**: 20 languages — TypeScr
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi, Interv |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down Expand Up @@ -889,6 +889,7 @@ is written):
| Astro | `.astro` | Full support (frontmatter + script extraction, template component/call references, `src/pages/` routes) |
| Liquid | `.liquid` | Full support |
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
| Interv | `.iv` | Full support (functions, algebraic data types with constructors, `import` module edges, call edges) |
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
Expand Down
67 changes: 67 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,73 @@ impl Describe for Reg {
});
});

// =============================================================================
// Interv — a small self-hosting functional language (v2)
// =============================================================================

describe('Interv Extraction', () => {
it('should detect Interv files', () => {
expect(detectLanguage('src/util.iv')).toBe('interv');
expect(detectLanguage('nested/path/module.iv')).toBe('interv');
expect(isSourceFile('src/main.iv')).toBe(true);
expect(isLanguageSupported('interv')).toBe(true);
});

it('should extract function definitions from `name :: fn(...)`', () => {
const code = `
area :: fn (s) {
case s {
Circle(r) -> 3 * r * r;
Rect(w, h) -> w * h
}
}
`;
const result = extractFromSource('shapes.iv', code);
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('area');
expect(funcNode?.signature).toContain('s');
});

it('should extract data types as enums with constructors as members', () => {
const code = `
data shape {
Circle(r);
Rect(w, h)
}
`;
const result = extractFromSource('shapes.iv', code);
const enumNode = result.nodes.find((n) => n.kind === 'enum');
expect(enumNode).toBeDefined();
expect(enumNode?.name).toBe('shape');
const members = result.nodes.filter((n) => n.kind === 'enum_member');
expect(members.map((m) => m.name).sort()).toEqual(['Circle', 'Rect']);
});

it('should extract import statements', () => {
const code = `
import std/string
main :: fn () { 1 }
`;
const result = extractFromSource('mod.iv', code);
const imp = result.nodes.find((n) => n.kind === 'import');
expect(imp).toBeDefined();
expect(imp?.name).toBe('std/string');
});

it('should record intra-file calls as resolvable references', () => {
const code = `
helper :: fn (x) { x }
run :: fn (y) { helper(y) }
`;
const result = extractFromSource('calls.iv', code);
const call = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'helper'
);
expect(call).toBeDefined();
});
});

describe('Java Extraction', () => {
it('should extract class declarations', () => {
const code = `
Expand Down
5 changes: 4 additions & 1 deletion src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
terraform: 'tree-sitter-terraform.wasm',
arkts: 'tree-sitter-arkts.wasm',
nix: 'tree-sitter-nix.wasm',
interv: 'tree-sitter-interv.wasm',
};

/**
Expand Down Expand Up @@ -141,6 +142,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.cu': 'cpp',
'.cuh': 'cpp',
'.nix': 'nix',
'.iv': 'interv',
// XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
// shape and emits SQL-statement nodes (other XML returns empty).
'.xml': 'xml',
Expand Down Expand Up @@ -290,7 +292,7 @@ export async function initGrammars(): Promise<void> {
*/
const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix', 'interv',
'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go',
// R7a (C/C++ kernel port prep): tree-sitter-c v0.24.2 (b780e47) +
// tree-sitter-cpp v0.23.4 (f41e1a0), parser.c/scanner.c sha-matched against
Expand Down Expand Up @@ -695,6 +697,7 @@ export function getLanguageDisplayName(language: Language): string {
objc: 'Objective-C',
solidity: 'Solidity',
nix: 'Nix',
interv: 'Interv',
yaml: 'YAML',
twig: 'Twig',
xml: 'XML',
Expand Down
2 changes: 2 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { solidityExtractor } from './solidity';
import { terraformExtractor } from './terraform';
import { arktsExtractor } from './arkts';
import { nixExtractor } from './nix';
import { intervExtractor } from './interv';

export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand Down Expand Up @@ -69,4 +70,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
terraform: terraformExtractor,
arkts: arktsExtractor,
nix: nixExtractor,
interv: intervExtractor,
};
67 changes: 67 additions & 0 deletions src/extraction/languages/interv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

/**
* Interv Language Extractor.
*
* Interv v2 is a small functional language: `name :: fn (binders) { body }`
* declares a function, `data Name { Ctor(fields…) ; … }` declares an algebraic
* data type, `import path/to/module` imports a module, and application is
* paren-call `f(a, b)`. There are no classes with dispatch in the core language
* (the `class`/`instance` declarations add trait-style methods), so functions
* and data constructors are the symbol backbone.
*
* Node shapes (from the vendored tree-sitter-interv grammar):
* - function_definition name [":" type] "::" "fn" parameters body
* - const_definition name [":" type] "::" value
* - data_declaration "data" name "{" constructor (";" constructor)* "}"
* - constructor upper_identifier ["(" field_list ")"]
* - import_statement "import" path
* - call_expression function arguments
* - class_declaration/instance_declaration with method_definition children
*/
export const intervExtractor: LanguageExtractor = {
functionTypes: ['function_definition'],
classTypes: ['class_declaration', 'instance_declaration'],
methodTypes: ['method_definition'],
interfaceTypes: [],
structTypes: [],
enumTypes: ['data_declaration'],
enumMemberTypes: ['constructor'],
typeAliasTypes: [],
importTypes: ['import_statement'],
callTypes: ['call_expression'],
variableTypes: [], // consts/bindings are not symbol-bearing for indexing purposes
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
returnField: 'return_type',

getSignature: (node, source) => {
const params = getChildByField(node, 'parameters');
return params ? getNodeText(params, source) : undefined;
},

// Top-level definitions are the module's public surface; a nested `fn` value
// is not. The member/hook bodies aren't walked for this because a nested
// function (lambda) has no name field and is captured by its own callable —
// only declarations whose parent is the file scope are exported.
isExported: (node) => node.parent?.type === 'source_file',

extractImport: (node, source) => {
const path = getChildByField(node, 'path');
if (!path) return null;
const moduleName = getNodeText(path, source).trim();
if (!moduleName) return null;
return {
moduleName,
signature: source.substring(node.startIndex, node.endIndex).trim().slice(0, 120),
};
},

// `data Name { … }` is an algebraic data type. The enum extraction walks the
// constructor_body's direct children; a `constructor` node's `name` field is
// the (namespaced-in-AST, bare-in-source) constructor name, so enum members
// are minted per constructor.
getReceiverType: () => undefined,
};
Binary file added src/extraction/wasm/tree-sitter-interv.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export const LANGUAGES = [
'vbnet',
'erlang',
'terraform',
'interv',
'unknown',
] as const;

Expand Down