diff --git a/CHANGELOG.md b/CHANGELOG.md index 1815c4150..fa57a6b52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index a73d3b2bc..4ffdfc622 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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`) | diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index ad0ba2374..4f3a323f7 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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 = ` diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index c7710f200..1a3bcb8cd 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -50,6 +50,7 @@ const WASM_GRAMMAR_FILES: Record = { terraform: 'tree-sitter-terraform.wasm', arkts: 'tree-sitter-arkts.wasm', nix: 'tree-sitter-nix.wasm', + interv: 'tree-sitter-interv.wasm', }; /** @@ -141,6 +142,7 @@ export const EXTENSION_MAP: Record = { '.cu': 'cpp', '.cuh': 'cpp', '.nix': 'nix', + '.iv': 'interv', // XML: file-level tracking; the MyBatis extractor matches `` // shape and emits SQL-statement nodes (other XML returns empty). '.xml': 'xml', @@ -290,7 +292,7 @@ export async function initGrammars(): Promise { */ const VENDORED_WASM_LANGS: ReadonlySet = 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 @@ -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', diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 6b760b01d..0a59de75d 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -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> = { typescript: typescriptExtractor, @@ -69,4 +70,5 @@ export const EXTRACTORS: Partial> = { terraform: terraformExtractor, arkts: arktsExtractor, nix: nixExtractor, + interv: intervExtractor, }; diff --git a/src/extraction/languages/interv.ts b/src/extraction/languages/interv.ts new file mode 100644 index 000000000..aaa4b9770 --- /dev/null +++ b/src/extraction/languages/interv.ts @@ -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, +}; diff --git a/src/extraction/wasm/tree-sitter-interv.wasm b/src/extraction/wasm/tree-sitter-interv.wasm new file mode 100755 index 000000000..959caa4c8 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-interv.wasm differ diff --git a/src/types.ts b/src/types.ts index 44ffaf4e4..29e81dfbf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -118,6 +118,7 @@ export const LANGUAGES = [ 'vbnet', 'erlang', 'terraform', + 'interv', 'unknown', ] as const;