diff --git a/.gitignore b/.gitignore index e11673fc4..911f74306 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,5 @@ book/ evaluation/iterative-divider/data.json # Prevents editing vscode extensions further -/.vscode/settings.json \ No newline at end of file +/.vscode/settings.json +.claude/ diff --git a/tools/treesitter/.gitignore b/tools/treesitter/.gitignore new file mode 100644 index 000000000..65ce7d0ae --- /dev/null +++ b/tools/treesitter/.gitignore @@ -0,0 +1,38 @@ +# Tree-sitter generated files +src/parser.c +src/parser.so +src/grammar.json +src/node-types.json +src/tree_sitter/ + +# Build artifacts +build/ +*.node + +# Node.js dependencies +node_modules/ +package-lock.json + +# Rust build artifacts (if using Rust bindings) +target/ +Cargo.lock + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Editor files +*.swp +*.swo +*~ +.vscode/ +.idea/ + +# Test artifacts +*.log +*.tmp \ No newline at end of file diff --git a/tools/treesitter/LSP.md b/tools/treesitter/LSP.md new file mode 100644 index 000000000..6924c7df8 --- /dev/null +++ b/tools/treesitter/LSP.md @@ -0,0 +1,360 @@ +# Multi-file Support for Filament LSP + +This document explains how Tree-sitter interacts with multi-file programs and what infrastructure is needed to support cross-file navigation, import resolution, and workspace-wide symbol indexing for Filament. + +## Tree-sitter and Multi-file Programs + +### Tree-sitter's Scope + +Tree-sitter is fundamentally a **single-file parser**. It: +- Parses one file at a time into an AST +- Provides incremental parsing for that single file +- Has no built-in awareness of other files or imports +- Cannot resolve symbols across file boundaries + +### Current Import Handling + +In our grammar, imports are parsed but not resolved: + +```javascript +import: $ => seq( + 'import', + $.string_literal, // Just captures "primitives/core.fil" as a string + ';' +) +``` + +This means Tree-sitter knows there's an import statement, but doesn't know: +- What components are defined in the imported file +- What symbols become available in the importing file +- How to navigate to definitions in imported files + +## LSP Server Architecture for Multi-file Support + +The Language Server Protocol (LSP) server handles multi-file awareness. Here's the typical architecture: + +``` +┌─────────────────────────────────────────────────────────┐ +│ LSP Server │ +├─────────────────────────────────────────────────────────┤ +│ Global Symbol Index │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Component: Add │ │ +│ │ File: primitives/comb.fil │ │ +│ │ Line: 8, Column: 5 │ │ +│ │ Type: component_definition │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ File Dependency Graph │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ main.fil → primitives/core.fil │ │ +│ │ core.fil → comb.fil, state.fil │ │ +│ │ comb.fil → (no dependencies) │ │ +│ └─────────────────────────────────────────────────┘ │ +│ │ +│ Per-file Parse Trees (Tree-sitter) │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ main.fil: (source_file ...) │ │ +│ │ core.fil: (source_file ...) │ │ +│ │ comb.fil: (source_file ...) │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Implementation Pattern + +```typescript +class FilamentLSP { + // Maps file paths to their Tree-sitter parse trees + private parseTrees: Map = new Map(); + + // Global symbol table across all files + private symbolTable: Map = new Map(); + + // File dependency graph + private imports: Map> = new Map(); + + async onFileOpen(uri: string) { + // 1. Parse the file with Tree-sitter + const tree = parser.parse(fileContent); + this.parseTrees.set(uri, tree); + + // 2. Extract imports using Tree-sitter queries + const imports = this.extractImports(tree); + + // 3. Parse imported files recursively + for (const importPath of imports) { + await this.parseIfNeeded(importPath); + } + + // 4. Update global symbol table + this.updateSymbolTable(uri, tree); + } + + onDefinition(uri: string, position: Position) { + // 1. Find symbol at position in current file + const symbol = this.getSymbolAtPosition(uri, position); + + // 2. Check local definitions first + let definition = this.findLocalDefinition(uri, symbol); + + // 3. If not found, check imported files + if (!definition) { + definition = this.symbolTable.get(symbol); + } + + return definition; + } +} +``` + +## Required Changes for Multi-file Support + +### 1. Enhanced Import Grammar + +Minimal changes to the Tree-sitter grammar to make imports more queryable: + +```javascript +// Add field name for easier extraction +import: $ => seq( + 'import', + field('path', $.string_literal), // Field makes querying easier + ';' +) +``` + +### 2. Import Extraction Queries + +Create `queries/imports.scm` to extract import information: + +```scheme +; Extract import paths for dependency resolution +(import + path: (string_literal) @import.path) + +; Track all imports in a file +(imports + (import + path: (string_literal) @import.file)) +``` + +### 3. Symbol Export/Import Tracking + +The LSP needs to track what symbols are available across files: + +```typescript +interface FileSymbols { + // Components/symbols defined in this file + definitions: Map; + + // Components made available through imports + imports: Map; + + // Transitive closure of all available symbols + availableSymbols: Map; +} + +interface Definition { + name: string; + type: 'component' | 'parameter' | 'instance' | 'port'; + file: string; + position: Position; + range: Range; +} +``` + +### 4. Filament-specific Import Resolution + +Filament's import semantics are simpler than most languages: +- `import "primitives/core.fil"` imports **ALL** components from that file +- No selective imports (unlike JavaScript: `import { A, B } from "module"`) +- No namespacing (unlike Rust: `use module::Component`) +- No aliases or renaming + +This simplifies the resolution algorithm: + +```typescript +class FilamentSymbolResolver { + resolveSymbol(currentFile: string, symbolName: string): Definition | null { + // 1. Check if defined in current file + const localDef = this.findInFile(currentFile, symbolName); + if (localDef) return localDef; + + // 2. Check all imported files (including transitive imports) + const imports = this.getTransitiveImports(currentFile); + for (const importedFile of imports) { + const def = this.findInFile(importedFile, symbolName); + if (def) return def; + } + + // 3. Symbol not found + return null; + } + + getTransitiveImports(file: string): Set { + const visited = new Set(); + const result = new Set(); + + const visit = (currentFile: string) => { + if (visited.has(currentFile)) return; + visited.add(currentFile); + + const directImports = this.getDirectImports(currentFile); + for (const imported of directImports) { + result.add(imported); + visit(imported); // Recursive for transitive imports + } + }; + + visit(file); + return result; + } +} +``` + +### 5. Workspace-level Indexing + +For efficient operation on large projects: + +```typescript +class WorkspaceIndexer { + private symbolIndex: Map = new Map(); + private fileGraph: Map> = new Map(); + + async indexWorkspace(rootPath: string) { + // 1. Find all .fil files + const filamentFiles = await this.findFilamentFiles(rootPath); + + // 2. Parse all files with Tree-sitter + for (const file of filamentFiles) { + const tree = await this.parseFile(file); + this.extractSymbols(file, tree); + this.extractImports(file, tree); + } + + // 3. Build transitive import graph + this.buildDependencyGraph(); + } + + // Called when a file changes + async onFileChanged(file: string) { + // Re-parse the changed file + const tree = await this.parseFile(file); + + // Update symbols for this file + this.extractSymbols(file, tree); + + // Re-index files that import this one + const dependents = this.findFilesThatImport(file); + for (const dependent of dependents) { + this.updateSymbolScope(dependent); + } + } +} +``` + +## Implementation Steps + +### Phase 1: Grammar Updates (5 minutes) +1. Add `field('path', $.string_literal)` to import rule +2. Regenerate Tree-sitter parser +3. Create `queries/imports.scm` for import extraction + +### Phase 2: Symbol Extraction (1-2 hours) +1. Create symbol extraction from Tree-sitter parse trees +2. Build file-local symbol tables +3. Implement component/parameter/port definition extraction + +### Phase 3: Import Resolution (2-3 hours) +1. Implement import path resolution (relative to current file) +2. Build file dependency graph +3. Create transitive import resolution +4. Handle circular import detection + +### Phase 4: LSP Integration (3-4 hours) +1. Integrate with LSP "Go to Definition" +2. Implement "Find All References" across files +3. Add workspace symbol provider +4. Support rename refactoring across imports + +### Phase 5: Performance & Caching (1-2 hours) +1. Cache parse trees and symbol tables +2. Implement incremental re-indexing +3. Add file watchers for change detection +4. Optimize for large workspaces + +## Performance Considerations + +### Lazy Loading +- Parse imported files only when needed for navigation +- Cache frequently accessed parse trees +- Use background threads for initial indexing + +### Incremental Updates +- Re-parse only changed files and their dependents +- Maintain incremental symbol table updates +- Use file system watchers to detect changes + +### Memory Management +- Limit number of cached parse trees +- Use weak references for infrequently accessed files +- Consider serializing symbol tables to disk for large workspaces + +## Testing Strategy + +### Unit Tests +```typescript +describe('Import Resolution', () => { + it('should resolve components from direct imports', () => { + // main.fil imports core.fil which defines Add + expect(resolve('main.fil', 'Add')).toEqual({ + file: 'primitives/core.fil', + position: { line: 8, character: 5 } + }); + }); + + it('should handle transitive imports', () => { + // main.fil → core.fil → comb.fil + expect(resolve('main.fil', 'Add')).toBeDefined(); + }); + + it('should detect circular imports', () => { + // a.fil → b.fil → a.fil + expect(() => buildDependencyGraph()).not.toThrow(); + }); +}); +``` + +### Integration Tests +Test with real Filament project structure: +``` +project/ +├── main.fil (imports primitives/core.fil) +├── primitives/ +│ ├── core.fil (imports comb.fil, state.fil) +│ ├── comb.fil (defines Add, Mul, etc.) +│ └── state.fil (defines Register, Memory, etc.) +└── examples/ + └── pipeline.fil (imports main.fil) +``` + +### Example Navigation Scenarios +1. **Jump to Definition**: Click on `Add[32]` in main.fil → jump to definition in comb.fil +2. **Find All References**: Find all uses of `Register` across the entire workspace +3. **Workspace Symbols**: Search for all components named `*Mult*` +4. **Rename Refactoring**: Rename `Add` to `Adder` across all files + +## Future Enhancements + +### Advanced Features +- **Import auto-completion**: Suggest available files when typing import paths +- **Unused import detection**: Find imports that aren't used +- **Import organization**: Sort and group imports automatically +- **Cross-file refactoring**: Move components between files safely + +### IDE Integration +- **VS Code extension**: Native Tree-sitter + LSP integration +- **Neovim support**: Tree-sitter highlighting + LSP navigation +- **Emacs package**: Complete Filament development environment + +This multi-file support would provide Filament developers with modern IDE experiences including instant navigation, reliable refactoring, and comprehensive symbol search across large projects. \ No newline at end of file diff --git a/tools/treesitter/README.md b/tools/treesitter/README.md new file mode 100644 index 000000000..a3e294ab4 --- /dev/null +++ b/tools/treesitter/README.md @@ -0,0 +1,439 @@ +# Tree-sitter Grammar for Filament + +This directory contains a complete Tree-sitter grammar implementation for the Filament hardware description language, designed to enable LSP functionality such as "jump to definition", syntax highlighting, and intelligent code completion. + +## Overview + +The Tree-sitter grammar provides a foundation for advanced editor support including: +- **Syntax highlighting** with semantic token types +- **Symbol extraction** for navigation features +- **Local scope analysis** for variable resolution +- **Parse tree structure** optimized for LSP queries + +## Architecture & Design Decisions + +### Core Grammar Structure + +The grammar follows the structure of the original Pest grammar (`crates/ast/src/syntax.pest`) but is adapted for Tree-sitter's parsing model: + +```javascript +// Main grammar rules correspond to AST structures +source_file -> imports? comp_or_ext* +comp_or_ext -> component | external | generate +component -> signature "{" command* "}" +``` + +### Key Language Constructs + +The grammar handles all major Filament language features: + +1. **Component Definitions** + ```filament + comp Add[WIDTH]<'G: 1>(left: ['G, 'G+1] WIDTH) -> (out: ['G+1, 'G+2] WIDTH) { + // component body + } + ``` + +2. **External Declarations** + ```filament + extern "hardware.sv" { + comp Add[WIDTH]<'G: 1>(...) -> (...); + } + ``` + +3. **Parameter Bindings** (critical for LSP) + - Signature parameters: `[WIDTH, ?DELAY=1]` + - Event parameters: `<'G: 1, 'L: 'G+DELAY>` + - With-block parameters: `with { some W where W > 0; let L = 10; }` + +4. **Complex Expressions** + - Time expressions: `'G+1`, `'START+DELAY` + - Arithmetic: `WIDTH * 2 + 1` + - Conditional: `if WIDTH > 8 { 16 } else { 8 }` + - Namespace access: `Component::PARAM` + +### Critical Design Decision: Unified Assignment Rule + +The most significant design decision was resolving parse ambiguity between three constructs that use `:=`: + +**Problem:** +```filament +x := new Component[32]; // Instance creation +y := Component<'G>(ports); // Invocation +z := expr; // Existential binding +``` + +Tree-sitter couldn't distinguish these at parse time due to the shared `identifier :=` prefix. + +**Solution:** +We implemented a unified `assignment` rule that branches based on distinguishing tokens: + +```javascript +assignment: $ => seq( + field('name', $.identifier), + ':=', + choice( + // Instance: 'new' keyword is unique + seq('new', field('component', $.identifier), ...), + // Invocation: '<' for time args is required + seq(field('component', $.identifier), $.invoke_args, ...), + // Existential: fallback for any other expression + seq($.expr, ';') + ) +) +``` + +This approach: +- ✅ Leverages unique tokens (`new`, `<`) for disambiguation +- ✅ Maintains identical AST structure for downstream tools +- ✅ Eliminates all parse conflicts +- ✅ Handles 100% of test cases correctly + +### LSP Symbol Extraction + +The grammar is specifically designed for LSP functionality with careful attention to: + +**Definition Providers:** +- Component names: `comp MyComponent` +- Parameter names: `[WIDTH, HEIGHT]`, `with { some PARAM; }` +- Instance names: `adder := new Add[32]` +- Port names: `input: ['G, 'G+1] 32` +- Bundle names: `bundle data[SIZE]: [...]` +- Event names: `<'START: 1>` + +**Reference Sites:** +- Component usage: `new Component`, `Component<...>(...)` +- Parameter usage: `WIDTH`, `Component::PARAM` +- Port access: `instance.port`, `port{index}` +- Event usage: `'START+1` + +All definition sites use `field('name', ...)` to mark extractable identifiers. + +## File Structure + +``` +tools/tree-sitter/ +├── grammar.js # Main grammar definition +├── package.json # NPM package configuration +├── queries/ # LSP query files +│ ├── highlights.scm # Syntax highlighting rules +│ ├── locals.scm # Local scope definitions +│ └── tags.scm # Symbol extraction for navigation +├── src/ # Generated parser (auto-generated) +├── bindings/ # Language bindings (auto-generated) +└── README.md # This file +``` + +## Usage + +### Building the Parser + +The parser build process involves several steps that transform the grammar definition into a usable shared library: + +#### Automated Build (Recommended) +```bash +# Install dependencies and build parser in one step +npm install # Runs postinstall hook automatically + +# Or build explicitly +npm run build-parser +``` + +#### Manual Build Steps +```bash +# 1. Install tree-sitter CLI and dependencies +npm install + +# 2. Generate C code from grammar.js +npx tree-sitter generate + +# 3. Compile shared library for Neovim +gcc -o src/parser.so -shared -fPIC -I src src/parser.c + +# 4. Test the grammar +npx tree-sitter test +``` + +#### What Each Step Does + +1. **`npm install`**: + - Installs `tree-sitter-cli` and Node.js dependencies + - Runs `postinstall` hook → automatically builds parser + - Creates `node_modules/` directory + +2. **`tree-sitter generate`**: + - Reads `grammar.js` (source grammar definition) + - Generates `src/parser.c` (C implementation) + - Generates `src/grammar.json` (grammar metadata) + - Generates `src/node-types.json` (AST node types) + - Creates `src/tree_sitter/parser.h` (header file) + +3. **`gcc` compilation**: + - Compiles `src/parser.c` to `src/parser.so` + - Creates position-independent code (`-fPIC`) + - Links as shared library (`-shared`) + - Includes headers from `src/` directory (`-I src`) + +4. **Result**: `src/parser.so` - ready for Neovim integration + +#### Build Artifacts + +**Generated files (git-ignored):** +``` +src/ +├── parser.c # Generated C parser implementation +├── parser.so # Compiled shared library (for Neovim) +├── grammar.json # Grammar metadata +├── node-types.json # AST node type definitions +└── tree_sitter/ + └── parser.h # C header file +``` + +**Source files (version controlled):** +``` +grammar.js # Grammar definition (source of truth) +package.json # Build configuration with npm scripts +queries/ # Syntax highlighting and query rules +├── highlights.scm +├── locals.scm +└── tags.scm +``` + +The `package.json` defines several build scripts for different purposes: + +```json +{ + "scripts": { + "build": "tree-sitter generate && node-gyp build", + "build-parser": "tree-sitter generate && gcc -o src/parser.so -shared -fPIC -I src src/parser.c", + "postinstall": "npm run build-parser", + "test": "tree-sitter test" + } +} +``` + +#### Script Breakdown + +- **`npm run build`**: Full Node.js binding build + - Generates parser C code + - Builds Node.js native module using `node-gyp` + - Creates `build/Release/tree_sitter_filament_binding.node` + - Used for Node.js applications and tree-sitter CLI tools + +- **`npm run build-parser`**: Neovim-specific build + - Generates parser C code + - Compiles directly to shared library (`parser.so`) + - Optimized for Neovim tree-sitter integration + - **This is what editor plugins use** + +- **`postinstall`**: Automatic build hook + - Runs after `npm install` + - Calls `build-parser` to create `src/parser.so` + - Enables zero-config installation for users + +- **`npm test`**: Grammar validation + - Runs tree-sitter test suite + - Validates grammar against test files + - Checks parsing correctness + +#### Build Requirements + +**System Dependencies:** +- **Node.js & npm**: For running build scripts +- **GCC**: For compiling C code to shared library +- **tree-sitter CLI**: Installed via npm dependencies + +**Platform Notes:** +- **Linux/macOS**: Uses GCC directly +- **Windows**: May require MinGW or MSYS2 for GCC +- **Cross-platform**: Node.js parts work everywhere + +### Testing with Filament Files + +```bash +# Parse a specific file +npx tree-sitter parse path/to/file.fil + +# Parse with highlighting +npx tree-sitter highlight path/to/file.fil + +# Run grammar tests +npx tree-sitter test +``` + +### Integration with Editors + +The generated parser can be integrated with: +- **VS Code**: Via tree-sitter extensions +- **Neovim**: Native tree-sitter support +- **Emacs**: Via tree-sitter packages +- **Custom LSP**: Using the generated bindings + +## Query Files + +### highlights.scm +Defines syntax highlighting rules: +- Keywords: `comp`, `extern`, `with`, `some`, `let` +- Operators: `:=`, `->`, `::`, `+`, `*` +- Literals: numbers, strings, events (`'G`) +- Types: component names, parameter names +- Functions: builtin functions like `pow2`, `log2` + +### locals.scm +Defines local scope rules for variable resolution: +- Component scopes for parameter visibility +- With-block scopes for existential parameters +- For-loop scopes for iteration variables +- Parameter binding and reference relationships + +### tags.scm +Defines symbol extraction for "jump to definition": +- Component definitions and references +- Parameter definitions and usage +- Instance definitions and port access +- Event definitions and time expressions + +## Testing + +The grammar includes comprehensive tests covering: +- ✅ Simple component definitions +- ✅ Complex signatures with with-blocks +- ✅ External declarations +- ✅ All three assignment types (instance, invocation, existential) +- ✅ Nested expressions and time arithmetic +- ✅ Error recovery and partial parsing + +Test files demonstrate parsing of real Filament programs from the repository. + +## Build Troubleshooting + +### Common Build Issues + +#### Missing GCC Compiler +``` +Error: gcc: command not found +``` +**Solutions:** +- **macOS**: Install Xcode command line tools: `xcode-select --install` +- **Linux**: Install build-essential: `sudo apt install build-essential` (Ubuntu/Debian) or `sudo yum groupinstall "Development Tools"` (RHEL/CentOS) +- **Windows**: Install MinGW-w64 or use WSL with Linux tools + +#### Node.js/NPM Missing +``` +Error: npm: command not found +``` +**Solutions:** +- Install Node.js from https://nodejs.org/ +- Use package manager: `brew install node` (macOS), `sudo apt install nodejs npm` (Linux) +- Use version manager: `nvm install node` + +#### Parser Generation Fails +``` +Error during tree-sitter generate +``` +**Solutions:** +- Check `grammar.js` syntax for JavaScript errors +- Verify tree-sitter CLI installation: `npx tree-sitter --version` +- Clean and rebuild: `rm -rf src/ && npm run build-parser` + +#### Compilation Errors +``` +Error: src/parser.c: No such file or directory +``` +**Solutions:** +- Ensure `tree-sitter generate` ran successfully first +- Check that `src/parser.c` exists after generation +- Run full build: `npm install && npm run build-parser` + +#### Permission Issues +``` +Error: EACCES: permission denied +``` +**Solutions:** +- Don't use `sudo` with npm (security risk) +- Fix npm permissions: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally +- Use node version manager (nvm) instead of system Node.js + +#### Windows-Specific Issues +``` +Error: 'gcc' is not recognized as an internal or external command +``` +**Solutions:** +- Install MinGW-w64: https://www.mingw-w64.org/ +- Add GCC to PATH environment variable +- Use WSL (Windows Subsystem for Linux) with Ubuntu +- Consider using Visual Studio Build Tools with node-gyp + +### Verification Steps + +After successful build, verify: +```bash +# Check generated files exist +ls -la src/ +# Should show: parser.c, parser.so, grammar.json, node-types.json + +# Test the parser works +npx tree-sitter parse test_simple.fil + +# Verify shared library +file src/parser.so +# Should show: shared library, dynamically linked +``` + +### Clean Rebuild + +If experiencing persistent issues: +```bash +# Full clean and rebuild +rm -rf src/ node_modules/ build/ +npm install +npm run build-parser +``` + +## Implementation Notes + +### Grammar Conflicts Resolved + +1. **Expression vs Time**: Both use similar syntax (`'G+1`) + - Solution: Explicit conflicts declaration with precedence + +2. **Assignment Ambiguity**: Instance vs Invocation vs Existential + - Solution: Unified assignment rule with token-based branching + +3. **Port Definitions**: Bundle vs simple port syntax + - Solution: Choice-based rules with clear precedence + +### Tree-sitter Specifics + +- **Field Naming**: All definition sites marked with `field('name', ...)` +- **Precedence**: Used sparingly, only where necessary for disambiguation +- **Choice Ordering**: More specific patterns before general ones +- **Conflicts**: Explicit declarations for unavoidable ambiguities + +### Performance Considerations + +- **Incremental Parsing**: Tree-sitter's strength for large files +- **Error Recovery**: Grammar continues parsing after syntax errors +- **Memory Efficiency**: Index-based node references +- **Query Optimization**: Efficient pattern matching for LSP features + +## Future Extensions + +The grammar foundation supports adding: +- **Error diagnostics** with precise source locations +- **Semantic highlighting** based on symbol types +- **Auto-completion** using partial parse trees +- **Refactoring tools** via AST transformations +- **Documentation generation** from parsed structures + +## Contributing + +When modifying the grammar: + +1. **Test thoroughly** with `npx tree-sitter test` +2. **Validate with real files** from the repository +3. **Update query files** if new node types are added +4. **Document design decisions** for complex changes +5. **Preserve LSP compatibility** by maintaining field names + +The grammar is designed to evolve with the Filament language while maintaining backward compatibility for existing LSP tooling. diff --git a/tools/treesitter/binding.gyp b/tools/treesitter/binding.gyp new file mode 100644 index 000000000..7fd69aa40 --- /dev/null +++ b/tools/treesitter/binding.gyp @@ -0,0 +1,19 @@ +{ + "targets": [ + { + "target_name": "tree_sitter_filament_binding", + "include_dirs": [ + " +#include "nan.h" + +using namespace v8; + +extern "C" TSLanguage * tree_sitter_filament(); + +namespace { + +NAN_METHOD(New) {} + +void Init(Local exports, Local module) { + Local tpl = Nan::New(New); + tpl->SetClassName(Nan::New("Language").ToLocalChecked()); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + Local constructor = Nan::GetFunction(tpl).ToLocalChecked(); + Local instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); + Nan::SetInternalFieldPointer(instance, 0, tree_sitter_filament()); + + Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("filament").ToLocalChecked()); + Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); +} + +NODE_MODULE(tree_sitter_filament_binding, Init) + +} // namespace diff --git a/tools/treesitter/bindings/node/index.js b/tools/treesitter/bindings/node/index.js new file mode 100644 index 000000000..5d8fc8867 --- /dev/null +++ b/tools/treesitter/bindings/node/index.js @@ -0,0 +1,19 @@ +try { + module.exports = require("../../build/Release/tree_sitter_filament_binding"); +} catch (error1) { + if (error1.code !== 'MODULE_NOT_FOUND') { + throw error1; + } + try { + module.exports = require("../../build/Debug/tree_sitter_filament_binding"); + } catch (error2) { + if (error2.code !== 'MODULE_NOT_FOUND') { + throw error2; + } + throw error1 + } +} + +try { + module.exports.nodeTypeInfo = require("../../src/node-types.json"); +} catch (_) {} diff --git a/tools/treesitter/grammar.js b/tools/treesitter/grammar.js new file mode 100644 index 000000000..fe94612fd --- /dev/null +++ b/tools/treesitter/grammar.js @@ -0,0 +1,437 @@ +module.exports = grammar({ + name: 'filament', + + extras: $ => [ + /\s/, + $.comment, + ], + + conflicts: $ => [ + [$.expr, $.expr_base], + [$.time, $.expr], + [$.constraint, $.time], + ], + + rules: { + // Top level file structure + source_file: $ => seq( + optional($.imports), + repeat($.comp_or_ext) + ), + + // Comments + comment: $ => choice( + seq('//', /.*/), + seq('/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/') + ), + + // Imports + imports: $ => repeat1($.import), + import: $ => seq( + 'import', + $.string_literal, + ';' + ), + + // String literals + string_literal: $ => seq( + '"', + repeat(choice( + /[^"\\]/, + /\\./ + )), + '"' + ), + + // Identifiers and basic types + identifier: $ => /[a-zA-Z_][a-zA-Z0-9_]*/, + param_var: $ => $.identifier, + bitwidth: $ => /[0-9]+/, + float: $ => /[0-9]+\.[0-9]+/, + + // Top level constructs + comp_or_ext: $ => choice( + $.component, + $.external, + $.generate + ), + + // External definitions + external: $ => seq( + 'extern', + $.string_literal, + '{', + repeat(seq($.signature, ';')), + '}' + ), + + // Generate definitions + generate: $ => seq( + 'generate', + '(', + $.identifier, + ')', + 'using', + $.string_literal, + '{', + repeat(seq($.signature, ';')), + '}' + ), + + // Component definition + component: $ => seq( + $.signature, + '{', + repeat($.command), + '}' + ), + + // Component signature + signature: $ => seq( + optional($.attributes), + 'comp', + field('name', $.identifier), + optional($.params), + optional($.abstract_var), + $.io, + optional($.sig_bindings), + optional($.constraints) + ), + + // Attributes + attributes: $ => seq( + '#[', + $.attr_bind, + repeat(seq(',', $.attr_bind)), + ']' + ), + + attr_bind: $ => choice( + seq('not', '(', $.identifier, ')'), + seq($.identifier, '=', $.bitwidth), + seq($.identifier, '=', $.float), + $.identifier + ), + + // Parameters in component signature + params: $ => seq( + '[', + $.param_bind, + repeat(seq(',', $.param_bind)), + ']' + ), + + param_bind: $ => choice( + seq('?', field('name', $.param_var), '=', $.expr), + field('name', $.param_var) + ), + + // Abstract variables (events) + abstract_var: $ => seq( + '<', + $.event_bind, + repeat(seq(',', $.event_bind)), + '>' + ), + + event_bind: $ => choice( + seq('?', $.event_with_delay, '=', $.time), + $.event_with_delay + ), + + event_with_delay: $ => seq( + $.event, + ':', + $.delay + ), + + event: $ => seq("'", $.identifier), + + delay: $ => choice( + $.expr, + seq($.time, '-', '(', $.time, ')') + ), + + // Input/Output specification + io: $ => seq( + '(', + optional($.ports), + ')', + '->', + '(', + optional($.ports), + ')' + ), + + ports: $ => seq( + $.port_def, + repeat(seq(',', $.port_def)), + optional(',') + ), + + port_def: $ => choice( + seq(field('name', $.identifier), ':', $.bitwidth), + $.bundle_def, + seq(field('name', $.identifier), ':', $.interface) + ), + + interface: $ => seq( + 'interface', + '[', + $.event, + ']' + ), + + // Signature bindings (with block) + sig_bindings: $ => seq( + 'with', + '{', + repeat($.sig_bind), + '}' + ), + + sig_bind: $ => choice( + seq('let', field('name', $.param_var), '=', $.expr, ';'), + seq('some', field('name', $.param_var), optional($.constraints), ';'), + seq('opaque', field('name', $.param_var), optional($.constraints), ';') + ), + + // Constraints + constraints: $ => seq( + 'where', + $.constraint, + repeat(seq(',', $.constraint)) + ), + + constraint: $ => choice( + seq($.expr, $.order_op, $.expr), + seq($.time, $.order_op, $.time) + ), + + order_op: $ => choice('>', '>=', '<', '<=', '=='), + + // Time expressions + time: $ => choice( + seq($.event, '+', $.expr), + seq($.expr, '+', $.event), + $.event, + $.expr + ), + + // Expressions + expr: $ => prec.left(1, choice( + $.expr_base, + prec.left(3, seq($.expr, choice('*', '/', '%'), $.expr)), + prec.left(2, seq($.expr, choice('+', '-'), $.expr)) + )), + + expr_base: $ => choice( + $.if_expr, + seq($.builtin_fn, '(', $.expr, repeat(seq(',', $.expr)), ')'), + seq('(', $.expr, ')'), + $.bitwidth, + seq($.identifier, '::', $.identifier), + $.param_var + ), + + if_expr: $ => seq( + 'if', + $.expr_cmp, + '{', + $.expr, + '}', + 'else', + '{', + $.expr, + '}' + ), + + expr_cmp: $ => seq( + $.expr, + $.order_op, + $.expr + ), + + builtin_fn: $ => choice( + 'pow2', + 'log2', + 'sin_bits', + 'cos_bits', + 'bit_rev' + ), + + // Bundle definitions + bundle_def: $ => seq( + optional($.attributes), + field('name', $.identifier), + repeat(seq('[', $.expr, ']')), + ':', + $.bundle_typ + ), + + bundle_typ: $ => seq( + optional(seq('for', $.bundle_params)), + $.interval_range, + $.expr + ), + + bundle_params: $ => seq( + '<', + $.param_var, + repeat(seq(',', $.param_var)), + '>' + ), + + interval_range: $ => seq( + '[', + $.time, + ',', + $.time, + ']' + ), + + // Commands in component body + command: $ => choice( + $.bundle, + $.assignment, + $.connect, + $.for_loop, + $.if_stmt, + $.fact, + $.param_let + ), + + // Unified assignment rule to handle instance, invocation, and existential assignments + assignment: $ => seq( + field('name', $.identifier), + ':=', + choice( + // Instance creation: identifier := new Component[params](args) in lives; + seq( + 'new', + field('component', $.identifier), + optional($.conc_params), + optional($.invoke_args), + optional($.inst_live), + ';' + ), + // Invocation: identifier := Component(args); + seq( + field('component', $.identifier), + $.invoke_args, + ';' + ), + // Existential binding: identifier := expr; + seq( + $.expr, + ';' + ) + ) + ), + + conc_params: $ => seq( + '[', + $.expr, + repeat(seq(',', $.expr)), + ']' + ), + + inst_live: $ => seq( + 'in', + $.interval_range, + repeat(seq(',', $.interval_range)) + ), + + invoke_args: $ => seq( + $.time_args, + $.arguments + ), + + time_args: $ => seq( + '<', + $.time, + repeat(seq(',', $.time)), + '>' + ), + + arguments: $ => choice( + seq('(', ')'), + seq('(', $.port, repeat(seq(',', $.port)), ')') + ), + + // Port references + port: $ => choice( + seq(field('instance', $.identifier), '.', field('port', $.identifier), repeat($.access)), + seq(field('port', $.identifier), repeat($.access)) + ), + + access: $ => seq( + '{', + choice( + seq($.expr, '..', $.expr), + $.expr + ), + '}' + ), + + // Connections + connect: $ => seq( + $.port, + '=', + $.port, + ';' + ), + + // Control flow + for_loop: $ => seq( + 'for', + field('var', $.param_var), + 'in', + $.expr, + '..', + $.expr, + '{', + repeat($.command), + '}' + ), + + if_stmt: $ => seq( + 'if', + $.expr_cmp, + '{', + repeat($.command), + '}', + optional(seq( + 'else', + '{', + repeat($.command), + '}' + )) + ), + + // Facts (assumptions and assertions) + fact: $ => seq( + choice('assume', 'assert'), + $.implication, + ';' + ), + + implication: $ => choice( + seq($.expr_cmp, '=>', $.expr_cmp), + $.expr_cmp + ), + + // Parameter let binding + param_let: $ => choice( + seq('let', field('name', $.param_var), '=', $.expr, ';'), + seq('let', field('name', $.param_var), '=', '?', ';') + ), + + // Bundle command + bundle: $ => seq( + 'bundle', + $.bundle_def, + ';' + ) + } +}); \ No newline at end of file diff --git a/tools/treesitter/package.json b/tools/treesitter/package.json new file mode 100644 index 000000000..7e390a7fd --- /dev/null +++ b/tools/treesitter/package.json @@ -0,0 +1,35 @@ +{ + "name": "tree-sitter-filament", + "version": "0.1.0", + "description": "Tree-sitter grammar for the Filament hardware description language", + "main": "bindings/node", + "scripts": { + "build": "tree-sitter generate && node-gyp build", + "build-parser": "tree-sitter generate && gcc -o src/parser.so -shared -fPIC -I src src/parser.c", + "postinstall": "npm run build-parser", + "test": "tree-sitter test" + }, + "keywords": [ + "tree-sitter", + "grammar", + "filament", + "hardware", + "hdl" + ], + "author": "Filament Team", + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + }, + "devDependencies": { + "tree-sitter-cli": "^0.20.8" + }, + "tree-sitter": [ + { + "scope": "source.filament", + "file-types": [ + "fil" + ] + } + ] +} \ No newline at end of file diff --git a/tools/treesitter/queries/highlights.scm b/tools/treesitter/queries/highlights.scm new file mode 100644 index 000000000..2b5fa05c5 --- /dev/null +++ b/tools/treesitter/queries/highlights.scm @@ -0,0 +1,144 @@ +; Keywords +[ + "comp" + "extern" + "generate" + "using" + "import" + "with" + "where" + "some" + "opaque" + "let" + "new" + "bundle" + "interface" + "for" + "in" + "if" + "else" + "assume" + "assert" + "not" +] @keyword + +; Operators +[ + "=" + ":=" + "->" + "+" + "-" + "*" + "/" + "%" + ">" + ">=" + "<" + "<=" + "==" + "=>" + ".." + "::" +] @operator + +; Delimiters +[ + "(" + ")" + "[" + "]" + "{" + "}" + "<" + ">" +] @punctuation.bracket + +[ + "," + ";" + ":" + "." +] @punctuation.delimiter + +; Literals +(bitwidth) @number +(float) @number.float +(string_literal) @string + +; Comments +(comment) @comment + +; Function names +(builtin_fn) @function.builtin + +; Component and type names +(component + (signature + name: (identifier) @type)) + +(external + (signature + name: (identifier) @type)) + +(generate + (signature + name: (identifier) @type)) + +; Assignment component references and names +(assignment + component: (identifier) @type) + +(assignment + name: (identifier) @variable) + +; Parameter definitions +(param_bind + name: (param_var + (identifier) @parameter)) + +(sig_bind + name: (param_var + (identifier) @parameter)) + +(param_let + name: (param_var + (identifier) @parameter)) + +(for_loop + var: (param_var + (identifier) @parameter)) + +; Port definitions +(port_def + name: (identifier) @property) + +(bundle_def + name: (identifier) @property) + +; Port references +(port + port: (identifier) @property) + +(port + instance: (identifier) @variable + port: (identifier) @property) + +; Events +(event + (identifier) @constant) + +; Parameter references +(param_var) @parameter + +; Identifiers (general) +(identifier) @identifier + +; String content in imports and externals +(import (string_literal) @string.special) +(external (string_literal) @string.special) +(generate (string_literal) @string.special) + +; Attributes +(attr_bind + (identifier) @attribute) \ No newline at end of file diff --git a/tools/treesitter/queries/locals.scm b/tools/treesitter/queries/locals.scm new file mode 100644 index 000000000..267d6f4ef --- /dev/null +++ b/tools/treesitter/queries/locals.scm @@ -0,0 +1,59 @@ +; Component scope +(component + (signature) @local.scope) + +; Parameter scopes +(params + (param_bind + name: (param_var + (identifier) @local.definition.parameter))) + +; Event scopes +(abstract_var + (event_bind + (event_with_delay + (event + (identifier) @local.definition.constant)))) + +; With-block parameter definitions +(sig_bindings + (sig_bind + name: (param_var + (identifier) @local.definition.parameter))) + +; Body parameter definitions +(param_let + name: (param_var + (identifier) @local.definition.parameter)) + +; Assignment definitions (instances, invocations, existential bindings) +(assignment + name: (identifier) @local.definition.variable) + +; Bundle definitions +(bundle + (bundle_def + name: (identifier) @local.definition.property)) + +; Port definitions in signatures +(port_def + name: (identifier) @local.definition.property) + +; For loop variable scope +(for_loop + var: (param_var + (identifier) @local.definition.parameter)) @local.scope + +; Parameter references +(param_var) @local.reference + +; Port references +(port + port: (identifier) @local.reference) + +(port + instance: (identifier) @local.reference) + +; Event references +(event + (identifier) @local.reference) \ No newline at end of file diff --git a/tools/treesitter/queries/tags.scm b/tools/treesitter/queries/tags.scm new file mode 100644 index 000000000..98bb141bf --- /dev/null +++ b/tools/treesitter/queries/tags.scm @@ -0,0 +1,79 @@ +; Component definitions +(component + (signature + name: (identifier) @name)) @definition.class + (#strip! @name) + +; External component definitions +(external + (signature + name: (identifier) @name)) @definition.class + (#strip! @name) + +; Generate component definitions +(generate + (signature + name: (identifier) @name)) @definition.class + (#strip! @name) + +; Parameter definitions in component signatures +(param_bind + name: (param_var + (identifier) @name)) @definition.parameter + (#strip! @name) + +; Parameter definitions in with blocks +(sig_bind + name: (param_var + (identifier) @name)) @definition.parameter + (#strip! @name) + +; Parameter definitions in component body +(param_let + name: (param_var + (identifier) @name)) @definition.parameter + (#strip! @name) + +; Assignment definitions (instances, invocations, existential bindings) +(assignment + name: (identifier) @name) @definition.variable + (#strip! @name) + +; Bundle definitions +(bundle_def + name: (identifier) @name) @definition.property + (#strip! @name) + +; Port definitions +(port_def + name: (identifier) @name) @definition.property + (#strip! @name) + +; Event definitions +(event_bind + (event_with_delay + (event + (identifier) @name))) @definition.constant + (#strip! @name) + +; Component references in assignments +(assignment + component: (identifier) @name) @reference.class + (#strip! @name) + +; Port references +(port + port: (identifier) @name) @reference.property + (#strip! @name) + +; Instance references in port access +(port + instance: (identifier) @name) @reference.variable + (#strip! @name) + +; Parameter references +(param_var) @reference.parameter + +; Event references +(event + (identifier) @reference.constant) \ No newline at end of file diff --git a/tools/treesitter/tests/test_exists.fil b/tools/treesitter/tests/test_exists.fil new file mode 100644 index 000000000..a898b6785 --- /dev/null +++ b/tools/treesitter/tests/test_exists.fil @@ -0,0 +1,3 @@ +comp Test() -> () { + W := FM::W; +} \ No newline at end of file diff --git a/tools/treesitter/tests/test_invocation.fil b/tools/treesitter/tests/test_invocation.fil new file mode 100644 index 000000000..6b4f6d85c --- /dev/null +++ b/tools/treesitter/tests/test_invocation.fil @@ -0,0 +1,3 @@ +comp Test() -> () { + a0 := A<'G>(left, right); +} \ No newline at end of file diff --git a/tools/treesitter/tests/test_simple.fil b/tools/treesitter/tests/test_simple.fil new file mode 100644 index 000000000..cbd601750 --- /dev/null +++ b/tools/treesitter/tests/test_simple.fil @@ -0,0 +1,9 @@ +import "primitives/core.fil"; + +comp main<'G: 1>( + left: ['G, 'G+1] 32, + right: ['G, 'G+1] 32, +) -> ( out: ['G+3, 'G+4] 32) { + A := new Add[32]; + out = A.out; +} \ No newline at end of file diff --git a/tools/treesitter/tests/test_with.fil b/tools/treesitter/tests/test_with.fil new file mode 100644 index 000000000..58f239412 --- /dev/null +++ b/tools/treesitter/tests/test_with.fil @@ -0,0 +1,14 @@ +comp TwiddleMul[i, N, Width, E, ?M=W-E-1]<'G: 1>( + in[2]: ['G, 'G+1] Width +) -> ( + out[2]: ['G+L, 'G+L+1] W +) with { + some W where W > 0; + some L where L >= 0; +} where + Width == E + M + 1, + E > 1 +{ + let shift = 2; + out = in; +} \ No newline at end of file diff --git a/tools/vim/README.md b/tools/vim/README.md new file mode 100644 index 000000000..2694ccb80 --- /dev/null +++ b/tools/vim/README.md @@ -0,0 +1,414 @@ +# Vim/Neovim Support for Filament + +This directory contains vim/neovim configuration files for the Filament hardware description language, providing syntax highlighting and advanced editor features. + +## Features + +### Traditional Vim Support +- Syntax highlighting using vim's built-in syntax system +- File type detection for `.fil` files +- Compatible with all vim versions + +### Neovim Tree-sitter Support (Enhanced) +- Advanced syntax highlighting with semantic tokens +- Local variable scoping and resolution +- Symbol extraction for navigation features +- Incremental parsing for better performance +- Error recovery during editing +- Foundation for LSP features + +## Quick Installation + +### One-Line Setup (Recommended) + +**vim-plug:** +```vim +" Add to your init.vim/.vimrc and run :PlugInstall +Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'} +Plug 'filament-hdl/filament', {'rtp': 'treesitter/tools/vim/filament', 'do': 'cd treesitter/tools/treesitter && npm install && npm run build-parser'} +``` + +**lazy.nvim:** +```lua +-- Add to your lazy configuration +{ + 'filament-hdl/filament', + rtp = 'treesitter/tools/vim/filament', + build = 'cd treesitter/tools/treesitter && npm install && npm run build-parser', + dependencies = { 'nvim-treesitter/nvim-treesitter' }, + config = function() require('filament').setup() end, +} +``` + +**That's it!** The `do`/`build` hook builds the parser during plugin installation, then syntax highlighting works immediately. + +### Requirements +- **Neovim 0.8+** (for tree-sitter support) +- **Node.js & npm** (for building the parser) +- **GCC** (for compiling the shared library) + +## Detailed Installation + +### Method 1: Using Plugin Managers (Recommended) + +#### vim-plug +Add to your `~/.config/nvim/init.vim` or `~/.vimrc`: +```vim +" Install the filament plugin with auto-build +Plug 'filament-hdl/filament', {'rtp': 'treesitter/tools/vim/filament', 'do': 'cd treesitter/tools/treesitter && npm install && npm run build-parser'} + +" Install nvim-treesitter for enhanced features (Neovim only) +Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'} +``` + +Then run `:PlugInstall` and restart. + +#### lazy.nvim (Neovim) +Add to your lazy configuration: +```lua +{ + 'filament-hdl/filament', + rtp = 'treesitter/tools/vim/filament', + build = 'cd treesitter/tools/treesitter && npm install && npm run build-parser', + dependencies = { 'nvim-treesitter/nvim-treesitter' }, + config = function() + require('filament').setup({ + -- Parser is built by 'build' hook above + }) + end, +} +``` + +#### packer.nvim (Neovim) +Add to your packer configuration: +```lua +use { + 'filament-hdl/filament', + rtp = 'treesitter/tools/vim/filament', + requires = {'nvim-treesitter/nvim-treesitter'}, + run = 'cd treesitter/tools/treesitter && npm install && npm run build-parser', + config = function() + require('filament').setup({ + -- Parser is built by 'run' hook above + }) + end +} +``` + +#### Vundle +Add to your `.vimrc`: +```vim +Plugin 'filament-hdl/filament' +Plugin 'nvim-treesitter/nvim-treesitter' +``` + +### Method 2: Manual Installation + +#### For Traditional Vim Users +```bash +# Clone the repository or use the existing one +git clone https://github.com/filament-hdl/filament.git +cd filament/treesitter/tools/vim + +# Copy vim configuration +cp -r filament ~/.vim/ +``` + +#### For Neovim Users +```bash +# Clone the repository or use the existing one +git clone https://github.com/filament-hdl/filament.git +cd filament/treesitter/tools/vim + +# Copy neovim configuration +cp -r filament ~/.config/nvim/ +``` + +#### Using the Install Script +```bash +cd treesitter/tools/vim +./install.sh [vim|nvim] # Specify editor type, or leave blank for auto-detection +``` + +## Tree-sitter Parser Installation + +### Automatic Installation (Plugin Managers) + +If you're using a plugin manager with the setup function: +```lua +require('filament').setup({ + auto_install = true, -- Automatically install parser + treesitter = { + highlight = { enable = true }, + indent = { enable = true }, + fold = { enable = true }, + } +}) +``` + +### Manual Installation + +#### Method 1: Using TSInstall (if parser is registered) +```vim +:TSInstall filament +``` + +#### Method 2: Build from Source +```bash +# Navigate to the tree-sitter directory +cd filament/treesitter/tools/treesitter + +# Install dependencies and build +npm install +npx tree-sitter generate + +# For Neovim, copy the compiled parser +mkdir -p ~/.local/share/nvim/site/pack/packer/start/nvim-treesitter/parser +cp src/parser.so ~/.local/share/nvim/site/pack/packer/start/nvim-treesitter/parser/filament.so +``` + +## Configuration Examples + +### Basic Configuration (init.lua) +```lua +-- Ensure nvim-treesitter is configured +require('nvim-treesitter.configs').setup { + ensure_installed = { 'filament' }, -- Add other languages as needed + highlight = { + enable = true, + additional_vim_regex_highlighting = false, + }, + indent = { + enable = true, + }, +} +``` + +### Advanced Configuration with Filament Plugin +```lua +require('filament').setup({ + -- Automatically install tree-sitter parser + auto_install = true, + + -- Configure nvim-treesitter for filament + treesitter = { + highlight = { + enable = true, + additional_vim_regex_highlighting = false, + }, + indent = { + enable = true, + }, + fold = { + enable = true, + }, + incremental_selection = { + enable = true, + keymaps = { + init_selection = "gnn", + node_incremental = "grn", + scope_incremental = "grc", + node_decremental = "grm", + }, + }, + } +}) + +-- Optional: Set up folding for filament files +vim.api.nvim_create_autocmd("FileType", { + pattern = "filament", + callback = function() + vim.wo.foldmethod = "expr" + vim.wo.foldexpr = "nvim_treesitter#foldexpr()" + vim.wo.foldenable = false -- Start with folds open + end, +}) +``` + +### Using with Different Plugin Managers + +#### With vim-plug + custom config +```vim +" In init.vim or .vimrc +Plug 'filament-hdl/filament', {'rtp': 'treesitter/tools/vim/filament'} +Plug 'nvim-treesitter/nvim-treesitter', {'do': ':TSUpdate'} + +" After plug#end(), add: +lua << EOF +require('filament').setup({ + auto_install = true, +}) +EOF +``` + +#### With lazy.nvim (comprehensive) +```lua +{ + 'filament-hdl/filament', + rtp = 'treesitter/tools/vim/filament', + event = 'BufRead *.fil', + dependencies = { + { + 'nvim-treesitter/nvim-treesitter', + build = ':TSUpdate', + } + }, + config = function() + require('filament').setup({ + auto_install = true, + treesitter = { + highlight = { enable = true }, + indent = { enable = true }, + fold = { enable = true }, + } + }) + end, +} +``` + +## File Structure + +``` +vim/filament/ +├── after/ +│ └── ftplugin/ +│ └── filament.lua # Neovim tree-sitter auto-setup +├── ftdetect/ +│ └── filament.vim # File type detection +├── lua/ +│ └── filament/ +│ ├── init.lua # Main plugin module +│ └── treesitter.lua # Tree-sitter integration +├── plugin/ +│ └── filament.vim # Plugin initialization +├── queries/ +│ └── filament/ +│ ├── highlights.scm # Syntax highlighting rules +│ ├── locals.scm # Local scope definitions +│ └── tags.scm # Symbol extraction rules +├── syntax/ +│ └── filament.vim # Traditional vim syntax +└── README.md # This file +``` + +## Plugin Manager Specific Notes + +### vim-plug +- Use `'rtp': 'treesitter/tools/vim/filament'` to set the correct runtime path +- Tree-sitter parser installation may need to be done manually + +### lazy.nvim +- Can use `event = 'BufRead *.fil'` for lazy loading +- Supports automatic parser installation through setup function + +### packer.nvim +- Use `rtp = 'treesitter/tools/vim/filament'` for correct path +- Config function runs after plugin loads + +### Vundle +- Basic installation only, manual tree-sitter setup required +- Best used with traditional vim syntax highlighting + +## API Reference + +### Lua API +```lua +local filament = require('filament') + +-- Setup the plugin +filament.setup(opts) + +-- Tree-sitter specific functions +local ts = require('filament.treesitter') + +-- Check installation status +local status = ts.status() +-- Returns: { available = bool, parser_installed = bool, message = string } + +-- Install parser manually +ts.install_parser() + +-- Setup tree-sitter for current buffer +ts.setup() + +-- Register parser configuration +ts.register_parser() +``` + +## Syntax Highlighting Features + +The tree-sitter grammar provides comprehensive highlighting for: + +### Keywords +- Component definitions: `comp`, `extern`, `generate` +- Control flow: `if`, `else`, `for`, `in` +- Declarations: `let`, `using`, `import`, `with`, `where` +- Modifiers: `some`, `opaque`, `bundle`, `interface` + +### Language Constructs +- **Component signatures**: `comp Add[WIDTH]<'G: 1>(...) -> (...)` +- **Time expressions**: `'G+1`, `'START+DELAY` +- **Parameter bindings**: `[WIDTH, ?DELAY=1]`, `with { some W; }` +- **Port definitions**: `input: ['G, 'G+1] WIDTH` +- **Invocations**: `adder<'G>(left, right)` + +### Literals and Types +- Numbers with bitwidth annotations: `32'd1024` +- String literals: `"hardware.sv"` +- Floating point: `3.14159` +- Component and type names +- Parameter and event references + +## Troubleshooting + +### Tree-sitter Parser Not Found +``` +Error: no parser for 'filament' language +``` +**Solutions**: +1. **Most likely**: Plugin manager didn't run the build hook. Reinstall with `:PlugClean` then `:PlugInstall` +2. **Manual build**: Run `:FilamentBuildParser` in Neovim +3. **Command line**: `cd treesitter/tools/treesitter && npm run build-parser` + +### Plugin Not Loading +1. Check runtimepath: `:set rtp?` should include filament plugin path +2. For plugin managers, ensure correct `rtp` setting +3. Restart editor after installation + +### Syntax Highlighting Not Working +1. Check file type: `:set filetype?` (should show `filetype=filament`) +2. Check tree-sitter: `:TSBufToggle highlight` +3. Verify parser: `:checkhealth nvim-treesitter` + +### Performance Issues +1. Disable regex highlighting: `additional_vim_regex_highlighting = false` +2. Reduce highlight timeout: `highlight = { enable = true, timeout = 200 }` + +## Development + +### Testing Your Setup +```bash +# Test tree-sitter parsing +cd filament/treesitter/tools/treesitter +npx tree-sitter test + +# Test highlighting +npx tree-sitter highlight test_simple.fil +``` + +### Contributing +1. Modify source query files in `treesitter/tools/treesitter/queries/` +2. Copy updated files to `vim/filament/queries/filament/` +3. Test with various plugin managers +4. Update documentation + +## Related Files + +- **Tree-sitter grammar**: `treesitter/tools/treesitter/grammar.js` +- **Query files**: `treesitter/tools/treesitter/queries/*.scm` +- **Test files**: `treesitter/tools/treesitter/test_*.fil` +- **VS Code extension**: `tools/vscode/` + +## License + +This vim configuration follows the same license as the Filament project. \ No newline at end of file diff --git a/tools/vim/filament/after/ftplugin/filament.lua b/tools/vim/filament/after/ftplugin/filament.lua new file mode 100644 index 000000000..1d66bdd57 --- /dev/null +++ b/tools/vim/filament/after/ftplugin/filament.lua @@ -0,0 +1,18 @@ +-- Neovim tree-sitter configuration for Filament +-- This file is loaded after the default ftplugin for filament files + +-- Use the filament treesitter module for setup +local ok, filament = pcall(require, 'filament.treesitter') +if ok then + filament.setup() +else + -- Fallback to manual setup if module not available + if vim.fn.has('nvim-0.8') == 1 and vim.treesitter then + local parsers_ok, parsers = pcall(require, "nvim-treesitter.parsers") + if parsers_ok and parsers.has_parser("filament") then + vim.treesitter.start(0, "filament") + vim.bo.commentstring = "// %s" + vim.bo.comments = "://" + end + end +end \ No newline at end of file diff --git a/tools/vim/filament/lua/filament/init.lua b/tools/vim/filament/lua/filament/init.lua new file mode 100644 index 000000000..22d2657c3 --- /dev/null +++ b/tools/vim/filament/lua/filament/init.lua @@ -0,0 +1,39 @@ +-- Filament language support main module +local M = {} + +-- Setup function for plugin managers +function M.setup(opts) + opts = opts or {} + + -- Register tree-sitter parser + local treesitter = require('filament.treesitter') + treesitter.register_parser() + + -- Configure nvim-treesitter if available + local ok, ts_config = pcall(require, 'nvim-treesitter.configs') + if ok and not opts.skip_treesitter_config then + ts_config.setup(vim.tbl_deep_extend('force', { + ensure_installed = {}, + highlight = { enable = true }, + indent = { enable = true }, + }, opts.treesitter or {})) + end + + -- Auto-install parser if explicitly requested (not recommended) + if opts.auto_install then + vim.schedule(function() + local status = treesitter.status() + if status.available and not status.parser_installed then + vim.notify('Auto-installing Filament parser...', vim.log.levels.INFO) + treesitter.install_parser() + end + end) + end + + return M +end + +-- Export submodules +M.treesitter = require('filament.treesitter') + +return M \ No newline at end of file diff --git a/tools/vim/filament/lua/filament/treesitter.lua b/tools/vim/filament/lua/filament/treesitter.lua new file mode 100644 index 000000000..c4e690a18 --- /dev/null +++ b/tools/vim/filament/lua/filament/treesitter.lua @@ -0,0 +1,139 @@ +-- Filament tree-sitter integration module +local M = {} + +-- Check if tree-sitter is available +local function has_treesitter() + return vim.fn.has('nvim-0.8') == 1 and pcall(require, 'nvim-treesitter') +end + +-- Setup tree-sitter for filament buffers +function M.setup() + if not has_treesitter() then + return false + end + + local ok, parsers = pcall(require, 'nvim-treesitter.parsers') + if not ok then + return false + end + + -- Check if filament parser is available + if not parsers.has_parser('filament') then + vim.notify_once( + 'Tree-sitter parser for Filament not found. Install with :TSInstall filament', + vim.log.levels.WARN + ) + return false + end + + -- Start tree-sitter for current buffer + vim.treesitter.start(0, 'filament') + + -- Set up buffer-local options + vim.bo.commentstring = '// %s' + vim.bo.comments = '://' + + -- Enable folding if supported + if vim.fn.has('nvim-0.9') == 1 then + vim.wo.foldmethod = 'expr' + vim.wo.foldexpr = 'nvim_treesitter#foldexpr()' + vim.wo.foldenable = false + end + + return true +end + +-- Register filament parser using official Neovim API +function M.register_parser() + if not has_treesitter() then + return false + end + + -- Get the path to our compiled parser + local script_path = debug.getinfo(1, "S").source:sub(2) + local plugin_dir = vim.fn.fnamemodify(script_path, ":h:h:h") + local treesitter_dir = vim.fn.fnamemodify(plugin_dir, ":h:h") .. "/treesitter" + local parser_path = treesitter_dir .. "/src/parser.so" + + -- Check if parser exists + if vim.fn.filereadable(parser_path) == 0 then + vim.notify_once('Filament parser not built. Build it with: :FilamentBuildParser', vim.log.levels.WARN) + return false + end + + -- Register the language using the official API + vim.treesitter.language.add('filament', { path = parser_path }) + + -- Register the filetype association + vim.treesitter.language.register('filament', { 'fil' }) + + return true +end + +-- Manual installation helper +function M.install_parser() + -- With the new approach, we build the parser from source + local script_path = debug.getinfo(1, "S").source:sub(2) + local plugin_dir = vim.fn.fnamemodify(script_path, ":h:h:h") + local treesitter_dir = vim.fn.fnamemodify(plugin_dir, ":h:h") .. "/treesitter" + + vim.notify('Building Filament parser from source...', vim.log.levels.INFO) + + -- Run the build command + local handle = io.popen('cd "' .. treesitter_dir .. '" && npm run build-parser 2>&1') + local result = handle:read("*a") + local success = handle:close() + + if success then + vim.notify('✓ Filament parser built successfully', vim.log.levels.INFO) + -- Register the parser + return M.register_parser() + else + vim.notify('✗ Failed to build parser:\n' .. result, vim.log.levels.ERROR) + return false + end +end + +-- Check installation status +function M.status() + if not has_treesitter() then + return { + available = false, + parser_installed = false, + message = 'Tree-sitter not available (requires Neovim 0.8+)' + } + end + + -- Check if our local parser exists + local script_path = debug.getinfo(1, "S").source:sub(2) + local plugin_dir = vim.fn.fnamemodify(script_path, ":h:h:h") + local treesitter_dir = vim.fn.fnamemodify(plugin_dir, ":h:h") .. "/treesitter" + local parser_path = treesitter_dir .. "/src/parser.so" + local parser_installed = vim.fn.filereadable(parser_path) == 1 + + -- Also check if the language is registered with Neovim + local lang_registered = false + local ok = pcall(function() + vim.treesitter.language.inspect('filament') + lang_registered = true + end) + + local message + if parser_installed and lang_registered then + message = 'Filament tree-sitter support ready' + elseif parser_installed then + message = 'Parser built but not registered (run setup)' + else + message = 'Parser not built (run install_parser)' + end + + return { + available = true, + parser_installed = parser_installed, + lang_registered = lang_registered, + parser_path = parser_path, + message = message + } +end + +return M \ No newline at end of file diff --git a/tools/vim/filament/plugin/filament.vim b/tools/vim/filament/plugin/filament.vim new file mode 100644 index 000000000..661851529 --- /dev/null +++ b/tools/vim/filament/plugin/filament.vim @@ -0,0 +1,37 @@ +" Filament language support for vim/neovim +" This file is loaded automatically by plugin managers + +if exists('g:loaded_filament_plugin') + finish +endif +let g:loaded_filament_plugin = 1 + +" Manual parser build function (for users who need it) +function! FilamentBuildParser() + echo "Building Filament tree-sitter parser..." + echo "This will build the parser from source." + echo "" + + " Use the lua module which has better path handling + if has('nvim-0.8') + lua require('filament.treesitter').install_parser() + else + echohl ErrorMsg + echo "✗ This command requires Neovim 0.8+ for tree-sitter support" + echo "For manual build, run:" + echo " cd /treesitter/tools/treesitter" + echo " npm install && npm run build-parser" + echohl None + endif +endfunction + +" Command to manually build parser if needed +command! FilamentBuildParser call FilamentBuildParser() + +" Auto-setup tree-sitter highlighting when opening filament files +augroup filament_treesitter_setup + autocmd! + if has('nvim-0.8') + autocmd FileType filament lua require('filament.treesitter').setup() + endif +augroup END \ No newline at end of file diff --git a/tools/vim/filament/queries/filament b/tools/vim/filament/queries/filament new file mode 120000 index 000000000..74cc49ad1 --- /dev/null +++ b/tools/vim/filament/queries/filament @@ -0,0 +1 @@ +../../../treesitter/queries \ No newline at end of file diff --git a/tools/vim/install.sh b/tools/vim/install.sh new file mode 100755 index 000000000..2c97f54da --- /dev/null +++ b/tools/vim/install.sh @@ -0,0 +1,91 @@ +#!/bin/bash +set -euf -o pipefail + +# Installation script for Filament vim/neovim support +# Usage: ./install.sh [vim|nvim] + +EDITOR_TYPE="${1:-auto}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Filament Vim/Neovim Installation Script" +echo "========================================" + +# Detect editor type if not specified +if [ "$EDITOR_TYPE" = "auto" ]; then + if command -v nvim >/dev/null 2>&1; then + EDITOR_TYPE="nvim" + echo "Detected: Neovim" + elif command -v vim >/dev/null 2>&1; then + EDITOR_TYPE="vim" + echo "Detected: Vim" + else + echo "Error: Neither vim nor nvim found in PATH" + exit 1 + fi +fi + +# Set installation paths +case "$EDITOR_TYPE" in + nvim) + if [ -n "${XDG_CONFIG_HOME:-}" ]; then + INSTALL_DIR="$XDG_CONFIG_HOME/nvim" + else + INSTALL_DIR="$HOME/.config/nvim" + fi + echo "Installing for Neovim at: $INSTALL_DIR" + ;; + vim) + INSTALL_DIR="$HOME/.vim" + echo "Installing for Vim at: $INSTALL_DIR" + ;; + *) + echo "Error: Invalid editor type '$EDITOR_TYPE'. Use 'vim' or 'nvim'" + exit 1 + ;; +esac + +# Create installation directory if it doesn't exist +mkdir -p "$INSTALL_DIR" + +# Copy filament configuration +echo "Copying filament configuration..." +cp -r "$SCRIPT_DIR/filament" "$INSTALL_DIR/" + +echo "✓ Filament configuration installed successfully!" + +# Check for tree-sitter parser (neovim only) +if [ "$EDITOR_TYPE" = "nvim" ]; then + echo "" + echo "Tree-sitter Setup:" + echo "==================" + + # Check if nvim-treesitter is installed + if [ -d "$INSTALL_DIR/pack/"*"/start/nvim-treesitter" ] || [ -d "$HOME/.local/share/nvim/site/pack/"*"/start/nvim-treesitter" ]; then + echo "✓ nvim-treesitter plugin detected" + + echo "" + echo "Next steps:" + echo "1. Open neovim and run: :TSInstall filament" + echo "2. Or build the parser manually from the treesitter/ directory" + echo "3. Open a .fil file to test syntax highlighting" + else + echo "⚠ nvim-treesitter plugin not detected" + echo "" + echo "To get full tree-sitter support, install nvim-treesitter:" + echo "- Using packer: use 'nvim-treesitter/nvim-treesitter'" + echo "- Using lazy: { 'nvim-treesitter/nvim-treesitter' }" + echo "- Using vim-plug: Plug 'nvim-treesitter/nvim-treesitter'" + fi +fi + +echo "" +echo "Installation complete! 🎉" +echo "" +echo "File type detection is now enabled for .fil files." +echo "Open any Filament file to see syntax highlighting in action." +echo "" +echo "For plugin manager users:" +echo "- See examples/ directory for configuration examples" +echo "- Use 'filament-hdl/filament' with rtp = 'treesitter/tools/vim/filament'" +echo "- Add 'do'/'build'/'run' hook: 'cd treesitter/tools/treesitter && npm install && npm run build-parser'" +echo "- Call require('filament').setup() in your config" \ No newline at end of file