diff --git a/CHANGELOG.md b/CHANGELOG.md index c9b68b091..094f62234 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New Features +- HDL: pyslang теперь показывает macro origins в initializer параметров и объявленных типах, различая override и исходный default. + +- HDL: `hdl-semantic --python` с pyslang показывает исходное написание и места вызова макросов, явно отмечая неполное сопоставление. + +- HDL: `hdl-semantic` с установленным slang показывает вычисленные параметры и ширины портов для выбранного профиля и экземпляра. + - **Codex and Astra read project guidance from `AGENTS.md`.** The canonical agent guide now lives in `AGENTS.md` (with a nested `docs/AGENTS.md` for long validation notes); `CLAUDE.md` is a thin `@AGENTS.md` wrapper for Claude Code. Codex/Astra no longer miss the old CLAUDE-only instructions. - **A big screen's picture stops wrapping into a column.** How wide a screen's lines run before they wrap was worked out with a formula, and the formula was wrong for the way these pictures are actually drawn: a part of a screen spends lines on its own structure — a step that fires things gets a line to itself, and what it fires starts another — so estimating the lines from the boxes alone badly undercounted them, and one screen's 98 boxes wrapped into a 4,356px column. Laying a picture out is cheap and exact, so the widths are now simply tried and the one that comes out closest to the shape of a window is kept. Across one app's 51 screens the tallest picture went from 4,356px to 3,796px, total height fell 8%, and — because a shorter picture is also a picture whose lines have less far to go — lines running over other boxes fell by a third and lines crossing each other went from 13 to 5. @@ -145,12 +151,51 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- HDL: именованные profiles в codegraph.json, filelists/include/define context, сохранение offsets при conditional selection, fingerprint/sync invalidation и диагностика configured/indexed profile. Watcher учитывает profile dependencies; полноценный macro expansion пока не выполняется. + +- HDL: source access roles read/write/readwrite/control/event и направления аргументов известных functions/tasks доступны через explore `hdlAccess` / CLI `--hdl-access`. Сохраняются разные позиции одной строки, initializer writes и переклассификация после изменения/удаления сигнатуры через sync. + +- HDL: точные procedural scopes, formal parameters и generate template declarations сохраняют локальные связи; исправлены defaults/for initializer calls, приоритет block imports и qualified cross-file replay после sync. + +- HDL: позиционные подключения следуют порядку ANSI/non-ANSI заголовка; `.*` учитывает явные подключения и локальную область видимости. Sync пересчитывает весь набор связей при изменении заголовка или удалении дубликата модуля. Grammar обновлена до tree-sitter-systemverilog 0.4.0 после сравнения 137 реальных HDL-файлов. + +- HDL: именованные подключения имеют отдельные переходы к формальным портам выбранного модуля и локальным сигналам; LHS bit/part selects и concatenations больше не теряют основания и индексы. Неоднозначные module declarations не дают предполагаемую formal binding. + +- Verilog/SystemVerilog: именованные экземпляры и generate scopes, порты/сигналы и always/assign доступны для навигации; связи локальных сигналов и package calls сохраняют scope, interfaces показывают modports. Полные elaboration и timing не моделируются. + +- Вызовы действий store больше не теряются из-за одноимённой сигнатуры в TypeScript-интерфейсе. + +- Вложенные вызовы JavaScript и TypeScript снова доступны Steps и распознаванию фреймворков без привязки встроенных коллекций к посторонним методам; существующие индексы нужно пересобрать. (#1794, #1566) +- `codegraph status` показывает непроиндексированные добавления, изменения и удаления даже после commit в Git. (#1829) +- `codegraph sync` отклоняет индекс устаревшего формата извлечения и предлагает полную переиндексацию. (#1798) + - Spring mappings now include every declared path combination and resolve constants declared in the same file, while unresolved paths no longer appear as false root routes. (#1461) - `codegraph callers`, `codegraph callees` and `codegraph impact` now resolve qualified names, group results and JSON edges by definition, and accept `--file` to narrow ambiguous names; thanks @ferrine. (#1512, #1656) - `codegraph callers`, `codegraph callees` and `codegraph impact` (CLI and MCP) now report missing names with did-you-mean suggestions instead of another symbol's results, and exact matches with no callers stay empty; thanks @uvmplus. (#1473, #1481) #### MCP / indexing +- Исправлены namespace и арность C++ local constructors, ложные вызовы от pointer/reference initializers и same-line C++ IDs. +- Macro recovery больше не принимает комментарии и noexcept-expression за объявление функции; условный undef сохраняет неопределённость. +- Symlink-псевдонимы проекта используют один MCP watcher и общий catch-up вместо параллельных экземпляров одного индекса. + +- Callback связывается с обработчиком регистрирующего класса, включая TSX→TS и наследование, а не с одноимённым методом другого класса. +- Исправлены C/C++ macro calls и local constructors; одноаргументные function macros получают подтверждённое имя. +- TS/JS getter и setter одной строки больше не перезаписывают друг друга; Dart extension types сохраняют правильные границы и членов. +- Явно выбранные MCP-проекты получают catch-up и watcher; shutdown дожидается записи, а тесты удаляют свой daemon. + +- Восстановлена поддержка Verilog/SystemVerilog и поиска пути от верхнего FPGA-модуля до вложенного; спасибо @FHYQ-Dong за исходный extractor. +- Обрыв подключения во время MCP-handshake больше не оставляет фантомного клиента. +- Prompt-hook пропускает служебные уведомления о завершении задач; тесты resolution удаляют временные каталоги после закрытия графа. + +- Проверка вызовов больше не связывает C-макросы с одноимёнными enum и Go-код с посторонними TypeScript-символами. +- Занятый `codegraph sync` сообщает о блокировке вместо ложного успеха, в том числе в quiet-режиме. +- Неисправные query workers завершаются; старые PID-lockfiles живых процессов сохраняются. + +- При отказе `codegraph sync --quiet` из-за устаревшего индекса причина теперь выводится одной строкой в stderr. +- Прерывание повторного разрешения связей больше не оставляет граф без исходной связи и записи восстановления. +- В TypeScript вызов метода у результата `await` с объявленным `Promise` больше не связывается с одноимённым методом постороннего класса. + - The prompt hook no longer injects unrelated projects when run from your home directory or a broader directory containing a stray workspace manifest. (#1454) - Indexing now succeeds when Node.js's SQLite lacks FTS5, with search falling back to name and fuzzy matching; thanks @aniruddhaadak80. (#1532) diff --git a/README.md b/README.md index 10a325ae2..e395682ad 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, Verilog/SystemVerilog | | **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 | @@ -892,6 +892,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) | +| Verilog / SystemVerilog | `.v`, `.vh`, `.sv`, `.svh` | Модули и именованные instances, build profiles (filelists/includes/defines), lexical/generate scopes и formal parameters, порты/сигналы и named/positional/wildcard port bindings, bit/part selects, read/write/control/event queries, always/assign, interfaces/modports, package calls и иерархия instantiates; source-граф без elaboration/timing, отдельный hdl-semantic через slang для параметров и ширин | | 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`) | @@ -970,3 +971,7 @@ MIT [Report Bug](https://github.com/colbymchenry/codegraph/issues) · [Request Feature](https://github.com/colbymchenry/codegraph/issues) + +Подробности HDL-профилей: [настройка и ограничения](docs/hdl-profiles.md). +Вычисленные параметры и ширины по явному запросу: [hdl-semantic с установленным slang](docs/hdl-semantics.md); +[происхождение макросов через pyslang](docs/hdl-macro-source-map.md). diff --git a/VERILOG_TRACKER.md b/VERILOG_TRACKER.md new file mode 100644 index 000000000..9190641cb --- /dev/null +++ b/VERILOG_TRACKER.md @@ -0,0 +1,346 @@ +# Трекер развития Verilog / SystemVerilog в CodeGraph + +Дата состояния: 2026-09-12. Проверенная база реализации: `b050fb8910e80116e327d9dc9a8af40bccee67bc`, extraction stamp **38**. + +Назначение: довести HDL-навигацию до проверяемых ответов об иерархии, типах, +параметрах, соединениях и влиянии изменений на реальных RTL-проектах. +Это рабочий план с критериями закрытия задач, а не заявление о полном охвате языка. + +Канонический рабочий файл находится в checkout `/home/danik/storage/codegraph-fix-20260911/VERILOG_TRACKER.md`. +Копия в `/home/danik/Projects_and_coding/codegraph/VERILOG_TRACKER.md` сохранена для доступа из открытого проекта. +При обновлении статуса синхронизировать эти два файла, пока checkout остаются раздельными. + +## Правила ведения + +- `[x]` — выполнено в указанной базе и подтверждено отчётом; `[ ]` — предстоит. +- Задача закрывается после реализации, regression tests, проверки на реальном + корпусе и фиксации результата. Прототип сам по себе не означает поддержку. +- Для каждого закрытия записывать commit, отчёт, корпус/revision, профиль, + версии инструментов и оставшиеся ограничения. +- Не ставить календарный срок до измерения объёма; порядок задают зависимости. +- Source graph, compiler semantics и результаты simulation/formal — отдельные + уровни доказательств. Не подменять один другим. +- После каждого пакета актуализировать этот трекер, документацию и описание PR. + +## 1. Что уже сделано + +| ID | Статус | Результат и граница | +| --- | --- | --- | +| HDL-001 | [x] | Разбор `.v/.vh/.sv/.svh`, tree-sitter-systemverilog 0.4.0; версия grammar не доказывает полный охват стандарта | +| HDL-002 | [x] | Модули, instances, ports/signals, always/assign, function/task, packages, interface/modport | +| HDL-003 | [x] | Named, positional и wildcard port bindings с проверкой неоднозначности и порядка портов | +| HDL-004 | [x] | Lexical scopes, shadowing, generate templates, escaped names и устойчивые идентичности в проверенных случаях | +| HDL-005 | [x] | Синтаксические read/write/readwrite/control/event роли и `hdlAccess` / `--hdl-access` | +| HDL-006 | [x] | Пересчёт bindings/access roles при изменениях сигнатур и удалении/восстановлении объявлений | +| HDL-007 | [x] | Именованные build profiles, files/filelists, includes/defines, fingerprint и configured/indexed status | +| HDL-008 | [x] | Ограниченный выбор условных веток с сохранением исходных offsets/hashes и диагностикой unknown | +| HDL-009 | [x] | Watcher для config/filelists/includes, согласованный snapshot, диагностика частичного sync | +| HDL-010 | [x] | Реальные UART/CRC/AXI controls, sync против clean rebuild; поставка в три remote и draft PR #1845 | + +Последний полный прогон: native **4894 passed / 9 skipped**, WASM **4884 passed / +19 skipped**, ошибок нет; 55 профильных тестов прошли. Это результаты базы выше, +а не гарантия для последующих коммитов. Десять активных индексов были проверены +на stamp 38, complete и 0 pending. Windows/macOS в последнем пакете не проверялись. + +Отчёты в рабочем checkout: + +- `docs/validation-hdl-navigation-2026-09-12.md` +- `docs/validation-hdl-port-bindings-2026-09-12.md` +- `docs/validation-hdl-wildcard-2026-09-12.md` +- `docs/validation-hdl-scopes-2026-09-12.md` +- `docs/validation-hdl-access-2026-09-12.md` +- `docs/validation-hdl-parse-gaps-2026-09-12.md` +- `docs/validation-hdl-profiles-2026-09-12.md` +- Инструкция профилей: `docs/hdl-profiles.md`; предыдущая история: `docs/hdl-roadmap.md`. + +[База реализации и отчётов в GitHub](https://github.com/danusha2345/codegraph/tree/b050fb8910e80116e327d9dc9a8af40bccee67bc). + +## 2. Открытые ограничения, которые нельзя забыть + +- Include передаёт macro state, но не вставляет объявления header в compilation unit. +- Нет полного macro expansion и source map раскрытия. Непрозрачный macro может + сделать дальнейший выбор веток неизвестным; такие ветки не угадываются. +- Каждый перечисленный source начинает с defines профиля. Общая compilation-unit + семантика порядка файлов пока не моделируется. +- `topModules` и `languageMode` фиксируют намерение; нет elaboration, pruning от top + и строгой проверки соответствия Verilog-only dialect. +- Один индекс содержит один профиль; одновременного semantic cache нескольких + профилей пока нет. +- Нет достоверных instance-specific parameter values, ширин, типов и generate iterations. +- Source snippets могут содержать неактивные ветки. Access roles не доказывают + электрического драйвера, race, latch, timing или CDC. +- Пределы профильного snapshot: 1 MiB на source/header и 64 MiB суммарно. +- UART sim имеет 5 диагностик неинтерпретированного timescale; оба проверенных + AXI-профиля — 17 диагностик macro expansion. Прежний corpus не содержал common_cells includes; + A2 восстановил их в отдельной compiler-копии. Исходный frozen corpus не меняется; + runtime CodeGraph по-прежнему не выполняет полный macro/include expansion. + +## 3. Очередь выполнения и зависимости + +| Пакет | Приоритет | Зависит от | Результат | +| --- | --- | --- | --- | +| A. Корпуса и frontend prototype | P0, следующий | текущая база | Обоснованный выбор адаптера и воспроизводимый benchmark | +| B. Preprocessing и source mapping | P0 | A | Раскрытие macros/includes с происхождением каждого результата | +| C. Типы и параметры | P0 | A–B | Проверенные значения, dimensions и type references | +| D. Elaboration и иерархия | P0 | B–C | Instance-specific graph и реальные generate branches | +| E. Сквозные сигнальные пути | P1 | C–D, HDL-005 | Трассировка соединений с явными unknown участками | +| F. Инженерные запросы и impact | P1 | E | Полезные CLI/MCP ответы с доказательствами | +| G. Verification SystemVerilog | P1 | B–D | Assertion/coverage/bind/DUT navigation | +| H. Class и UVM navigation | P2 | G, реальные сценарии | Обоснованный ограниченный охват testbench OOP | +| I. Масштаб и переносимость | сквозной | каждый пакет | Измеренные ресурсы, recovery и проверки ОС | +| J. Поставка и актуальность | сквозной | каждый пакет | Документация, индексы, commit/push/PR readback | + +Пакеты идут последовательно по зависимостям. Внутри пакета независимые corpus, +frontend и review задачи можно отдавать агентам; общий schema/API контракт и +финальную интеграцию держать у одного владельца. + +## A. Корпуса и выбор compiler frontend + +- [x] **A1** Зафиксировать manifest UART, eMMC/CRC, AXI: URL/revision, hashes, + license, source lists, includes, defines, top, tool versions и отсутствующие зависимости. + Выполнено 2026-09-12: 137 HDL-файлов, `docs/hdl-corpus-manifest.json`; + один локальный card-reader testbench закреплён hash, а не Git revision. + Проверены clean/change/add/remove/restore controls. +- [x] **A2** Восстановить необходимые открытые зависимости AXI в изолированной + копии, зафиксировать версии и подтвердить корректный compiler invocation. + Выполнено 2026-09-12: три lockfile dependencies; demux preprocessing default/VCS + и xbar lint/elaboration при NoSlvMst=1/2/4. Отчёт: `docs/validation-hdl-corpora-build-2026-09-12.md`. +- [x] **A3** Сравнить кандидатов: slang, Surelog/UHDM, Verilator; при необходимости + Yosys для синтезируемого подмножества. Актуальные версии, лицензии, экспорт и + ограничения проверить по официальным источникам во время прототипа. +- [x] **A4** Измерить на одинаковых profiles: parse/elaboration success, diagnostics, + source locations, types/parameters export, instance paths, время и peak RSS. +- [x] **A5** Проверить интеграцию: отдельный процесс, формат обмена, доступность на + Linux/Windows/macOS, версия API, redistributability и необязательность установки. +- [x] **A6** Сохранить решение в `docs/hdl-frontend-decision.md` с матрицей результатов; + выбрать один основной адаптер. Второй внедрять только при доказанном пробеле. + A3–A6 выполнены 2026-09-12: `docs/hdl-frontend-decision.md`, 24 comparative + controls на UART/CRC/AXI, source/parameter/type assertions и instance probe. + Выбран slang для следующей реализации; A5 platform availability проверена + по docs, runtime только Linux. Surelog/UHDM изучен без runtime. + Full macro mapping не доказан: macro-generated columns могут быть 0. + +**Приёмка:** воспроизводимый запуск минимум на трёх реальных корпусах; сравнение +не менее двух подходящих кандидатов, либо документированный конкретный blocker. +Отсутствие frontend не ломает существующую source-навигацию. Производственный +адаптер до результатов сравнения не выбирать. + +## B. Полное preprocessing через выбранный frontend + +**Первая вертикаль реализована (2026-09-12):** `hdl-semantic` / +`CodeGraph.getHdlSemantics()` возвращают on-demand parameters/port widths с +provenance и проверенными sourceNodeId. Отчёт `docs/validation-hdl-semantics-2026-09-12.md`, +инструкция `docs/hdl-semantics.md`. Это частичное выполнение B1/B2 и C1/C2/C5; +checkbox ниже остаётся открытым для полного заявленного объёма. + +- [ ] **B1** Получать include/macro expansion и diagnostics из frontend; не писать + собственный полный HDL-препроцессор внутри extractor. +- [ ] **B2** Сопоставить expansion с исходным файлом, диапазоном, macro definition + и invocation. Для неоднозначного mapping возвращать явную неизвестность. + Частично выполнено 2026-09-12: optional --python/pyslang backend возвращает + provenance имени parameter/port/typedef через обе SourceManager parent relations. + Реальный AXI aw_chan_t width35: аргумент415:31, macro body178:44, вызов415:3–49. + Collapsed ranges отмечены incomplete; origins всех выражений ещё не охвачены. + Отчёт `docs/validation-hdl-macro-source-map-2026-09-12.md`. + Следующий пакет 2026-09-12: expressionOrigins для effective/default initializer + и type/dimensions (включая non-ANSI), explicit coverage/truncation; реальные + axi_from_mem types35/36/34 и header-change sync/rebuild parity. Отчёт + `docs/validation-hdl-expression-origins-2026-09-12.md`. Транзитивные зависимости + значения не объявляются полной origin map. +- [ ] **B3** Проверить function-like macros, arguments/defaults, nested expansion, + token concatenation/stringification, conditional includes и include guards. +- [ ] **B4** Определить compilation-unit mode и порядок sources явно; не смешивать + его с текущим independent-source режимом и не менять существующие profiles молча. +- [ ] **B5** Учесть compiler predefines, include search order и missing dependencies + в effective configuration и fingerprint. +- [ ] **B6** Изменения header/macro/flags должны инвалидировать semantic context; + old source snapshot не выдавать за новый compiler result. + +**Приёмка:** текущие AXI macro controls разобраны либо получили точную compiler +ошибку; результат имеет исходные locations. Правки macro definition и invocation, +удаление include и смена compilation-unit mode дают sync/rebuild parity. + +## C. Parameters, dimensions и types + +- [ ] **C1** Ввести отдельные semantic records с provenance и версией формата; + сохранить совместимость source IDs и существующих query/API. +- [ ] **C2** Parameters/localparams: declaration/default и evaluated value раздельно; + instance overrides и type parameters не смешиваются между экземплярами. +- [ ] **C3** Packed/unpacked dimensions, signedness, range direction, structs/unions, + enums, typedef aliases, package-qualified types и interface parameters. +- [ ] **C4** Отображать unresolved/external/unsupported состояния без фиктивных + значений и без молчаливого преобразования unknown/X/Z в обычный integer. +- [ ] **C5** Добавить запрос «тип/ширина/параметры этого порта в этом instance» + с переходами к declaration и override. + +**Приёмка:** ручные controls и compiler export совпадают на UART/CRC/AXI; +два instance одного module с разными параметрами имеют разные правильные widths. +Смена package typedef/parameter не оставляет прежнюю semantic запись после sync. + +## D. Elaboration и instance-specific hierarchy + +- [ ] **D1** Явно выбрать top(s), конфигурацию и libraries; multi-top и неизвестный + top возвращают объяснимый результат. +- [ ] **D2** Связать elaborated instance с source template, типом module и profile; + arrayed instances и escaped hierarchical names имеют устойчивые идентичности. +- [ ] **D3** Generate if/case/for: фактические ветки/iterations отдельно от templates; + genvar/localparams и parameter overrides брать из frontend. +- [ ] **D4** Сохранить interface/modport instances и формальные/фактические порты; + black boxes, external libraries и unsupported primitives показывать явно. +- [ ] **D5** Определить invalidation для смены top/parameter/source/frontend version; + запись нового semantic graph публикуется согласованно после успешного прохода. + +**Приёмка:** полные instance paths совпадают с frontend; несколько экземпляров и +условный generate не сливаются. Прерванная/неуспешная elaboration не уничтожает +source graph и не маркирует старый semantic graph как актуальный. + +## E. Связность и пути сигналов + +- [ ] **E1** Соединить actual expression → instance port → formal signal с + declaration direction и instance-specific widths. +- [ ] **E2** Представить slices, concatenations, replication, casts и expression + boundaries; разделить точное bit mapping и зависимость без точного mapping. +- [ ] **E3** Учесть interface/modport directions, arrays of interfaces, inout/ref; + спорные resolved-net/tri-state/alias случаи оставлять unknown до отдельного control. +- [ ] **E4** Трассировать source-level data/control dependencies через assign и + procedural blocks, отдельно обозначая sequential boundaries. +- [ ] **E5** Ограничить глубину/объём обхода, обработать циклы, показать место + остановки и доказательство каждого перехода. + +**Приёмка:** UART RX → FIFO, вход CRC → crc_out, AXI valid/ready через wrapper +сверены с ручной трассировкой. Отрицательные controls для shadowing, разных +instances/profiles и неверных slices не создают ложных переходов. Source path +не называется временной трассой или доказательством физической передачи сигнала. + +## F. Полезные запросы CLI/MCP и анализ изменений + +- [ ] **F1** Запросы «где объявлен», «кто читает/пишет», «куда подключён», + «какой instance/top/profile», «тип и ширина», «почему unknown». +- [ ] **F2** Запрос «что затронет изменение порта/параметра/typedef»: различать + подтверждённые прямые зависимости и возможные транзитивные последствия. +- [ ] **F3** Сравнение двух сохранённых profile/revision результатов — сначала + последовательное; необходимость постоянных нескольких индексов измерить отдельно. +- [ ] **F4** В ответах показывать configured/indexed/semantic context, freshness, + diagnostics и source links; строки header/expansion не приписывать другому файлу. +- [ ] **F5** Обновить CLI help, MCP instructions и пользовательские примеры. + Viewer hierarchy/path UI добавлять после стабилизации query contract. +- [ ] **F6** Сравнить ответы агента с CodeGraph и без него на фиксированных задачах: + точность, пропуски, ложные связи, число запросов, время и расход контекста. + +**Приёмка:** контрольные инженерные вопросы имеют ожидаемые ответы и ссылки; +пользователь видит причину неполного результата, а не пустую «доказанную» выборку. +A/B результаты записываются отдельно от extractor unit tests. + +## G. Verification-конструкции SystemVerilog + +- [ ] **G1** Property/sequence declarations и references, assert/assume/cover, + immediate/concurrent assertions, disable iff и используемые signals/clocks. +- [ ] **G2** Clocking blocks и sampled-value/event expressions: найти исходное + использование без заявления о полном temporal semantics. +- [ ] **G3** Bind targets и DUT ↔ checker/testbench connections с provenance профиля. +- [ ] **G4** Covergroup/coverpoint/cross declarations и связи с выражениями; + declaration coverage не путать с выполненным runtime coverage. +- [ ] **G5** Реальный открытый verification corpus плюс минимальные негативные cases. + +**Приёмка:** запрос по RTL-сигналу находит относящиеся assertions/checkers и +исходные строки. Результат не обозначается formal proof или simulation pass. + +## H. Classes и UVM — после RTL и verification navigation + +- [ ] **H1** Собрать реальные сценарии, где текущей навигации не хватает, прежде + чем расширять общий class resolver. +- [ ] **H2** Class inheritance, parameterized classes, methods, constructors, + virtual methods/interfaces и lexical visibility — с отдельными ambiguity controls. +- [ ] **H3** UVM macros, factory registration/overrides, config_db и phases: + статически подтверждённые связи отдельно от runtime possibilities. +- [ ] **H4** Измерить retrieval value на открытом testbench и выбрать полезное + подмножество; непредсказуемые runtime overrides не угадывать. + +**Приёмка:** контрольные testbench queries находят declarations и подтверждённые +связи, не выдают одну предполагаемую factory/runtime цель за точную. + +## I. Производительность, надёжность и переносимость + +- [ ] **I1** Для каждого пакета измерять cold index, no-op sync, source/header/profile + change, время queries, peak RSS, DB size и стоимость semantic cache. +- [ ] **I2** На большом corpus проверить dependency invalidation; оптимизировать + только после baseline, сохраняя согласованность source/compiler snapshots. +- [ ] **I3** Frontend process: timeout/cancel, bounded output, crash recovery, + missing executable/version mismatch, отсутствие shell-интерполяции аргументов. +- [ ] **I4** Проверить interrupted writes, watcher churn, concurrent source changes, + stale cache и отсутствие orphan processes после shutdown. +- [ ] **I5** Реальные Linux/Windows/macOS проверки paths, case sensitivity, + symlinks, CRLF, Unicode, file locks и watchers при наличии соответствующих сред. +- [ ] **I6** Проекты с generated HDL и файлами больше текущих limits: измерить + потребность, затем определить streaming/limits policy без безусловного расширения. +- [ ] **I7** Регулярно проверять обновления grammar/frontend и актуальные стандарты + по официальным источникам; перед обновлением — corpus diff и source-map controls. + +**Приёмка:** budgets фиксируются после baseline; нет необъяснённой регрессии. +Непроверенные ОС и неподдержанные constructs перечислены в отчёте явно. + +## J. Проверки и поставка каждого пакета + +- [ ] **J1** Сначала воспроизведение дефекта/ожидаемого поведения и минимальный + regression; для чистой документации достаточно проверки содержания и ссылок. +- [ ] **J2** Focused tests и независимое review изменений; исправление findings. +- [ ] **J3** Реальные corpus runs и сравнение полного графа sync/clean rebuild, + включая отрицательные случаи, diagnostics и неизменность исходников. +- [ ] **J4** Native/WASM suites при изменениях общих extraction/resolution/storage + путей; typecheck/build/assets. Не выдавать skipped за passed. +- [ ] **J5** Обновить extraction/schema/semantic cache version, если изменилась + интерпретация графа; проверить миграцию или понятное требование reindex. +- [ ] **J6** README/help/instructions, этот tracker, feature coverage matrix и + validation report привести к фактически поставленному состоянию. +- [ ] **J7** Переиндексировать согласованные активные проекты, проверить freshness; + для уже работающих MCP-сессий учесть необходимость restart новой реализации. +- [ ] **J8** В рамках согласованной поставки: JJ diff/status → commit → push + GitHub fork/GitLab/Forgejo → одинаковый SHA → PR body/head readback. +- [ ] **J9** Перед обновлением интеграционного PR проверить уже принятые upstream + изменения и убрать подтверждённо лишний diff. Не push в upstream main. + +J — повторяемый checklist, его отметки относятся к конкретному пакету, а не +закрывают всю будущую поставку навсегда. Npm release/deploy — отдельное действие, +не подразумеваемое обновлением draft PR. + +## 4. Матрица покрытия, которую нужно вести + +- [ ] Создать `docs/hdl-feature-coverage.md`: для каждой конструкции отметить + parse / extract / resolve / query / semantic и ссылку на реальный пример. +- [ ] Начальный перечень: modules/programs/interfaces/packages; ports/bindings; + parameters/types/dimensions; lexical/generate scopes; assignments/processes; + functions/tasks; directives/macros/includes; assertions/clocking/bind/coverage; + classes/UVM; primitives/configurations/libraries. +- [ ] Для каждого пробела определить: grammar, extractor, resolver, query, + отсутствующий build context или frontend limitation. Исправлять нужный слой. + +## 5. Ближайшая конкретная итерация + +1. **A1–A2 выполнены**: manifests и восстановленный AXI context; см. `docs/validation-hdl-corpora-build-2026-09-12.md`. +2. **A3–A6 выполнены**: slang выбран по probes; adapter contract и ограничения + сохранены в `docs/hdl-frontend-decision.md`. +3. Начать реализацию согласованного контракта, сохраняя strict/compatible profiles + раздельно и unknown coordinates для macro-generated declarations. +4. **Минимальная on-demand вертикаль выполнена**: параметры/порты для одного + profile/top, source links, ошибка compiler не меняет source graph. Дальше: + Прямые initializer/type token origins B2 добавлены; дальше — расширенные + types/dimensions model C3 и persistent semantic + context/invalidation C1/D5; затем semantic queries MCP. +5. Провести J1–J9 для этого ограниченного пакета; затем расширять B/C и переходить к D. + +## 6. Журнал выполнения + +| Дата | Пакет | Результат | Commit / отчёт | Следующее действие | +| --- | --- | --- | --- | --- | +| 2026-09-12 | Source navigation, scopes, access | Реализованные части перечислены в HDL-001–006 | Отчёты раздела 1 | Сохранить regression coverage | +| 2026-09-12 | Build profiles | Ограниченный source-режим, реальные profile parity проверки | `b050fb89`, `docs/validation-hdl-profiles-2026-09-12.md` | A: frontend comparison | +| 2026-09-12 | B2 expression origins | Effective/default/type tokens, non-ANSI, AXI35/36/34 и parity | `docs/validation-hdl-expression-origins-2026-09-12.md` | C3, cache/invalidation | +| 2026-09-12 | B2 declaration origins | Pyslang SourceManager, реальные macro points/ranges и typedef35 | `docs/validation-hdl-macro-source-map-2026-09-12.md` | Expression origins, C3, cache | +| 2026-09-12 | B/C первая вертикаль | API/CLI, реальные UART/CRC/AXI, include sync/rebuild parity | `docs/validation-hdl-semantics-2026-09-12.md` | B2, C3, persisted context | +| 2026-09-12 | A3–A6 | 24 controls, slang выбран; CLI macro mapping неполон | `docs/hdl-frontend-decision.md` | B1/B2 + C1/C2 | +| 2026-09-12 | A1–A2 | Manifest 137 HDL, три locked AXI dependencies, шесть compiler controls | `docs/validation-hdl-corpora-build-2026-09-12.md` | A3–A6, сравнение frontends | +| 2026-09-12 | Полный план | Создан этот tracker; новые implementation задачи не объявлены выполненными | `VERILOG_TRACKER.md` | A1–A2 | + +Для следующей записи: дата → ID задач → наблюдаемый результат → commit/report → +непроверенное/ограничения → следующий шаг. Исторические результаты не перезаписывать +как свежие без нового запуска. diff --git a/__tests__/awaited-receiver.test.ts b/__tests__/awaited-receiver.test.ts new file mode 100644 index 000000000..1a92c476a --- /dev/null +++ b/__tests__/awaited-receiver.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +describe('awaited receiver return types (#1840)', () => { + it.each(['string', 'PaneManager'])('resolves awaited %s without guessing unrelated methods', async (type) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-awaited-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'source.ts'), ` +export class PaneManager { split() { return 'pane'; } } +export async function make(): Promise<${type}> { return ${type === 'string' ? "'a,b'" : 'new PaneManager()'}; } +export async function snapshot() { + const listed = await make(); + return listed.split(','); +} +export function actual() { const pane = new PaneManager(); return pane.split(); } +`); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const callees = (name: string) => { + const node = cg!.getNodesByName(name).find(n => n.kind === 'function')!; + return cg!.getCallees(node.id).filter(x => x.edge.kind === 'calls').map(x => x.node.name); + }; + expect(callees('snapshot')).toContain('make'); + if (type === 'string') expect(callees('snapshot')).not.toContain('split'); + else expect(callees('snapshot')).toContain('split'); + expect(callees('actual')).toContain('split'); + } finally { cg?.close(); fs.rmSync(root, {recursive: true, force: true}); } + }); +}); diff --git a/__tests__/bare-call-no-method.test.ts b/__tests__/bare-call-no-method.test.ts index ba5e1d3f8..029a98697 100644 --- a/__tests__/bare-call-no-method.test.ts +++ b/__tests__/bare-call-no-method.test.ts @@ -130,6 +130,34 @@ describe('a receiver-less JS/TS call never binds to a method (#1714)', () => { expect(names).toContain('test'); }); + it('a const that picks a same-named store action out of a hook is a re-binding, not a local definition', async () => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1714-')); + fs.writeFileSync(path.join(tempDir, 'package.json'), '{"name":"t","dependencies":{"zustand":"^4"}}\n'); + fs.writeFileSync( + path.join(tempDir, 'store.ts'), + "import { create } from 'zustand'\nexport const useStore = create((set) => ({\n setZipUri: (zipUri: string) => set({ zipUri }),\n reset: () => set({}),\n}))\n" + ); + fs.writeFileSync( + path.join(tempDir, 'screen.ts'), + [ + "import { useStore } from './store'", + 'export function onZipComplete(uri: string) {', + ' const setZipUri = useStore((s) => s.setZipUri)', + ' const { reset } = useStore.getState()', + ' setZipUri(uri)', + ' reset()', + '}', + '', + ].join('\n') + ); + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + const from = cg.getNodesByKind('function').find((n) => n.name === 'onZipComplete')!; + const targets = cg.getOutgoingEdges(from.id).filter((e) => e.kind === 'calls').map((e) => cg!.getNode(e.target)).map((n) => `${n?.filePath}:${n?.name}`); + expect(targets).toContain('store.ts:setZipUri'); + expect(targets).toContain('store.ts:reset'); + }); + it('keeps `other.serialize()` — a call through a receiver', async () => { const callees = await callsFromMethod( [ diff --git a/__tests__/callback-cross-family.test.ts b/__tests__/callback-cross-family.test.ts new file mode 100644 index 000000000..62b806ec7 --- /dev/null +++ b/__tests__/callback-cross-family.test.ts @@ -0,0 +1,35 @@ +import { it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +// Reduced from Excalidraw App.tsx -> element/src/Scene.ts: the field receiver +// crosses TSX/TS, then the observer edge must return to the registering class. +it.each(['tsx', 'jsx'])('resolves %s -> TS field calls before synthesizing the callback', async (ext) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-callback-family-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'scene.ts'), `export class Scene { + private callbacks = new Set(); + onUpdate(cb: Function) { this.callbacks.add(cb); } + triggerUpdate() { for (const callback of Array.from(this.callbacks)) { callback(); } } + }`); + fs.writeFileSync(path.join(root, 'a_decoy.ts'), 'export class Decoy { triggerRender() {} }'); + fs.writeFileSync(path.join(root, `app.${ext}`), `import { Scene } from './scene'; + export class App { + ${ext === 'tsx' ? 'public scene: Scene;' : 'scene = new Scene();'} + componentDidMount() { this.scene.onUpdate(this.triggerRender); } + triggerRender = () => {}; + }`); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const registrar = cg.getNodesByName('onUpdate')[0]; + expect(cg.getCallers(registrar.id).some(x => x.node.qualifiedName === 'App::componentDidMount')).toBe(true); + const dispatcher = cg.getNodesByName('triggerUpdate')[0]; + const callbacks = cg.getCallees(dispatcher.id).filter(x => x.edge.metadata?.synthesizedBy === 'callback'); + expect(callbacks.map(x => x.node.qualifiedName)).toEqual(['App::triggerRender']); + } finally { + cg?.close(); fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/__tests__/callback-owner-regression.test.ts b/__tests__/callback-owner-regression.test.ts new file mode 100644 index 000000000..eb9f30854 --- /dev/null +++ b/__tests__/callback-owner-regression.test.ts @@ -0,0 +1,42 @@ +import { it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +it.each([[false, false, false], [true, false, false], [false, true, false], [false, false, true]])('callback belongs to registering class, inherited=%s bound=%s oneLine=%s', async (inherited, bound, oneLine) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-callback-owner-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'a_decoy.ts'), 'export class Decoy { triggerRender() {} }'); + fs.writeFileSync(path.join(root, 'store.ts'), `class OtherStore { + private handlers = new Set(); + subscribe(cb: Function) { this.handlers.add(cb); } + emit() { this.handlers.forEach(h => h()); } + } + export class Store { + private handlers = new Set(); + subscribe(cb: Function) { this.handlers.add(cb); } + emit() { this.handlers.forEach(h => h()); } + }`); + fs.writeFileSync(path.join(root, 'base.ts'), 'export class Base { triggerRender() {} }'); + fs.writeFileSync(path.join(root, 'real.ts'), `import { Store } from './store'; + import { Base } from './base'; + class SameFileDecoy { triggerRender() {} } + export class Real ${inherited ? 'extends Base' : ''} { + store = new Store(); + init() { this.store.subscribe(this.triggerRender${bound ? ".bind(this)" : ""}); } + ${inherited ? '' : 'triggerRender() {}'} + }`); + if (oneLine) { + const file = path.join(root, 'real.ts'); + fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace(/\n/g, ' ')); + } + cg = CodeGraph.initSync(root); await cg.indexAll(); + const emit = cg.getNodesByName('emit').find(n => n.qualifiedName === 'Store::emit')!; + const other = cg.getNodesByName('emit').find(n => n.qualifiedName === 'OtherStore::emit')!; + expect(cg.getCallees(other.id).filter(x => x.edge.metadata?.synthesizedBy === 'callback')).toEqual([]); + const targets = cg.getCallees(emit.id).filter(x => x.edge.metadata?.synthesizedBy === 'callback').map(x => x.node.qualifiedName); + expect(targets).toEqual([inherited ? 'Base::triggerRender' : 'Real::triggerRender']); + } finally { cg?.close(); fs.rmSync(root, {recursive:true,force:true}); } +}); diff --git a/__tests__/cli-sync-stale-index.test.ts b/__tests__/cli-sync-stale-index.test.ts new file mode 100644 index 000000000..6d86c0622 --- /dev/null +++ b/__tests__/cli-sync-stale-index.test.ts @@ -0,0 +1,102 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph, FileLock } from '../src'; +import { EXTRACTION_VERSION } from '../src/extraction/extraction-version'; + +const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); +const { DatabaseSync } = require('node:sqlite'); +let root: string; + +function run(...args: string[]) { + return spawnSync(process.execPath, [BIN, ...args], { + cwd: root, + encoding: 'utf8', + timeout: 30_000, + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' }, + }); +} + +function setStamp(stamp: number | null) { + const db = new DatabaseSync(path.join(root, '.codegraph', 'codegraph.db')); + try { + if (stamp === null) { + db.prepare('DELETE FROM project_metadata WHERE key = ?').run('indexed_with_extraction_version'); + } else { + db.prepare('UPDATE project_metadata SET value = ? WHERE key = ?') + .run(String(stamp), 'indexed_with_extraction_version'); + } + } finally { + db.close(); + } +} + +beforeEach(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-sync-stale-')); + fs.writeFileSync(path.join(root, 'original.ts'), 'export function original() { return 1; }\n'); + const cg = CodeGraph.initSync(root); + try { await cg.indexAll(); } finally { cg.close(); } +}); + +afterEach(() => fs.rmSync(root, { recursive: true, force: true })); + +describe('explicit CLI sync with stale extraction', () => { + for (const stamp of [EXTRACTION_VERSION - 1, null]) { + for (const quiet of [false, true]) { + it(`rejects stamp ${stamp} before syncing, quiet=${quiet}`, () => { + setStamp(stamp); + const args = quiet ? ['sync', '--quiet'] : ['sync']; + for (const changed of [false, true]) { + if (changed) fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + const result = run(...args); + expect(result.status).toBe(1); + const output = result.stdout + result.stderr; + expect(output).toContain('Run "codegraph index"'); + if (quiet) { + expect(result.stdout).toBe(''); + expect(result.stderr.trim().split('\n')).toHaveLength(1); + } + expect(output).not.toContain('Already up to date'); + const cg = CodeGraph.openSync(root); + try { + expect(cg.isIndexStale()).toBe(true); + expect(cg.getStats().fileCount).toBe(1); + } finally { cg.close(); } + } + }); + } + } + + it.each([false, true])('still syncs a current index, quiet=%s', (quiet) => { + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 2;\n'); + expect(run('sync', ...(quiet ? ['--quiet'] : [])).status).toBe(0); + const cg = CodeGraph.openSync(root); + try { + expect(cg.isIndexStale()).toBe(false); + expect(cg.getStats().fileCount).toBe(2); + } finally { cg.close(); } + }); + + it('accepts sync after the prescribed full rebuild', () => { + setStamp(EXTRACTION_VERSION - 1); + expect(run('sync').status).toBe(1); + expect(run('index').status).toBe(0); + expect(run('sync').status).toBe(0); + }); +}); + + +it.each([false, true])('reports lock contention instead of clean success, quiet=%s', quiet => { + fs.writeFileSync(path.join(root, 'added.ts'), 'export const added = 1;'); + const lock = new FileLock(path.join(root, '.codegraph', 'codegraph.lock')); + lock.acquire(); + try { + const result = run('sync', ...(quiet ? ['--quiet'] : [])); + expect(result.status).toBe(1); + expect(result.stdout + result.stderr).toMatch(/busy|lock/i); + expect(result.stdout + result.stderr).not.toContain('Already up to date'); + if (quiet) { expect(result.stdout).toBe(''); expect(result.stderr.trim().split('\n')).toHaveLength(1); } + } finally { lock.release(); } +}); diff --git a/__tests__/cpp-macro-visibility.test.ts b/__tests__/cpp-macro-visibility.test.ts new file mode 100644 index 000000000..77a395f7a --- /dev/null +++ b/__tests__/cpp-macro-visibility.test.ts @@ -0,0 +1,46 @@ +import { it, expect } from 'vitest'; +import { isVisibleCppMacro } from '../src/resolution/cpp-macro-visibility'; +import type { ResolutionContext } from '../src/resolution/types'; +function context(files: Record): ResolutionContext { + return { + getNodesByName: () => [{ kind: 'constant', signature: '#define TRACE(x) (x)' }], + readFile: (file: string) => files[file], fileExists: (file: string) => file in files, + getAllFiles: () => Object.keys(files), + } as unknown as ResolutionContext; +} +function visible(ctx: ResolutionContext, filePath: string, line: number): boolean { + return isVisibleCppMacro({fromNodeId:'test',referenceName:'TRACE',referenceKind:'calls',language:'c',filePath,line,column:0},ctx); +} +it('keeps a possible macro when an unknown conditional include undefines it', () => { + const ctx=context({'main.c':'#define TRACE(x) (x)\n#if UNKNOWN_FLAG\n#include "undef.h"\n#endif\nvoid run(){TRACE(1);}', 'undef.h':'#undef TRACE'}); + expect(visible(ctx,'main.c',5)).toBe(true); +}); +it.each([['a.c','b.c'],['b.c','a.c']])('include cycle does not depend on query order: %s first', (first,second) => { + const ctx=context({'a.h':'#ifndef A_H\n#define A_H\n#include "b.h"\n#define TRACE(x) (x)\n#endif','b.h':'#ifndef B_H\n#define B_H\n#include "a.h"\n#endif','a.c':'#include "a.h"\nvoid run(){TRACE(1);}','b.c':'#include "b.h"\nvoid run(){TRACE(1);}'}); + expect(visible(ctx,first,2)).toBe(true); + expect(visible(ctx,second,2)).toBe(true); +}); +it('passes a known parent flag into the included header', () => { + const ctx=context({'main.c':'#define ENABLE_TRACE 0\n#include "trace.h"\nvoid run(){TRACE(1);}','trace.h':'#if ENABLE_TRACE\n#define TRACE(x) (x)\n#endif'}); + expect(visible(ctx,'main.c',3)).toBe(false); +}); +it('does not let a later definition affect a preceding call', () => { + const ctx=context({'main.c':'void first(){TRACE(1);}\n#define TRACE(x) (x)\nvoid last(){TRACE(1);}'}); + expect(visible(ctx,'main.c',1)).toBe(false); + expect(visible(ctx,'main.c',3)).toBe(true); +}); + +it('keeps local header facts within a hypothetical include path', () => { + const ctx=context({'main.c':'#ifdef OPTIONAL\n#include "maths.h"\n#endif\n#include "maths.h"\nvoid run(){TRACE(1);}', + 'maths.h':'#pragma once\n#define FAST_MATH\n#if defined(FAST_MATH)\nvoid TRACE(int);\n#else\n#define TRACE(x) (x)\n#endif'}); + expect(visible(ctx,'main.c',5)).toBe(false); +}); + +it('does not turn an unknown definition into absence after an unknown undef', () => { + const ctx=context({'main.c':'#if ENABLE_TRACE\n#define TRACE(x) ((void)(x))\n#endif\n#if CLEAR_TRACE\n#undef TRACE\n#endif\nvoid run(){TRACE(1);}'}); + expect(visible(ctx,'main.c',7)).toBe(true); +}); +it('a definite undef still removes a possible definition', () => { + const ctx=context({'main.c':'#if ENABLE_TRACE\n#define TRACE(x) ((void)(x))\n#endif\n#undef TRACE\nvoid run(){TRACE(1);}'}); + expect(visible(ctx,'main.c',5)).toBe(false); +}); diff --git a/__tests__/cpp-remaining-regressions.test.ts b/__tests__/cpp-remaining-regressions.test.ts new file mode 100644 index 000000000..0b0b48566 --- /dev/null +++ b/__tests__/cpp-remaining-regressions.test.ts @@ -0,0 +1,104 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { CodeGraph } from '../src'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; + +const roots: string[] = []; +beforeAll(async () => { await initGrammars(); await loadGrammarsForLanguages(['c', 'cpp']); }); +afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); + +describe('remaining C/C++ regressions', () => { + it.each(['c', 'cpp'])('keeps function-like macros as non-callable constants without indexing numeric defines (%s)', (language) => { + const result = extractFromSource(`marker.${language}`, '#define TRACE_POINT(value) ((void)(value))\n#define VERSION 1\n'); + expect(result.nodes.filter(n => n.kind === 'constant').map(n => [n.name, n.signature])).toEqual([ + ['TRACE_POINT', '#define TRACE_POINT(value) ((void)(value))'], + ]); + }); + it.each(['c', 'cpp'])('recovers a single-argument declared function macro (%s)', (language) => { + const result = extractFromSource(`case.${language}`, '#define NATIVE_FN(name) int name(void)\nNATIVE_FN(get_version) { return 1; }\nint use_it(void) { return get_version(); }\n'); + expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name)).toEqual(['get_version', 'use_it']); + expect(result.nodes.find(n => n.name === 'get_version')?.signature).toBeUndefined(); + }); + it.each(['c', 'cpp'])('resolves a real call to the macro-declared function (%s)', async (language) => { + const root = mkdtempSync(join(tmpdir(), 'codegraph-cpp-macro-name-')); roots.push(root); + writeFileSync(join(root, `case.${language}`), '#define NATIVE_FN(name) int name(void)\nNATIVE_FN(get_version) { return 1; }\nint use_it(void) { return get_version(); }\n'); + const cg = CodeGraph.initSync(root); + try { + await cg.indexAll(); + const caller = cg.searchNodes('use_it').map(r => r.node).find(n => n.name === 'use_it')!; + expect(cg.getCallees(caller.id).some(r => r.node.name === 'get_version' && r.edge.kind === 'calls')).toBe(true); + } finally { cg.close(); } + }); + it('does not guess a one-argument macro name without a matching definition', () => { + const result = extractFromSource('case.cpp', '#define NATIVE_FN(name) int generated(void)\nNATIVE_FN(get_version) { return 1; }'); + expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name)).not.toContain('get_version'); + }); + it.each(['c', 'cpp'])('suppresses only macros visible through nested includes and honors undef (%s)', async (language) => { + const root = mkdtempSync(join(tmpdir(), 'codegraph-cpp-macros-')); roots.push(root); + mkdirSync(join(root, 'src/main/common'), { recursive: true }); + mkdirSync(join(root, 'src/main/pg'), { recursive: true }); + mkdirSync(join(root, 'src/main/drivers'), { recursive: true }); + writeFileSync(join(root, 'src/main/pg/pinio.h'), '#define HEADER_TRACE(v) ((void)(v))\n'); + writeFileSync(join(root, 'src/main/drivers/pinio.h'), '// unrelated header with the same basename\n'); + writeFileSync(join(root, `src/main/pg/pinio.${language}`), '#include "pinio.h"\nvoid sibling_header_use() { HEADER_TRACE(1); }\n'); + writeFileSync(join(root, 'src/main/common/marker.h'), '#define ROOT_TRACE(v) ((void)(v))\n'); + writeFileSync(join(root, 'inner.h'), '#define TRACE_POINT(value) ((void)(value))\n'); + writeFileSync(join(root, 'outer.h'), '#include "inner.h"\n'); + writeFileSync(join(root, `exercise.${language}`), '#include "outer.h"\n#include "common/marker.h"\nvoid root_macro_use() { ROOT_TRACE(1); }\nvoid macro_use() { TRACE_POINT(1); }\nvoid local_macro() {\n#define INNER_TRACE(v) ((void)(v))\nINNER_TRACE(1);\n}\n#undef TRACE_POINT\nvoid real_use() { TRACE_POINT(1); }\n'); + writeFileSync(join(root, `decoy.${language}`), 'void HEADER_TRACE(int value) {}\nvoid ROOT_TRACE(int value) {}\nvoid INNER_TRACE(int value) {}\nvoid TRACE_POINT(int value) {}\nvoid unrelated_use() { TRACE_POINT(1); }'); + const cg = CodeGraph.initSync(root); + try { + await cg.indexAll(); + const callees = (name: string) => { + const node = cg.searchNodes(name).map(r => r.node).find(n => n.name === name)!; + return cg.getCallees(node.id).map(r => r.node.name); + }; + expect(callees('macro_use')).not.toContain('TRACE_POINT'); + expect(callees('root_macro_use')).not.toContain('ROOT_TRACE'); + expect(callees('sibling_header_use')).not.toContain('HEADER_TRACE'); + expect(callees('local_macro')).not.toContain('INNER_TRACE'); + expect(callees('real_use')).toContain('TRACE_POINT'); + expect(callees('unrelated_use')).toContain('TRACE_POINT'); + } finally { cg.close(); } + }); + it.each(['c', 'cpp'])('preserves real functions when an inactive fallback branch defines a macro (%s)', async (language) => { + const root = mkdtempSync(join(tmpdir(), 'codegraph-cpp-conditional-')); roots.push(root); + writeFileSync(join(root, 'maths.h'), '#define FAST_MATH\n#if defined(FAST_MATH)\nfloat sin_approx(float x);\n#else\n#define sin_approx(x) external_sin(x)\n#endif\n#if 0\n#define real_fn(x) ((void)(x))\n#endif\n'); + writeFileSync(join(root, `maths.${language}`), '#include "maths.h"\nfloat sin_approx(float x) { return x; }\nvoid real_fn(int value) {}\nfloat invoke_real(float value) { real_fn(1); return sin_approx(value); }\n'); + const cg = CodeGraph.initSync(root); + try { + await cg.indexAll(); + const caller = cg.searchNodes('invoke_real').map(r => r.node).find(n => n.name === 'invoke_real')!; + expect(cg.getCallees(caller.id).map(r => r.node.name)).toEqual(expect.arrayContaining(['sin_approx', 'real_fn'])); + } finally { cg.close(); } + }); + it('records local constructor methods and keeps aggregate/pointer/prototype controls non-callable', async () => { + const root = mkdtempSync(join(tmpdir(), 'codegraph-cpp-remaining-')); roots.push(root); + writeFileSync(join(root, 'case.cpp'), ` +struct Aggregate { int value; }; +namespace unrelated { void Aggregate() {} } +class WithConstructor { public: WithConstructor(); explicit WithConstructor(int value); }; +WithConstructor::WithConstructor() {} +WithConstructor::WithConstructor(int value) {} +void default_ctor() { WithConstructor item; } +void braced_ctor() { WithConstructor item{}; } +void value_ctor() { WithConstructor item(1); } +void temporary_ctor() { WithConstructor(); } +void controls() { Aggregate item{}; WithConstructor *pointer; WithConstructor &reference = *pointer; WithConstructor prototype(); extern WithConstructor external; } +`); + const cg = CodeGraph.initSync(root); + try { + await cg.indexAll(); + for (const caller of ['default_ctor', 'braced_ctor', 'value_ctor', 'temporary_ctor']) { + const node = cg.searchNodes(caller, { limit: 10 }).map(r => r.node).find(n => n.name === caller)!; + const callees = cg.getCallees(node.id).map(r => r.node); + expect(callees.some(n => n.kind === 'method' && n.qualifiedName === 'WithConstructor::WithConstructor'), caller).toBe(true); + } + const control = cg.searchNodes('controls', { limit: 10 }).map(r => r.node).find(n => n.name === 'controls')!; + expect(cg.getCallees(control.id).filter(r => r.edge.kind === 'calls')).toEqual([]); + } finally { cg.close(); } + }); +}); diff --git a/__tests__/cpp-review-regressions.test.ts b/__tests__/cpp-review-regressions.test.ts new file mode 100644 index 000000000..82f214f73 --- /dev/null +++ b/__tests__/cpp-review-regressions.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeAll, describe, expect, it } from 'vitest'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { CodeGraph } from '../src'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; + +const roots: string[] = []; +beforeAll(async () => { await initGrammars(); await loadGrammarsForLanguages(['cpp', 'c']); }); +afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); +async function indexed(source: string, check: (cg: CodeGraph) => void): Promise { + const root = mkdtempSync(join(tmpdir(), 'codegraph-cpp-review-')); roots.push(root); + writeFileSync(join(root, 'case.cpp'), source); + const cg = CodeGraph.initSync(root); + try { await cg.indexAll(); check(cg); } finally { cg.close(); } +} +function calls(cg: CodeGraph, caller: string) { + const node = cg.getNodesByKind('function').find(n => n.name === caller)!; + return cg.getOutgoingEdges(node.id).filter(e => e.kind === 'calls').map(e => cg.getNode(e.target)!); +} + +describe('C++ constructor and macro recovery review regressions', () => { + it('does not construct objects for zero-initialized pointers, references or function pointers', async () => { + await indexed(`struct Widget { Widget() {} }; +void pointers() { Widget *p{}; Widget *arr[2]{}; Widget (*fn)(){}; } +void reference_bind(Widget &other) { Widget &r{other}; } +void actual() { Widget object{}; } +`, cg => { + expect(calls(cg, 'pointers')).toEqual([]); + expect(calls(cg, 'reference_bind')).toEqual([]); + expect(calls(cg, 'actual').map(n => n.qualifiedName)).toEqual(['Widget::Widget']); + }); + }); + it('keeps per-declarator arity and records constructor parameter signatures', () => { + const result = extractFromSource('case.cpp', `struct Widget { Widget() {} Widget(int value) {} }; +void run() { Widget a, b(1), c{}; }`); + expect(result.nodes.filter(n => n.kind === 'method').map(n => n.signature)).toEqual(['()', '(int value)']); + expect(result.unresolvedReferences.filter(r => r.referenceKind === 'calls').map(r => r.referenceName)) + .toEqual(['Widget::Widget/0', 'Widget::Widget/1', 'Widget::Widget/0']); + }); + it('selects the appropriate zero/one-argument constructor without guessing equal-arity types', async () => { + await indexed(`struct Widget { Widget(); Widget(int); }; +Widget::Widget() {} +Widget::Widget(int value) {} +struct Ambiguous { Ambiguous(int) {} Ambiguous(double) {} }; +void default_use() { Widget w; } +void int_use() { Widget w(1); } +void ambiguous_use(int value) { Ambiguous w(value); } +`, cg => { + expect(calls(cg, 'default_use').map(n => n.signature)).toEqual(['()']); + expect(calls(cg, 'int_use').map(n => n.signature)).toEqual(['(int value)']); + expect(calls(cg, 'ambiguous_use')).toEqual([]); + }); + }); + it('resolves lexical namespace constructors and falls back to an enclosing/global type', async () => { + await indexed(`struct Global { Global() {} }; +namespace first { struct Widget { Widget() {} }; } +namespace second { struct Widget { Widget() {} }; +void local_use() { Widget w; } +void global_use() { Global g; } +} +void explicit_use() { first::Widget w; } +`, cg => { + expect(calls(cg, 'local_use').map(n => n.qualifiedName)).toEqual(['second::Widget::Widget']); + expect(calls(cg, 'global_use').map(n => n.qualifiedName)).toEqual(['Global::Global']); + expect(calls(cg, 'explicit_use').map(n => n.qualifiedName)).toEqual(['first::Widget::Widget']); + }); + }); + it('preserves an ordinary constructor after an earlier function macro was undefined', () => { + const result = extractFromSource('case.cpp', `#define NATIVE_FN(name) int name(void) +#undef NATIVE_FN +using value_t = int; +struct NATIVE_FN { NATIVE_FN(value_t) {} };`); + expect(result.nodes.filter(n => n.kind === 'method').map(n => n.name)).toEqual(['NATIVE_FN']); + }); + it('does not treat a documented define in a comment as a live name-generating macro', () => { + const result = extractFromSource('case.cpp', `/* +#define NATIVE_FN(name) int name(void) +*/ +using value_t = int; +int NATIVE_FN(value_t) { return 1; }`); + expect(result.nodes.filter(n => n.kind === 'function').map(n => n.name)).toEqual(['NATIVE_FN']); + }); + it('does not mistake a parameter use inside noexcept for the generated function name', () => { + const result = extractFromSource('case.cpp', `#define NATIVE_FN(name) int generated(void) noexcept(noexcept(name())) +int callback() { return 1; } +NATIVE_FN(callback) { return 1; }`); + expect(result.nodes.filter(n => n.kind === 'function' && n.name === 'callback')).toHaveLength(1); + }); +}); diff --git a/__tests__/cpp-same-line-ids.test.ts b/__tests__/cpp-same-line-ids.test.ts new file mode 100644 index 000000000..3c768bee0 --- /dev/null +++ b/__tests__/cpp-same-line-ids.test.ts @@ -0,0 +1,30 @@ +import { it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +it('persists same-line C++ constructors with separate body calls and stable reindex IDs', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-constructor-ids-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'widget.cpp'), `void zero() {} void one() {}\n/* 😀 */ struct Widget { Widget() { zero(); } Widget(int) { one(); } };\nvoid useDefault() { Widget w; }\nvoid useInt() { Widget w(1); }\n`); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const constructors = cg.getNodesByName('Widget').filter(n => n.kind === 'method').sort((a, b) => a.startColumn - b.startColumn); + expect(constructors).toHaveLength(2); + expect(new Set(constructors.map(n => n.id)).size).toBe(2); + const callees = (id: string) => cg!.getCallees(id).filter(x => x.edge.kind === 'calls').map(x => x.node.name); + expect(callees(constructors[0].id)).toEqual(['zero']); + expect(callees(constructors[1].id)).toEqual(['one']); + for (const [name, constructor] of [['useDefault', constructors[0]], ['useInt', constructors[1]]] as const) { + const caller = cg.getNodesByName(name)[0]; + expect(cg.getCallees(caller.id).filter(x => x.edge.kind === 'calls').map(x => x.node.id)).toEqual([constructor.id]); + } + await cg.indexAll(); + expect(cg.getNodesByName('Widget').filter(n => n.kind === 'method').map(n => n.id).sort()).toEqual(constructors.map(n => n.id).sort()); + } finally { + cg?.close(); + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/__tests__/daemon-client-liveness.test.ts b/__tests__/daemon-client-liveness.test.ts index 63e8c2cc9..7406ad5da 100644 --- a/__tests__/daemon-client-liveness.test.ts +++ b/__tests__/daemon-client-liveness.test.ts @@ -179,3 +179,26 @@ describe('Daemon.backstopShouldExit', () => { expect(d.backstopShouldExit(() => false)).toBe(false); }); }); + +it('does not attach a phantom client when the socket closes during hello', async () => { + const net = await import('node:net'); + const d = new Daemon('/tmp/codegraph-hello-close-test', { idleTimeoutMs: 0 }) as any; + const server = net.createServer(socket => d.handleConnection(socket)); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + const address = server.address() as import('node:net').AddressInfo; + const client = net.createConnection({host: '127.0.0.1', port: address.port}); + try { + await new Promise((resolve, reject) => { + client.once('error', reject); + client.once('data', () => { client.destroy(); resolve(); }); + }); + await new Promise(resolve => setTimeout(resolve, 50)); + expect(d.clients.size).toBe(0); + expect(d.clientPeers.size).toBe(0); + } finally { + client.destroy(); + for (const session of [...d.clients]) d.dropClient(session); + d.disarmIdleTimer(); + await new Promise(resolve => server.close(() => resolve())); + } +}); diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 16e0fc244..5f5c542a8 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -4856,6 +4856,7 @@ CPPType* ApiHelper { expect(instNames('Calculator calc(0);')).toEqual(['Calculator']); expect(instNames('Widget w{1, 2};')).toEqual(['Widget']); + expect(instNames('Calculator deferred;')).toEqual(['Calculator']); // default construction }); it('strips template args and namespace to the bare class name', () => { @@ -4864,11 +4865,11 @@ CPPType* ApiHelper { + it('does not emit for primitives, extern declarations, or the most-vexing parse', () => { expect(instNames('int x(5);')).toEqual([]); // primitive direct-init expect(instNames('int y{6};')).toEqual([]); // primitive brace-init expect(instNames('auto z = make();')).toEqual([]); // auto + call (handled elsewhere) - expect(instNames('Calculator deferred;')).toEqual([]); // default construction, no args + expect(instNames('extern Calculator deferred;')).toEqual([]); // declaration, no construction expect(instNames('Calculator calc();')).toEqual([]); // function declaration (most-vexing parse) }); @@ -12782,3 +12783,188 @@ describe('Unsupported-language projects report what they skipped (#1502)', () => expect(stats.unsupportedByExtension.size).toBe(0); }); }); + +describe('Verilog / SystemVerilog Extraction', () => { + it('should report Verilog as supported', () => { + expect(isLanguageSupported('verilog')).toBe(true); + expect(getSupportedLanguages()).toContain('verilog'); + }); + + it('should extract modules as containers with scoped functions and tasks', () => { + const code = ` +module top (input logic clk, output logic [7:0] z); + function automatic int square(int n); + return n * n; + endfunction + task automatic do_reset(); + z = '0; + endtask +endmodule +`; + const result = extractFromSource('top.sv', code); + + const mod = result.nodes.find((n) => n.kind === 'class' && n.name === 'top'); + expect(mod).toBeDefined(); + expect(mod?.language).toBe('verilog'); + + const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'square'); + expect(fn).toBeDefined(); + expect(fn?.qualifiedName).toBe('top::square'); // scoped under the module + + const task = result.nodes.find((n) => n.kind === 'function' && n.name === 'do_reset'); + expect(task).toBeDefined(); + }); + + it('should emit an instantiates reference from a module to its submodule type', () => { + const code = ` +module alu #(parameter int WIDTH = 8) (input logic [WIDTH-1:0] a, output logic [WIDTH-1:0] y); +endmodule + +module top (input logic [7:0] x, output logic [7:0] z); + alu #(.WIDTH(8)) u_alu (.a(x), .y(z)); +endmodule +`; + const result = extractFromSource('soc.sv', code); + + const top = result.nodes.find((n) => n.kind === 'class' && n.name === 'top'); + expect(top).toBeDefined(); + + const inst = result.unresolvedReferences.find( + (r) => r.referenceKind === 'instantiates' && r.referenceName === 'alu' + ); + expect(inst).toBeDefined(); + expect(inst?.fromNodeId).toBe(top?.id); // the parent module is the source + + // parameter is captured as a constant + const param = result.nodes.find((n) => n.kind === 'constant' && n.name === 'WIDTH'); + expect(param).toBeDefined(); + }); + + it('should extract package typedefs, functions, imports and call edges', () => { + const code = ` +package math_pkg; + typedef enum logic [1:0] { IDLE, RUN, DONE } state_t; + function automatic int add(int a, int b); + return a + b; + endfunction +endpackage + +module worker (input logic clk); + import math_pkg::*; + function automatic int caller(int y); + return add(y); + endfunction +endmodule +`; + const result = extractFromSource('pkg.sv', code); + + const pkg = result.nodes.find((n) => n.kind === 'class' && n.name === 'math_pkg'); + expect(pkg).toBeDefined(); + + const typedef = result.nodes.find((n) => n.kind === 'type_alias' && n.name === 'state_t'); + expect(typedef).toBeDefined(); + expect(typedef?.qualifiedName).toBe('math_pkg::state_t'); + + const importNode = result.nodes.find((n) => n.kind === 'import' && n.name === 'math_pkg'); + expect(importNode).toBeDefined(); + + const callRef = result.unresolvedReferences.find( + (r) => r.referenceKind === 'calls' && r.referenceName === 'add' + ); + expect(callRef).toBeDefined(); + }); + + it('should extract every package in a multi-package import statement', () => { + const code = ` +package a_pkg; + function automatic int fa(); return 1; endfunction +endpackage + +package b_pkg; + function automatic int fb(); return 2; endfunction +endpackage + +module worker (input logic clk); + import a_pkg::*, b_pkg::*; + function automatic int caller(); + return fa() + fb(); + endfunction +endmodule +`; + const result = extractFromSource('multi.sv', code); + + const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(imports).toContain('a_pkg'); + expect(imports).toContain('b_pkg'); // the second import must not be dropped + + // call edges still resolve when multiple imports share one statement + const calls = result.unresolvedReferences + .filter((r) => r.referenceKind === 'calls') + .map((r) => r.referenceName); + expect(calls).toContain('fa'); + expect(calls).toContain('fb'); + }); + + it('should capture function calls inside instantiation port/param expressions', () => { + const code = ` +module sub #(parameter int W = 8) (input logic [W-1:0] a); +endmodule + +module top (input logic [7:0] x); + function automatic int dbl(int v); return v * 2; endfunction + function automatic int wid(); return 8; endfunction + sub #(.W(wid())) u_sub (.a(dbl(x))); +endmodule +`; + const result = extractFromSource('inst_calls.sv', code); + + const calls = result.unresolvedReferences + .filter((r) => r.referenceKind === 'calls') + .map((r) => r.referenceName); + expect(calls).toContain('dbl'); // call inside a port connection + expect(calls).toContain('wid'); // call inside a parameter override + + // the instantiation edge itself is still emitted + const inst = result.unresolvedReferences.find( + (r) => r.referenceKind === 'instantiates' && r.referenceName === 'sub' + ); + expect(inst).toBeDefined(); + }); + + it('should emit an import for each `include directive, named by the included path', () => { + const code = ` +\`include "defs.vh" +\`include "axi/typedef.svh" +\`include +module top (input logic clk); +endmodule +`; + const result = extractFromSource('top.sv', code); + + const includes = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(includes).toEqual(['defs.vh', 'axi/typedef.svh', 'uvm_macros.svh']); + + // the path is what the import ref carries, so the file-path matcher can + // land it on the header by suffix + const refs = result.unresolvedReferences + .filter((r) => r.referenceKind === 'imports') + .map((r) => r.referenceName); + expect(refs).toEqual(['defs.vh', 'axi/typedef.svh', 'uvm_macros.svh']); + }); + + it('should extract `define macros and top-level parameters from a header', () => { + const code = ` +\`ifndef DEFS_VH +\`define DEFS_VH +\`define DATA_W 16 +\`define ADDR(x) (x + 1) +parameter CLK_HZ = 27_000_000; +localparam BAUD = 115200; +\`endif +`; + const result = extractFromSource('defs.vh', code); + + const constants = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name); + expect(constants).toEqual(['DEFS_VH', 'DATA_W', 'ADDR', 'CLK_HZ', 'BAUD']); + }); +}); diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts index 95cd83a0b..50085e762 100644 --- a/__tests__/frontload-hook.test.ts +++ b/__tests__/frontload-hook.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { planFrontload, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; +import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; // Make the built-in exports configurable so HOME can point at a real temp // fixture without changing the process environment or the user's home files. @@ -376,3 +376,15 @@ describe('prompt-hook injection cap (#1694)', () => { expect(out.length).toBeLessThan(CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT); }); }); + + +describe('system task notifications (#1832)', () => { + it('skips the complete system envelope', () => { + expect(isTaskNotification(' trace AuthService login flow\n')).toBe(true); + }); + it('does not suppress a user question discussing the marker', () => { + expect(isTaskNotification('Why does trace trigger the hook?')).toBe(false); + expect(isTaskNotification('trace Explain this.')).toBe(false); + expect(isTaskNotification('trace AuthService login')).toBe(false); + }); +}); diff --git a/__tests__/hdl-access-query.test.ts b/__tests__/hdl-access-query.test.ts new file mode 100644 index 000000000..f2ffb0644 --- /dev/null +++ b/__tests__/hdl-access-query.test.ts @@ -0,0 +1,130 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; +import { ToolHandler } from '../src/mcp/tools'; +import { formatHdlAccess } from '../src/mcp/hdl-access'; + +let root: string, cg: CodeGraph, handler: ToolHandler; +const rtl = `module top(input clk, input enable, input d, output logic q); + always @(posedge clk) begin + if (enable) q <= q + d; + end + leaf u(.d(d)); +endmodule +module leaf(input d); endmodule +module other(input d, output q); assign q=d; endmodule +module updates(output int q); initial q += 1; endmodule +`; +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-access-query-')); + fs.writeFileSync(path.join(root, 'top.sv'), rtl); + // Integral seconds survive Date/fs.utimes round trips on all supported filesystems. + const fixtureTime = new Date('2024-01-01T00:00:00.000Z'); + fs.utimesSync(path.join(root, 'top.sv'), fixtureTime, fixtureTime); + fs.writeFileSync(path.join(root, 'lib.sv'), 'package lib; task automatic fill(output int y); y=1; endtask endpackage'); + fs.writeFileSync(path.join(root, 'call.sv'), 'module call_top; int y; initial lib::fill(y); endmodule'); + fs.writeFileSync(path.join(root, 'plain.ts'), 'export function hello() { return 42; }\n'); + cg = CodeGraph.initSync(root); + expect((await cg.indexAll()).success).toBe(true); + handler = new ToolHandler(cg); +}); +afterAll(() => { cg?.close(); if (root) fs.rmSync(root, { recursive: true, force: true }); }); +async function query(signal: string, hdlAccess?: string) { + const result = await handler.execute('codegraph_explore', { query: signal, ...(hdlAccess ? { hdlAccess } : {}) }); + expect(result.isError).not.toBe(true); + return result.content.map(c => c.text).join('\n'); +} + +describe('HDL access MCP query', () => { + it('returns actual source sites and retains reads and writes on the same signal and line', async () => { + const readers = await query('top::q', 'read'); + const writers = await query('top::q', 'write'); + for (const result of [readers, writers]) { + expect(result).toContain('1 matching access site(s).'); + expect(result).toContain('3\t if (enable) q <= q + d;'); + expect(result).toContain('top.sv:3'); + } + expect(readers).toContain('[read]'); + expect(writers).toContain('[write]'); + expect(await query('top.q', 'write')).toContain('**top::q**'); + expect(await query('top::q', 'all')).toContain('2 matching access site(s).'); + expect(await query('updates::q', 'readwrite')).toContain('[readwrite]'); + }); + it('explains writes proven by a known task output formal', async () => { + const result = await query('call_top::y', 'write'); + expect(result).toContain('1 matching access site(s).'); + expect(result).toContain('[write]'); + expect(result).toContain('Formal direction: lib::fill(output y).'); + expect(result).toContain('initial lib::fill(y);'); + expect(result).toContain("known callable formal's argument direction"); + }); + it('includes control and event uses as readers and permits exact category filters', async () => { + expect(await query('top::clk', 'read')).toContain('[event]'); + expect(await query('top::clk', 'event')).toContain('[event]'); + expect(await query('top::enable', 'control')).toContain('[control]'); + expect(await query('top::enable', 'read')).toContain('[control]'); + expect(await query('top::d', 'write')).toContain('0 matching access site(s).'); + }); + it('keeps unclassified connections separate from access evidence and disambiguates exact targets', async () => { + expect(await query('top::d', 'all')).toContain('unclassified reference(s) excluded'); + const all = await query('q', 'write'); + expect(all).toContain('**top::q**'); + expect(all).toContain('**other::q**'); + expect(await query('top::q', 'write')).not.toContain('**other::q**'); + expect(await query('where is q written', 'write')).toContain('No exact HDL signal matches'); + }); + it('does not change normal non-HDL exploration and validates the filter', async () => { + const normal = await query('hello'); + expect(normal).toContain('hello'); + expect(normal).toContain('return 42'); + expect(normal).not.toContain('HDL access:'); + const bad = await handler.execute('codegraph_explore', { query: 'q', hdlAccess: 'driver' }); + expect(bad.isError).toBe(true); + expect(bad.content[0]!.text).toContain('hdlAccess must be one of'); + }); + it('rejects same-size edited source with restored mtime and labels the indexed definition', () => { + const file = path.join(root, 'top.sv'); + const stat = fs.statSync(file); + try { + fs.writeFileSync(file, rtl.replace('q <= q + d', 'q <= q - d')); + fs.utimesSync(file, stat.atime, stat.mtime); + const changed = fs.statSync(file); + expect(changed.size).toBe(stat.size); + expect(Math.floor(changed.mtimeMs)).toBe(Math.floor(stat.mtimeMs)); + const result = formatHdlAccess(cg, 'top::q', 'write'); + expect(result).toContain('1 matching access site(s).'); + expect(result).toContain('Definition location is from the index; current source is unverified'); + expect(result).toContain('Source omitted: changed/unavailable since indexing'); + expect(result).not.toContain('q <= q - d'); + expect(result).not.toContain('q <= q + d'); + } finally { fs.writeFileSync(file, rtl); fs.utimesSync(file, stat.atime, stat.mtime); } + }); + it('does not attach stale indexed access positions to modified source', () => { + const file = path.join(root, 'top.sv'); + const stat = fs.statSync(file); + try { + fs.writeFileSync(file, `// changed source\n${rtl}`); + const result = formatHdlAccess(cg, 'top::q', 'write'); + expect(result).toContain('Source omitted: changed/unavailable since indexing'); + expect(result).not.toContain('3\t if (enable)'); + } finally { fs.writeFileSync(file, rtl); fs.utimesSync(file, stat.atime, stat.mtime); } + }); +}); + +it('HDL access CLI forwards the existing explore filter and rejects invalid values', () => { + const bin = path.resolve(__dirname, '../dist/bin/codegraph.js'); + const run = (filter: string) => spawnSync(process.execPath, [bin, 'explore', '--path', root, '--hdl-access', filter, 'top::q'], { + encoding: 'utf8', timeout: 30000, + env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', CODEGRAPH_WASM_RELAUNCHED: '1', NO_COLOR: '1' }, + }); + const result = run('write'); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain('HDL access: top::q — write'); + expect(result.stdout).toContain('q <= q + d;'); + const invalid = run('driver'); + expect(invalid.status).toBe(1); + expect(invalid.stdout + invalid.stderr).toContain('hdlAccess must be one of'); +}); diff --git a/__tests__/hdl-expression-origin.test.ts b/__tests__/hdl-expression-origin.test.ts new file mode 100644 index 000000000..7709f9382 --- /dev/null +++ b/__tests__/hdl-expression-origin.test.ts @@ -0,0 +1,74 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { createHash } from 'node:crypto'; +import { importMappedSemantics } from '../src/hdl/source-map-import'; +import { analyzeHdlSemantics, type HdlSemanticSourceGraph } from '../src/hdl/semantics'; +import type { HdlProfileStatus } from '../src/hdl/status'; +import type { FileRecord, Node } from '../src/types'; +const {run}=vi.hoisted(()=>({run:vi.fn()})); +vi.mock('../src/hdl/semantic-runner',()=>({runSlangSemantics:run})); +afterEach(()=>run.mockReset()); +const sources={'defs.svh':'`define WIDTH 8\r\n','top.sv':'// Ω\r\nmodule top #(parameter W = `WIDTH)(); endmodule\r\n'}; +function point(file:keyof typeof sources,byteOffset:number){const prefix=Buffer.from(sources[file]).subarray(0,byteOffset);return {file,byteOffset,line:[...prefix].filter(n=>n===10).length+1,column:byteOffset-prefix.lastIndexOf(10)};} +const start=Buffer.from(sources['top.sv']).indexOf('`WIDTH'); +function origin(role='initializer') {return {role,sourceOrigin:'macro',source:{file:'top.sv',line:2,column:28},macroExpansionComplete:true, + macroExpansion:[{name:'WIDTH',argument:false,spelling:point('defs.svh',Buffer.from(sources['defs.svh']).indexOf('8')),invocation:{start:point('top.sv',start),end:point('top.sv',start+6)}}]};} +function fact(){return {kind:'parameter',name:'W',instancePath:'top',value:'8',type:'int',width:32,sourceOrigin:'direct',source:{file:'top.sv',line:2,column:24}, + expressionOrigins:[origin()],expressionOriginCoverage:{initializer:'checked',declaredInitializer:'not-applicable',type:'not-applicable',truncated:false}};} +const payload=(f:unknown=fact())=>({codegraphSemanticVersion:1,facts:[f]}); + +it('keeps a direct declaration link when its initializer comes from a macro, preserving distinct scopes of provenance',async()=>{ + const graph:HdlSemanticSourceGraph={profile:()=>({state:'matches',indexed:{configurationFingerprint:'cfg'}} as HdlProfileStatus),stale:()=>false, + file:()=>({contentHash:createHash('sha256').update(sources['top.sv']).digest('hex')} as FileRecord),nodes:()=>[{id:'decl-W',kind:'constant',name:'W',startLine:2} as Node]}; + run.mockResolvedValue({frontend:'pyslang',ast:payload(),sources,sourceRoot:'/snapshot',profileName:'p',configurationFingerprint:'cfg',fingerprint:'snapshot', + version:'fixture',executableSha256:'python',exporterSha256:'exporter',librarySha256:'native',top:'top',parameters:{},allowUseBeforeDeclare:false, + languageStandard:'1800-2023',compilationUnitMode:'separate',runnerVersion:'test',compilerLimits:[],diagnostics:[]}); + const result=await analyzeHdlSemantics('/project',{pythonExecutable:'python',query:'top.W'},graph); + expect(result.facts[0]!.sourceNodeId).toBe('decl-W'); + expect(result.facts[0]!.sourceOrigin).toBe('direct'); + expect(result.facts[0]!.expressionOrigins![0]!.sourceOrigin).toBe('macro'); + expect(result.facts[0]!.expressionOrigins![0]!.macroExpansion[0]!.spelling!.file).toBe('defs.svh'); +}); + +it('preserves separate declared-initializer and type coverage when the effective initializer is supplied on the command line',()=>{ + const f=fact();f.expressionOriginCoverage={initializer:'command-line',declaredInitializer:'checked',type:'checked',truncated:true}; + f.expressionOrigins=[origin('declared-initializer'),origin('type')]; + const normalized=importMappedSemantics(payload(f),sources)[0]!; + expect(normalized.expressionOrigins!.map(e=>e.role)).toEqual(['declared-initializer','type']); + expect(normalized.expressionOriginCoverage).toEqual(f.expressionOriginCoverage); +}); + +it.each(['initializer','declaredInitializer','type'])('rejects non-string coverage even when coercion would resemble a known enum: %s',key=>{ + const f:any=fact();f.expressionOrigins=[];f.expressionOriginCoverage[key]=['checked']; + expect(()=>importMappedSemantics(payload(f),sources)).toThrow('coverage'); +}); + +it('rejects missing coverage, invalid roles/origins, contradictory coverage and malformed truncation',()=>{ + let f:any=fact();delete f.expressionOriginCoverage;expect(()=>importMappedSemantics(payload(f),sources)).toThrow('origins'); + f=fact();f.expressionOrigins[0].sourceOrigin='direct';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('role'); + f=fact();f.expressionOrigins[0].role='expression';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('role'); + f=fact();f.expressionOriginCoverage.initializer='unavailable';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('contradicts'); + f=fact();f.expressionOriginCoverage.declaredInitializer='command-line';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('coverage'); + f=fact();f.expressionOriginCoverage.truncated='false';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('coverage'); +}); + +it('uses immutable source and byte-coordinate checks for every expression macro frame and source',()=>{ + let f:any=fact();f.expressionOrigins[0].macroExpansion[0].spelling.file='../defs.svh';expect(()=>importMappedSemantics(payload(f),sources)).toThrow('snapshot'); + f=fact();f.expressionOrigins[0].macroExpansion[0].spelling.byteOffset++;expect(()=>importMappedSemantics(payload(f),sources)).toThrow('disagree'); + f=fact();f.expressionOrigins[0].source={file:'top.sv',line:1,column:5};expect(()=>importMappedSemantics(payload(f),sources)).toThrow('UTF-8'); + f=fact();f.expressionOrigins[0].macroExpansion[0].invocation.end={...point('top.sv',start),file:'defs.svh'};expect(()=>importMappedSemantics(payload(f),sources)).toThrow(); +}); + +it('rejects duplicate normalized entries even if their raw objects differ by unrecognized fields',()=>{ + const f:any=fact();f.expressionOrigins.push({...origin(),ignored:'not-an-identity'}); + expect(()=>importMappedSemantics(payload(f),sources)).toThrow('duplicate expression'); +}); + +it('enforces the per-fact expression cap and one shared frame budget for declaration and expression origins',()=>{ + const f:any=fact();f.expressionOrigins=Array.from({length:33},()=>origin());expect(()=>importMappedSemantics(payload(f),sources)).toThrow('origins'); + const chain=(name:string)=>Array.from({length:128},(_,i)=>({name:i===0?name:'nested',argument:false})); + const facts=Array.from({length:25},(_,i)=>({...fact(),name:'W'+i,sourceOrigin:'macro',macroExpansion:chain('declaration'),macroExpansionComplete:false, + expressionOrigins:Array.from({length:31},(_,j)=>({role:'initializer',sourceOrigin:'macro',macroExpansion:chain('expression'+j),macroExpansionComplete:false}))})); + // Expressions alone have 99,200 frames; only a shared declaration+expression + // budget rejects this 102,400-frame payload. + expect(()=>importMappedSemantics({codegraphSemanticVersion:1,facts},sources)).toThrow('frame count'); +}); diff --git a/__tests__/hdl-instances.test.ts b/__tests__/hdl-instances.test.ts new file mode 100644 index 000000000..70e74656f --- /dev/null +++ b/__tests__/hdl-instances.test.ts @@ -0,0 +1,46 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +it('keeps named HDL instances, source connections and generate scopes without elaborating arrays', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-hdl-instances-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'top.sv'), `module leaf #(parameter W=8) (input a); endmodule +module top(input a); + leaf #(.W(4)) first(.a(a)), bank[3:0](a); + if (1) begin : yes leaf u(.a(a)); end else begin : no leaf u(.*); end + for(genvar i=0;i<4;i++) begin : lanes leaf repeated(.a); end + if (1) begin leaf anonymous(.a(a)); end +endmodule +`); + cg = CodeGraph.initSync(root); + expect((await cg.indexAll()).success).toBe(true); + const instances = ['first', 'bank', 'u', 'repeated', 'anonymous'].flatMap(name => + cg!.getNodesByName(name).filter(n => n.kind === 'variable')); + expect(instances).toHaveLength(6); + expect(new Set(instances.map(n => n.id)).size).toBe(6); + expect(instances.find(n => n.name === 'first')?.signature).toBe('leaf #(.W(4)) first(.a(a))'); + expect(instances.find(n => n.name === 'bank')?.signature).toBe('leaf #(.W(4)) bank[3:0](a)'); + expect(instances.filter(n => n.name === 'u').map(n => n.qualifiedName).sort()).toEqual(['top::no::u', 'top::yes::u']); + expect(instances.find(n => n.name === 'repeated')?.qualifiedName).toBe('top::lanes::repeated'); + expect(instances.find(n => n.name === 'anonymous')?.qualifiedName).toMatch(/^top::generate@6:\d+::anonymous$/); + const targets = (id: string) => cg!.getCallees(id).filter(e => e.edge.kind === 'instantiates').map(e => e.node.name); + for (const instance of instances) expect(targets(instance.id)).toEqual(['leaf']); + expect(targets(cg.getNodesByName('top')[0].id)).toEqual(['leaf']); + for (const instance of instances.filter(n => n.signature?.includes('.a(') || n.name === 'bank' || n.name === 'repeated')) { + expect(cg.getCallees(instance.id).filter(e => e.edge.kind === 'references').map(e => e.node.qualifiedName)).toEqual(['top::a']); + } + const wildcard = instances.find(n => n.signature === 'leaf u(.*)')!; + expect(cg.getCallees(wildcard.id).filter(e => e.edge.kind === 'references')).toEqual([]); + const before = instances.map(n => n.id).sort(); + await cg.indexAll(); + expect(['first', 'bank', 'u', 'repeated', 'anonymous'].flatMap(name => + cg!.getNodesByName(name).filter(n => n.kind === 'variable').map(n => n.id)).sort()).toEqual(before); + } finally { + cg?.close(); + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/__tests__/hdl-port-bindings.test.ts b/__tests__/hdl-port-bindings.test.ts new file mode 100644 index 000000000..e906b39d8 --- /dev/null +++ b/__tests__/hdl-port-bindings.test.ts @@ -0,0 +1,112 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +async function graph(files: Record, check: (cg: CodeGraph, root: string) => Promise | void): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-ports-')); + let cg: CodeGraph | undefined; + try { + for (const [file, source] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), source); + } + cg = CodeGraph.initSync(root); + expect((await cg.indexAll()).success).toBe(true); + await check(cg, root); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +} +function bindings(cg: CodeGraph) { + return (cg as any).db.db.prepare("select id, name, qualified_name qualifiedName, signature from nodes where kind='property' and language='verilog' and decorators LIKE '%hdl:named%'").all() as Array<{ id: string; name: string; qualifiedName: string; signature: string }>; +} +const leaf = 'module leaf(input rx, output tx); endmodule'; + +it('maps named and shorthand ports to formal declarations and local expressions, including explicit open ports', async () => { + await graph({ 'leaf.sv': leaf, 'top.sv': `module top(input wire rx, output wire tx); +leaf u(.rx(rx), .tx()); leaf bank[3:0](.rx, .tx(tx)); +leaf positional(rx,tx); leaf wildcard(.*); +endmodule` }, cg => { + const ports = bindings(cg); + expect(ports).toHaveLength(4); + const endpoints = (q: string) => cg.getCallees(ports.find(n => n.qualifiedName === q)!.id) + .filter(e => e.edge.kind === 'references').map(e => e.node.qualifiedName).sort(); + expect(endpoints('top::u::rx')).toEqual(['leaf::rx', 'top::rx']); + expect(endpoints('top::u::tx')).toEqual(['leaf::tx']); + expect(endpoints('top::bank::rx')).toEqual(['leaf::rx', 'top::rx']); + expect(endpoints('top::bank::tx')).toEqual(['leaf::tx', 'top::tx']); + expect(ports.find(n => n.qualifiedName === 'top::u::tx')!.signature).toBe('.tx()'); + expect(cg.getNodesByName('bank').filter(n => n.kind === 'variable')).toHaveLength(1); + }); +}); + +it('does not borrow missing formal ports from another module and drops removed targets after reindex', async () => { + await graph({ 'leaf.sv': leaf, 'foreign.sv': 'module foreign(input missing); endmodule', + 'top.sv': 'module top(input sig); leaf u(.rx(sig), .missing(sig)); endmodule' }, async (cg, root) => { + const formalTargets = (name: string) => cg.getCallees(bindings(cg).find(n => n.name === name)!.id) + .filter(e => e.edge.metadata?.binding === 'hdl-named-port').map(e => e.node.qualifiedName); + expect(formalTargets('rx')).toEqual(['leaf::rx']); + expect(formalTargets('missing')).toEqual([]); + const before = bindings(cg).map(n => n.id).sort(); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(output tx); endmodule'); + expect((await cg.indexAll()).success).toBe(true); + expect(formalTargets('rx')).toEqual([]); + expect(bindings(cg).map(n => n.id).sort()).toEqual(before); + }); +}); + +it('uses the same synthesis versus simulation target as the instance', async () => { + await graph({ 'rtl/leaf.sv': leaf, 'sim/leaf_stub.sv': leaf, + 'top.sv': 'module top(input sig); leaf u(.rx(sig)); endmodule', + 'sim/tb_top.sv': 'module tb_top(input sig); leaf dut(.rx(sig)); endmodule' }, cg => { + for (const binding of bindings(cg)) { + const instanceName = binding.qualifiedName.startsWith('top::') ? 'u' : 'dut'; + const instance = cg.getNodesByName(instanceName).find(n => n.kind === 'variable')!; + const target = cg.getCallees(instance.id).find(e => e.edge.kind === 'instantiates')!.node; + const formal = cg.getCallees(binding.id).find(e => e.edge.metadata?.binding === 'hdl-named-port')!.node; + expect(formal.filePath).toBe(target.filePath); + } + }); +}); + +it.each([ + { 'a.sv': leaf, 'b.sv': leaf }, + { 'a.sv': `${leaf} ${leaf}` }, +])('leaves formal endpoints unresolved for duplicate module definitions: %j', async definitions => { + await graph({ ...definitions, 'top.sv': 'module top(input sig); leaf u(.rx(sig)); endmodule' } as Record, cg => { + expect(cg.getCallees(bindings(cg)[0]!.id).filter(e => e.edge.metadata?.binding === 'hdl-named-port')).toEqual([]); + }); +}); + +it('does not use fuzzy module-name guesses for a formal endpoint', async () => { + await graph({ 'leaf.sv': leaf, 'top.sv': 'module top(input sig); leaff u(.rx(sig)); endmodule' }, cg => { + expect(cg.getCallees(bindings(cg)[0]!.id).filter(e => e.edge.metadata?.binding === 'hdl-named-port')).toEqual([]); + }); +}); + +it('rehydrates named-port tokens when only the target changes during incremental sync', async () => { + const top = 'module top(input sig); leaf u(.rx(sig)); endmodule'; + await graph({ 'leaf.sv': leaf, 'top.sv': top }, async (cg, root) => { + const binding = bindings(cg)[0]!; + const formal = () => cg.getCallees(binding.id).filter(e => e.edge.metadata?.binding === 'hdl-named-port'); + const originalTarget = formal()[0]!.node; + expect(originalTarget.qualifiedName).toBe('leaf::rx'); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(output tx); endmodule'); + const removed = await cg.sync(); + expect(removed.filesModified).toBe(1); + expect(formal()).toEqual([]); + expect(bindings(cg)[0]!.id).toBe(binding.id); + // Restore at a different source position: a stale retained edge cannot pass. + fs.writeFileSync(path.join(root, 'leaf.sv'), `// moved port declaration\n${leaf}`); + const restored = await cg.sync(); + expect(restored.filesModified).toBe(1); + const restoredFormal = formal(); + expect(restoredFormal).toHaveLength(1); + expect(restoredFormal[0]!.node.qualifiedName).toBe('leaf::rx'); + expect(restoredFormal[0]!.node.filePath).toBe('leaf.sv'); + expect(restoredFormal[0]!.node.id).not.toBe(originalTarget.id); + expect(bindings(cg)[0]!.id).toBe(binding.id); + expect(fs.readFileSync(path.join(root, 'top.sv'), 'utf8')).toBe(top); + expect(cg.getCallees(binding.id).some(e => e.node.qualifiedName === 'top::sig')).toBe(true); + }); +}); diff --git a/__tests__/hdl-positional-ports.test.ts b/__tests__/hdl-positional-ports.test.ts new file mode 100644 index 000000000..6df3bad25 --- /dev/null +++ b/__tests__/hdl-positional-ports.test.ts @@ -0,0 +1,106 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +async function graph(leaf: string, top: string, check: (cg: CodeGraph, root: string) => void | Promise) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-positional-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'leaf.sv'), leaf); + fs.writeFileSync(path.join(root, 'top.sv'), top); + cg = CodeGraph.initSync(root); expect((await cg.indexAll()).success).toBe(true); + await check(cg, root); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +} +function slots(cg: CodeGraph) { + return (cg as any).db.db.prepare("select id,name,signature from nodes where kind='property' and decorators like '%hdl:position:%' order by name").all() as Array<{id:string;name:string;signature:string}>; +} +function formal(cg: CodeGraph, id: string) { + return cg.getCallees(id).filter(e => e.edge.metadata?.binding === 'hdl-positional-port').map(e => e.node.name); +} +const ansi = 'module leaf(input rx, output tx, input enable); endmodule'; + +it('maps ANSI positions, preserving empty slots and local expressions without expanding instance arrays', async () => { + await graph(ansi, 'module top(input sig); leaf bank[3:0](sig,,sig); endmodule', cg => { + const connections = slots(cg); + expect(connections).toHaveLength(3); + expect(connections.map(n => formal(cg,n.id))).toEqual([['rx'], ['tx'], ['enable']]); + expect(connections[1]!.signature).toBe(''); + expect(cg.getCallees(connections[1]!.id).filter(e => e.node.qualifiedName === 'top::sig')).toEqual([]); + expect(cg.getCallees(connections[2]!.id).some(e => e.node.qualifiedName === 'top::sig')).toBe(true); + expect(cg.getNodesByName('bank').filter(n => n.kind === 'variable')).toHaveLength(1); + }); +}); + +it('uses the non-ANSI header order rather than declaration body order', async () => { + await graph('module leaf(enable, rx, tx); output tx; input rx; input enable; endmodule', + 'module top(input sig); leaf u(sig,sig,sig); endmodule', cg => { + expect(slots(cg).map(n => formal(cg,n.id))).toEqual([['enable'], ['rx'], ['tx']]); + }); +}); + +it.each([ + ['extra arguments', ansi, 'leaf u(sig,sig,sig,sig);'], + ['mixed named and positional', ansi, 'leaf u(sig,.tx(sig));'], + ['complex non-ANSI header', 'module leaf(.rx({rx0,rx1})); input rx0,rx1; endmodule', 'leaf u(sig);'], +])('does not guess formal endpoints for %s', async (_label, declaration, instance) => { + await graph(declaration, `module top(input sig); ${instance} endmodule`, cg => { + expect(slots(cg).flatMap(n => formal(cg,n.id))).toEqual([]); + }); +}); + +it('rebinds unchanged positional callers after target order changes, removal and restoration through sync', async () => { + const top = 'module top(input sig); leaf u(sig,sig); endmodule'; + await graph('module leaf(rx,tx); input rx; output tx; endmodule', top, async (cg, root) => { + const ids = slots(cg).map(n => n.id); + const targets = () => slots(cg).map(n => formal(cg,n.id)); + expect(targets()).toEqual([['rx'], ['tx']]); + // Keep names and body declaration positions identical; only the header order changes. + fs.writeFileSync(path.join(root,'leaf.sv'), 'module leaf(tx,rx); input rx; output tx; endmodule'); + expect((await cg.sync()).filesModified).toBe(1); + expect(targets()).toEqual([['tx'], ['rx']]); + fs.writeFileSync(path.join(root,'leaf.sv'), 'module leaf(rx); input rx; endmodule'); + expect((await cg.sync()).filesModified).toBe(1); + expect(targets()).toEqual([[], []]); + fs.writeFileSync(path.join(root,'leaf.sv'), 'module leaf(rx,tx); input rx; output tx; endmodule'); + expect((await cg.sync()).filesModified).toBe(1); + expect(targets()).toEqual([['rx'], ['tx']]); + expect(slots(cg).map(n => n.id)).toEqual(ids); + expect(fs.readFileSync(path.join(root,'top.sv'),'utf8')).toBe(top); + }); +}); + +it('keeps leading and trailing empty slots while allowing an omitted trailing port list', async () => { + await graph(ansi, 'module top(input sig); leaf u(,sig,); leaf short_list(sig); endmodule', cg => { + const nodes = (cg as any).db.db.prepare("select id,qualified_name q from nodes where kind='property' and decorators like '%hdl:position:%' order by qualified_name").all(); + expect(nodes.map((n: any) => [n.q, formal(cg,n.id)])).toEqual([ + ['top::short_list::port[0]', ['rx']], ['top::u::port[0]', ['rx']], + ['top::u::port[1]', ['tx']], ['top::u::port[2]', ['enable']], + ]); + }); +}); + +it('invalidates formal mappings when an equally near empty duplicate module is added and recovers when removed', async () => { + await graph(ansi, 'module top(input sig); leaf u(sig); endmodule', async (cg, root) => { + const slot = slots(cg)[0]!; + expect(formal(cg,slot.id)).toEqual(['rx']); + fs.writeFileSync(path.join(root,'duplicate.sv'),'module leaf; endmodule'); + await cg.sync(); + expect(formal(cg,slot.id)).toEqual([]); + fs.rmSync(path.join(root,'duplicate.sv')); + await cg.sync(); + expect(formal(cg,slot.id)).toEqual(['rx']); + }); +}); + +it.each(['/* second slot */', '// second slot\n'])('keeps positional slot order across comments: %s', async comment => { + await graph(ansi, `module top(input sig); leaf u(sig, ${comment} sig,); endmodule`, cg => { + const connections = slots(cg); + expect(connections.map(n => formal(cg,n.id))).toEqual([['rx'], ['tx'], ['enable']]); + expect(connections[1]!.signature).toContain(comment); + expect(cg.getCallees(connections[1]!.id).some(e => e.node.qualifiedName === 'top::sig')).toBe(true); + expect(cg.getCallees(connections[2]!.id).some(e => e.node.qualifiedName === 'top::sig')).toBe(false); + }); +}); diff --git a/__tests__/hdl-preprocess.test.ts b/__tests__/hdl-preprocess.test.ts new file mode 100644 index 000000000..631c32555 --- /dev/null +++ b/__tests__/hdl-preprocess.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { preprocessVerilog } from '../src/hdl/preprocess'; +const run=(source:string,defines:Record={})=>preprocessVerilog(source,{filePath:'top.sv',defines}); +const unchangedOffsets=(before:string,after:string)=>{ + expect(after.length).toBe(before.length); + expect([...after.matchAll(/\r|\n/g)].map(m=>m.index)).toEqual([...before.matchAll(/\r|\n/g)].map(m=>m.index)); +}; +describe('bounded HDL profile preprocessing',()=>{ + it('selects nested branches with per-unit defines and exact UTF16/CRLF positions',()=>{ + const source='// 😀\r\nmodule m(\r\n`ifdef VCS\r\ninput a\r\n`ifndef OMIT\r\n, input b\r\n`endif\r\n`else\r\ninput c\r\n`endif\r\n); endmodule'; + const selected=run(source,{VCS:null});unchangedOffsets(source,selected.source); + expect(selected.source).toContain('input a');expect(selected.source).toContain('input b');expect(selected.source).not.toContain('input c'); + expect(selected.source.indexOf('input a')).toBe(source.indexOf('input a'));expect(selected.complete).toBe(true); + const other=run(source);expect(other.source).toContain('input c');expect(other.source).not.toContain('input a'); + }); + it('supports inline conditionals, elsif and active define/undef without deleting following code',()=>{ + const source='`define A 1\nmodule m; `ifdef A wire a; `elsif B wire b; `else wire c; `endif\n`undef A\n`ifdef A wire bad; `else wire good; `endif endmodule'; + const result=run(source,{B:null});unchangedOffsets(source,result.source); + expect(result.source).toContain('`define A 1');expect(result.source).toContain('`undef A'); + expect(result.source).toContain('wire a;');expect(result.source).toContain('wire good;');expect(result.source).not.toMatch(/wire (bad|b|c);/); + }); + it('ignores directive-looking comments, strings, escaped identifiers and macro bodies',()=>{ + const source='/* `ifdef BAD */\n// `else\nmodule m; string s="`endif"; wire \\`ifdef ; endmodule\n`define MULTI(x) x \\\n `ifdef not_a_directive\n'; + const result=run(source);expect(result.source).toBe(source);expect(result.diagnostics).toEqual([]); + }); + it('does not leak macro definitions across translation units or inactive branches',()=>{ + const source='`ifdef MISSING\n`define LEAK 1\n`endif\n`ifdef LEAK\nwire bad;\n`else\nwire good;\n`endif'; + expect(run(source).source).toContain('wire good;');expect(run('`define LEAK 1').complete).toBe(true); + expect(run('`ifdef LEAK\nwire bad;\n`else\nwire good;\n`endif').source).toContain('wire good;'); + }); + it('uses nested includes for macro state while retaining original include source',()=>{ + const files:Record={ + 'a.svh':'`ifndef A_GUARD\n`define A_GUARD\n`include "b.svh"\n`endif', + 'b.svh':'`define FEATURE 1\n`include "a.svh"', + }; + const source='`include "a.svh"\n`ifdef FEATURE\nwire yes;\n`else\nwire no;\n`endif'; + const calls:string[]=[]; + const result=preprocessVerilog(source,{filePath:'top.sv',includeDirs:['inc'],readInclude:(request,_from,dirs)=>{ + expect(dirs).toEqual(['inc']);calls.push(request);return files[request]===undefined?null:{filePath:request,source:files[request]!}; + }}); + expect(result.source).toContain('`include "a.svh"');expect(result.source).toContain('wire yes;');expect(result.source).not.toContain('wire no;'); + expect(result.dependencies.sort()).toEqual(['a.svh','b.svh']);expect(result.complete).toBe(true);expect(calls.length).toBeLessThan(5); + }); + it('fails closed on unknown include macro state but keeps unrelated source',()=>{ + const source='`include "missing.svh"\nwire outside;\n`ifdef X\nwire first;\n`else\nwire second;\n`endif\n`define CERTAIN\n`ifdef CERTAIN\nwire restored;\n`endif'; + const result=run(source);expect(result.source).toContain('wire outside;');expect(result.source).toContain('wire restored;'); + expect(result.source).not.toMatch(/wire (first|second);/);expect(result.complete).toBe(false); + expect(result.diagnostics.map(d=>d.code)).toContain('unknown-conditional');unchangedOffsets(source,result.source); + }); + it('bounds unguarded include cycles with explicit diagnostics',()=>{ + const result=preprocessVerilog('`include "cycle.svh"',{filePath:'top.sv',readInclude:()=>({filePath:'cycle.svh',source:'`include "cycle.svh"'})}); + expect(result.diagnostics.map(d=>d.code)).toContain('include-cycle-or-depth');expect(result.complete).toBe(false); + }); + it.each(['`else\nwire unsafe;','`ifdef X\nwire unsafe;','`ifdef X\n`else\n`else\nwire unsafe;\n`endif'])('blanks structurally malformed conditional units: %s',source=>{ + const result=run(source);expect(result.source.trim()).toBe('');unchangedOffsets(source,result.source); + expect(result.diagnostics.map(d=>d.code)).toContain('malformed-conditional'); + }); + it('does not choose a stale branch after an opaque macro can define another macro',()=>{ + const source='`define ENABLE `define FLAG\n`ENABLE\n`ifdef FLAG\nmodule chosen; endmodule\n`else\nmodule wrong; endmodule\n`endif'; + const result=run(source);unchangedOffsets(source,result.source); + expect(result.source).toContain('`ENABLE');expect(result.source).not.toMatch(/module (chosen|wrong)/); + expect(result.diagnostics.map(d=>d.code)).toContain('unknown-conditional'); + }); + it('invalidates later nested and sibling conditions after an active opaque invocation',()=>{ + const source='`ifdef ACTIVE\n`SIDE_EFFECT\n`ifdef FLAG\nwire first;\n`else\nwire second;\n`endif\n`endif\n`ifdef ACTIVE\nwire stale;\n`endif'; + const result=run(source,{ACTIVE:null});unchangedOffsets(source,result.source); + expect(result.source).not.toMatch(/wire (first|second|stale)/);expect(result.complete).toBe(false); + }); + it('caps diagnostics while continuing macro state changes and source selection',()=>{ + const source='`OPAQUE\n'.repeat(540)+'`define RESTORED\n`ifdef RESTORED\nmodule good; endmodule\n`else\nmodule bad; endmodule\n`endif'; + const result=run(source);unchangedOffsets(source,result.source); + expect(result.diagnostics).toHaveLength(513);expect(result.diagnostics.filter(d=>d.code==='diagnostic-limit')).toHaveLength(1); + expect(result.complete).toBe(false);expect(result.source).toContain('module good;');expect(result.source).not.toContain('module bad;'); + }); + it('preserves unexpanded macros and unknown directives with diagnostics',()=>{ + const source='`define ASSUME(name, expr) name: assume property(expr);\nmodule m; `ASSUME(ok, x) `timescale 1ns/1ps\nendmodule'; + const result=run(source);expect(result.source).toBe(source); + expect(result.diagnostics.map(d=>d.code)).toEqual(['unsupported-macro-expansion','unsupported-directive']); + }); +}); diff --git a/__tests__/hdl-profile-cli.test.ts b/__tests__/hdl-profile-cli.test.ts new file mode 100644 index 000000000..db98755e2 --- /dev/null +++ b/__tests__/hdl-profile-cli.test.ts @@ -0,0 +1,35 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { createHash } from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { CodeGraph } from '../src'; + +it('passes selected source through compiled workers and preserves raw hashes across CLI index/sync/status', () => { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-profile-cli-')); + const bin=path.resolve(__dirname,'../dist/bin/codegraph.js'); + const run=(args:string[])=>{const r=spawnSync(process.execPath,[bin,...args],{encoding:'utf8',timeout:30000, + env:{...process.env,CODEGRAPH_NO_DAEMON:'1',NO_COLOR:'1'}});expect(r.status,r.stderr).toBe(0);return r.stdout;}; + const source='`include "flags.svh"\nmodule top;\n`ifdef ENABLED\nfunction int selected_fn(); return 1; endfunction\n`else\nfunction int fallback_fn(); return 0; endfunction\n`endif\nendmodule'; + try{ + fs.mkdirSync(path.join(root,'inc'));fs.writeFileSync(path.join(root,'inc/flags.svh'),'`define ENABLED\n'); + fs.writeFileSync(path.join(root,'top.sv'),source);fs.writeFileSync(path.join(root,'rtl.f'),'top.sv\n'); + fs.writeFileSync(path.join(root,'codegraph.json'),JSON.stringify({hdl:{activeProfile:'synth',profiles:{synth:{filelists:['rtl.f'],includeDirs:['inc'],topModules:['top']}}}})); + CodeGraph.initSync(root).close(); + run(['index',root,'--quiet']); + let cg=CodeGraph.openSync(root);try{ + expect(cg.getNodesByName('selected_fn')).toHaveLength(1);expect(cg.getNodesByName('fallback_fn')).toHaveLength(0); + expect(cg.getFile('top.sv')?.contentHash).toBe(createHash('sha256').update(source).digest('hex')); + }finally{cg.close();} + let status=JSON.parse(run(['status',root,'--json']));expect(status.index.hdlProfile.indexed.name).toBe('synth'); + fs.writeFileSync(path.join(root,'inc/flags.svh'),'`undef ENABLED\n'); + status=JSON.parse(run(['status',root,'--json']));expect(status.index.hdlProfile.reindexRecommended).toBe(true); + run(['sync',root,'--quiet']); + cg=CodeGraph.openSync(root);try{ + expect(cg.getNodesByName('selected_fn')).toHaveLength(0);expect(cg.getNodesByName('fallback_fn')).toHaveLength(1); + }finally{cg.close();} + status=JSON.parse(run(['status',root,'--json']));expect(status.index.hdlProfile.state).toBe('matches'); + expect(run(['explore','fallback_fn','--path',root])).toContain('HDL context: indexed "synth"'); + }finally{fs.rmSync(root,{recursive:true,force:true});} +}); diff --git a/__tests__/hdl-profile-index.test.ts b/__tests__/hdl-profile-index.test.ts new file mode 100644 index 000000000..7c10edd4a --- /dev/null +++ b/__tests__/hdl-profile-index.test.ts @@ -0,0 +1,87 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +it('switches profiles and include/filelist dependencies without mixing HDL graphs or changing source offsets', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-profile-index-')); + let cg: CodeGraph | undefined; + const write = (file: string, text: string) => { fs.mkdirSync(path.dirname(path.join(root, file)), {recursive:true}); fs.writeFileSync(path.join(root,file),text); }; + const config = (activeProfile: string) => JSON.stringify({hdl:{activeProfile,profiles:{synth:{filelists:['rtl.f'],includeDirs:['inc'],defines:{SYNTHESIS:'1'},topModules:['top']},sim:{files:['top.sv','sim.sv'],includeDirs:['inc']}}}}); + const source = '`include "defs.svh"\nmodule top;\n`ifdef SYNTHESIS\nfunction int hardware(); return 1; endfunction\n`else\nfunction int software(); return 2; endfunction\n`endif\n`ifdef EXTRA\nfunction int extra(); return 3; endfunction\n`endif\nendmodule\n'; + try { + write('top.sv',source); write('rtl.f','top.sv\n'); write('sim.sv','module sim; endmodule'); write('inc/defs.svh','// empty\n'); + write('plain.ts','export function ordinary(){return 1;}'); write('codegraph.json',config('synth')); + cg = CodeGraph.initSync(root); expect((await cg.indexAll()).success).toBe(true); + const names = () => (cg as any).db.db.prepare("SELECT name FROM nodes WHERE kind IN ('function','class') ORDER BY name").all().map((n:any)=>n.name); + expect(names()).toEqual(['hardware','ordinary','top']); + expect(cg.getNodesByName('hardware')[0].startLine).toBe(4); + expect(cg.getHdlProfileStatus()?.state).toBe('matches'); + expect(cg.getChangedFiles()).toEqual({added:[],modified:[],removed:[]}); + write('codegraph.json',config('sim')); + expect(cg.getHdlProfileStatus()?.state).toBe('mismatch'); + await cg.sync({paths:['codegraph.json']}); + expect(names()).toEqual(['ordinary','sim','software','top']); + expect(cg.getHdlProfileStatus()?.state).toBe('matches'); + write('inc/defs.svh','`define EXTRA\n'); + expect(cg.getHdlProfileStatus()?.reindexRecommended).toBe(true); + await cg.sync({paths:['inc/defs.svh']}); + expect(names()).toContain('extra'); + write('codegraph.json',config('synth')); write('rtl.f','top.sv\nsim.sv\n'); + await cg.sync({paths:['rtl.f']}); + expect(names()).toEqual(['extra','hardware','ordinary','sim','top']); + const snapshot=names(); await cg.indexAll(); expect(names()).toEqual(snapshot); + expect(fs.readFileSync(path.join(root,'top.sv'),'utf8')).toBe(source); + write('codegraph.json','{}'); await cg.sync({paths:['codegraph.json']}); + expect(names()).toContain('software'); expect(names()).toContain('hardware'); + expect(cg.getHdlProfileStatus()?.indexed.mode).toBe('raw'); + } finally { cg?.close(); fs.rmSync(root,{recursive:true,force:true}); } +}); + +it('fails closed on invalid active profile and preserves the previously indexed graph', async () => { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-profile-invalid-'));let cg:CodeGraph|undefined; + try { + fs.writeFileSync(path.join(root,'top.sv'),'module top; endmodule'); + fs.writeFileSync(path.join(root,'codegraph.json'),JSON.stringify({hdl:{activeProfile:'ok',profiles:{ok:{files:['top.sv']}}}})); + cg=CodeGraph.initSync(root);await cg.indexAll(); + fs.writeFileSync(path.join(root,'codegraph.json'),JSON.stringify({hdl:{activeProfile:'missing',profiles:{ok:{files:['top.sv']}}}})); + await expect(cg.sync()).rejects.toThrow('Invalid HDL profile'); + expect(cg.getNodesByName('top')).toHaveLength(1); + expect(cg.getHdlProfileStatus()?.state).toBe('configuration-error'); + fs.writeFileSync(path.join(root,'codegraph.json'),'{'); + await expect(cg.sync()).rejects.toThrow('Invalid HDL profile'); + expect(cg.getHdlProfileStatus()?.indexed.name).toBe('ok'); + }finally{cg?.close();fs.rmSync(root,{recursive:true,force:true});} +}); + +it('stores one coherent source snapshot when a profile unit changes during a bulk index', async () => { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-profile-race-'));let cg:CodeGraph|undefined; + try { + const file=path.join(root,'top.sv'); + fs.writeFileSync(file,'module old_unit; endmodule'); + fs.writeFileSync(path.join(root,'codegraph.json'),JSON.stringify({hdl:{activeProfile:'p',profiles:{p:{files:['top.sv']}}}})); + cg=CodeGraph.initSync(root);await cg.indexAll();let changed=false; + const result=await cg.indexAll({onProgress:p=>{if(!changed&&p.phase==='scanning'){changed=true;fs.writeFileSync(file,'module new_unit; endmodule');}}}); + expect(result.success).toBe(true);expect(cg.getNodesByName('old_unit')).toHaveLength(1); + expect(cg.getChangedFiles().modified).toContain('top.sv'); + await cg.sync();expect(cg.getNodesByName('new_unit')).toHaveLength(1);expect(cg.getNodesByName('old_unit')).toHaveLength(0); + }finally{cg?.close();fs.rmSync(root,{recursive:true,force:true});} +}); + +it('publishes only diagnostics for units actually updated by partial operations', async () => { + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-profile-partial-'));let cg:CodeGraph|undefined; + const opaque='`OPAQUE\nmodule other; endmodule'; + try{ + fs.writeFileSync(path.join(root,'top.sv'),'module top; endmodule'); + fs.writeFileSync(path.join(root,'other.sv'),opaque); + fs.writeFileSync(path.join(root,'codegraph.json'),JSON.stringify({hdl:{activeProfile:'p',profiles:{p:{files:['top.sv','other.sv']}}}})); + cg=CodeGraph.initSync(root);await cg.indexAll();expect(cg.getHdlProfileStatus()?.incomplete).toBe(true); + fs.writeFileSync(path.join(root,'other.sv'),'module other; endmodule'); + fs.writeFileSync(path.join(root,'top.sv'),'module top; wire value; endmodule'); + await cg.indexFiles(['top.sv']);expect(cg.getHdlProfileStatus()?.incomplete).toBe(true); + await cg.sync({paths:['other.sv']});expect(cg.getHdlProfileStatus()?.incomplete).toBe(false); + fs.writeFileSync(path.join(root,'top.sv'),'`OPAQUE\nmodule top; endmodule'); + await cg.indexFiles(['top.sv']);expect(cg.getHdlProfileStatus()?.incomplete).toBe(true); + }finally{cg?.close();fs.rmSync(root,{recursive:true,force:true});} +}); diff --git a/__tests__/hdl-profile-status.test.ts b/__tests__/hdl-profile-status.test.ts new file mode 100644 index 000000000..5e509996c --- /dev/null +++ b/__tests__/hdl-profile-status.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from 'vitest'; +import { buildHdlProfileStatus, formatHdlProfileStatus, type HdlProfileConfiguration, type HdlIndexedProfileMetadata } from '../src/hdl/status'; +const active = (name='simulation',fingerprint='configured-sim'): HdlProfileConfiguration => ({status:'active',profile:{name,fingerprint},diagnostics:[]}); +const indexed = (name='simulation',configurationFingerprint='configured-sim',effective='effective-with-headers'):HdlIndexedProfileMetadata => ({ + name,fingerprint:effective,context:JSON.stringify({mode:'profile',configurationFingerprint,dependencies:[{path:'defs.svh',sha256:'header-sha'}]}), +}); +const none:HdlProfileConfiguration={status:'none',profile:null,diagnostics:[]}; +const raw:HdlIndexedProfileMetadata={name:null,fingerprint:'raw',context:JSON.stringify({mode:'raw'})}; +const legacy:HdlIndexedProfileMetadata={name:null,fingerprint:null,context:null}; + +describe('HDL configured versus indexed profile visibility',()=>{ + it('compares configured fingerprint against stored configuration context, not the effective header hash',()=>{ + const status=buildHdlProfileStatus(active(),indexed(),true)!; + expect(status.state).toBe('matches'); + expect(status.mismatch).toBe(false); + expect(status.reindexRecommended).toBe(false); + expect(status.indexed.fingerprint).toBe('effective-with-headers'); + expect(status.indexed.configurationFingerprint).toBe('configured-sim'); + }); + it('keeps indexed identity when configuration changes to another real build variant',()=>{ + const status=buildHdlProfileStatus(active('synthesis','configured-synth'),indexed(),true)!; + expect(status.state).toBe('mismatch'); + expect(status.reindexRecommended).toBe(true); + expect(status.indexed.name).toBe('simulation'); + expect(status.configured.name).toBe('synthesis'); + const text=formatHdlProfileStatus(status); + expect(text).toContain('indexed "simulation"'); + expect(text).toContain('configured "synthesis"'); + expect(text).toContain('rebuild the index'); + expect(text).toContain('not fully expanded'); + }); + it('detects defines or filelists changing under the same profile name',()=>{ + expect(buildHdlProfileStatus(active('simulation','changed-config'),indexed(),true)?.mismatch).toBe(true); + }); + it('distinguishes explicit raw-source indexing from missing legacy provenance',()=>{ + expect(buildHdlProfileStatus(none,raw,true)?.state).toBe('matches'); + expect(buildHdlProfileStatus(none,legacy,true)?.state).toBe('unknown'); + expect(buildHdlProfileStatus(none,legacy,true)?.reindexRecommended).toBe(true); + expect(formatHdlProfileStatus(buildHdlProfileStatus(none,raw,true))).toContain('multiple conditional alternatives'); + expect(buildHdlProfileStatus(active(),raw,true)?.state).toBe('mismatch'); + expect(buildHdlProfileStatus(none,indexed(),true)?.state).toBe('mismatch'); + }); + it('reports invalid configuration without claiming a rebuild alone can fix it',()=>{ + const status=buildHdlProfileStatus({status:'invalid-profile',profile:null,diagnostics:['Missing filelist build/sim.f']},indexed(),true)!; + expect(status.state).toBe('configuration-error'); + expect(status.reindexRecommended).toBe(false); + expect(status.indexed.name).toBe('simulation'); + expect(formatHdlProfileStatus(status)).toContain('Fix the HDL profile configuration'); + expect(formatHdlProfileStatus(status)).toContain('Missing filelist build/sim.f'); + }); + it('marks corrupt or partial stored context as unknown instead of trusting current configuration',()=>{ + for(const context of ['{broken','[]','{}']) { + const status=buildHdlProfileStatus(active(),{...indexed(),context},true)!; + expect(status.indexed.mode).toBe('unknown'); + expect(status.reindexRecommended).toBe(true); + } + }); + it('retains indexed preprocessing diagnostics and bounds their text rendering',()=>{ + const metadata=indexed(); + metadata.context=JSON.stringify({mode:'profile',configurationFingerprint:'configured-sim',incomplete:true, + diagnostics:[{filePath:'rtl/top.sv',line:4,message:'Unknown include macro'},'Macro expansion unsupported','Third diagnostic','Fourth diagnostic']}); + const status=buildHdlProfileStatus(active(),metadata,true)!; + expect(status.incomplete).toBe(true); + expect(status.indexedDiagnostics).toHaveLength(4); + const text=formatHdlProfileStatus(status); + expect(text).toContain('rtl/top.sv:4: Unknown include macro'); + expect(text).toContain('1 additional diagnostics'); + expect(text).not.toContain('Fourth diagnostic'); + expect(status.reindexRecommended).toBe(false); + }); + it('keeps unrelated projects quiet',()=>{ + expect(buildHdlProfileStatus(none,legacy,false)).toBeNull(); + expect(buildHdlProfileStatus(none,raw,false)).toBeNull(); + expect(formatHdlProfileStatus(null)).toBe(''); + }); +}); + +it('prefixes normal, empty and access-filtered MCP exploration with the indexed context', async()=>{ + const fs=await import('node:fs'), path=await import('node:path'), os=await import('node:os'); + const {CodeGraph}=await import('../src'); + const {ToolHandler}=await import('../src/mcp/tools'); + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-profile-notice-')); + let cg: InstanceType | undefined; + try { + fs.writeFileSync(path.join(root,'top.sv'),'module top(input a, output y); assign y=a; endmodule'); + cg=CodeGraph.initSync(root); expect((await cg.indexAll()).success).toBe(true); + const state=buildHdlProfileStatus(active('synthesis','configured-synth'),indexed(),true)!; + Object.assign(cg,{getHdlProfileStatus:()=>state}); + const handler=new ToolHandler(cg); + for (const args of [{query:'top'}, {query:'missing_name_that_does_not_exist'}, {query:'top::a',hdlAccess:'all'}]) { + const result=await handler.executeReadTool('codegraph_explore',args); + expect(result.isError).not.toBe(true); + const text=result.content.map(c=>c.text ?? '').join('\n'); + expect(text).toContain('HDL context: indexed "simulation"'); + expect(text).toContain('configured "synthesis"'); + expect(text).toContain('Profile mismatch: rebuild the index'); + } + } finally {cg?.close();fs.rmSync(root,{recursive:true,force:true});} +}); + +it('summarizes configured and indexed settings separately without exposing defines values or source filelists',()=>{ + const configuration=active('synthesis','configured-synth'); + configuration.profile={...configuration.profile!,files:['rtl/new_top.v'],includeDirs:['rtl/include'], + defines:{SYNTHESIS:'1',PRIVATE_VALUE:'do-not-print-this-value'},topModules:['new_top'],languageMode:'verilog'}; + const metadata=indexed(); + metadata.context=JSON.stringify({mode:'profile',configurationFingerprint:'configured-sim', + files:['sim/testbench.sv','rtl/old_top.sv'],includeDirs:['sim/include','common/include'], + defines:{SIMULATION:'1',OTHER_SECRET:'never-display-this'},topModules:['tb_top'],languageMode:'systemverilog'}); + const before=JSON.stringify({configuration,metadata}); + const status=buildHdlProfileStatus(configuration,metadata,true)!; + expect(status.configured).toMatchObject({fileCount:1,includeDirs:['rtl/include'],defineNames:['PRIVATE_VALUE','SYNTHESIS'],topModules:['new_top'],languageMode:'verilog'}); + expect(status.indexed).toMatchObject({fileCount:2,includeDirs:['sim/include','common/include'],defineNames:['OTHER_SECRET','SIMULATION'],topModules:['tb_top'],languageMode:'systemverilog'}); + const json=JSON.stringify(status), text=formatHdlProfileStatus(status); + for(const hidden of ['do-not-print-this-value','never-display-this','sim/testbench.sv','rtl/old_top.sv']) { + expect(json).not.toContain(hidden);expect(text).not.toContain(hidden); + } + expect(text).toContain('2 file(s)'); + expect(text).toContain('intended dialect systemverilog'); + expect(text).toContain('does not enforce Verilog-only conformance'); + expect(text).toContain('topModules are recorded roots'); + expect(text).not.toContain('sim/include'); + expect(text).not.toContain('OTHER_SECRET'); + expect(JSON.stringify({configuration,metadata})).toBe(before); + status.configured.includeDirs!.push('local-only-change'); + expect(configuration.profile.includeDirs).toEqual(['rtl/include']); +}); diff --git a/__tests__/hdl-profile-watch.test.ts b/__tests__/hdl-profile-watch.test.ts new file mode 100644 index 000000000..8e27309da --- /dev/null +++ b/__tests__/hdl-profile-watch.test.ts @@ -0,0 +1,90 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; +import { FileWatcher } from '../src/sync/watcher'; +import { HdlWatchScope } from '../src/sync/hdl-watch-scope'; + +async function waitFor(check: () => boolean, timeout = 8000) { + const end = Date.now() + timeout; + while (!check()) { if (Date.now() > end) throw new Error('HDL watcher did not converge'); await new Promise(r => setTimeout(r, 25)); } +} +function project() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-profile-watch-')); + const write = (file: string, text: string) => { fs.mkdirSync(path.dirname(path.join(root, file)), {recursive:true}); fs.writeFileSync(path.join(root,file),text); }; + const config = (activeProfile: string) => JSON.stringify({hdl:{activeProfile,profiles:{ + first:{filelists:['build/units.f']},second:{filelists:['alt/units.f'],includeDirs:['alt/inc']}, + }}}); + write('.gitignore', 'build/\nalt/\nprivate/\nunrelated/\n'); + write('build/top.sv', '`include "../private/flags.inc"\nmodule top;\n`ifdef EXTRA\nfunction int extra();return 1;endfunction\n`endif\nendmodule'); + write('build/spare.sv', 'module spare; endmodule'); write('build/units.f','build/top.sv\n'); + write('private/flags.inc','// empty\n'); + write('alt/top.sv','`include "flags.txt"\nmodule other;\n`ifdef SECOND\nfunction int second();return 2;endfunction\n`endif\nendmodule'); + write('alt/units.f','alt/top.sv\n'); write('alt/inc/flags.txt','// empty\n'); + write('codegraph.json',config('first')); + return {root,write,config}; +} + +it('watches profile filelists, explicit ignored sources and external-to-source header dependencies through the real event filter', async () => { + const {root,write} = project(); const calls: Array = []; + const watcher = new FileWatcher(root, async paths => { calls.push(paths); return {filesChanged:1,durationMs:1}; }, + {inertForTests:true,debounceMs:5}, () => ['private/flags.inc']); + try { + expect(watcher.start()).toBe(true); + watcher.ingestEventForTests('private/flags.inc'); + await waitFor(() => calls.length === 1); + expect(calls[0]).toContain('private/flags.inc'); + write('build/units.f','build/top.sv\nbuild/spare.sv\n'); watcher.ingestEventForTests('build/units.f'); + await waitFor(() => calls.length === 2); + expect(calls[1]).toBeUndefined(); // profile membership needs full HDL reconciliation + watcher.ingestEventForTests('build/spare.sv'); await waitFor(() => calls.length === 3); + expect(calls[2]).toContain('build/spare.sv'); + watcher.ingestEventForTests('unrelated/noise.txt'); watcher.ingestEventForTests('.codegraph/graph.db'); + await new Promise(r => setTimeout(r, 30)); expect(calls).toHaveLength(3); + } finally { watcher.stop(); fs.rmSync(root,{recursive:true,force:true}); } +}); + +it('retains the last valid HDL watch scope through missing filelists and malformed configuration', () => { + const {root,write,config} = project(); const scope = new HdlWatchScope(root, () => ['private/flags.inc']); + try { + scope.refresh(); expect(scope.matchesDirectory('build')).toBe(true); expect(scope.matchesDirectory('private')).toBe(true); + fs.unlinkSync(path.join(root,'build/units.f')); scope.refresh(); expect(scope.isFilelist('build/units.f')).toBe(true); + write('codegraph.json','{'); scope.refresh(); expect(scope.matchesFile('private/flags.inc')).toBe(true); + write('build/units.f','build/top.sv\n'); write('codegraph.json',config('second')); scope.refresh(); + expect(scope.matchesDirectory('alt/inc/nested')).toBe(true); expect(scope.matchesFile('alt/inc/new.flags')).toBe(true); + write('codegraph.json','{}'); scope.refresh(); expect(scope.matchesFile('private/flags.inc')).toBe(false); + } finally { fs.rmSync(root,{recursive:true,force:true}); } +}); + +it('auto-syncs native fs.watch events for ignored filelists, arbitrary-extension includes and newly selected directories', async () => { + const {root,write,config} = project(); let cg: CodeGraph|undefined; const errors: string[]=[]; + try { + cg=CodeGraph.initSync(root); expect((await cg.indexAll()).success).toBe(true); + expect(cg.getNodesByName('top')).toHaveLength(1); expect(cg.getNodesByName('spare')).toHaveLength(0); + expect(cg.watch({debounceMs:30,onSyncError:e=>errors.push(e.message)})).toBe(true); + write('build/units.f','build/top.sv\nbuild/spare.sv\n'); + await waitFor(()=>cg!.getNodesByName('spare').length===1); + write('private/flags.inc','`define EXTRA\n'); + await waitFor(()=>cg!.getNodesByName('extra').length===1); + write('codegraph.json',config('second')); + await waitFor(()=>cg!.getNodesByName('other').length===1&&cg!.getNodesByName('top').length===0); + write('alt/inc/flags.txt','`define SECOND\n'); + await waitFor(()=>cg!.getNodesByName('second').length===1); + expect(errors).toEqual([]); + await waitFor(()=>cg!.getHdlProfileStatus()?.state==='matches'); + } finally { cg?.close(); fs.rmSync(root,{recursive:true,force:true}); } +}); + +it('does not admit reserved or outside paths from untrusted indexed dependency metadata', () => { + const {root} = project(); + const scope = new HdlWatchScope(root, () => ['../outside/flags.inc','.codegraph-other/flags.inc','vendor/.git/config','.jj/state','private/flags.inc',123] as string[]); + try { + scope.refresh(); + expect(scope.matchesFile('private/flags.inc')).toBe(true); + expect(scope.matchesDirectory('outside')).toBe(false); + expect(scope.matchesDirectory('.codegraph-other')).toBe(false); + expect(scope.matchesFile('vendor/.git/config')).toBe(false); + expect(scope.matchesDirectory('.jj')).toBe(false); + } finally { fs.rmSync(root,{recursive:true,force:true}); } +}); diff --git a/__tests__/hdl-profile.test.ts b/__tests__/hdl-profile.test.ts new file mode 100644 index 000000000..2df1d9d13 --- /dev/null +++ b/__tests__/hdl-profile.test.ts @@ -0,0 +1,119 @@ +import { afterEach, beforeEach, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { loadHdlProfile } from '../src/hdl/profile'; + +let root: string; +beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-profile-')); }); +afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); }); +function write(file: string, content: string) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), content); +} +function config(profile: Record, extra: Record = {}) { + write('codegraph.json', JSON.stringify({ ...extra, hdl: { activeProfile: 'synth', profiles: { synth: profile } } })); +} +function active() { + const result = loadHdlProfile(root); + expect(result.status, result.diagnostics.join('\n')).toBe('active'); + return result.profile!; +} + +it('preserves no-HDL legacy behavior and reports malformed overall JSON separately', () => { + expect(loadHdlProfile(root)).toMatchObject({ status: 'none', profile: null, configPresent: false }); + write('codegraph.json', JSON.stringify({ exclude: ['cache'] })); + expect(loadHdlProfile(root)).toMatchObject({ status: 'none', configPresent: true }); + write('codegraph.json', '{'); + expect(loadHdlProfile(root)).toMatchObject({ status: 'invalid-config', configInvalid: true, profile: null }); +}); + +it('loads explicit files in order with root-relative canonical identity and no implicit includes as source files', () => { + write('rtl/a.sv', 'module a; endmodule'); write('rtl/b.v', 'module b; endmodule'); write('inc/defs.svh', '`define W 2'); + config({ files: ['rtl/b.v', './rtl/a.sv', 'rtl/b.v'], includeDirs: ['inc'], defines: { SYNTHESIS: '1' }, topModules: ['a'], languageMode: 'systemverilog' }); + expect(active()).toMatchObject({ name: 'synth', files: ['rtl/b.v', 'rtl/a.sv'], includeDirs: ['inc'], defines: { SYNTHESIS: '1' }, topModules: ['a'], languageMode: 'systemverilog', dependencies: [] }); +}); + +it('treats an explicitly empty profile as an empty design', () => { + write('unrelated.sv', 'module unrelated; endmodule'); config({ files: [] }); + expect(active().files).toEqual([]); +}); + +it('handles nested -f/-F bases, quotes, comments and multiple include/define entries without executing anything', () => { + write('rtl/a.sv', 'module a; endmodule'); write('lists/local file.sv', 'module local_file; endmodule'); + write('inc one/defs.svh', ''); write('inc+two/defs.svh', ''); + write('root.f', '// comment\n+incdir+"inc one"+"inc+two"\n+define+ONE+BITS=1\'b1+TEXT="a+b c"\n-f lists/rootpaths.f\n-F lists/local.F\n'); + write('lists/rootpaths.f', '# comment\nrtl/a.sv /* end comment */\n'); + write('lists/local.F', '"local file.sv"\n'); + config({ files: [], filelists: ['root.f'] }); + const p = active(); + expect(p.files).toEqual(['rtl/a.sv', 'lists/local file.sv']); + expect(p.includeDirs).toEqual(['inc one', 'inc+two']); + expect(p.defines).toEqual({ ONE: '1', BITS: "1'b1", TEXT: 'a+b c' }); + expect(p.dependencies.map(d => d.path)).toEqual(['lists/local.F', 'lists/rootpaths.f', 'root.f']); + expect(p.dependencies.every(d => /^[a-f0-9]{64}$/.test(d.sha256))).toBe(true); +}); + +it('uses top-level .F paths relative to that list and rejects source paths missing under -f root semantics', () => { + write('lists/only.sv', 'module only; endmodule'); write('lists/relative.F', 'only.sv'); + config({ filelists: ['lists/relative.F'] }); expect(active().files).toEqual(['lists/only.sv']); + write('lists/root.f', 'only.sv'); config({ filelists: ['lists/root.f'] }); + expect(loadHdlProfile(root).status).toBe('invalid-profile'); +}); + +it.each([ + { files: 'rtl/a.sv' }, { files: [], unexpected: true }, { files: [], defines: { FLAG: true } }, + { files: [], languageMode: 'vhdl' }, { files: ['missing.sv'] }, +])('rejects invalid recognized HDL profiles instead of scanning everything: %j', profile => { + config(profile); expect(loadHdlProfile(root)).toMatchObject({ status: 'invalid-profile', profile: null, configInvalid: false }); +}); + +it.each(['-sv rtl/a.sv', '+libext+.v', '-f', '+incdir+', '+define+', '"unfinished', '/* unfinished'])('rejects unsupported/malformed filelist syntax: %s', text => { + write('list.f', text); config({ filelists: ['list.f'] }); + expect(loadHdlProfile(root)).toMatchObject({ status: 'invalid-profile', profile: null }); +}); + +it('rejects recursive filelists, conflicting defines and external/symlink escapes', () => { + write('a.f', '-f b.f'); write('b.f', '-f a.f'); config({ filelists: ['a.f'] }); + expect(loadHdlProfile(root).diagnostics.join()).toContain('cycle'); + write('a.f', '+define+FLAG=2'); config({ filelists: ['a.f'], defines: { FLAG: '1' } }); + expect(loadHdlProfile(root).diagnostics.join()).toContain('Conflicting'); + const external = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-profile-external-')); + try { + fs.writeFileSync(path.join(external, 'foreign.sv'), 'module foreign; endmodule'); + config({ files: [path.join(external, 'foreign.sv')] }); + expect(loadHdlProfile(root).diagnostics.join()).toContain('outside'); + fs.symlinkSync(external, path.join(root, 'escape'), 'dir'); config({ includeDirs: ['escape'], files: [] }); + expect(loadHdlProfile(root).diagnostics.join()).toContain('outside'); + } finally { fs.rmSync(external, { recursive: true, force: true }); } +}); + +it('fingerprints profile switching, filelist content and include configuration, preserving inactive/whitespace changes', () => { + write('a.sv', 'module a; endmodule'); write('src.f', 'a.sv\n'); write('inc/defs.svh', ''); write('alt/defs.svh', ''); + const cfg = { hdl: { activeProfile: 'synth', profiles: { synth: { filelists: ['src.f'], includeDirs: ['inc'] }, sim: { files: ['a.sv'] } } } }; + write('codegraph.json', JSON.stringify(cfg)); const a = active().fingerprint; + write('codegraph.json', JSON.stringify(cfg, null, 4)); expect(active().fingerprint).toBe(a); + cfg.hdl.profiles.sim.files = []; write('codegraph.json', JSON.stringify(cfg)); expect(active().fingerprint).toBe(a); + write('src.f', '// dependency edit\na.sv\n'); const b = active().fingerprint; expect(b).not.toBe(a); + cfg.hdl.profiles.synth.includeDirs = ['alt']; write('codegraph.json', JSON.stringify(cfg)); const c = active().fingerprint; expect(c).not.toBe(b); + cfg.hdl.activeProfile = 'sim'; write('codegraph.json', JSON.stringify(cfg)); expect(active().fingerprint).not.toBe(c); expect(active().files).toEqual([]); +}); + +it('enforces filelist resource limits', () => { + write('huge.f', ' '.repeat(1024 * 1024 + 1)); config({ filelists: ['huge.f'] }); + expect(loadHdlProfile(root).diagnostics.join()).toContain('1 MiB'); +}); + +it('rejects reserved VCS and CodeGraph files and include directories before indexing', () => { + write('.git/units.f',''); write('.codegraph-alt/unit.sv','module unit; endmodule'); write('.jj/headers/a.svh',''); + for (const profile of [{filelists:['.git/units.f']},{files:['.codegraph-alt/unit.sv']},{files:[],includeDirs:['.jj/headers']}]) { + config(profile); expect(loadHdlProfile(root).status).toBe('invalid-profile'); + expect(loadHdlProfile(root).diagnostics.join()).toContain('Reserved'); + } +}); + +it('rejects ordinary aliases resolving into reserved CodeGraph paths', () => { + write('.codegraph-cache/unit.sv','module unit; endmodule'); + fs.symlinkSync(path.join(root,'.codegraph-cache'),path.join(root,'alias'),'dir'); + config({files:['alias/unit.sv']}); expect(loadHdlProfile(root).diagnostics.join()).toContain('Reserved'); +}); diff --git a/__tests__/hdl-semantic-cli.test.ts b/__tests__/hdl-semantic-cli.test.ts new file mode 100644 index 000000000..9058c51ac --- /dev/null +++ b/__tests__/hdl-semantic-cli.test.ts @@ -0,0 +1,53 @@ +import { expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import CodeGraph from '../src/index'; + +it.skipIf(process.platform === 'win32')('compiled hdl-semantic CLI passes explicit overrides and rejects duplicate/invalid options', () => { + const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-semantic-cli-')); + const root = path.join(temp, 'project'); fs.mkdirSync(root); + const tool = path.join(temp, 'fake-slang'); + fs.writeFileSync(tool, `#!${process.execPath} +const fs=require('node:fs'),a=process.argv.slice(2),python=a[0]==='-I'; +if(a.includes('--version')) console.log(python?'codegraph pyslang 11.0.0 exporter 1 native '+'a'.repeat(64):'slang version 11.0.448'); +else { + const override=a[a.indexOf('-G')+1],value=override?.startsWith('W=')?override.slice(2):'8'; + const output=python?{codegraphSemanticVersion:1,facts:[{kind:'parameter',name:'W',instancePath:'top',value,sourceOrigin:'direct',source:{file:'top.sv',line:1,column:24}}]}: + {design:{kind:'Root',members:[{kind:'Instance',name:'top',body:{kind:'InstanceBody',members:[{kind:'Parameter',name:'W',value,type:{kind:'PredefinedIntegerType',name:'int'},source_file:'top.sv',source_line:1,source_column:24}]}}]}}; + fs.writeFileSync(a[a.indexOf('--ast-json')+1],JSON.stringify(output)); +} +`, { mode: 0o700 }); + fs.writeFileSync(path.join(root, 'top.sv'), 'module top #(parameter W=8)(); endmodule\n'); + fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ hdl: { activeProfile: 'synth', profiles: { + synth: { files: ['top.sv'], topModules: ['top'] }, + } } })); + const bin = path.resolve(__dirname, '../dist/bin/codegraph.js'); + const run = (args: string[]) => spawnSync(process.execPath, [bin, ...args], { + encoding: 'utf8', timeout: 30000, env: { ...process.env, CODEGRAPH_NO_DAEMON: '1', + CODEGRAPH_SEMANTIC_TMPDIR: path.join(temp, 'snapshots'), NO_COLOR: '1' }, + }); + try { + CodeGraph.initSync(root).close(); + const indexed = run(['index', root, '--quiet']); expect(indexed.status, indexed.stderr).toBe(0); + const result = run(['hdl-semantic', 'top.W', '--path', root, '--slang', tool, '--parameter', 'W=16']); + expect(result.status, result.stderr).toBe(0); + const body = JSON.parse(result.stdout); + expect(body.provenance.parameters).toEqual({ W: '16' }); + expect(body.provenance.compilationUnitMode).toBe('separate'); + expect(body.facts[0]).toMatchObject({ name: 'W', value: '16' }); + expect(body.facts[0].sourceNodeId).toBeTruthy(); + const mapped = run(['hdl-semantic', 'W', '--path', root, '--python', tool, '--parameter', 'W=12']); + expect(mapped.status, mapped.stderr).toBe(0); + expect(JSON.parse(mapped.stdout).provenance.frontend).toBe('pyslang'); + expect(JSON.parse(mapped.stdout).facts[0]).toMatchObject({ value: '12', sourceOrigin: 'direct' }); + const dual = run(['hdl-semantic', '--path', root, '--slang', tool, '--python', tool]); + expect(dual.status).toBe(1); expect(dual.stderr).toContain('exactly one'); + const duplicate = run(['hdl-semantic', '--path', root, '--slang', tool, '--parameter', 'W=1', 'W=2']); + expect(duplicate.status).toBe(1); expect(duplicate.stderr).toContain('Duplicate HDL parameter'); + const invalid = run(['hdl-semantic', '--path', root, '--slang', tool, '--limit', '1junk']); + expect(invalid.status).toBe(1); expect(invalid.stderr).toContain('1..1000'); + expect(fs.readdirSync(path.join(temp, 'snapshots'))).toHaveLength(0); + } finally { fs.rmSync(temp, { recursive: true, force: true }); } +}); diff --git a/__tests__/hdl-semantic-import.test.ts b/__tests__/hdl-semantic-import.test.ts new file mode 100644 index 000000000..a41abbaff --- /dev/null +++ b/__tests__/hdl-semantic-import.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from 'vitest'; +import { importSlangSemantics } from '../src/hdl/semantic-import'; +const scalar={kind:'ScalarType',name:'logic',addr:1,isSigned:false}; +const parameter=(name:string,value:string)=>({kind:'Parameter',name,value,type:{kind:'PredefinedIntegerType',name:'int'}}); +const instance=(name:string,body:unknown)=>({kind:'Instance',name,body}); +const body=(members:unknown[],addr?:number)=>({kind:'InstanceBody',name:'leaf',members,...(addr?{addr}:{})}); +const design=(...members:unknown[])=>({design:{kind:'Root',name:'$root',members}}); + +describe('bounded slang semantic importer',()=>{ + it('imports primitive facts with source points and detailed packed widths',()=>{ + const facts=importSlangSemantics(design(instance('top',body([ + {...parameter('N',"32'd2"),source_file:'rtl/top.sv',source_line:4,source_column:9}, + {kind:'Port',name:'q',direction:'Out',type:{kind:'PackedArrayType',name:'',elementType:scalar,range:'[15:0]'},source_file:'rtl/top.sv',source_line:8,source_column:0}, + ]))),'/project'); + expect(facts).toEqual([ + {kind:'parameter',name:'N',instancePath:'top',value:"32'd2",type:'int',width:32,source:{file:'rtl/top.sv',line:4,column:9}}, + {kind:'port',name:'q',instancePath:'top',type:'logic[15:0]',width:16,direction:'Out',source:{file:'rtl/top.sv',line:8,column:null}}, + ]); + }); + it('revisits shared bodies per instance and keeps specialized parameter values separate',()=>{ + const shared=body([parameter('P','2')],10); + const ast=design(instance('top',body([instance('u1',shared),instance('u2','10 leaf'),instance('u3',body([parameter('P','4')],11))]))); + expect(importSlangSemantics(ast,'/project').map(f=>[f.instancePath,f.value])).toEqual([['top.u1','2'],['top.u2','2'],['top.u3','4']]); + }); + it('uses implicit generate index values, not iteration ordinals, and skips uninstantiated branches',()=>{ + const loop={kind:'GenerateBlockArray',name:'lanes',loopVariable:'999 i',members:[{kind:'Genvar',name:'i'}, + {kind:'GenerateBlock',name:'',constructIndex:0,isUninstantiated:false,members:[parameter('i','2'),instance('u',body([parameter('P','8')]))]}, + {kind:'GenerateBlock',name:'',constructIndex:1,isUninstantiated:false,members:[parameter('i','4'),instance('u',body([parameter('P','8')]))]}, + ]}; + const ast=design(instance('top',body([loop,{kind:'GenerateBlock',name:'inactive',isUninstantiated:true,members:[instance('bad','404 missing')]}]))); + expect(importSlangSemantics(ast,'/project').filter(f=>f.name==='P').map(f=>f.instancePath)).toEqual(['top.lanes[2].u','top.lanes[4].u']); + }); + it('maps instance arrays in Slang numeric ascending element order',()=>{ + const array={kind:'InstanceArray',name:'a',range:'[3:2]',members:[instance('',body([parameter('P','10')])),instance('',body([parameter('P','20')]))]}; + expect(importSlangSemantics(design(instance('top',body([array]))),'/project').map(f=>[f.instancePath,f.value])).toEqual([['top.a[2]','10'],['top.a[3]','20']]); + }); + it('does not guess complex/unpacked widths or retain process addresses in type text',()=>{ + const ast=design(instance('top',body([ + {kind:'Port',name:'s',type:{kind:'PackedStructType',name:'Packet',members:[]}}, + {kind:'Port',name:'a',type:{kind:'FixedSizeUnpackedArrayType',name:'',elementType:scalar,range:'[7:0]'}}, + {kind:'Port',name:'pretty',type:'logic[7:0]'}, + {kind:'Port',name:'alias',type:{kind:'TypeAlias',name:'word_t',addr:5,target:{kind:'PackedArrayType',name:'',elementType:scalar,range:'[3:0]'}}}, + {kind:'Port',name:'ref',type:'5 top.word_t'}, + ]))); + const facts=importSlangSemantics(ast,'/project');expect(facts.slice(0,3).map(f=>f.width)).toEqual([undefined,undefined,undefined]); + expect(facts[4]).toMatchObject({type:'top.word_t',width:4});expect(JSON.stringify(facts)).not.toContain('5 top'); + }); + it('confines sources and preserves frontend Unicode columns without conversion',()=>{ + const facts=importSlangSemantics(design(instance('top',body([ + {...parameter('unicode','1'),source_file:'rtl/😀.sv',source_line:2,source_column:17}, + {...parameter('outside','1'),source_file:'../external.sv',source_line:2,source_column:17}, + {...parameter('absolute','1'),source_file:'/elsewhere/file.sv',source_line:2,source_column:17}, + {...parameter('windows','1'),source_file:'C:\\elsewhere\\file.sv',source_line:2,source_column:17}, + {...parameter('relativeWindows','1'),source_file:'..\\elsewhere\\file.sv',source_line:2,source_column:17}, + ]))),'/project'); + expect(facts[0]?.source).toEqual({file:'rtl/😀.sv',line:2,column:17});expect(facts.slice(1).map(f=>f.source)).toEqual([undefined,undefined,undefined,undefined]); + }); + it('does not treat Object prototype names as known integer widths',()=>{ + const facts=importSlangSemantics(design(instance('top',body([{kind:'Port',name:'x',type:'constructor'}]))),'/project'); + expect(facts[0]?.width).toBeUndefined(); + }); + it('escapes hierarchy segments that contain literal dots',()=>{ + const facts=importSlangSemantics(design(instance('top',body([instance('a.b',body([parameter('P','1')]))]))),'/project'); + expect(facts[0]?.instancePath).toBe('top.\\a.b '); + }); + it('rejects malformed schema, missing references, cycles and excessive hierarchy names',()=>{ + expect(()=>importSlangSemantics({},'/project')).toThrow(); + expect(()=>importSlangSemantics({design:{kind:'Root',members:new Array(1_000_001)}},'/project')).toThrow('node limit'); + expect(()=>importSlangSemantics(design(instance('top','99 missing')),'/project')).toThrow('unresolved'); + expect(()=>importSlangSemantics(design(instance('x'.repeat(4097),body([]))),'/project')).toThrow('oversized'); + const cyclic=body([],10);cyclic.members.push(instance('again','10 leaf')); + expect(()=>importSlangSemantics(design(instance('top',cyclic)),'/project')).toThrow('cyclic instance'); + const input:any=design();input.extra=input; + expect(()=>importSlangSemantics(input,'/project')).toThrow('cyclic input'); + }); + it('rejects ambiguous generated indices and array shape mismatches',()=>{ + expect(()=>importSlangSemantics(design(instance('top',body([{kind:'GenerateBlockArray',name:'g',loopVariable:'1 i',members:[{kind:'GenerateBlock',name:'',members:[]}]}]))),'/project')).toThrow('index parameter'); + expect(()=>importSlangSemantics(design(instance('top',body([{kind:'InstanceArray',name:'a',range:'[3:0]',members:[]}]))),'/project')).toThrow('length disagrees'); + }); +}); diff --git a/__tests__/hdl-semantic-runner.test.ts b/__tests__/hdl-semantic-runner.test.ts new file mode 100644 index 000000000..06b43e8f6 --- /dev/null +++ b/__tests__/hdl-semantic-runner.test.ts @@ -0,0 +1,144 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { runSlangSemantics } from '../src/hdl/semantic-runner'; + +const suite = process.platform==='win32' ? describe.skip : describe; +suite('isolated optional slang runner',()=>{ + let base:string,root:string,temp:string,exe:string,oldTemp:string|undefined; + const write=(file:string,text:string)=>{fs.mkdirSync(path.dirname(path.join(root,file)),{recursive:true});fs.writeFileSync(path.join(root,file),text);}; + const fake=(body='')=>{ + const script=`#!${process.execPath}\nconst fs=require('node:fs'),cp=require('node:child_process');const args=process.argv.slice(2);if(args.includes('--version')){console.log('slang version 11.0.448+e222e7dc0');process.exit(0);}const output=args[args.indexOf('--ast-json')+1];${body}\nfs.writeFileSync(output,JSON.stringify({kind:'Root',argv:args,source:fs.readFileSync('rtl/top.sv','utf8'),include:fs.readFileSync('inc/common.flags','utf8'),cwd:process.cwd()}));`; + fs.writeFileSync(exe,script,{mode:0o700}); + }; + const fakePython=(body='',version=`codegraph pyslang 11.0.0 exporter 1 native ${'a'.repeat(64)}`)=>{ + const script=`#!${process.execPath}\nconst fs=require('node:fs');const args=process.argv.slice(2);if(args.includes('--version')){console.log(${JSON.stringify(version)});process.exit(0);}if(args[0]!=='-I'||!args[1].endsWith('source-map-export.py'))process.exit(3);const output=args[args.indexOf('--ast-json')+1];${body}\nfs.writeFileSync(output,JSON.stringify({codegraphSemanticVersion:1,facts:[],argv:args,launcher:process.argv[1]}));`; + fs.writeFileSync(exe,script,{mode:0o700}); + }; + beforeEach(()=>{ + const storage=path.join(os.homedir(),'storage');fs.mkdirSync(storage,{recursive:true}); + base=fs.mkdtempSync(path.join(storage,'cg-semantic-test-'));root=path.join(base,'project');temp=path.join(base,'snapshots');exe=path.join(base,'fake-slang'); + fs.mkdirSync(root);oldTemp=process.env.CODEGRAPH_SEMANTIC_TMPDIR;process.env.CODEGRAPH_SEMANTIC_TMPDIR=temp; + write('rtl/top.sv','`include "../inc/common.flags"\nmodule top; endmodule\n');write('inc/common.flags','`define VALUE 1\n'); + write('codegraph.json',JSON.stringify({hdl:{activeProfile:'synth',profiles:{synth:{files:['rtl/top.sv'],includeDirs:['inc'],topModules:['top'],defines:{SAFE:'1'}}}}}));fake(); + }); + afterEach(()=>{if(oldTemp===undefined)delete process.env.CODEGRAPH_SEMANTIC_TMPDIR;else process.env.CODEGRAPH_SEMANTIC_TMPDIR=oldTemp;fs.rmSync(base,{recursive:true,force:true});}); + it('requires exactly one explicit backend selector',async()=>{ + await expect(runSlangSemantics(root,{})).rejects.toThrow('exactly one'); + await expect(runSlangSemantics(root,{executable:exe,pythonExecutable:exe})).rejects.toThrow('exactly one'); + }); + it('runs only isolated Python and records exporter/native hashes with normalized facts',async()=>{ + fakePython(); + const launcher=path.join(base,'venv/bin/python');fs.mkdirSync(path.dirname(launcher),{recursive:true});fs.symlinkSync(exe,launcher); + const result=await runSlangSemantics(root,{pythonExecutable:launcher,parameters:{WIDTH:'4'}}); + expect(result.frontend).toBe('pyslang');expect(result.exporterSha256).toMatch(/^[a-f0-9]{64}$/);expect(result.librarySha256).toBe('a'.repeat(64)); + expect((result.ast as any).codegraphSemanticVersion).toBe(1);expect((result.ast as any).argv[0]).toBe('-I'); + expect((result.ast as any).launcher).toBe(launcher);expect((result.ast as any).argv).toContain('WIDTH=4');expect(fs.readdirSync(temp)).toEqual([]); + }); + it('rejects unsupported Python exporter/library versions and malformed envelopes',async()=>{ + fakePython('','codegraph pyslang 12.0.0 exporter 1 native '+ 'a'.repeat(64)); + await expect(runSlangSemantics(root,{pythonExecutable:exe})).rejects.toThrow('schema family'); + fakePython("fs.writeFileSync(output,JSON.stringify({codegraphSemanticVersion:2,facts:[]}));process.exit(0);"); + await expect(runSlangSemantics(root,{pythonExecutable:exe})).rejects.toThrow('exporter envelope'); + }); + it('rejects a Python native library fingerprint that changes across the run',async()=>{ + const marker=path.join(base,'library-changed'); + const script=`#!${process.execPath}\nconst fs=require('node:fs'),args=process.argv.slice(2);if(args.includes('--version')){console.log('codegraph pyslang 11.0.0 exporter 1 native '+(fs.existsSync(${JSON.stringify(marker)})?'b':'a').repeat(64));process.exit(0);}fs.writeFileSync(${JSON.stringify(marker)},'changed');fs.writeFileSync(args[args.indexOf('--ast-json')+1],JSON.stringify({codegraphSemanticVersion:1,facts:[]}));`; + fs.writeFileSync(exe,script,{mode:0o700});await expect(runSlangSemantics(root,{pythonExecutable:exe})).rejects.toThrow('library changed'); + expect(fs.readdirSync(temp)).toEqual([]); + }); + it('applies the same output and cancellation bounds to Python',async()=>{ + fakePython("process.stdout.write('x'.repeat(1024*1024+1));setInterval(()=>{},1000);return;"); + await expect(runSlangSemantics(root,{pythonExecutable:exe})).rejects.toThrow('output exceeds'); + fakePython("setInterval(()=>{},1000);return;"); + const controller=new AbortController(),pending=runSlangSemantics(root,{pythonExecutable:exe,signal:controller.signal}); + setTimeout(()=>controller.abort(),100);await expect(pending).rejects.toThrow('cancelled');expect(fs.readdirSync(temp)).toEqual([]); + }); + it('copies literal headers and argv safely, returns provenance/source map and removes snapshots',async()=>{ + const result=await runSlangSemantics(root,{executable:exe,parameters:{WIDTH:'8'},allowUseBeforeDeclare:true}); + const ast=result.ast as any; + expect(ast.source).toBe(result.sources['rtl/top.sv']);expect(ast.include).toBe(result.sources['inc/common.flags']); + expect(ast.argv).toContain('-DSAFE=1');expect(ast.argv).toContain('WIDTH=8');expect(ast.argv).toContain('--allow-use-before-declare'); + expect(result.configurationFingerprint).toMatch(/^[a-f0-9]{64}$/);expect(result.executableSha256).toMatch(/^[a-f0-9]{64}$/); + expect(result.sourceRoot).toBe(ast.cwd);expect(fs.existsSync(result.sourceRoot)).toBe(false);expect(fs.readdirSync(temp)).toEqual([]); + expect((await runSlangSemantics(root,{executable:exe,parameters:{WIDTH:'8'},allowUseBeforeDeclare:true})).fingerprint).toBe(result.fingerprint); + }); + it('captures caller options before awaits and records the explicit compilation mode',async()=>{ + const replacement=new AbortController();replacement.abort(); + const options={executable:exe,allowUseBeforeDeclare:false,parameters:{WIDTH:'4'},signal:new AbortController().signal}; + const pending=runSlangSemantics(root,options); + options.allowUseBeforeDeclare=true;options.parameters.WIDTH='9';options.signal=replacement.signal; + const result=await pending;expect(result.allowUseBeforeDeclare).toBe(false);expect(result.parameters.WIDTH).toBe('4'); + expect(result.languageStandard).toBe('1800-2023');expect(result.compilationUnitMode).toBe('separate'); + expect((result.ast as any).argv).not.toContain('--allow-use-before-declare'); + expect((result.ast as any).argv).toContain('--max-generate-steps=10000'); + }); + it('rejects include directives injected through configured macro values',async()=>{ + const file=path.join(root,'codegraph.json'),cfg=JSON.parse(fs.readFileSync(file,'utf8')); + cfg.hdl.profiles.synth.defines.LOAD='`include "/etc/passwd"';fs.writeFileSync(file,JSON.stringify(cfg)); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('Absolute include'); + }); + it('preserves UTF-8 BOM and source-relative headers outside include directories',async()=>{ + write('rtl/top.sv','\ufeff`include "../private/local.inc"\nmodule top; endmodule\n');write('private/local.inc','// header\n'); + const result=await runSlangSemantics(root,{executable:exe});expect(result.sources['rtl/top.sv'].charCodeAt(0)).toBe(0xfeff); + expect(result.sources['private/local.inc']).toBe('// header\n'); + }); + it('requires an available approved version and an explicit profile/top',async()=>{ + await expect(runSlangSemantics(root,{executable:path.join(base,'missing')})).rejects.toThrow('unavailable'); + fs.writeFileSync(exe,`#!${process.execPath}\nconsole.log('slang version 12.0');`,{mode:0o700}); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('schema family'); + write('codegraph.json','{}');await expect(runSlangSemantics(root,{executable:exe,top:'top'})).rejects.toThrow('active HDL profile'); + }); + it.each(['`include "/etc/passwd"','`include "../../../outside"','`include `FILE'])('rejects unsafe/unresolved snapshot include forms %s',async line=>{ + write('rtl/top.sv',line+'\nmodule top; endmodule');await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow(/include|escapes/); + }); + it('rejects symlink headers and oversized individual inputs',async()=>{ + fs.symlinkSync(path.join(base,'external'),path.join(root,'inc/link.flags')); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('Symbolic links'); + fs.unlinkSync(path.join(root,'inc/link.flags'));write('inc/large.flags','x'.repeat(1024*1024+1)); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('input size'); + }); + it('rejects original-content changes even with restored size/mtime',async()=>{ + const file=path.join(root,'inc/common.flags'); + fake(`const p=${JSON.stringify(file)},s=fs.statSync(p);fs.writeFileSync(p,'\x60define VALUE 2\\n');fs.utimesSync(p,s.atime,s.mtime);`); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('sources or configuration changed'); + expect(fs.readdirSync(temp)).toEqual([]); + }); + it('rejects creation of a previously missing source-relative shadow candidate',async()=>{ + write('rtl/top.sv','`include "common.flags"\nmodule top; endmodule'); + fake(`fs.writeFileSync(${JSON.stringify(path.join(root,'rtl/common.flags'))},'// new shadow');`); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('sources or configuration changed'); + }); + it('rejects a frontend that edits the snapshot, malformed AST and oversized output',async()=>{ + fake("fs.writeFileSync('rtl/top.sv','module changed; endmodule');"); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('modified its source snapshot'); + fake("fs.writeFileSync(output,'not JSON');process.exit(0);"); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow(); + fake("fs.writeFileSync(output,'');fs.truncateSync(output,64*1024*1024+1);process.exit(0);"); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow(/oversized|exceeds/); + expect(fs.readdirSync(temp)).toEqual([]); + }); + it('times out and cancels its own process group without leaving snapshots',async()=>{ + fake("setInterval(()=>{},1000);return;"); + await expect(runSlangSemantics(root,{executable:exe,timeoutMs:100})).rejects.toThrow('timed out'); + const controller=new AbortController();const promise=runSlangSemantics(root,{executable:exe,signal:controller.signal}); + setTimeout(()=>controller.abort(),100);await expect(promise).rejects.toThrow('cancelled');expect(fs.readdirSync(temp)).toEqual([]); + }); + it('cancels descendants in the owned process group',async()=>{ + const heartbeat=path.join(base,'heartbeat'); + const childCode=`setInterval(()=>require('node:fs').appendFileSync(${JSON.stringify(heartbeat)},'x'),20)`; + fake(`cp.spawn(process.execPath,['-e',${JSON.stringify(childCode)}],{stdio:'ignore'});setInterval(()=>{},1000);return;`); + const controller=new AbortController(),pending=runSlangSemantics(root,{executable:exe,signal:controller.signal}); + const end=Date.now()+4000;while(!fs.existsSync(heartbeat)){if(Date.now()>end)throw new Error('descendant did not start');await new Promise(r=>setTimeout(r,20));} + controller.abort();await expect(pending).rejects.toThrow('cancelled'); + const before=fs.statSync(heartbeat).size;await new Promise(r=>setTimeout(r,100));expect(fs.statSync(heartbeat).size).toBe(before); + expect(fs.readdirSync(temp)).toEqual([]); + }); + it('bounds stdout/stderr and rejects frontend errors',async()=>{ + fake("process.stdout.write('x'.repeat(1024*1024+1));setInterval(()=>{},1000);return;"); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('output exceeds'); + fake("process.stderr.write('invalid design');process.exit(2);"); + await expect(runSlangSemantics(root,{executable:exe})).rejects.toThrow('invalid design'); + }); +}); diff --git a/__tests__/hdl-semantics-review.test.ts b/__tests__/hdl-semantics-review.test.ts new file mode 100644 index 000000000..9cb419164 --- /dev/null +++ b/__tests__/hdl-semantics-review.test.ts @@ -0,0 +1,57 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { createHash } from 'node:crypto'; +import type { Node, FileRecord } from '../src/types'; +import type { HdlProfileStatus } from '../src/hdl/status'; +import { analyzeHdlSemantics, type HdlSemanticOptions, type HdlSemanticSourceGraph } from '../src/hdl/semantics'; +const { run }=vi.hoisted(()=>({run:vi.fn()})); +vi.mock('../src/hdl/semantic-runner',()=>({runSlangSemantics:run})); +afterEach(()=>run.mockReset()); +const text='module top #(parameter W = 8)(input logic data); endmodule'; +function result(source=text,column=24,kind:'Parameter'|'Port'='Parameter',name='W') { + return {ast:{design:{kind:'Root',members:[{kind:'Instance',name:'top',body:{kind:'InstanceBody',members:[{ + kind,name,...(kind==='Parameter'?{value:'8',type:'int'}:{type:'logic',direction:'In'}), + source_file:'/snapshot/top.sv',source_line:1,source_column:column, + }]}}]}},sourceRoot:'/snapshot',profileName:'p',configurationFingerprint:'cfg',fingerprint:'captured', + version:'slang fixture',executableSha256:'sha',sources:{'top.sv':source},top:'top',parameters:{W:'8'},allowUseBeforeDeclare:false,diagnostics:[]}; +} +function sourceGraph(nodes:Partial[],source=text):HdlSemanticSourceGraph { + return {profile:()=>({state:'matches',indexed:{configurationFingerprint:'cfg'}} as HdlProfileStatus),stale:()=>false, + file:()=>({contentHash:createHash('sha256').update(source).digest('hex')} as FileRecord), + nodes:()=>nodes.map(n=>({id:'node',name:'W',startLine:1,filePath:'top.sv',language:'verilog',...n} as Node))}; +} + +it('reports the captured compiler options and initial query even if the caller mutates options during the run',async()=>{ + const options:HdlSemanticOptions={executable:'slang',query:'top.W',parameters:{W:'8'},allowUseBeforeDeclare:false}; + run.mockImplementation(async()=>{ + options.parameters!.W='12';options.allowUseBeforeDeclare=true;options.query='missing'; + return result(); + }); + const value=await analyzeHdlSemantics('/project',options,sourceGraph([{kind:'constant'}])); + expect(value.facts).toHaveLength(1); + expect(value.facts[0]!.value).toBe('8'); + expect(value.provenance.parameters).toEqual({W:'8'}); + expect(value.provenance.allowUseBeforeDeclare).toBe(false); +}); + +it.each([ + {kind:'Parameter' as const,name:'W',column:24,nodes:[{kind:'function' as const}],linked:false}, + {kind:'Port' as const,name:'data',column:42,nodes:[{kind:'field' as const,name:'data'}],linked:false}, + {kind:'Port' as const,name:'data',column:42,nodes:[{kind:'field' as const,name:'data',decorators:['hdl:port']}],linked:true}, + {kind:'Parameter' as const,name:'W',column:100000,nodes:[{kind:'constant' as const}],linked:false}, + {kind:'Parameter' as const,name:'W',column:0,nodes:[{kind:'constant' as const}],linked:false}, + {kind:'Parameter' as const,name:'W',column:24,nodes:[{id:'one',kind:'constant' as const},{id:'two',kind:'constant' as const}],linked:false}, +])('links only compatible unambiguous source declarations: %j',async({kind,name,column,nodes,linked})=>{ + run.mockResolvedValue(result(text,column,kind,name)); + const value=await analyzeHdlSemantics('/project',{executable:'slang'},sourceGraph(nodes)); + expect(!!value.facts[0]!.sourceNodeId).toBe(linked); +}); + +it('accepts frontend byte coordinates beyond the UTF-16 line length without reinterpreting their units',async()=>{ + const unicode='/* '+'😀'.repeat(40)+' */ '+text; + const column=Buffer.byteLength(unicode.slice(0,unicode.indexOf('W')))+1; + expect(column).toBeGreaterThan(unicode.length); + run.mockResolvedValue(result(unicode,column)); + const value=await analyzeHdlSemantics('/project',{executable:'slang'},sourceGraph([{kind:'constant'}],unicode)); + expect(value.facts[0]!.sourceNodeId).toBe('node'); + expect(value.facts[0]!.source?.column).toBe(column); +}); diff --git a/__tests__/hdl-semantics.test.ts b/__tests__/hdl-semantics.test.ts new file mode 100644 index 000000000..bce988a0c --- /dev/null +++ b/__tests__/hdl-semantics.test.ts @@ -0,0 +1,72 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import CodeGraph from '../src/index'; +import { loadHdlProfile } from '../src/hdl/profile'; +const { run } = vi.hoisted(() => ({ run: vi.fn() })); +vi.mock('../src/hdl/semantic-runner', () => ({ runSlangSemantics: run })); + +let root: string; +let cg: CodeGraph; +const source = 'module top #(parameter W = 8)(input logic [W-1:0] data);\nendmodule\n'; +function ast(width = 8, column: number = 24) { + return { design: { kind: 'Root', name: '$root', members: [{ kind: 'Instance', name: 'top', body: { + kind: 'InstanceBody', name: 'top', members: [ + { kind: 'Parameter', name: 'W', value: String(width), type: { kind: 'PredefinedIntegerType', name: 'int' }, + source_file: path.join(root, 'top.sv'), source_line: 1, source_column: column }, + { kind: 'Port', name: 'data', type: { kind: 'PackedArrayType', range: `[${width - 1}:0]`, elementType: { kind: 'ScalarType', name: 'logic' } }, + direction: 'In', source_file: path.join(root, 'top.sv'), source_line: 1, source_column: 49 }, + ], + } }] } }; +} +function result(value = ast()) { + return { ast: value, sourceRoot: root, profileName: 'synth', configurationFingerprint: loadHdlProfile(root).profile!.fingerprint, + fingerprint: 'semantic-snapshot', version: 'slang version 11.0.448', executableSha256: 'executable-hash', + sources: { 'top.sv': source }, top: 'top', diagnostics: [], parameters: {}, allowUseBeforeDeclare: false }; +} +beforeEach(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-semantics-')); + fs.writeFileSync(path.join(root, 'top.sv'), source); + fs.writeFileSync(path.join(root, 'codegraph.json'), JSON.stringify({ hdl: { activeProfile: 'synth', profiles: { + synth: { files: ['top.sv'], topModules: ['top'] }, + } } })); + cg = await CodeGraph.init(root, { index: true }); + run.mockResolvedValue(result()); +}); +afterEach(() => { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); run.mockReset(); }); + +describe('on-demand HDL semantics on the source graph', () => { + it('returns computed facts and links the matching source declaration', async () => { + const value = await cg.getHdlSemantics({ executable: 'slang', query: 'top.W' }); + expect(value.sourceGraphProfileMatches).toBe(true); + expect(value.facts).toHaveLength(1); + expect(value.facts[0]).toMatchObject({ name: 'W', value: '8', sourceNodeId: cg.getNodesByName('W')[0].id }); + expect(value.provenance.top).toBe('top'); + }); + it('does not link an index from another profile or different source bytes', async () => { + const changed = result(); changed.configurationFingerprint = 'different-profile'; + run.mockResolvedValue(changed); + expect((await cg.getHdlSemantics({ executable: 'slang' })).facts.every(f => !f.sourceNodeId)).toBe(true); + run.mockResolvedValue({ ...result(), sources: { 'top.sv': source.replace('8', '9') } }); + expect((await cg.getHdlSemantics({ executable: 'slang' })).facts.every(f => !f.sourceNodeId)).toBe(true); + }); + it('preserves unknown macro columns, exact queries and truncation', async () => { + run.mockResolvedValue(result(ast(8, 0))); + const value = await cg.getHdlSemantics({ executable: 'slang', query: 'top', limit: 1 }); + expect(value.totalMatches).toBe(2); expect(value.truncated).toBe(true); + expect(value.facts[0].source?.column).toBeNull(); expect(value.facts[0].sourceNodeId).toBeUndefined(); + expect((await cg.getHdlSemantics({ executable: 'slang', query: 'missing' })).facts).toHaveLength(0); + await expect(cg.getHdlSemantics({ executable: 'slang', limit: 0 })).rejects.toThrow('1..1000'); + }); + it('recomputes overrides and keeps source graph after compiler failure', async () => { + run.mockResolvedValue(result(ast(12))); + expect((await cg.getHdlSemantics({ executable: 'slang', parameters: { W: '12' }, query: 'W' })).facts[0].value).toBe('12'); + expect(run.mock.calls[0][1].parameters).toEqual({ W: '12' }); + const before = cg.getNodesByName('W'); + run.mockRejectedValue(new Error('slang unavailable')); + await expect(cg.getHdlSemantics({ executable: 'missing' })).rejects.toThrow('unavailable'); + expect(cg.getNodesByName('W')).toEqual(before); + expect(cg.getHdlProfileStatus()?.state).toBe('matches'); + }); +}); diff --git a/__tests__/hdl-source-map-asset.test.ts b/__tests__/hdl-source-map-asset.test.ts new file mode 100644 index 000000000..6888c3847 --- /dev/null +++ b/__tests__/hdl-source-map-asset.test.ts @@ -0,0 +1,8 @@ +import { expect, it } from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +it('ships the Python exporter byte-for-byte with the compiled engine', () => { + const root = path.resolve(__dirname, '..'); + expect(fs.readFileSync(path.join(root, 'dist/hdl/source-map-export.py'))) + .toEqual(fs.readFileSync(path.join(root, 'src/hdl/source-map-export.py'))); +}); diff --git a/__tests__/hdl-source-map-import.test.ts b/__tests__/hdl-source-map-import.test.ts new file mode 100644 index 000000000..7d9da77f6 --- /dev/null +++ b/__tests__/hdl-source-map-import.test.ts @@ -0,0 +1,41 @@ +import { expect, it } from 'vitest'; +import { importMappedSemantics } from '../src/hdl/source-map-import'; +const sources = { 'inc/ports.svh': '`define P(N) input logic N\r\n', 'top.sv': '// Ω\r\nmodule top(`P(data)); endmodule\r\n' }; +const point = (file: keyof typeof sources, offset: number) => { + const prefix = Buffer.from(sources[file]).subarray(0, offset); + const line = [...prefix].filter(v => v === 10).length + 1; + return { file, line, column: offset - prefix.lastIndexOf(10), byteOffset: offset }; +}; +const start = Buffer.from(sources['top.sv']).indexOf('`P'); +const payload = () => ({ codegraphSemanticVersion: 1, facts: [{ kind: 'port', name: 'data', instancePath: 'top', type: 'logic', width: 1, + sourceOrigin: 'macro', source: { file: 'top.sv', line: 2, column: 15 }, macroExpansionComplete: true, + macroExpansion: [{ name: 'P', argument: false, spelling: point('inc/ports.svh', 24), invocation: { start: point('top.sv', start), end: point('top.sv', start + 8) } }], +}] }); +it('preserves compiler byte coordinates and macro call/spelling across headers, UTF8 and CRLF', () => { + const result = importMappedSemantics(payload(), sources); + expect(result[0].macroExpansion?.[0]).toEqual(payload().facts[0]!.macroExpansion[0]); + expect(result[0].sourceOrigin).toBe('macro'); +}); +it('rejects tampered source paths, byte offsets, reversed ranges and UTF8 continuation positions', () => { + let p = payload(); p.facts[0]!.macroExpansion[0]!.spelling.file = '../secret' as any; + expect(() => importMappedSemantics(p, sources)).toThrow('outside snapshot'); + p = payload(); p.facts[0]!.macroExpansion[0]!.spelling.byteOffset++; + expect(() => importMappedSemantics(p, sources)).toThrow('disagree'); + p = payload(); const range = p.facts[0]!.macroExpansion[0]!.invocation; [range.start, range.end] = [range.end, range.start]; + expect(() => importMappedSemantics(p, sources)).toThrow('order'); + p = payload(); p.facts[0]!.macroExpansion[0]!.spelling = point('top.sv', 4); + expect(() => importMappedSemantics(p, sources)).toThrow('UTF-8'); +}); +it('does not accept invented complete chains or duplicate identities', () => { + const p = payload(); p.facts[0]!.macroExpansion = []; + expect(() => importMappedSemantics(p, sources)).toThrow('complete'); + p.facts[0]!.macroExpansionComplete = false; + expect(importMappedSemantics(p, sources)[0].macroExpansionComplete).toBe(false); + p.facts.push(p.facts[0]!); + expect(() => importMappedSemantics(p, sources)).toThrow('duplicate'); +}); +it('rejects line/offset disagreement at a newline and unsupported schema', () => { + const p = payload(); p.facts[0]!.macroExpansion[0]!.spelling = { file: 'top.sv', line: 1, column: 8, byteOffset: 7 }; + expect(() => importMappedSemantics(p, sources)).toThrow('column'); + expect(() => importMappedSemantics({ codegraphSemanticVersion: 2, facts: [] }, sources)).toThrow('schema'); +}); diff --git a/__tests__/hdl-source-map-review.test.ts b/__tests__/hdl-source-map-review.test.ts new file mode 100644 index 000000000..1c448ba20 --- /dev/null +++ b/__tests__/hdl-source-map-review.test.ts @@ -0,0 +1,67 @@ +import { afterEach, expect, it, vi } from 'vitest'; +import { createHash } from 'node:crypto'; +import { importMappedSemantics } from '../src/hdl/source-map-import'; +import { analyzeHdlSemantics, type HdlSemanticSourceGraph } from '../src/hdl/semantics'; +import type { FileRecord, Node } from '../src/types'; +import type { HdlProfileStatus } from '../src/hdl/status'; +const {run}=vi.hoisted(()=>({run:vi.fn()})); +vi.mock('../src/hdl/semantic-runner',()=>({runSlangSemantics:run})); +afterEach(()=>run.mockReset()); +const source='// Ω\r\nmodule top(input logic data);\r\nendmodule\r\n'; +const sources={'top.sv':source}; +const fact=()=>({kind:'port',name:'data',instancePath:'top',width:1,direction:'In',sourceOrigin:'direct', + source:{file:'top.sv',line:2,column:24}}); +const payload=(item:unknown=fact())=>({codegraphSemanticVersion:1,facts:[item]}); +const graph:HdlSemanticSourceGraph={profile:()=>({state:'matches',indexed:{configurationFingerprint:'cfg'}} as HdlProfileStatus),stale:()=>false, + file:()=>({contentHash:createHash('sha256').update(source).digest('hex')} as FileRecord), + nodes:()=>[{id:'decl-data',kind:'field',name:'data',startLine:2,decorators:['hdl:port']} as Node]}; +function result(ast:unknown){return {ast,frontend:'pyslang',sourceRoot:'/deleted-snapshot',profileName:'p',configurationFingerprint:'cfg',fingerprint:'snapshot', + version:'pyslang fixture',executableSha256:'python-sha',exporterSha256:'exporter-sha',librarySha256:'native-sha',sources,top:'top',parameters:{},allowUseBeforeDeclare:false, + diagnostics:[],languageStandard:'1800-2023',compilationUnitMode:'separate',runnerVersion:'review',compilerLimits:[]};} + +it('never links macro-origin facts despite precise same-point coordinates, while direct declarations can link',async()=>{ + run.mockResolvedValue(result(payload())); + const direct=await analyzeHdlSemantics('/project',{pythonExecutable:'python'},graph); + expect(direct.facts[0]!.sourceNodeId).toBe('decl-data'); + run.mockResolvedValue(result(payload({...fact(),sourceOrigin:'macro',macroExpansion:[],macroExpansionComplete:false}))); + const macro=await analyzeHdlSemantics('/project',{pythonExecutable:'python'},graph); + expect(macro.facts[0]!.source).toEqual(direct.facts[0]!.source); + expect(macro.facts[0]!.sourceOrigin).toBe('macro'); + expect(macro.facts[0]!.sourceNodeId).toBeUndefined(); + expect(macro.provenance).toMatchObject({frontend:'pyslang',exporterSha256:'exporter-sha',librarySha256:'native-sha'}); +}); + +it('normalizes away unsolicited source links and does not trust unknown snapshot filenames',()=>{ + const normalized=importMappedSemantics(payload({...fact(),sourceNodeId:'forged',extra:'untrusted'}),sources)[0]!; + expect(normalized).not.toHaveProperty('sourceNodeId');expect(normalized).not.toHaveProperty('extra'); + for(const file of ['../top.sv','./top.sv','top.sv/../top.sv','__proto__','C:/top.sv','top\\file.sv']) { + expect(()=>importMappedSemantics(payload({...fact(),source:{...fact().source,file}}),sources)).toThrow('snapshot'); + } +}); + +it('rejects UTF8 continuation and previous-line aliases of the next CRLF line, including macro byte-offset tampering',()=>{ + expect(()=>importMappedSemantics(payload({...fact(),source:{file:'top.sv',line:1,column:5}}),sources)).toThrow('UTF-8'); + expect(()=>importMappedSemantics(payload({...fact(),source:{file:'top.sv',line:1,column:8}}),sources)).toThrow('column'); + const point={file:'top.sv',line:2,column:1,byteOffset:7}; + const mapped={...fact(),sourceOrigin:'macro',macroExpansionComplete:false, + macroExpansion:[{name:'P',argument:false,spelling:point,invocation:{start:point,end:point}}]}; + expect(importMappedSemantics(payload(mapped),sources)[0]!.macroExpansion![0]!.spelling!.byteOffset).toBe(7); + expect(()=>importMappedSemantics(payload({...mapped,macroExpansionComplete:true}),sources)).toThrow('complete'); + const tampered={...mapped,macroExpansion:[{...mapped.macroExpansion[0]!,spelling:{...point,byteOffset:8}}]}; + expect(()=>importMappedSemantics(payload(tampered),sources)).toThrow('disagree'); +}); + +it('bounds normalized fact counts, fields and chains before accepting exporter output',()=>{ + expect(()=>importMappedSemantics({codegraphSemanticVersion:1,facts:Array(100001).fill(fact())},sources)).toThrow('count'); + expect(()=>importMappedSemantics(payload({...fact(),value:'x'.repeat(65537)}),sources)).toThrow('value'); + expect(()=>importMappedSemantics(payload({...fact(),sourceOrigin:'macro',macroExpansionComplete:false,macroExpansion:Array(129).fill({name:null,argument:false})}),sources)).toThrow('chain'); + expect(()=>importMappedSemantics(payload({...fact(),sourceOrigin:'macro',macroExpansionComplete:true,macroExpansion:[]}),sources)).toThrow('complete'); +}); + +it('links direct typedefs by type_alias kind, never through a same-name field',async()=>{ + const typeFact={...fact(),kind:'type',type:'logic[31:0]',width:32}; + run.mockResolvedValue(result(payload(typeFact))); + expect((await analyzeHdlSemantics('/project',{pythonExecutable:'python'},graph)).facts[0]!.sourceNodeId).toBeUndefined(); + const types={...graph,nodes:()=>[{id:'type-data',kind:'type_alias',name:'data',startLine:2} as Node]}; + expect((await analyzeHdlSemantics('/project',{pythonExecutable:'python'},types)).facts[0]!.sourceNodeId).toBe('type-data'); +}); diff --git a/__tests__/hdl-wildcard-ports.test.ts b/__tests__/hdl-wildcard-ports.test.ts new file mode 100644 index 000000000..5a783e5c1 --- /dev/null +++ b/__tests__/hdl-wildcard-ports.test.ts @@ -0,0 +1,131 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +async function graph(files: Record, check: (cg: CodeGraph, root: string) => Promise | void): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-wildcard-')); + let cg: CodeGraph | undefined; + try { + for (const [file, source] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), source); + } + cg = CodeGraph.initSync(root); + expect((await cg.indexAll()).success).toBe(true); + await check(cg, root); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +} +function wildcard(cg: CodeGraph, qname = 'top::u::*') { + const node = cg.getNodesByName('*').find(n => n.qualifiedName === qname)!; + expect(node).toBeDefined(); + return cg.getCallees(node.id).filter(e => e.edge.metadata?.binding === 'hdl-wildcard-port') + .map(e => ({ target: e.node.qualifiedName, endpoint: e.edge.metadata!.endpoint, + port: e.edge.metadata!.portName })).sort((a, b) => a.target.localeCompare(b.target)); +} +const pair = (name: string) => [ + { target: `leaf::${name}`, endpoint: 'formal', port: name }, + { target: `top::${name}`, endpoint: 'actual', port: name }, +]; + +it('pairs wildcard ports only with the exact local signal and keeps endpoint identity', async () => { + await graph({ 'leaf.sv': 'module leaf(input rx,output tx,input missing); endmodule', + 'top.sv': 'module top(input rx,output tx,input extra); leaf u(.*); endmodule', + 'other.sv': 'module other(input missing); endmodule' }, cg => { + expect(wildcard(cg)).toEqual([...pair('rx'), ...pair('tx')].sort((a, b) => a.target.localeCompare(b.target))); + }); +}); + +it('explicit named and open connections override .* independently of source order', async () => { + await graph({ 'leaf.sv': 'module leaf(input a,b,c); endmodule', + 'top.sv': 'module top(input a,b,c,x); leaf u(.a(x),.*, .b()); endmodule' }, cg => { + expect(wildcard(cg)).toEqual(pair('c')); + }); +}); + +it('does not bind an outer signal through an enclosing genvar or parameter', async () => { + await graph({ 'leaf.sv': 'module leaf(input i,input a); endmodule', + 'top.sv': 'module top(input i,input a); for(genvar i=0;i<2;i++) begin: lanes localparam a=1; leaf u(.*); end endmodule' }, cg => { + expect(wildcard(cg, 'top::lanes::u::*')).toEqual([]); + }); +}); + +it.each([ + { 'a.sv': 'module leaf(input a); endmodule', 'b.sv': 'module leaf(input a); endmodule' }, + { 'a.sv': 'module leaf(input a); endmodule module leaf(input a); endmodule' }, +])('rejects ambiguous module definitions %j', async definitions => { + await graph({ ...definitions, 'top.sv': 'module top(input a); leaf u(.*); endmodule' } as Record, cg => { + expect(wildcard(cg)).toEqual([]); + }); +}); + +it('refreshes added and removed formal endpoints when the wildcard source is unchanged', async () => { + await graph({ 'leaf.sv': 'module leaf(input a); endmodule', + 'top.sv': 'module top(input a,b); leaf u(.*); endmodule' }, async (cg, root) => { + expect(wildcard(cg)).toEqual(pair('a')); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(input a,b); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual([...pair('a'), ...pair('b')].sort((a,b)=>a.target.localeCompare(b.target))); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(input b); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual(pair('b')); + }); +}); + +it('retries a known module with no matches and updates added or removed local signals', async () => { + await graph({ 'leaf.sv': 'module leaf(input absent); endmodule', + 'top.sv': 'module top(input b); leaf u(.*); endmodule' }, async (cg, root) => { + expect(wildcard(cg)).toEqual([]); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(input b); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual(pair('b')); + fs.writeFileSync(path.join(root, 'top.sv'), 'module top; leaf u(.*); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual([]); + fs.writeFileSync(path.join(root, 'top.sv'), 'module top(input b); leaf u(.*); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual(pair('b')); + }); +}); + +it('discovers a previously unknown module and removes bindings when a duplicate appears', async () => { + await graph({ 'top.sv': 'module top(input a); leaf u(.*); endmodule' }, async (cg, root) => { + expect(wildcard(cg)).toEqual([]); + fs.writeFileSync(path.join(root, 'leaf.sv'), 'module leaf(input a); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual(pair('a')); + fs.writeFileSync(path.join(root, 'duplicate.sv'), 'module leaf(input a); endmodule'); + await cg.sync(); + expect(wildcard(cg)).toEqual([]); + fs.unlinkSync(path.join(root, 'duplicate.sv')); + await cg.sync(); + expect(wildcard(cg)).toEqual(pair('a')); + fs.unlinkSync(path.join(root, 'leaf.sv')); + await cg.sync(); + expect(wildcard(cg)).toEqual([]); + }); +}); + +it('normalizes escaped source identifiers for matching and explicit exclusions', async () => { + await graph({ 'leaf.sv': 'module leaf(input \\a , input b); endmodule', + 'top.sv': 'module top(input a, input \\b ); leaf u(.*, .\\a ()); endmodule' }, cg => { + expect(wildcard(cg)).toEqual([ + { target: 'leaf::b', endpoint: 'formal', port: 'b' }, + { target: 'top::\\b', endpoint: 'actual', port: 'b' }, + ]); + }); +}); + +it.each([ + 'module leaf(.p(a)); input a; endmodule', + 'module leaf(.p({a,b})); input a,b; endmodule', + '`define PORTS input a\nmodule leaf(`PORTS); endmodule', +])('keeps only the module dependency for unknown or complex non-ANSI headers: %s', async leaf => { + await graph({ 'leaf.sv': leaf, 'top.sv': 'module top(input a,b,p); leaf u(.*); endmodule' }, cg => { + expect(wildcard(cg)).toEqual([]); + const node = cg.getNodesByName('*').find(n => n.qualifiedName === 'top::u::*')!; + expect(cg.getCallees(node.id).filter(e => e.edge.metadata?.binding === 'hdl-wildcard-dependency') + .map(e => e.node.qualifiedName)).toEqual(['leaf']); + }); +}); diff --git a/__tests__/kernel-ccpp-parity.test.ts b/__tests__/kernel-ccpp-parity.test.ts index fce31f357..0a0c4a3d9 100644 --- a/__tests__/kernel-ccpp-parity.test.ts +++ b/__tests__/kernel-ccpp-parity.test.ts @@ -95,6 +95,20 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => { expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes); } + it('same-line C++ overloads retain distinct IDs and body references, including UTF-16 columns', () => { + const source = `void zero() {} void one() {} +/* 😀 */ struct Widget { Widget() { zero(); } Widget(int) { one(); } }; +`; + assertParity('same-line.cpp', source, 'cpp', 6); + assertParity('same-line-crlf.cpp', source.replace(/\n/g, '\r\n'), 'cpp', 6); + const result = extractFromSource('same-line.cpp', source, 'cpp'); + const constructors = result.nodes.filter(n => n.kind === 'method' && n.name === 'Widget'); + expect(constructors).toHaveLength(2); + expect(new Set(constructors.map(n => n.id)).size).toBe(2); + expect(result.unresolvedReferences.find(r => r.referenceName === 'zero')?.fromNodeId).toBe(constructors[0].id); + expect(result.unresolvedReferences.find(r => r.referenceName === 'one')?.fromNodeId).toBe(constructors[1].id); + }); + it('torture fixture (c): fn-ptr tables, typedefs, file-scope consts, value-refs', () => { const file = path.join(FIXTURE_DIR, 'torture.c'); assertParity('fixtures/torture.c', fs.readFileSync(file, 'utf8'), 'c'); diff --git a/__tests__/kernel-dart-parity.test.ts b/__tests__/kernel-dart-parity.test.ts index 812f9f8c6..a68906cea 100644 --- a/__tests__/kernel-dart-parity.test.ts +++ b/__tests__/kernel-dart-parity.test.ts @@ -125,6 +125,28 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => { }); } + it('extension types contain their getter and method with complete bodies', () => { + const result = assertParity('extension.dart', `extension type Meters(double value) { + double get km => value / 1000; + void report() { + print(km); + } +} +`, 4); + const owner = result.nodes.find(n => n.name === 'Meters')!; + expect(owner.kind).toBe('class'); + const getter = result.nodes.find(n => n.name === 'km')!; + const method = result.nodes.find(n => n.name === 'report')!; + expect(getter.qualifiedName).toBe('Meters::km'); + expect(method.qualifiedName).toBe('Meters::report'); + expect(method.endLine).toBe(5); + for (const member of [getter, method]) { + expect(member.kind).toBe('method'); + expect(result.edges.some(e => e.kind === 'contains' && e.source === owner.id && e.target === member.id)).toBe(true); + } + expect(result.unresolvedReferences.some(r => r.referenceName === 'print' && r.fromNodeId === method.id)).toBe(true); + }); + it('double-walk pins: duplicate local-fn nodes share an id; refs interleave', () => { const src = fs.readFileSync(path.join(FIXTURE_DIR, 'TortureDoubleWalk.dart'), 'utf8'); const result = assertParity('fixtures/TortureDoubleWalk.dart', src, 5); diff --git a/__tests__/kernel-deep-nesting.test.ts b/__tests__/kernel-deep-nesting.test.ts index 7529ff3b3..6743abe0f 100644 --- a/__tests__/kernel-deep-nesting.test.ts +++ b/__tests__/kernel-deep-nesting.test.ts @@ -113,7 +113,8 @@ function deepBraces(depth: number): string { const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS', 'CODEGRAPH_KERNEL_PATH'] as const; let savedEnv: Record; -describe.skipIf(!kernelBuilt)('kernel deep-nesting guard (#1581)', () => { +// This suite requires native routing; the WASM-only matrix disables it explicitly. +describe.skipIf(!kernelBuilt || process.env.CODEGRAPH_KERNEL === '0')('kernel deep-nesting guard (#1581)', () => { let routed: Language[] = []; beforeAll(async () => { diff --git a/__tests__/kernel-retry-materialize.test.ts b/__tests__/kernel-retry-materialize.test.ts index 23965bd2d..d9545664a 100644 --- a/__tests__/kernel-retry-materialize.test.ts +++ b/__tests__/kernel-retry-materialize.test.ts @@ -37,7 +37,8 @@ const KERNEL_PATH = path.join( ); const kernelBuilt = fs.existsSync(KERNEL_PATH); -describe.skipIf(!kernelBuilt)('kernel buffer-transport storage (#1541)', () => { +// This suite requires native routing; the WASM-only matrix disables it explicitly. +describe.skipIf(!kernelBuilt || process.env.CODEGRAPH_KERNEL === '0')('kernel buffer-transport storage (#1541)', () => { let dir: string; let cg: CodeGraph; diff --git a/__tests__/kernel-scaffold.test.ts b/__tests__/kernel-scaffold.test.ts index f74bb897a..5254258c3 100644 --- a/__tests__/kernel-scaffold.test.ts +++ b/__tests__/kernel-scaffold.test.ts @@ -144,7 +144,7 @@ describe.skipIf(!kernelBuilt)('kernel scaffold', () => { const result = tryKernelExtract('src/utils.ts', FIXTURE, 'typescript')!; for (const n of result.nodes) { if (n.kind === 'file') continue; - expect(n.id).toBe(generateNodeId('src/utils.ts', n.kind, n.name, n.startLine)); + expect(n.id).toBe(generateNodeId('src/utils.ts', n.kind, n.name, n.startLine, n.startColumn)); } }); diff --git a/__tests__/kernel-tsjs-parity.test.ts b/__tests__/kernel-tsjs-parity.test.ts index 3d4316a34..dbaeb0ae6 100644 --- a/__tests__/kernel-tsjs-parity.test.ts +++ b/__tests__/kernel-tsjs-parity.test.ts @@ -98,24 +98,50 @@ describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => { it.each([ ['ts', 'typescript'], ['tsx', 'tsx'], ['js', 'javascript'], ['jsx', 'jsx'], - ] as const)('leaves nested identifier receivers unresolved and keeps argument calls: %s (#1566)', (ext, language) => { - const result = assertParity(`fixture.${ext}`, ` + ] as const)('keeps same-line accessor IDs distinct: %s (#1349)', (ext, language) => { + const result = assertParity(`accessors.${ext}`, `const emoji = '😀'; class Box { get value() { return read(); } set value(v) { write(v); } }`, language); + const accessors = result.nodes.filter(n => n.kind === 'method' && n.name === 'value'); + expect(accessors).toHaveLength(2); + expect(new Set(accessors.map(n => n.id)).size).toBe(2); + expect(result.unresolvedReferences.find(r => r.referenceName === 'read')?.fromNodeId).toBe(accessors[0].id); + expect(result.unresolvedReferences.find(r => r.referenceName === 'write')?.fromNodeId).toBe(accessors[1].id); + }); + + it.each([ + ['ts', 'typescript'], ['tsx', 'tsx'], ['js', 'javascript'], ['jsx', 'jsx'], + ] as const)('preserves nested receivers and argument calls: %s (#1794)', (ext, language) => { + const source = ` function readKey() { return 'answer'; } function local() { const values = new Map(); return values.get(readKey()); } -function nested(holder) { +function nested(holder, höldér) { holder.values.get(readKey()); holder.values?.get(readKey()); holder['values'].get(readKey()); holder.deep.values.get(readKey()); + holder?.values.get(readKey()); + holder[readKey()].get(readKey()); + holder[0].get(readKey()); + holder["odd.key"].get(readKey()); + holder /* receiver */.values.get(readKey()); + höldér.values.get(readKey()); } -`, language); +`; + const result = assertParity(`fixture.${ext}`, source, language); + assertParity(`fixture-crlf.${ext}`, source.replace(/\n/g, '\r\n'), language); const nested = result.nodes.find((n) => n.name === 'nested' && n.kind === 'function'); expect(nested).toBeDefined(); expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'calls' && r.fromNodeId === nested!.id) - .map((r) => r.referenceName)).toEqual(['readKey', 'readKey', 'readKey', 'readKey']); + .map((r) => r.referenceName)).toEqual([ + 'holder.values.get', 'readKey', 'holder.values.get', 'readKey', + "holder['values'].get", 'readKey', 'holder.deep.values.get', 'readKey', + 'holder?.values.get', 'readKey', 'holder[readKey()].get', 'readKey', 'readKey', + 'holder[0].get', 'readKey', 'holder["odd.key"].get', 'readKey', + 'holder /* receiver */.values.get', 'readKey', + 'höldér.values.get', 'readKey', + ]); expect(result.unresolvedReferences.some((r) => r.referenceName === 'values.get')).toBe(true); }); diff --git a/__tests__/mcp-callers-truncation.test.ts b/__tests__/mcp-callers-truncation.test.ts index ccb608369..503cf998d 100644 --- a/__tests__/mcp-callers-truncation.test.ts +++ b/__tests__/mcp-callers-truncation.test.ts @@ -35,7 +35,8 @@ beforeAll(async () => { Array.from({ length: CALLERS }, (_, i) => `export function caller${i}(): number { return warm(${i}); }`).join('\n') + '\n' ); - // `hot` shares its name with its file, so the answer groups per definition. + // Two real callable definitions exercise grouping. A same-named file is + // correctly excluded when the symbol lookup finds the function itself. fs.writeFileSync(path.join(tmpDir, 'src', 'hot.ts'), 'export function hot(n: number): number { return n; }\n'); fs.writeFileSync( path.join(tmpDir, 'src', 'hot-callers.ts'), @@ -43,6 +44,12 @@ beforeAll(async () => { Array.from({ length: CALLERS }, (_, i) => `export function hotCaller${i}(): number { return hot(${i}); }`).join('\n') + '\n' ); + fs.writeFileSync( + path.join(tmpDir, 'src', 'other-hot.ts'), + 'export function hot(n: number): number { return n + 1; }\n' + + Array.from({ length: CALLERS }, (_, i) => + `export function otherHotCaller${i}(): number { return hot(${i}); }`).join('\n') + '\n' + ); fs.writeFileSync( path.join(tmpDir, 'src', 'fan.ts'), Array.from({ length: CALLERS }, (_, i) => `export function helper${i}(): number { return ${i}; }`).join('\n') + @@ -76,8 +83,8 @@ describe('codegraph_callers truncation', () => { it('marks the cut inside each per-definition section too', async () => { const out = await text('codegraph_callers', { symbol: 'hot' }); - expect(out).toContain('distinct definitions'); - expect(out).toMatch(/- … \+\d+ more \(pass `limit` to widen\)/); + expect(out).toContain('2 distinct definitions'); + expect(out.match(/- … \+\d+ more \(pass `limit` to widen\)/g)).toHaveLength(2); expect(await text('codegraph_callers', { symbol: 'hot', limit: 100 })).not.toContain('more (pass'); }); }); diff --git a/__tests__/mcp-project-lifecycle.test.ts b/__tests__/mcp-project-lifecycle.test.ts new file mode 100644 index 000000000..0e1fb7c30 --- /dev/null +++ b/__tests__/mcp-project-lifecycle.test.ts @@ -0,0 +1,235 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { spawn } from 'child_process'; +import { once } from 'events'; +// Exercise the shipped CommonJS path, including lazy project opens. +const { CodeGraph } = require('../dist') as typeof import('../src'); +const { MCPEngine } = require('../dist/mcp/engine') as typeof import('../src/mcp/engine'); +type MCPEngine = import('../src/mcp/engine').MCPEngine; +import { getWriterPidPath } from '../src/mcp/writer-lock'; + +const roots: string[] = []; +const engines: MCPEngine[] = []; +async function project(): Promise { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-project-lifecycle-'))); + roots.push(root); + fs.writeFileSync(path.join(root, 'before.ts'), 'export function before() { return 1; }'); + const cg = await CodeGraph.init(root); + await cg.indexAll(); + cg.close(); + return root; +} +function engine(options = {}): MCPEngine { + const result = new MCPEngine(options); + engines.push(result); + return result; +} +async function files(e: MCPEngine, root: string): Promise { + return JSON.stringify(await e.getToolHandler().execute('codegraph_files', { projectPath: root })); +} +afterEach(async () => { + for (const e of engines.splice(0)) await e.stop(); + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); +}); + +describe('explicit project MCP lifecycle (#1835)', () => { + it('catches up each existing project once and shares concurrent opens', async () => { + const a = await project(); + const b = await project(); + fs.unlinkSync(path.join(a, 'before.ts')); + fs.unlinkSync(path.join(b, 'before.ts')); + fs.writeFileSync(path.join(a, 'after.ts'), 'export function after() {}'); + fs.writeFileSync(path.join(b, 'after.ts'), 'export function after() {}'); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + const e = engine(); + const results = await Promise.all([files(e, a), files(e, a), files(e, b)]); + for (const result of results) { + expect(result).toContain('after.ts'); + expect(result).not.toContain('before.ts'); + } + expect(sync).toHaveBeenCalledTimes(2); + expect(fs.existsSync(getWriterPidPath(a))).toBe(true); + expect(fs.existsSync(getWriterPidPath(b))).toBe(true); + await e.stop(); + expect(fs.existsSync(getWriterPidPath(a))).toBe(false); + expect(fs.existsSync(getWriterPidPath(b))).toBe(false); + }); + + it.skipIf(process.platform === 'win32').each(['explicit', 'default', 'promoted'])( + 'shares the watcher and pending catch-up across a symlink alias (%s)', async (mode) => { + const root = await project(); + const aliasDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-project-alias-')); + roots.push(aliasDir); + const alias = path.join(aliasDir, 'link'); + fs.symlinkSync(root, alias, 'dir'); + fs.writeFileSync(path.join(root, 'added.ts'), 'export function added() {}'); + const { ExtractionOrchestrator } = require('../dist/extraction') as typeof import('../src/extraction'); + let release!: () => void; + const paused = new Promise(resolve => { release = resolve; }); + let entered!: () => void; + const started = new Promise(resolve => { entered = resolve; }); + const original = ExtractionOrchestrator.prototype.sync; + vi.spyOn(ExtractionOrchestrator.prototype, 'sync').mockImplementation(async function (...args) { + entered(); + await paused; + return original.apply(this, args); + }); + const watch = vi.spyOn(CodeGraph.prototype, 'watch'); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + const e = engine(); + const handler = e.getToolHandler(); + if (mode === 'default') await e.ensureInitialized(alias); + const first = mode === 'default' + ? handler.execute('codegraph_files', {}).then(result => JSON.stringify(result)) + : files(e, root); + await started; + let secondSettled = false; + let second: Promise | undefined; + try { + if (mode === 'promoted') await e.ensureInitialized(alias); + second = (mode === 'promoted' + ? handler.execute('codegraph_files', {}).then(result => JSON.stringify(result)) + : files(e, alias)).then(result => { secondSettled = true; return result; }); + await new Promise(resolve => setTimeout(resolve, 80)); + expect(secondSettled).toBe(false); + expect(watch).toHaveBeenCalledTimes(1); + expect(sync).toHaveBeenCalledTimes(1); + if (mode !== 'explicit') expect(e.getProjectPath()).toBe(root); + } finally { + release(); + await first; + await second; + } + expect(await first).toContain('added.ts'); + expect(await second).toContain('added.ts'); + }, + ); + + it('auto-syncs subsequent edits in explicit projects', async () => { + vi.stubEnv('CODEGRAPH_WATCH_DEBOUNCE_MS', '100'); + const root = await project(); + const e = engine(); + await files(e, root); + fs.writeFileSync(path.join(root, 'live.ts'), 'export function live() {}'); + await vi.waitFor(async () => expect(await files(e, root)).toContain('live.ts'), { timeout: 10000, interval: 100 }); + }, 15000); + + it('reuses default project lifecycle for explicit requests', async () => { + const root = await project(); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + const e = engine(); + await e.ensureInitialized(root); + await files(e, root); + expect(sync).toHaveBeenCalledTimes(1); + }); + + it.each(['option', 'environment'])('respects no-watch %s without catch-up writes', async (mode) => { + const root = await project(); + fs.unlinkSync(path.join(root, 'before.ts')); + if (mode === 'environment') vi.stubEnv('CODEGRAPH_NO_WATCH', '1'); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + const e = engine(mode === 'option' ? { watch: false } : {}); + expect(await files(e, root)).toContain('before.ts'); + expect(sync).not.toHaveBeenCalled(); + expect(fs.existsSync(getWriterPidPath(root))).toBe(false); + }); + + it('does not catch up, steal or remove another live writer lock', async () => { + const root = await project(); + const lock = JSON.stringify({ pid: process.ppid, mode: 'test', startedAt: Date.now() }); + fs.writeFileSync(getWriterPidPath(root), lock); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + const e = engine(); + await files(e, root); + expect(sync).not.toHaveBeenCalled(); + await e.stop(); + expect(fs.readFileSync(getWriterPidPath(root), 'utf8')).toBe(lock); + }); + + it('takes over an explicit project on the next request after another engine exits', async () => { + vi.stubEnv('CODEGRAPH_WATCH_DEBOUNCE_MS', '100'); + const root = await project(); + const script = ` + const { MCPEngine } = require(process.argv[1]); + const e = new MCPEngine(); + process.on('SIGTERM', async () => { await e.stop(); process.exit(0); }); + e.getToolHandler().execute('codegraph_status', { projectPath: process.argv[2] }) + .then(() => process.send('ready')); + `; + const child = spawn(process.execPath, ['--liftoff-only', '-e', script, + path.resolve(__dirname, '../dist/mcp/engine.js'), root], { + stdio: ['ignore', 'ignore', 'pipe', 'ipc'], env: process.env, + }); + child.stderr!.on('data', () => {}); + const closed = once(child, 'close'); + try { + await once(child, 'message'); + const e = engine(); + const sync = vi.spyOn(CodeGraph.prototype, 'sync'); + await files(e, root); + expect(sync).not.toHaveBeenCalled(); + child.kill('SIGTERM'); + await closed; + fs.writeFileSync(path.join(root, 'takeover.ts'), 'export function takeover() {}'); + expect(await files(e, root)).toContain('takeover.ts'); + expect(sync).toHaveBeenCalledTimes(1); + const lock = JSON.parse(fs.readFileSync(getWriterPidPath(root), 'utf8')); + expect(lock.pid).toBe(process.pid); + fs.writeFileSync(path.join(root, 'after-takeover.ts'), 'export function afterTakeover() {}'); + await vi.waitFor(async () => expect(await files(e, root)).toContain('after-takeover.ts'), { timeout: 10000, interval: 100 }); + } finally { + if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL'); + await closed; + } + }, 20000); + + it('drains an in-flight reconcile before closing its database or releasing ownership', async () => { + const root = await project(); + fs.writeFileSync(path.join(root, 'added.ts'), 'export function added() {}'); + const { DatabaseConnection } = require('../dist/db') as typeof import('../src/db'); + let release!: () => void; + const paused = new Promise(resolve => { release = resolve; }); + let entered!: () => void; + const started = new Promise(resolve => { entered = resolve; }); + const maintenance = DatabaseConnection.prototype.runMaintenance; + vi.spyOn(DatabaseConnection.prototype, 'runMaintenance').mockImplementation(async function () { + entered(); + await paused; + return maintenance.call(this); + }); + const close = vi.spyOn(DatabaseConnection.prototype, 'close'); + const open = vi.spyOn(CodeGraph, 'openSync'); + const e = engine(); + const query = files(e, root); + await started; + const stopped = e.stop(); + try { + expect(e.stop()).toBe(stopped); + await Promise.resolve(); + expect(close).not.toHaveBeenCalled(); + expect(fs.existsSync(getWriterPidPath(root))).toBe(true); + } finally { + release(); + } + await stopped; + await query; + expect(close).toHaveBeenCalledTimes(1); + expect(open).toHaveBeenCalledTimes(1); + expect(fs.existsSync(getWriterPidPath(root))).toBe(false); + const cg = CodeGraph.openSync(root); + expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] }); + cg.close(); + }); + + it('never creates an index for an unindexed project', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-unindexed-lifecycle-')); + roots.push(root); + const e = engine(); + expect(await files(e, root)).toContain("isn't indexed"); + expect(fs.existsSync(path.join(root, '.codegraph'))).toBe(false); + }); +}); diff --git a/__tests__/mcp-unindexed.test.ts b/__tests__/mcp-unindexed.test.ts index efc4e67f2..a93eff18d 100644 --- a/__tests__/mcp-unindexed.test.ts +++ b/__tests__/mcp-unindexed.test.ts @@ -118,7 +118,7 @@ describe('No-root-index session policy', () => { const instructions = (res.result as { instructions: string }).instructions; // No longer an "inactive, do nothing" note — the tools are available. - expect(instructions).not.toMatch(/inactive/i); + expect(instructions).not.toMatch(/(?:codegraph|server|tools?)[^\n.]{0,60}\binactive\b/i); // It steers the agent to target a project explicitly via projectPath... expect(instructions).toMatch(/projectPath/); expect(instructions).toMatch(/codegraph_explore/); @@ -176,7 +176,7 @@ describe('No-root-index session policy', () => { const init = await request(child, { id: 0, method: 'initialize', params: initializeParams(tempDir) }); const instructions = (init.result as { instructions: string }).instructions; expect(instructions).toMatch(/How to query/); - expect(instructions).not.toMatch(/inactive/i); + expect(instructions).not.toMatch(/(?:codegraph|server|tools?)[^\n.]{0,60}\binactive\b/i); const list = await request(child, { id: 1, method: 'tools/list' }); const tools = (list.result as { tools: Array<{ name: string }> }).tools; diff --git a/__tests__/mcp-writer-lock.test.ts b/__tests__/mcp-writer-lock.test.ts index be68f944c..e53cffed5 100644 --- a/__tests__/mcp-writer-lock.test.ts +++ b/__tests__/mcp-writer-lock.test.ts @@ -9,6 +9,7 @@ import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { CodeGraph } from '../src'; +import { stopDaemonAt } from '../src/mcp/daemon-registry'; import { getWriterPidPath } from '../src/mcp/writer-lock'; const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js'); @@ -50,12 +51,20 @@ describe('issue #1740 — direct-mode writer lock', () => { }); afterEach(async () => { - for (const c of children) { - try { c.kill('SIGTERM'); } catch { /* ignore */ } - } + const exits = children.map((c) => new Promise((resolve, reject) => { + if (c.exitCode !== null || c.signalCode !== null) { resolve(); return; } + const timer = setTimeout(() => reject(new Error(`MCP child ${c.pid} did not exit`)), 5000); + c.once('close', () => { clearTimeout(timer); resolve(); }); + c.kill('SIGTERM'); + })); + // The proxies are not the detached writer. Verify its identity and stop + // only the daemon belonging to this uniquely-created test root. + await stopDaemonAt(realRoot); + await Promise.all(exits); children.length = 0; - await sleep(300); - try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* ignore */ } + if (process.platform !== 'win32') expect(fs.existsSync(getWriterPidPath(realRoot))).toBe(false); + fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 50 }); + expect(fs.existsSync(tempDir)).toBe(false); }); it('second CODEGRAPH_NO_DAEMON serve --mcp exits with writer-lock error', async () => { diff --git a/__tests__/object-literal-methods.test.ts b/__tests__/object-literal-methods.test.ts index ed12f45f8..8acfb2a3f 100644 --- a/__tests__/object-literal-methods.test.ts +++ b/__tests__/object-literal-methods.test.ts @@ -180,4 +180,23 @@ describe('object-literal method resolution (end-to-end)', () => { cg.close(); }); + + it.each([ + ['interface only', 'export interface S { reset(): void }'], + ['two implementations', 'export const useStore = create(() => ({ reset: () => {} }));\nexport class Other { reset() {} }'], + ])('does not guess a store action with %s', async (_label, declarations) => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-store-')); + fs.writeFileSync(path.join(tmpDir, 'store.ts'), declarations); + fs.writeFileSync(path.join(tmpDir, 'caller.ts'), + "import { useStore } from './store'; export function hardReset() { useStore.getState().reset(); }"); + const cg = CodeGraph.initSync(tmpDir); + try { + await cg.indexAll(); + const caller = cg.getNodesByName('hardReset')[0]!; + expect(cg.getCallees(caller.id).filter(({ node, edge }) => + edge.kind === 'calls' && node.name === 'reset')).toEqual([]); + } finally { + cg.close(); + } + }); }); diff --git a/__tests__/query-pool.test.ts b/__tests__/query-pool.test.ts index d7bf3be91..05555541d 100644 --- a/__tests__/query-pool.test.ts +++ b/__tests__/query-pool.test.ts @@ -196,3 +196,31 @@ describe('QueryPool', () => { await pool.destroy(); }); }); + +it('retires a failed-opening worker and retries with a working one', async () => { + const workers: FakeWorker[] = []; + const pool = new QueryPool({ root: '/x', size: 1, createWorker: () => { + const w = new FakeWorker(() => ({result: ok('recovered')}), workers.length > 0); + workers.push(w); return w; + }}); + try { + const result = await pool.run('codegraph_explore', {query: 'q'}); + expect(result.content[0].text).toBe('recovered'); + expect(workers[0].alive).toBe(false); + expect(pool.liveWorkers).toBe(1); + } finally { await pool.destroy(); } +}); + +it('bounds repeated open failures and completes pending work', async () => { + let spawned = 0; + const pool = new QueryPool({ root: '/x', size: 1, createWorker: () => { + spawned++; return new FakeWorker(() => ({result: ok('must not run')}), false); + }}); + try { + const result = await pool.run('codegraph_explore', {query: 'q'}); + expect(result.isError).toBe(true); + expect(pool.healthy).toBe(false); + expect(pool.liveWorkers).toBe(0); + expect(spawned).toBeLessThanOrEqual(12); + } finally { await pool.destroy(); } +}); diff --git a/__tests__/real-project-regressions.test.ts b/__tests__/real-project-regressions.test.ts new file mode 100644 index 000000000..46c6de335 --- /dev/null +++ b/__tests__/real-project-regressions.test.ts @@ -0,0 +1,45 @@ +import { it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { decodeLockInfo } from '../src/mcp/daemon-paths'; +import { clearStaleDaemonLock } from '../src/mcp/daemon'; + +it('ADRC macro calls do not resolve to enum values; ordinary calls survive', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-adrc-reg-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'maths.h'), '#define MAX(a,b) ((a) > (b) ? (a) : (b))\n'); + fs.writeFileSync(path.join(root, 'cli.c'), 'enum { MIN, MAX };\nint helper(void) { return 1; }\n'); + fs.writeFileSync(path.join(root, 'blackbox.c'), '#include "maths.h"\nint helper(void);\nint blackbox(void) { return MAX(helper(), 2); }\n'); + cg = CodeGraph.initSync(root); await cg.indexAll(); + const node = cg.getNodesByName('blackbox').find(n => n.kind === 'function')!; + const calls = cg.getCallees(node.id).filter(x => x.edge.kind === 'calls'); + expect(calls.some(x => x.node.kind === 'enum_member')).toBe(false); + expect(calls.some(x => x.node.name === 'helper')).toBe(true); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +}); + +it('CSQTT Go standard-library Size cannot bind a TypeScript interface', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-csqtt-reg-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'update.go'), 'package core\nimport "net"\nfunc verify(mask []byte) { net.IPMask(mask).Size() }\n'); + fs.writeFileSync(path.join(root, 'runtime.ts'), 'export interface Size { width: number; height: number }\n'); + cg = CodeGraph.initSync(root); await cg.indexAll(); + const node = cg.getNodesByName('verify').find(n => n.kind === 'function')!; + expect(cg.getCallees(node.id).filter(x => x.edge.kind === 'calls' && x.node.language === 'typescript')).toEqual([]); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +}); + +it('preserves a live legacy daemon lock even without identity metadata', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-legacy-reg-')); + const file = path.join(root, 'daemon.pid'); + try { + fs.writeFileSync(file, String(process.pid)); + expect(decodeLockInfo(String(process.pid))?.pid).toBe(process.pid); + expect(clearStaleDaemonLock(file, process.pid, { allowLivePid: true })).toBe(false); + expect(fs.existsSync(file)).toBe(true); + } finally { fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index b8d25f6b8..aaffebef8 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -23,16 +23,17 @@ describe('Resolution Module', () => { let cg: CodeGraph; beforeEach(() => { + cg = undefined!; // Create temp directory tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-resolution-test-')); }); afterEach(() => { // Clean up - if (cg) { - cg.destroy(); - } else if (fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true }); + try { cg?.destroy(); } + finally { + cg = undefined!; + if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true }); } }); @@ -1294,7 +1295,7 @@ class Widget { public: Widget(int a, int b) {} }; int runStack(int a, int b) { Calculator calc(0); return calc.add(a, b); } int runBrace() { Widget w{1, 2}; return 0; } int runHeap(int a, int b) { Calculator* c = new Calculator(0); return c->add(a, b); } -void noise() { int x(5); int y{6}; Calculator deferred; } +void noise() { int x(5); int y{6}; extern Calculator deferred; } ` ); cg = await CodeGraph.init(tempDir, { index: true }); @@ -1313,8 +1314,8 @@ void noise() { int x(5); int y{6}; Calculator deferred; } expect(instTargets('runBrace').map((n) => `${n.kind}:${n.name}`)).toContain('class:Widget'); // Heap still works (regression guard). expect(instTargets('runHeap').map((n) => `${n.kind}:${n.name}`)).toContain('class:Calculator'); - // Primitives (`int x(0)`/`int y{6}`) and bare default construction - // (`Calculator deferred;`) must NOT mint an instantiates edge. + // Primitives and extern declarations do not construct an object. + // Actual default construction is covered by cpp-remaining-regressions. expect(instTargets('noise')).toHaveLength(0); }); @@ -2316,6 +2317,30 @@ func main() { }); describe('Local-variable receiver-type inference (#1108)', () => { + it.each(['ts', 'tsx', 'js', 'jsx'])('keeps compound receiver evidence without guessing targets — %s (#1794)', async (ext) => { + fs.writeFileSync(path.join(tempDir, `holder.${ext}`), 'export const holder = { values: new Map() };'); + const receivers = [ + 'holder?.values', 'holder[readKey()]', 'holder[0]', 'holder["odd.key"]', + 'holder /* receiver */.values', 'höldér.values', 'imported.values', 'data.holder.values', + ]; + fs.writeFileSync(path.join(tempDir, `calls.${ext}`), ` +import { holder as imported } from './holder'; +import * as data from './holder'; +export class Collision { get(key) { return key; } } +export function readKey() { return 'values'; } +${receivers.map((receiver, i) => `export function nested${i}(holder, höldér) { return ${receiver}.get(readKey()); }`).join('\n')} +`); + cg = await CodeGraph.init(tempDir, { index: true }); + for (let i = 0; i < receivers.length; i++) { + const caller = cg.getNodesByName(`nested${i}`).find(n => n.kind === 'function')!; + const callees = cg.getCallees(caller.id).filter(({ edge }) => edge.kind === 'calls'); + // Argument and computed-key calls survive, but neither an imported + // root object nor Collision.get is evidence of the called member. + expect(callees.length, receivers[i]).toBeGreaterThan(0); + expect(callees.every(({ node }) => node.name === 'readKey'), receivers[i]).toBe(true); + } + }); + it.each(['ts', 'tsx', 'js', 'jsx'])('keeps built-in Map calls off project methods — %s (#1566)', async (ext) => { const typed = ext === 'ts' || ext === 'tsx'; fs.writeFileSync(path.join(tempDir, `cache.${ext}`), ` diff --git a/__tests__/same-line-ids.test.ts b/__tests__/same-line-ids.test.ts new file mode 100644 index 000000000..e31a84cfe --- /dev/null +++ b/__tests__/same-line-ids.test.ts @@ -0,0 +1,26 @@ +import { it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +it('persists both same-line accessors and keeps their calls separate (#1349)', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-accessor-ids-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'box.ts'), `function read() {} function write(v) {}\nexport class Box { get value() { return read(); } set value(v) { write(v); } }`); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const members = cg.getNodesByName('value').sort((a, b) => a.startColumn - b.startColumn); + expect(members).toHaveLength(2); + expect(new Set(members.map(n => n.id)).size).toBe(2); + const callees = (id: string) => cg!.getCallees(id).filter(x => x.edge.kind === 'calls').map(x => x.node.name); + expect(callees(members[0].id)).toEqual(['read']); + expect(callees(members[1].id)).toEqual(['write']); + await cg.indexAll(); + expect(cg.getNodesByName('value').map(n => n.id).sort()).toEqual(members.map(n => n.id).sort()); + } finally { + cg?.close(); + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/__tests__/sync-rebuild-convergence.test.ts b/__tests__/sync-rebuild-convergence.test.ts index fc9c626ae..688e5e37c 100644 --- a/__tests__/sync-rebuild-convergence.test.ts +++ b/__tests__/sync-rebuild-convergence.test.ts @@ -146,6 +146,86 @@ describe('Incremental sync converges to a full rebuild (CG-33)', () => { expect(describeDiff(synced, rebuilt)).toBe('missing from synced: 0, stale in synced: 0'); }); + it('keeps one edge when re-resolution selects the same target', async () => { + write('src/caller.ts', `export function run(): number {\n return pct(1);\n}\n`); + write('src/alpha.ts', `export function pct(n: number): number {\n return n;\n}\n`); + cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } }); + await cg.indexAll(); + + // zeta.ts introduces a competing definition, so the existing edge is + // reopened, but alpha.ts remains the deterministic first candidate. + write('src/zeta.ts', `export function pct(n: number): number {\n return n * 2;\n}\n`); + const result = await cg.sync(); + expect(result.definitionDelta).toContain('pct'); + + const targets = withDb((db) => + ( + db + .prepare( + `SELECT target.file_path AS file + FROM edges edge + JOIN nodes source ON source.id = edge.source + JOIN nodes target ON target.id = edge.target + WHERE source.name = 'run' + AND target.name = 'pct' + AND edge.kind = 'calls'` + ) + .all() as Array<{ file: string }> + ).map((row) => row.file) + ); + expect(targets).toEqual(['src/alpha.ts']); + }); + + it('rolls back edge deletion when requeueing its reference is interrupted', async () => { + write('src/caller.ts', `export function run(): number {\n return pct(1);\n}\n`); + write('src/zeta.ts', `export function pct(n: number): number {\n return n;\n}\n`); + cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } }); + await cg.indexAll(); + + const originalEdge = withDb((db) => { + const row = db + .prepare( + `SELECT edge.source, edge.target, edge.kind + FROM edges edge + JOIN nodes source ON source.id = edge.source + JOIN nodes target ON target.id = edge.target + WHERE source.name = 'run' + AND target.name = 'pct' + AND edge.kind = 'calls'` + ) + .get() as { source: string; target: string; kind: string }; + db.exec( + `CREATE TRIGGER interrupt_pct_requeue + BEFORE INSERT ON unresolved_refs + WHEN NEW.reference_name = 'pct' + BEGIN + SELECT RAISE(ABORT, 'forced rebind interruption'); + END;` + ); + return `${row.source}|${row.target}|${row.kind}`; + }); + + write('src/alpha.ts', `export function pct(n: number): number {\n return n * 2;\n}\n`); + await expect(cg.sync()).rejects.toThrow(/forced rebind interruption/); + + // A failed requeue leaves the last committed graph answer untouched. + expect(edgeSet().has(originalEdge)).toBe(true); + const queued = withDb( + (db) => + ( + db + .prepare( + `SELECT COUNT(*) AS count + FROM unresolved_refs ref + JOIN nodes source ON source.id = ref.from_node_id + WHERE source.name = 'run' AND ref.reference_name = 'pct'` + ) + .get() as { count: number } + ).count + ); + expect(queued).toBe(0); + }); + /** * The mirror direction: removing a definition narrows the candidate set too, * so the delta must include names the sync DROPPED, not just names it added. diff --git a/__tests__/sync.test.ts b/__tests__/sync.test.ts index 3ce6a0a86..3cd5f336a 100644 --- a/__tests__/sync.test.ts +++ b/__tests__/sync.test.ts @@ -219,6 +219,51 @@ describe('Sync Module', () => { } }); + it.each(['added', 'modified', 'removed'] as const)( + 'reports %s files after committing without indexing (#1829)', + async (kind) => { + const relativePath = kind === 'added' ? 'src/new.ts' : 'src/index.ts'; + const filePath = path.join(testDir, relativePath); + if (kind === 'removed') fs.unlinkSync(filePath); + else fs.writeFileSync(filePath, 'export function changed() { return 99; }'); + + const expected = { added: [], modified: [], removed: [], [kind]: [relativePath] }; + // In particular, unstaged deletions remain in git ls-files. + expect(cg.getChangedFiles()).toEqual(expected); + git('add', '--', 'src'); + git('commit', '-m', 'changed without indexing'); + expect(cg.getChangedFiles()).toEqual(expected); + + await cg.sync(); + expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] }); + } + ); + + it('detects a committed rename and preserves the graph across repeated no-op syncs', async () => { + git('mv', 'src/index.ts', 'src/renamed.ts'); + git('commit', '-m', 'rename without indexing'); + + expect(cg.getChangedFiles()).toEqual({ + added: ['src/renamed.ts'], modified: [], removed: ['src/index.ts'], + }); + // Status must not discard the last usable graph before sync succeeds. + expect(cg.searchNodes('hello').some(r => r.node.filePath === 'src/index.ts')).toBe(true); + + const renamed = await cg.sync(); + expect(renamed.filesAdded).toBe(1); + expect(renamed.filesRemoved).toBe(1); + const nodes = cg.searchNodes('hello').map(r => r.node); + expect(nodes).toHaveLength(1); + expect(nodes[0].filePath).toBe('src/renamed.ts'); + + for (let i = 0; i < 2; i++) { + expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] }); + const unchanged = await cg.sync(); + expect([unchanged.filesAdded, unchanged.filesModified, unchanged.filesRemoved]).toEqual([0, 0, 0]); + expect(cg.searchNodes('hello').map(r => r.node.id)).toEqual(nodes.map(node => node.id)); + } + }); + it('should detect modified files via git', async () => { fs.writeFileSync( path.join(testDir, 'src', 'index.ts'), @@ -881,3 +926,212 @@ describe('Scoped sync parity (#watcher-scoped)', () => { expect(cg.searchNodes('gamma').length).toBe(1); }); }); + +// A change that is COMMITTED but not yet indexed used to read as zero pending +// changes: getChangedFiles' git fast path built its candidate list from +// `git status --porcelain`, and committing is exactly what removes a file from +// that output. The hash comparison below it was correct and simply never +// reached. Committed work is now sourced from `git diff HEAD`. +// (#1829) +describe('committed-but-unindexed changes (#1829)', () => { + let testDir: string; + let cg: CodeGraph; + + const git = (...args: string[]) => + execFileSync('git', args, { cwd: testDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); + + beforeEach(async () => { + testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1829-')); + git('init'); + git('config', 'user.email', 'test@test.com'); + git('config', 'user.name', 'Test'); + + fs.mkdirSync(path.join(testDir, 'src')); + fs.writeFileSync(path.join(testDir, 'src', 'one.ts'), `export function alpha() { return 1; }`); + git('add', '-A'); + git('commit', '-m', 'initial'); + + cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts'], exclude: [] } }); + await cg.indexAll(); + }); + + afterEach(() => { + if (cg) cg.destroy(); + if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true }); + }); + + it('sees a committed NEW file (git status shows nothing; the DB has no row)', async () => { + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), `export function beta() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'add two'); + + const changes = cg.getChangedFiles(); + expect(changes.added).toContain('src/two.ts'); + + // status and sync must agree — the whole point is that the number a user + // reads matches the work that is actually outstanding. + const result = await cg.sync(); + expect(result.filesAdded).toBe(1); + expect(cg.searchNodes('beta').length).toBeGreaterThan(0); + }); + + it('sees a committed MODIFICATION to an already-tracked file', async () => { + fs.writeFileSync(path.join(testDir, 'src', 'one.ts'), `export function alphaRenamed() { return 99; }`); + git('add', '-A'); + git('commit', '-m', 'edit one'); + + expect(cg.getChangedFiles().modified).toContain('src/one.ts'); + const result = await cg.sync(); + expect(result.filesModified).toBe(1); + expect(cg.searchNodes('alphaRenamed').length).toBeGreaterThan(0); + }); + + it('sees a committed DELETE', async () => { + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), `export function beta() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'add two'); + await cg.sync(); + + fs.rmSync(path.join(testDir, 'src', 'two.ts')); + git('add', '-A'); + git('commit', '-m', 'remove two'); + + expect(cg.getChangedFiles().removed).toContain('src/two.ts'); + const result = await cg.sync(); + expect(result.filesRemoved).toBe(1); + }); + + it('reports zero once the sync has absorbed the commit (the stamp advances)', async () => { + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), `export function beta() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'add two'); + await cg.sync(); + + const after = cg.getChangedFiles(); + expect(after.added).toHaveLength(0); + expect(after.modified).toHaveLength(0); + expect(after.removed).toHaveLength(0); + }); + + it('counts a file once when it was committed AND edited again since', async () => { + // The same path now reaches the candidate list from both sources — the + // committed diff and `git status`. It is still one changed file. + fs.writeFileSync(path.join(testDir, 'src', 'one.ts'), `export function alpha() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'edit one'); + fs.writeFileSync(path.join(testDir, 'src', 'one.ts'), `export function alpha() { return 3; }`); + + const changes = cg.getChangedFiles(); + expect(changes.modified.filter((f) => f === 'src/one.ts')).toHaveLength(1); + expect(changes.added).toHaveLength(0); + + const result = await cg.sync(); + expect(result.filesModified).toBe(1); + }); + + it('sees a committed RENAME as a removal plus an add', async () => { + // `--no-renames` on the committed diff is deliberate: the index keys files + // by path, so a rename IS a removal and an add, and pairing them up would + // only have to be taken apart again. + fs.renameSync(path.join(testDir, 'src', 'one.ts'), path.join(testDir, 'src', 'renamed.ts')); + git('add', '-A'); + git('commit', '-m', 'rename one'); + + const changes = cg.getChangedFiles(); + expect(changes.removed).toContain('src/one.ts'); + expect(changes.added).toContain('src/renamed.ts'); + + const result = await cg.sync(); + expect(result.filesRemoved).toBe(1); + expect(result.filesAdded).toBe(1); + expect(cg.searchNodes('alpha').every((r) => r.node.filePath !== 'src/one.ts')).toBe(true); + }); + + it('still filters committed changes by the rules the full index uses', async () => { + // vendor/ is a built-in exclude git knows nothing about. Sourcing candidates + // from `git diff` must not smuggle in files `git status` would have had + // filtered out (#766) — same classifier, both sources. + fs.mkdirSync(path.join(testDir, 'vendor')); + fs.writeFileSync(path.join(testDir, 'vendor', 'lib.ts'), `export function vendored() { return 1; }`); + git('add', '-A'); + git('commit', '-m', 'add vendor'); + + const changes = cg.getChangedFiles(); + expect(changes.added).not.toContain('vendor/lib.ts'); + expect(changes.modified).not.toContain('vendor/lib.ts'); + }); + + it('falls back to the full scan when history moved under the index', async () => { + // A rebase/gc/shallow clone can leave the stamped commit unreachable. The + // fast path cannot diff against a commit that is gone, so the (correct, + // slower) full scan has to answer instead of silently reporting zero. + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), `export function beta() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'add two'); + (cg as unknown as { queries: { setMetadata(k: string, v: string): void } }) + .queries.setMetadata('indexed_at_commit', '0'.repeat(40)); + + expect(cg.getChangedFiles().added).toContain('src/two.ts'); + }); + + it('an index with no stamp still answers correctly (pre-#1829 index upgrading)', async () => { + (cg as unknown as { queries: { setMetadata(k: string, v: string): void } }) + .queries.setMetadata('indexed_at_commit', ''); + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), `export function beta() { return 2; }`); + git('add', '-A'); + git('commit', '-m', 'add two'); + + expect(cg.getChangedFiles().added).toContain('src/two.ts'); + + // ...and it self-heals: the sync writes a stamp, so the next read is clean. + await cg.sync(); + expect(cg.getChangedFiles().added).toHaveLength(0); + }); + it('keeps the remaining committed diff after a scoped sync', async () => { + fs.writeFileSync(path.join(testDir, 'src', 'one.ts'), 'export function changed() {}'); + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), 'export function beta() {}'); + git('add', '-A'); git('commit', '-m', 'two changes'); + await cg.sync({ paths: ['src/two.ts'] }); + expect(cg.getChangedFiles().modified).toContain('src/one.ts'); + await cg.sync(); + expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] }); + }); + + it('preserves a committed deletion recreated with the indexed content', async () => { + const file = path.join(testDir, 'src', 'one.ts'); + const content = fs.readFileSync(file, 'utf8'); + fs.rmSync(file); git('add', '-A'); git('commit', '-m', 'delete'); + fs.writeFileSync(file, content); + expect(cg.getChangedFiles()).toEqual({ added: [], modified: [], removed: [] }); + await cg.sync(); + expect(cg.searchNodes('alpha').length).toBeGreaterThan(0); + }); + + it('reads committed non-ASCII paths without git quoting', () => { + fs.writeFileSync(path.join(testDir, 'src', 'пример.ts'), 'export function beta() {}'); + git('add', '-A'); git('commit', '-m', 'unicode'); + expect(cg.getChangedFiles().added).toContain('src/пример.ts'); + }); + + it('finds an unstaged removal when an old stamp requires the fallback', () => { + (cg as unknown as { queries: { setMetadata(k: string, v: string): void } }) + .queries.setMetadata('indexed_at_commit', ''); + fs.rmSync(path.join(testDir, 'src', 'one.ts')); + expect(cg.getChangedFiles().removed).toContain('src/one.ts'); + }); + + it('does not absorb a commit made during full indexing', async () => { + const before = git('rev-parse', 'HEAD').trim(); + let committed = false; + await cg.indexAll({ onProgress: () => { + if (committed) return; + committed = true; + fs.writeFileSync(path.join(testDir, 'src', 'two.ts'), 'export function beta() {}'); + git('add', '-A'); git('commit', '-m', 'during index'); + }}); + expect(committed).toBe(true); + const queries = (cg as unknown as { queries: { getMetadata(k: string): string } }).queries; + expect(queries.getMetadata('indexed_at_commit')).toBe(before); + }); + +}); diff --git a/__tests__/ts-chained-receiver.test.ts b/__tests__/ts-chained-receiver.test.ts index 267aadb8c..a544163b9 100644 --- a/__tests__/ts-chained-receiver.test.ts +++ b/__tests__/ts-chained-receiver.test.ts @@ -3,8 +3,8 @@ * .get(k)`, `document.body.querySelector(s)` — ends in a platform API. Emitting * the bare method name for it let every such call exact-match whatever project * symbol shared the name, so a storage wrapper's `get` called itself (#1707). - * Those are dropped, as are untyped identifier chains (#1566). The existing - * `window.MyNs.run()` and `this..m()` paths remain outside that guard. + * Complete call references are retained; only proven targets become edges. + * `window.MyNs.run()` and `this..m()` keep their existing resolution. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; diff --git a/__tests__/verilog-access-sync.test.ts b/__tests__/verilog-access-sync.test.ts new file mode 100644 index 000000000..3913c8c9c --- /dev/null +++ b/__tests__/verilog-access-sync.test.ts @@ -0,0 +1,47 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +it('reclassifies untouched caller arguments after signature changes, deletion and reappearance', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-access-sync-')); + let cg: CodeGraph | undefined; + const pkg = (dir: string) => `package lib; task move(${dir} int x); x=1; endtask endpackage`; + const top = 'module top; int value; initial lib::move(value); endmodule'; + try { + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg('output')); + fs.writeFileSync(path.join(root, 'top.sv'), top); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const rows = () => (cg as any).db.db.prepare("SELECT e.metadata FROM edges e JOIN nodes t ON t.id=e.target JOIN nodes s ON s.id=e.source WHERE e.kind='references' AND t.qualified_name='top::value' AND s.file_path='top.sv'").all(); + const roles = () => rows().flatMap((r: any) => JSON.parse(r.metadata)?.hdlAccess ?? []).sort(); + expect(roles()).toEqual(['write']); + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg('input')); + await cg.sync(); + expect(roles()).toEqual(['read']); + fs.unlinkSync(path.join(root, 'pkg.sv')); + await cg.sync(); + expect(rows()).toHaveLength(1); // Keep the ordinary reference without inferred direction. + expect(roles()).toEqual([]); + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg('inout')); + await cg.sync(); + expect(roles()).toEqual(['readwrite']); + expect(fs.readFileSync(path.join(root, 'top.sv'), 'utf8')).toBe(top); + await cg.indexAll(); + expect(roles()).toEqual(['readwrite']); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +}); + +it('preserves distinct same-line read/write occurrences and their columns', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-access-sites-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'top.sv'), 'module top; int x; initial x=x+1; endmodule'); + cg = CodeGraph.initSync(root); await cg.indexAll(); + const rows = (cg as any).db.db.prepare("SELECT e.col, e.metadata FROM edges e JOIN nodes t ON t.id=e.target WHERE e.kind='references' AND t.qualified_name='top::x' ORDER BY e.col").all(); + expect(rows).toHaveLength(2); + expect(rows[0].col).not.toBe(rows[1].col); + expect(rows.map((r: any) => JSON.parse(r.metadata).hdlAccess)).toEqual([['write'], ['read']]); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +}); diff --git a/__tests__/verilog-access.test.ts b/__tests__/verilog-access.test.ts new file mode 100644 index 000000000..7a9c278ab --- /dev/null +++ b/__tests__/verilog-access.test.ts @@ -0,0 +1,111 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { CodeGraph } from '../src'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +beforeAll(async()=>{await initGrammars();await loadGrammarsForLanguages(['verilog']);}); +const access=(source:string)=>extractFromSource('access.sv',source).unresolvedReferences + .filter(r=>r.referenceName.startsWith('hdl:signal:')) + .map(r=>({name:r.referenceName.slice('hdl:signal:'.length),line:r.line,access:r.candidates?.find(c=>c.startsWith('hdl:access:'))?.slice('hdl:access:'.length),event:r.candidates?.find(c=>c.startsWith('hdl:event:'))?.slice('hdl:event:'.length)})); +const compact=(source:string)=>access(source).map(({name,access})=>[name,access]); +describe('syntactic HDL signal access',()=>{ + it('distinguishes assignment destinations from indexes and RHS expressions',()=>{ + expect(compact('module m; assign x[i+:W]=y; endmodule')).toEqual([['x','write'],['i','read'],['W','read'],['y','read']]); + expect(compact('module m; initial x[i] <= y; endmodule')).toEqual([['x','write'],['i','read'],['y','read']]); + expect(compact('module m; assign {a,b[j]}=v; endmodule')).toEqual([['a','write'],['b','write'],['j','read'],['v','read']]); + }); + it('labels compound and increment accesses without classifying their indexes as writes',()=>{ + expect(compact('module m; initial begin x[i]+=y; x[j]++; --x[k]; end endmodule')).toEqual([['x','readwrite'],['i','read'],['y','read'],['x','readwrite'],['j','read'],['x','readwrite'],['k','read']]); + }); + it('separates conditions and edge event expressions from data reads',()=>{ + const refs=access(`module m; +always @(posedge clk or negedge rst) begin + if(en) q=d; + case(sel) choice: q=d; endcase + while(run) q=d; +end endmodule`); + expect(refs.filter(r=>r.line===2).map(r=>[r.name,r.access,r.event])).toEqual([['clk','event','posedge'],['rst','event','negedge']]); + expect(refs.filter(r=>r.access==='control').map(r=>r.name)).toEqual(['en','sel','choice','run']); + expect(refs.filter(r=>r.name==='q').every(r=>r.access==='write')).toBe(true); + expect(refs.filter(r=>r.name==='d').every(r=>r.access==='read')).toBe(true); + }); + it('separates loop initialization, condition, increment, and indexed body assignment',()=>{ + expect(compact('module m; initial for(i=0;i{ + expect(access('module m; always @(posedge clk iff en) q=d; endmodule').map(r=>[r.name,r.access,r.event])).toEqual([ + ['clk','event','posedge'],['en','control',undefined],['q','write',undefined],['d','read',undefined], + ]); + }); + it('records formal order and directions for grouped ANSI and legacy task declarations',()=>{ + const result=extractFromSource('formals.sv','module m; task f(output int x, int y, const ref int z, input q=0); endtask task g; output a; input b,c; endtask endmodule'); + const formals=result.nodes.filter(n=>n.decorators?.includes('hdl:formal')); + expect(formals.map(n=>[n.qualifiedName,n.decorators?.find(d=>d.startsWith('hdl:formal-index:')),n.decorators?.find(d=>d.startsWith('hdl:direction:'))])).toEqual([ + ['m::f::x','hdl:formal-index:0','hdl:direction:output'],['m::f::y','hdl:formal-index:1','hdl:direction:output'], + ['m::f::z','hdl:formal-index:2','hdl:direction:const-ref'],['m::f::q','hdl:formal-index:3','hdl:direction:input'], + ['m::g::a','hdl:formal-index:0','hdl:direction:output'],['m::g::b','hdl:formal-index:1','hdl:direction:input'],['m::g::c','hdl:formal-index:2','hdl:direction:input'], + ]); + expect(formals.find(n=>n.name==='q')?.decorators).toContain('hdl:default'); + }); + it('preserves explicit increment side effects inside call arguments',()=>{ + const result=extractFromSource('effect.sv','module m; initial consume(i++); endmodule'); + const ref=result.unresolvedReferences.find(r=>r.referenceName==='hdl:signal:i')!; + expect(ref.candidates).toContain('hdl:access:readwrite'); + expect(ref.candidates?.some(c=>c.startsWith('hdl:call-arg:'))).toBe(false); + }); + it('does not certify a recovered callable signature for access inference',()=>{ + const result=extractFromSource('recovered.sv','module m; function int f(input int x); for(int i=0,j=0;i<3;i++) x=i; return x; endfunction endmodule'); + const formal=result.nodes.find(n=>n.qualifiedName==='m::f::x')!; + expect(formal.decorators).toContain('hdl:formal'); + expect(formal.decorators?.some(d=>d.startsWith('hdl:formal-index:')||d.startsWith('hdl:direction:'))).toBe(false); + }); + it('classifies returned values and delay operands as reads',()=>{ + const refs=access(`module m; +function int f(); return d; endfunction +initial #(d) q=d; +initial q = #(delay_value) d; +initial #wait_time q=d; +endmodule`); + expect(refs.filter(r=>r.line===2).map(r=>[r.name,r.access])).toEqual([['d','read']]); + expect(refs.filter(r=>r.line===3).map(r=>[r.name,r.access])).toEqual([['d','read'],['q','write'],['d','read']]); + expect(refs.filter(r=>r.line===4).map(r=>[r.name,r.access])).toEqual([['q','write'],['delay_value','read'],['d','read']]); + expect(refs.filter(r=>r.line===5).map(r=>[r.name,r.access])).toEqual([['wait_time','read'],['q','write'],['d','read']]); + }); + it('retains readwrite and control roles for increments inside conditions',()=>{ + const refs=extractFromSource('effect-control.sv','module m; initial while(i++) q=d; initial if(j++) q=d; endmodule').unresolvedReferences; + for(const name of ['i','j']) { + const ref=refs.find(r=>r.referenceName===`hdl:signal:${name}`)!; + expect(ref.candidates).toContain('hdl:access:readwrite'); + expect(ref.candidates).toContain('hdl:access:control'); + } + }); + it('records an explicit initializer write only on initialized net/data declarators',()=>{ + const source='module m; wire feedback = crc[15] ^ bit_in; logic q=0; reg idle, ready=1; localparam WIDTH=8; logic array[WIDTH]; endmodule'; + const result=extractFromSource('initializers.sv',source); + const writes=result.unresolvedReferences.filter(r=>r.candidates?.includes('hdl:access:write')); + expect(writes.map(r=>r.referenceName)).toEqual(['hdl:signal:feedback','hdl:signal:q','hdl:signal:ready']); + for(const ref of writes) expect(result.nodes.find(n=>n.id===ref.fromNodeId)?.name).toBe(ref.referenceName.slice('hdl:signal:'.length)); + expect(result.unresolvedReferences.filter(r=>r.candidates?.includes('hdl:access:read')).map(r=>r.referenceName)).toContain('hdl:signal:crc'); + }); + it('persists declarator self-writes alongside RHS reads in the graph',async()=>{ + const root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-initializer-write-')); + let cg:CodeGraph|undefined; + try { + fs.writeFileSync(path.join(root,'crc.sv'),'module crc(input [15:0] bits, input bit_in); wire feedback=bits[15]^bit_in; logic q=0; logic idle; endmodule'); + cg=CodeGraph.initSync(root);await cg.indexAll(); + const feedback=cg.getNodesByName('feedback')[0]!; + const refs=cg.getCallees(feedback.id).filter(e=>e.edge.kind==='references'); + const self=(cg as any).db.db.prepare('SELECT metadata FROM edges WHERE source=? AND target=? AND kind=?').get(feedback.id,feedback.id,'references'); + expect(JSON.parse(self.metadata).hdlAccess).toContain('write'); + expect(refs.filter(e=>e.node.id!==feedback.id).map(e=>e.node.name).sort()).toEqual(['bit_in','bits']); + const idle=cg.getNodesByName('idle')[0]!; + expect((cg as any).db.db.prepare('SELECT id FROM edges WHERE source=? AND target=?').get(idle.id,idle.id)).toBeUndefined(); + } finally {cg?.close();fs.rmSync(root,{recursive:true,force:true});} + }); + it('keeps qualified targets suppressed while classifying their local indexes and RHS',()=>{ + expect(compact('module m; initial p::x[i]=y; endmodule')).toEqual([['i','read'],['y','read']]); + expect(compact('module m; assign obj.x[i]=y; endmodule')).toEqual([['i','read'],['y','read']]); + }); +}); diff --git a/__tests__/verilog-call-access.test.ts b/__tests__/verilog-call-access.test.ts new file mode 100644 index 000000000..95b028868 --- /dev/null +++ b/__tests__/verilog-call-access.test.ts @@ -0,0 +1,101 @@ +import { beforeAll, it, expect } from 'vitest'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { resolveVerilogCallArgumentAccess } from '../src/resolution/verilog-call-access'; +import { matchVerilogMember } from '../src/resolution/verilog-members'; +import type { ResolutionContext, UnresolvedRef } from '../src/resolution/types'; +import { VERILOG_CALL_ARGUMENT_PREFIX } from '../src/extraction/languages/verilog-call-access'; +beforeAll(async()=>{await initGrammars();await loadGrammarsForLanguages(['verilog']);}); + +function accesses(files: Record) { + const results = Object.entries(files).map(([file,source]) => ({file,result:extractFromSource(file,source)})); + const nodes = results.flatMap(r=>r.result.nodes); + const context = { + getNodeById: (id:string)=>nodes.find(n=>n.id===id) ?? null, + getNodesInFile: (file:string)=>nodes.filter(n=>n.filePath===file), + getNodesByName: (name:string)=>nodes.filter(n=>n.name===name), + getNodesByQualifiedName: (q:string)=>nodes.filter(n=>n.qualifiedName===q), + } as ResolutionContext; + return results.flatMap(({file,result})=>result.unresolvedReferences + .filter(r=>r.candidates?.some(c=>c.startsWith(VERILOG_CALL_ARGUMENT_PREFIX))) + .map(r=>{ + const ref={...r,filePath:file,language:'verilog'} as UnresolvedRef; + const access=resolveVerilogCallArgumentAccess(ref,context,matchVerilogMember); + return {name:r.referenceName.replace('hdl:signal:',''),access:access?.access, + callable:nodes.find(n=>n.id===access?.callableId)?.qualifiedName}; + })); +} + +it('classifies input, output, inout, ref and const ref actuals using known formal directions',()=>{ + expect(accesses({'top.sv':`module top; +task t(input int a, output int b, inout int c, ref int d, const ref int e); endtask +int ai,ao,aio,ar,acr; +initial t(ai,ao,aio,ar,acr); +endmodule`}).map(({name,access})=>[name,access])).toEqual([ + ['ai','read'],['ao','write'],['aio','readwrite'],['ar','readwrite'],['acr','read'], + ]); +}); + +it('supports omitted defaults and named actuals with default input direction',()=>{ + expect(accesses({'top.sv':`module top; +task t(int a=0, output int b); endtask +int out; +initial begin t(,out); t(.b(out)); end +endmodule`}).map(({name,access})=>[name,access])).toEqual([['out','write'],['out','write']]); +}); + +it('keeps output indexes as reads and never marks a non-lvalue output expression as a write',()=>{ + expect(accesses({'top.sv':`module top; +task t(output int x); endtask +int data[8],idx,a,b; +initial begin t(data[idx]); t(a+b); end +endmodule`}).map(({name,access})=>[name,access])).toEqual([ + ['data','write'],['idx','read'],['a',undefined],['b',undefined], + ]); +}); + +it('uses the nearest nested call and retains unknown-call occurrences without access claims',()=>{ + expect(accesses({'top.sv':`module top; +function int inner(input int x); return x; endfunction +task outer(input int x); endtask +int a,b; +initial begin outer(inner(a)); external_call(b); end +endmodule`}).map(({name,access,callable})=>[name,access,callable])).toEqual([ + ['a','read','top::inner'],['b',undefined,undefined], + ]); +}); + +it('respects lexical package imports and does not borrow a direction from a same-name task elsewhere',()=>{ + expect(accesses({'pkg.sv':'package p; task t(output int x); endtask endpackage', + 'top.sv':`module top; +task t(input int x); endtask +int a,b; +initial begin + begin : imported import p::*; t(a); end + begin : local_scope t(b); end +end +endmodule`}).map(({name,access,callable})=>[name,access,callable])).toEqual([ + ['a','write','p::t'],['b','read','top::t'], + ]); +}); + +it('rejects too many actuals and missing required formals while retaining their references',()=>{ + expect(accesses({'top.sv':`module top; +task t(input int a, output int b); endtask +int x,y,z; +initial begin t(x); t(x,y,z); end +endmodule`}).map(({name,access})=>[name,access])).toEqual([ + ['x',undefined],['x',undefined],['y',undefined],['z',undefined], + ]); +}); + +it('permits output concatenations but does not invent a reference-variable binding for a concat actual',()=>{ + expect(accesses({'top.sv':`module top; +task output_task(output int x); endtask +task reference_task(ref int x); endtask +int a,b,c,d; +initial begin output_task({a,b}); reference_task({c,d}); end +endmodule`}).map(({name,access})=>[name,access])).toEqual([ + ['a','write'],['b','write'],['c',undefined],['d',undefined], + ]); +}); diff --git a/__tests__/verilog-call-scope.test.ts b/__tests__/verilog-call-scope.test.ts new file mode 100644 index 000000000..3e4f1247e --- /dev/null +++ b/__tests__/verilog-call-scope.test.ts @@ -0,0 +1,53 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +it('retains callable owners and block imports when package definitions change through sync', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-call-scope-')); + let cg: CodeGraph | undefined; + const pkg = 'package p; function int run(); return 1; endfunction endpackage\npackage q; function int run(); return 2; endfunction endpackage'; + try { + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg); + fs.writeFileSync(path.join(root, 'top.sv'), `module top; +function int f(); begin : chosen import p::*; f = run(); end endfunction +function int g(); begin : other import q::*; g = run(); end endfunction +initial begin : direct p::run(); end +endmodule`); + cg = CodeGraph.initSync(root); + expect((await cg.indexAll()).success).toBe(true); + const calls = () => (cg as any).db.db.prepare("SELECT s.qualified_name source,t.qualified_name target FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target WHERE e.kind='calls' ORDER BY source,target").all(); + const before = calls(); + expect(before).toEqual([ + { source: 'top::f', target: 'p::run' }, + { source: 'top::g', target: 'q::run' }, + { source: 'top::initial@4:0', target: 'p::run' }, + ]); + fs.writeFileSync(path.join(root, 'pkg.sv'), '// move declarations\n' + pkg); + await cg.sync(); + expect(calls()).toEqual(before); + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg.replace('int run()', 'int renamed()')); + await cg.sync(); + expect(calls()).toEqual([{ source: 'top::g', target: 'q::run' }]); + fs.writeFileSync(path.join(root, 'pkg.sv'), pkg); + await cg.sync(); + expect(calls()).toEqual(before); + await cg.indexAll(); + expect(calls()).toEqual(before); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } +}); + +it.each(['import p::*;', 'function int run(); return 1; endfunction'])( + 'prefers an inner package import over outer declarations: %s', async outer => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-import-shadow-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root, 'pkg.sv'), 'package p; function int run(); return 1; endfunction endpackage\npackage q; function int run(); return 2; endfunction endpackage'); + fs.writeFileSync(path.join(root, 'top.sv'), `module top; ${outer} initial begin : local_scope import q::*; $display(run()); end endmodule`); + cg = CodeGraph.initSync(root); + await cg.indexAll(); + const calls = (cg as any).db.db.prepare("SELECT t.qualified_name target FROM edges e JOIN nodes t ON t.id=e.target WHERE e.kind='calls'").all(); + expect(calls).toEqual([{ target: 'q::run' }]); + } finally { cg?.close(); fs.rmSync(root, { recursive: true, force: true }); } + }); diff --git a/__tests__/verilog-flow.test.ts b/__tests__/verilog-flow.test.ts new file mode 100644 index 000000000..57563c14a --- /dev/null +++ b/__tests__/verilog-flow.test.ts @@ -0,0 +1,56 @@ +/** + * In an HDL the design hierarchy IS the flow: how `top` reaches `uart_rx` is + * `top` instantiating `uart_bridge` instantiating `uart_rx`. The flow finder + * rides `instantiates` edges between Verilog modules (indexed as `class`), + * and only there — a TS `new X()` stays a dependency, not a step. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { resolveNamedSymbolFlow } from '../src/graph/named-symbol-flow'; + +let tempDir: string; +let cg: CodeGraph | null = null; + +afterEach(() => { + cg?.close(); + cg = null; + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +async function index(files: Record): Promise { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-verilog-flow-')); + for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(tempDir, rel), content); + cg = await CodeGraph.init(tempDir, { index: true }); + cg.resolveReferences(); + return cg; +} + +describe('Verilog: the module-instantiation hierarchy is the flow', () => { + it('connects top to a leaf module through an unnamed bridge module', async () => { + const g = await index({ + 'top.v': 'module top(input clk, input rx, output tx);\n uart_bridge bridge (.clk(clk), .rx(rx), .tx(tx));\nendmodule\n', + 'uart_bridge.v': 'module uart_bridge(input clk, input rx, output tx);\n uart_rx rx_inst (.clk(clk), .rx(rx));\n uart_tx tx_inst (.clk(clk), .tx(tx));\nendmodule\n', + 'uart_rx.v': 'module uart_rx(input clk, input rx);\nendmodule\n', + 'uart_tx.v': 'module uart_tx(input clk, output tx);\nendmodule\n', + }); + const flow = resolveNamedSymbolFlow(g, 'top uart_rx'); + expect(flow.chains.length).toBeGreaterThan(0); + const steps = flow.chains[0]!.steps; + expect(steps.map((s) => s.node.name)).toEqual(['top', 'uart_bridge', 'uart_rx']); + expect(steps.slice(1).map((s) => s.edge?.kind)).toEqual(['instantiates', 'instantiates']); + }); + + it('does not ride a TypeScript `new X()` the same way', async () => { + const g = await index({ + 'top.ts': "import { Bridge } from './bridge';\nexport function top() { return new Bridge(); }\n", + 'bridge.ts': "import { Leaf } from './leaf';\nexport class Bridge { leaf = new Leaf(); }\n", + 'leaf.ts': 'export class Leaf {}\n', + }); + const flow = resolveNamedSymbolFlow(g, 'top Leaf'); + expect(flow.chains.map((c) => c.steps.map((s) => s.edge?.kind))).not.toContainEqual([undefined, 'instantiates', 'instantiates']); + }); +}); diff --git a/__tests__/verilog-generate-scopes.test.ts b/__tests__/verilog-generate-scopes.test.ts new file mode 100644 index 000000000..0a934e949 --- /dev/null +++ b/__tests__/verilog-generate-scopes.test.ts @@ -0,0 +1,88 @@ +import { it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +async function graph(source: string, check: (cg: CodeGraph, root: string) => void | Promise) { + const root = fs.mkdtempSync(path.join(os.tmpdir(),'hdl-generate-')); + let cg: CodeGraph | undefined; + try { + fs.writeFileSync(path.join(root,'top.sv'),source); + cg = CodeGraph.initSync(root); expect((await cg.indexAll()).success).toBe(true); + await check(cg,root); + } finally { cg?.close(); fs.rmSync(root,{recursive:true,force:true}); } +} +function targets(cg: CodeGraph, name: string) { + const source = cg.getNodesByName(name).find(n => n.kind === 'variable')!; + return cg.getCallees(source.id).filter(e => e.edge.kind === 'references').map(e => e.node.qualifiedName).sort(); +} + +it('binds inline iteration templates in nested and sibling generate scopes instead of outer ports', async () => { + await graph(`module leaf(input i); endmodule +module top(input i); +for(genvar i=0;i<2;i++) begin: outer + leaf a(.i(i)); + for(genvar i=0;i<3;i++) begin: inner leaf b(.i); end +end +for(genvar i=0;i<4;i++) begin: sibling leaf c(i); end +leaf outside(i); +endmodule`, cg => { + expect(targets(cg,'a')).toEqual(['top::outer::i']); + expect(targets(cg,'b')).toEqual(['top::outer::inner::i']); + expect(targets(cg,'c')).toEqual(['top::sibling::i']); + expect(targets(cg,'outside')).toEqual(['top::i']); + const iterations = cg.getNodesByName('i').filter(n => n.decorators?.includes('hdl:generate-parameter')); + expect(iterations).toHaveLength(3); + expect(new Set(iterations.map(n => n.id)).size).toBe(3); + expect(iterations.every(n => n.kind === 'constant' && n.decorators?.includes('hdl:template'))).toBe(true); + }); +}); + +it('separates predeclared genvars from their per-loop templates and retains anonymous scope identities', async () => { + await graph(`module leaf(input p); endmodule +module top; +genvar i,j; +for(i=0;i<2;i++) leaf a(i); +for(i=0;i<3;i++) leaf b(i); +endmodule`, async cg => { + const declared = cg.getNodesByName('i').filter(n => n.decorators?.includes('hdl:genvar')); + expect(declared.map(n => n.qualifiedName)).toEqual(['top::i']); + const a = targets(cg,'a'), b = targets(cg,'b'); + expect(a[0]).toMatch(/^top::generate@4:\d+::i$/); + expect(b[0]).toMatch(/^top::generate@5:\d+::i$/); + expect(a).not.toEqual(b); + const before = cg.getNodesByName('i').map(n => n.id).sort(); + await cg.indexAll(); + expect(cg.getNodesByName('i').map(n => n.id).sort()).toEqual(before); + }); +}); + +it('keeps references on declarations and assignment expressions in the nearest generated template scope', async () => { + await graph(`module top(input i, output wire[3:0] out); +for(genvar i=0;i<4;i++) begin: lanes +wire local_sig = i; +assign out[i] = local_sig; +end +endmodule`, cg => { + const local = cg.getNodesByName('local_sig')[0]!; + expect(cg.getCallees(local.id).filter(e => e.edge.kind === 'references').map(e => e.node.qualifiedName)).toEqual(['top::lanes::i']); + const db=(cg as any).db.db; + const wrong = db.prepare("select count(*) n from edges e join nodes s on s.id=e.source join nodes t on t.id=e.target where e.kind='references' and s.qualified_name like 'top::lanes::%' and t.qualified_name='top::i'").get(); + expect(wrong.n).toBe(0); + }); +}); + +it('converges sync and clean indexing after a generate variable and scope rename', async () => { + const initial = 'module leaf(input p); endmodule module top; for(genvar i=0;i<2;i++) begin: lanes leaf u(i); end endmodule'; + await graph(initial, async (cg,root) => { + const changed = initial.replace(/genvar i=0;i<2;i\+\+/, 'genvar j=0;j<3;j++').replace('lanes leaf u(i)', 'channels leaf u(j)'); + fs.writeFileSync(path.join(root,'top.sv'),changed); + expect((await cg.sync()).filesModified).toBe(1); + expect(targets(cg,'u')).toEqual(['top::channels::j']); + const snapshot = () => (cg as any).db.db.prepare("select s.qualified_name source,t.qualified_name target,e.kind kind from edges e join nodes s on s.id=e.source join nodes t on t.id=e.target order by source,target,kind").all(); + const synced = snapshot(); + await cg.indexAll(); + expect(snapshot()).toEqual(synced); + }); +}); diff --git a/__tests__/verilog-grammar-version.test.ts b/__tests__/verilog-grammar-version.test.ts new file mode 100644 index 000000000..861c8397f --- /dev/null +++ b/__tests__/verilog-grammar-version.test.ts @@ -0,0 +1,43 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { Parser, Language } from 'web-tree-sitter'; +import path from 'node:path'; + +let parser: Parser; +let grammar: Language; +beforeAll(async () => { + await Parser.init(); + grammar = await Language.load(path.join(__dirname, '../src/extraction/wasm/tree-sitter-systemverilog.wasm')); + parser = new Parser(); + parser.setLanguage(grammar); +}); +afterAll(() => parser?.delete()); + +describe('SystemVerilog grammar capabilities', () => { + it('loads the shipped grammar with the supported ABI', () => { + expect(grammar.abiVersion).toBe(15); + }); + it.each([ + ['class timeunits', 'class C; timeunit 1ns; timeprecision 1ps; endclass', 'timeunits_declaration'], + ['generate inside expression', 'module m; parameter N=1; if (N inside {1,2}) begin: g wire x; end endmodule', 'inside_expression'], + ['keywords in macro arguments', 'module m; `M(input, output, always_ff) endmodule', 'text_macro_usage'], + ])('parses %s without recovery', (_name, source, expectedType) => { + const tree = parser.parse(source)!; + try { + expect(tree.rootNode.hasError).toBe(false); + expect(tree.rootNode.descendantsOfType(expectedType).length).toBeGreaterThan(0); + } finally { tree.delete(); } + }); + it('rejects whitespace between the backtick and a macro name', () => { + const tree = parser.parse('module m; ` M(x) endmodule')!; + try { expect(tree.rootNode.hasError).toBe(true); } + finally { tree.delete(); } + }); + it('represents replication multipliers as constant expressions', () => { + const tree = parser.parse('module m(input a, output [3:0] q); parameter N=4; assign q={N{a}}; endmodule')!; + try { + expect(tree.rootNode.hasError).toBe(false); + const replication = tree.rootNode.descendantsOfType('multiple_concatenation')[0]!; + expect(replication.namedChildren[0]?.type).toBe('constant_expression'); + } finally { tree.delete(); } + }); +}); diff --git a/__tests__/verilog-packages.test.ts b/__tests__/verilog-packages.test.ts new file mode 100644 index 000000000..c62c6a693 --- /dev/null +++ b/__tests__/verilog-packages.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { CodeGraph } from '../src'; + +async function graph(sources: Record, check: (db: any) => void) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hdl-packages-')); + let cg: CodeGraph | undefined; + try { + for (const [name, source] of Object.entries(sources)) fs.writeFileSync(path.join(dir, name), source); + cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + check((cg as any).db.db); + } finally { cg?.destroy(); fs.rmSync(dir, { recursive: true, force: true }); } +} +const functions = 'package a; function int convert(); return 1; endfunction endpackage\npackage b; function int convert(); return 2; endfunction endpackage'; +function calls(db: any) { return db.prepare("SELECT t.qualified_name target FROM edges e JOIN nodes t ON t.id=e.target WHERE e.kind='calls'").all().map((r: any) => r.target); } + +describe('SystemVerilog package and interface identity', () => { + it('keeps the package qualifier and does not match a same-named foreign function', async () => { + await graph({ 'pkg.sv': functions, 'top.sv': 'module top; initial begin b::convert(); missing::convert(); end endmodule' }, db => { + expect(calls(db)).toEqual(['b::convert']); + }); + }); + it('resolves a visible package import but never an unrelated global package member', async () => { + await graph({ 'pkg.sv': functions, 'top.sv': 'module top; import b::*; initial convert(); endmodule\nmodule other; initial convert(); endmodule' }, db => { + expect(calls(db)).toEqual(['b::convert']); + }); + }); + it('does not choose between conflicting package imports', async () => { + await graph({ 'pkg.sv': functions, 'top.sv': 'module top; import a::*, b::*; initial convert(); endmodule' }, db => { + expect(calls(db)).toEqual([]); + }); + }); + it('does not flatten hierarchical calls to an unrelated package function', async () => { + await graph({ 'pkg.sv': functions, 'top.sv': 'module top; initial obj.convert(); endmodule' }, db => { + expect(calls(db)).toEqual([]); + }); + }); + it('does not import compilation-unit functions from an unrelated file', async () => { + await graph({ 'foreign.sv': 'function int convert(); return 1; endfunction', + 'top.sv': 'module top; initial convert(); endmodule' }, db => expect(calls(db)).toEqual([])); + }); + it('keeps nested package calls inside argument expressions', async () => { + await graph({ 'pkg.sv': functions, 'top.sv': 'module top; initial b::convert(a::convert()); endmodule' }, db => { + expect(calls(db).sort()).toEqual(['a::convert', 'b::convert']); + }); + }); + it('exposes modport directions and resolves an interface port to its exact view', async () => { + await graph({ 'bus.sv': 'interface bus; logic valid, ready; modport Master(output valid, input ready), Slave(input valid, output ready); endinterface', + 'top.sv': 'module top(bus.Master axi); endmodule' }, db => { + const views = db.prepare("SELECT qualified_name name, signature FROM nodes WHERE signature LIKE 'modport %' ORDER BY name").all(); + expect(views).toEqual([{ name: 'bus::Master', signature: 'modport Master(output valid, input ready)' }, { name: 'bus::Slave', signature: 'modport Slave(input valid, output ready)' }]); + const types = db.prepare("SELECT s.name source, t.qualified_name target FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target WHERE e.kind='type_of'").all(); + const signals = db.prepare("SELECT t.qualified_name target FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target WHERE e.kind='references' AND s.qualified_name='bus::Master' ORDER BY target").all(); + expect(signals).toEqual([{ target: 'bus::ready' }, { target: 'bus::valid' }]); + expect(types).toContainEqual({ source: 'axi', target: 'bus::Master' }); + }); + }); +}); diff --git a/__tests__/verilog-port-order.test.ts b/__tests__/verilog-port-order.test.ts new file mode 100644 index 000000000..da2ca372a --- /dev/null +++ b/__tests__/verilog-port-order.test.ts @@ -0,0 +1,47 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { Parser, Language } from 'web-tree-sitter'; +import path from 'node:path'; +import { getVerilogPortOrder } from '../src/extraction/languages/verilog-signals'; + +let parser: Parser; +beforeAll(async () => { + await Parser.init(); + parser = new Parser(); + parser.setLanguage(await Language.load(path.join(__dirname, '../src/extraction/wasm/tree-sitter-systemverilog.wasm'))); +}); +afterAll(() => parser?.delete()); +function order(source: string): string[] | null { + const tree = parser.parse(source)!; + try { return getVerilogPortOrder(tree.rootNode.namedChildren[0]!, source); } + finally { tree.delete(); } +} + +describe('authoritative HDL positional port order', () => { + it('takes ANSI comma continuation ports in header source order, ignoring comments', () => { + expect(order('module m(input a, /* comment `ifdef FALSE */ b, output [3:0] c); endmodule')).toEqual(['a', 'b', 'c']); + }); + it('uses non-ANSI header order even when body declarations are reversed', () => { + expect(order('module m(a,b,c); input c; output b; input a; endmodule')).toEqual(['a', 'b', 'c']); + }); + it('preserves escaped identifiers and supports empty headers', () => { + expect(order('module m(input \\x.y , output z); endmodule')).toEqual(['\\x.y', 'z']); + expect(order('module m(\\x.y ,z); input z; output \\x.y ; endmodule')).toEqual(['\\x.y', 'z']); + expect(order('module m(); endmodule')).toEqual([]); + expect(order('module m; endmodule')).toEqual([]); + }); + it.each([ + 'module m(a,,b); input a,b; endmodule', + 'module m({a,b},c); input a,b,c; endmodule', + 'module m(.a(x),b); input x,b; endmodule', + 'module m(input a, a); endmodule', + 'module m(input a, \\a ); endmodule', + 'module m(input a, `ifdef TEST input b, `endif output c); endmodule', + 'module m(`PORTS); endmodule', + 'module m(input a); assign = ; endmodule', + ])('does not infer order from ambiguous or incomplete syntax: %s', source => { + expect(order(source)).toBeNull(); + }); + it('accepts interfaces and ignores parameter declarations in port numbering', () => { + expect(order('interface bus #(parameter W=8)(input clk, input [W-1:0] data); endinterface')).toEqual(['clk', 'data']); + }); +}); diff --git a/__tests__/verilog-resolution.test.ts b/__tests__/verilog-resolution.test.ts new file mode 100644 index 000000000..d9a5d9f0c --- /dev/null +++ b/__tests__/verilog-resolution.test.ts @@ -0,0 +1,87 @@ +/** + * Verilog module resolution across files. + * + * A module instantiation names its target by bare name — no import pins + * `pll u_pll (...)` to a file, the tool's file list does. A design commonly + * keeps a simulation stand-in beside the real module (`src/pll.v` and + * `sim/pll_stub.v`, both `module pll`); a synthesis file must land on the real + * one whatever the directory layout, while a testbench may mean either. And a + * `` `include "defs.vh" `` must become an edge to the header file. + */ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { CodeGraph } from '../src'; + +type Edge = { kind: string; sn: string; sf: string; tn: string; tf: string }; + +async function edges(dir: string): Promise { + const cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + const db = (cg as any).db.db; + const rows = db + .prepare( + `SELECT e.kind kind, s.name sn, s.file_path sf, t.name tn, t.file_path tf + FROM edges e JOIN nodes s ON s.id = e.source JOIN nodes t ON t.id = e.target + WHERE e.kind IN ('instantiates', 'imports') AND s.language = 'verilog'` + ) + .all(); + cg.destroy(); + return rows; +} + +function write(dir: string, rel: string, body: string): void { + fs.mkdirSync(path.dirname(path.join(dir, rel)), { recursive: true }); + fs.writeFileSync(path.join(dir, rel), body); +} + +const PLL = 'module pll (input clk, output lock);\nendmodule\n'; +const TOP = 'module top (input clk, output lock);\n pll u_pll (.clk(clk), .lock(lock));\nendmodule\n'; + +describe('verilog module resolution', () => { + let dir: string; + beforeEach(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'verilog-res-')); }); + afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); }); + + it('a synthesis file skips the simulation stub even when no directory is closer', async () => { + // `top.v` at the root shares no directory with either candidate, so + // proximity cannot break the tie — the simulation path must. + write(dir, 'top.v', TOP); + write(dir, 'rtl/pll.v', PLL); + write(dir, 'sim/pll_stub.v', PLL); + const inst = (await edges(dir)).filter((e) => e.kind === 'instantiates'); + expect(inst.map(e => [e.sn, e.tn, e.tf]).sort()).toEqual([['top', 'pll', 'rtl/pll.v'], ['u_pll', 'pll', 'rtl/pll.v']]); + }); + + it('a stub named by file suffix is skipped too', async () => { + write(dir, 'top.v', TOP); + write(dir, 'rtl/pll.v', PLL); + write(dir, 'models/pll_stub.v', PLL); + const inst = (await edges(dir)).filter((e) => e.kind === 'instantiates'); + expect(inst.map((e) => e.tf)).toEqual(['rtl/pll.v', 'rtl/pll.v']); + }); + + it('a testbench keeps every candidate and lands on the stub beside it', async () => { + write(dir, 'src/pll.v', PLL); + write(dir, 'sim/pll_stub.v', PLL); + write(dir, 'sim/tb_top.v', 'module tb_top;\n reg clk; wire lock;\n pll dut (.clk(clk), .lock(lock));\nendmodule\n'); + const inst = (await edges(dir)).filter((e) => e.kind === 'instantiates'); + expect(inst.map((e) => e.tf)).toEqual(['sim/pll_stub.v', 'sim/pll_stub.v']); + }); + + it('`include resolves to the header file, by suffix and closest to the includer', async () => { + write(dir, 'src/defs.vh', '`define DATA_W 16\n'); + write(dir, 'include/axi/typedef.svh', '`define AXI_TYPEDEF(x) x\n'); + write( + dir, + 'src/top.v', + '`include "defs.vh"\n`include "axi/typedef.svh"\nmodule top (input clk);\nendmodule\n' + ); + const imports = (await edges(dir)).filter((e) => e.kind === 'imports'); + expect(imports.map((e) => [e.sf, e.tf]).sort()).toEqual([ + ['src/top.v', 'include/axi/typedef.svh'], + ['src/top.v', 'src/defs.vh'], + ]); + }); +}); diff --git a/__tests__/verilog-scopes.test.ts b/__tests__/verilog-scopes.test.ts new file mode 100644 index 000000000..67e882ffb --- /dev/null +++ b/__tests__/verilog-scopes.test.ts @@ -0,0 +1,84 @@ +import { afterEach, beforeEach, expect, it } from 'vitest'; +import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; +import { CodeGraph } from '../src'; +let root:string; let cg:CodeGraph|undefined; +beforeEach(()=>{root=fs.mkdtempSync(path.join(os.tmpdir(),'hdl-scopes-'));}); +afterEach(()=>{cg?.close();cg=undefined;fs.rmSync(root,{recursive:true,force:true});}); +const rows=()=> (cg as any).db.db.prepare(`SELECT s.qualified_name source,t.qualified_name target,e.line line FROM edges e JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target WHERE e.kind='references' AND json_extract(e.metadata,'$.refName') LIKE 'hdl:signal:%' ORDER BY source,target,line`).all() as {source:string,target:string,line:number}[]; +const code=`module crc(input clk, input feedback, output logic [15:0] crc); +always @(posedge clk) begin : update + crc[0] <= feedback; + begin : inner + logic feedback; + feedback = crc[15]; + crc[1] <= feedback; + end + begin : sibling + crc[2] <= feedback; + end +end +endmodule`; +it('binds nested shadows in their own scope and keeps outer/sibling uses',async()=>{ + fs.writeFileSync(path.join(root,'crc.sv'),code);cg=CodeGraph.initSync(root);await cg.indexAll(); + const refs=rows().filter(r=>r.target.endsWith('::feedback')); + expect(refs.filter(r=>r.line===3).map(r=>r.target)).toEqual(['crc::feedback']); + expect(refs.filter(r=>r.line===7).map(r=>r.target)).toEqual(['crc::always@2:0::update::inner::feedback']); + expect(refs.filter(r=>r.line===10).map(r=>r.target)).toEqual(['crc::feedback']); +}); +it('keeps anonymous siblings and for/foreach bindings distinct',async()=>{ + fs.writeFileSync(path.join(root,'uart.sv'),`module uart(input i, input j, input data); logic [7:0] bits; +initial begin + begin integer count; count=1; end + begin integer count; count=2; end + for(int i=0;i<8;i++) bits[i]=data; + foreach(bits[j]) bits[j]=data; + bits[i]=data; +end endmodule`);cg=CodeGraph.initSync(root);await cg.indexAll(); + const counts=cg.getNodesByName('count');expect(counts).toHaveLength(2);expect(new Set(counts.map(n=>n.qualifiedName)).size).toBe(2); + const refs=rows();expect(refs.filter(r=>r.line===5&&r.target.endsWith('::i')).every(r=>r.target.includes('::for@'))).toBe(true); + expect(refs.filter(r=>r.line===5&&r.target.endsWith('::i')).length).toBeGreaterThan(0); + expect(refs.filter(r=>r.line===6&&r.target.endsWith('::j')).every(r=>r.target.includes('::foreach@'))).toBe(true); + expect(refs.filter(r=>r.line===7&&r.target.endsWith('::i')).map(r=>r.target)).toEqual(['uart::i']); +}); +it('rebinds a removed local shadow identically on sync and full rebuild',async()=>{ + const file=path.join(root,'crc.sv');fs.writeFileSync(file,code);cg=CodeGraph.initSync(root);await cg.indexAll(); + fs.writeFileSync(file,code.replace(' logic feedback;',' // feedback now belongs to module')); + await cg.sync();const synced=rows();await cg.indexAll();expect(rows()).toEqual(synced); + expect(synced.filter(r=>r.line===7&&r.target.endsWith('::feedback')).map(r=>r.target)).toEqual(['crc::feedback']); +}); +it('models function/task formal ports and preserves function call owners inside blocks',async()=>{ + fs.writeFileSync(path.join(root,'functions.sv'),`module m(input x); +function int g(input int x); return x; endfunction +function int f(input int x); + begin : work + f = g(x); + end +endfunction +task t; input x; begin x=x; end endtask +endmodule`);cg=CodeGraph.initSync(root);await cg.indexAll(); + const refs=rows();expect(refs.filter(r=>r.line===5&&r.target.endsWith('::x')).map(r=>r.target)).toEqual(['m::f::x']); + expect(refs.filter(r=>r.line===8&&r.target.endsWith('::x')).every(r=>r.target==='m::t::x')).toBe(true); + expect(refs.filter(r=>r.line===8&&r.target.endsWith('::x')).length).toBeGreaterThan(0); + const f=cg.getNodesByName('f').find(n=>n.kind==='function')!; + expect(cg.getCallees(f.id).filter(e=>e.edge.kind==='calls').map(e=>e.node.qualifiedName)).toContain('m::g'); +}); +it('keeps calls and signal dependencies in formal defaults and loop initializers',async()=>{ + fs.writeFileSync(path.join(root,'defaults.sv'),`module m(input seed); +function int g(); return 1; endfunction +function int f(input int x=g(), input int y=seed); return x+y; endfunction +initial for(int i=g();i<3;i++) $display(i); +endmodule`);cg=CodeGraph.initSync(root);await cg.indexAll(); + const f=cg.getNodesByName('f').find(n=>n.kind==='function')!; + const initial=cg.getNodesByName('initial@4:0')[0]!; + const callees=(id:string)=>cg!.getCallees(id).filter(e=>e.edge.kind==='calls').map(e=>e.node.qualifiedName); + expect(callees(f.id)).toEqual(['m::g']);expect(callees(initial.id)).toEqual(['m::g']); + expect(rows().filter(r=>r.source==='m::f::y').map(r=>r.target)).toEqual(['m::seed']); +}); +it('does not invent outer bindings from a loop with recovered declaration syntax',async()=>{ + fs.writeFileSync(path.join(root,'recovery.sv'),`module m(input j); +initial for(int i=0,j=0;i<3;i++) $display(j); +initial $display(j); +endmodule`);cg=CodeGraph.initSync(root);await cg.indexAll(); + expect(rows().filter(r=>r.line===2&&r.target==='m::j')).toEqual([]); + expect(rows().filter(r=>r.line===3&&r.target==='m::j').length).toBeGreaterThan(0); +}); diff --git a/__tests__/verilog-signals.test.ts b/__tests__/verilog-signals.test.ts new file mode 100644 index 000000000..58076e3fe --- /dev/null +++ b/__tests__/verilog-signals.test.ts @@ -0,0 +1,148 @@ +import { beforeAll, describe, expect, it } from 'vitest'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadAllGrammars } from '../src/extraction/grammars'; + +beforeAll(async () => { await initGrammars(); await loadAllGrammars(); }); + +describe('HDL signals and procedural source navigation', () => { + it('keeps ANSI direction, non-ANSI declarations and exact source signatures', () => { + const code = `module top(input logic clk, rst, output reg [3:0] q); +wire a,b=1; logic [7:0] x,y; +endmodule +module old(a,b); input [3:0] a; output reg b; endmodule`; + const result = extractFromSource('ports.sv', code); + const fields = result.nodes.filter(n => n.kind === 'field'); + expect(fields.map(n => n.qualifiedName)).toEqual(['top::clk', 'top::rst', 'top::q', 'top::a', 'top::b', 'top::x', 'top::y', 'old::a', 'old::b']); + expect(fields.find(n => n.name === 'rst')?.decorators).toContain('hdl:input'); + expect(fields.find(n => n.name === 'q')?.signature).toBe('output reg [3:0] q'); + expect(fields.find(n => n.name === 'x')?.signature).toBe('logic [7:0] x,y;'); + expect(fields.find(n => n.qualifiedName === 'old::a')?.signature).toBe('input [3:0] a'); + }); + + it('indexes separate same-line processes and records only local signal candidates', () => { + const code = `module top(input logic clk, output logic q); +logic x; +always_ff @(posedge clk) q <= helper(x); always_comb x = q; +initial x=0; assign q = x; +endmodule`; + const result = extractFromSource('logic.sv', code); + const processes = result.nodes.filter(n => n.decorators?.includes('hdl:process')); + expect(processes.map(n => n.name.split('@')[0])).toEqual(['always_ff', 'always_comb', 'initial', 'assign']); + expect(new Set(processes.map(n => n.id)).size).toBe(4); + expect(processes[0].signature).toBe('always_ff @(posedge clk) q <= helper(x);'); + const signals = result.unresolvedReferences.filter(r => r.referenceName.startsWith('hdl:signal:')); + expect(signals.some(r => r.referenceName === 'hdl:signal:helper')).toBe(false); + expect(signals.filter(r => r.fromNodeId === processes[0].id).map(r => r.referenceName)).toEqual(['hdl:signal:clk', 'hdl:signal:q', 'hdl:signal:x']); + }); + + it('does not inherit a scalar output direction onto an interface port', () => { + const result = extractFromSource('interface.sv', 'module top(output q, bus.master link); endmodule'); + const link = result.nodes.find(n => n.name === 'link')!; + expect(link.decorators).toContain('hdl:interface:bus'); + expect(link.decorators).not.toContain('hdl:output'); + expect(result.unresolvedReferences.find(r => r.fromNodeId === link.id)?.referenceName).toBe('bus.master'); + }); + + it('does not bind a block-local shadow to a module signal', () => { + const result = extractFromSource('shadow.sv', 'module top; logic x,y; initial begin logic x; x = y; end endmodule'); + const local = result.nodes.find(n => n.name === 'x' && n.qualifiedName.includes('::block@'))!; + const refs = result.unresolvedReferences.filter(r => r.referenceName.startsWith('hdl:signal:')); + expect(refs.map(r => r.referenceName)).toEqual(['hdl:signal:x', 'hdl:signal:y']); + expect(result.nodes.find(n => n.id === refs[0].fromNodeId)?.qualifiedName).toBe(local.qualifiedName.split('::').slice(0, -1).join('::')); + }); + + it('merges non-ANSI reg redeclarations and indexes initializer dependencies', () => { + const result = extractFromSource('legacy.sv', 'module top(a); output a; reg a; wire x = a; endmodule'); + expect(result.nodes.filter(n => n.qualifiedName === 'top::a')).toHaveLength(1); + const x = result.nodes.find(n => n.name === 'x')!; + expect(result.unresolvedReferences.filter(r => r.fromNodeId === x.id).map(r => r.referenceName)).toContain('hdl:signal:a'); + }); + + it('does not confuse generate iteration bindings with outer ports', () => { + const result = extractFromSource('generate.sv', `module top(input i, input x); +for(genvar i=0;i<2;i++) begin : lanes leaf u(.a(i), .b(x)); leaf short(.i); end +leaf outside(.a(i)); endmodule`); + const inside = result.nodes.find(n => n.name === 'u')!; + const outside = result.nodes.find(n => n.name === 'outside')!; + const shorthand = result.nodes.find(n => n.name === 'short')!; + expect(result.nodes.find(n => n.qualifiedName === 'top::lanes::i')?.decorators).toContain('hdl:generate-parameter'); + expect(result.unresolvedReferences.filter(r => r.fromNodeId === shorthand.id).map(r => r.referenceName)).toContain('hdl:signal:i'); + expect(result.unresolvedReferences.filter(r => r.fromNodeId === inside.id && r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName)).toEqual(['hdl:signal:i', 'hdl:signal:x']); + expect(result.unresolvedReferences.filter(r => r.fromNodeId === outside.id).map(r => r.referenceName)).toContain('hdl:signal:i'); + }); + + it('skips package qualifiers while preserving method-call argument signals', () => { + const result = extractFromSource('package-call.sv', `module top(input p, input x, output integer q); +initial q=p::value(x); endmodule`); + expect(result.unresolvedReferences.filter(r => r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName)).toEqual(['hdl:signal:q', 'hdl:signal:x']); + }); + + it('records bit/part-select bases, bounds and concatenated assignment targets', () => { + const result = extractFromSource('selects.sv', `module top; +assign x[3] = y; +assign x[i+:W] = y; +assign x[msb:lsb] = y; +assign {a, b[j]} = v; +endmodule`); + const refs = (line: number) => result.unresolvedReferences.filter(r => r.line === line && r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName); + expect(refs(2)).toEqual(['hdl:signal:x', 'hdl:signal:y']); + expect(refs(3)).toEqual(['hdl:signal:x', 'hdl:signal:i', 'hdl:signal:W', 'hdl:signal:y']); + expect(refs(4)).toEqual(['hdl:signal:x', 'hdl:signal:msb', 'hdl:signal:lsb', 'hdl:signal:y']); + expect(refs(5)).toEqual(['hdl:signal:a', 'hdl:signal:b', 'hdl:signal:j', 'hdl:signal:v']); + }); + + it('keeps index expressions but never flattens hierarchical LHS members', () => { + const result = extractFromSource('member-select.sv', `module top; +assign remote.x[i] = y; +assign {a, remote.x[j]} = v; +assign x[bound(i)+:pkg::W] = y; +assign pkg::x[i] = y; +assign x[i].field[j] = y; +assign $root.top.x[i] = y; +endmodule`); + const refs = (line: number) => result.unresolvedReferences.filter(r => r.line === line && r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName); + expect(refs(2)).toEqual(['hdl:signal:i', 'hdl:signal:y']); + expect(refs(3)).toEqual(['hdl:signal:a', 'hdl:signal:j', 'hdl:signal:v']); + expect(refs(4)).toEqual(['hdl:signal:x', 'hdl:signal:i', 'hdl:signal:y']); + expect(refs(5)).toEqual(['hdl:signal:i', 'hdl:signal:y']); + expect(refs(6)).toEqual(['hdl:signal:i', 'hdl:signal:j', 'hdl:signal:y']); + expect(refs(7)).toEqual(['hdl:signal:i', 'hdl:signal:y']); + }); + + it('does not bind procedural for/foreach variables to matching module ports', () => { + const result = extractFromSource('procedural-loop.sv', `module top(input i, input j, input y); +logic [7:0] x; +initial for (int i=0;i<4;i++) x[i]=y; +initial foreach (x[j]) x[j]=y; +endmodule`); + const refs = result.unresolvedReferences.filter(r => r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName); + expect(result.nodes.some(n => n.name === 'i' && n.qualifiedName.includes('::for@'))).toBe(true); + expect(result.nodes.some(n => n.name === 'j' && n.qualifiedName.includes('::foreach@'))).toBe(true); + expect(refs).toContain('hdl:signal:i'); + expect(refs).toContain('hdl:signal:j'); + expect(refs).toContain('hdl:signal:x'); + expect(refs).toContain('hdl:signal:y'); + }); + + it('does not bind qualified procedural targets to same-named local ports', () => { + const result = extractFromSource('procedural-package.sv', `package p; logic x; endpackage +module top(input a, input x, input i, input j, output [7:0] y); +always_comb p::x = a; +always_comb p::x[i] = helper(a); +always_comb {p::x[i], y[j]} = a; +always_comb remote.x[i] = a; +assign y = p::x[i]; +endmodule`); + const refs = (line: number) => result.unresolvedReferences.filter(r => r.line === line && r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName); + expect(refs(3)).toEqual(['hdl:signal:a']); + expect(refs(4)).toEqual(['hdl:signal:i', 'hdl:signal:a']); + expect(refs(5)).toEqual(['hdl:signal:i', 'hdl:signal:y', 'hdl:signal:j', 'hdl:signal:a']); + expect(refs(6)).toEqual(['hdl:signal:i', 'hdl:signal:a']); + expect(refs(7)).toEqual(['hdl:signal:y', 'hdl:signal:i']); + }); + + it('does not flatten hierarchical members into local signal references', () => { + const result = extractFromSource('scope.sv', 'module top; wire x,y; assign x = remote.y; endmodule'); + expect(result.unresolvedReferences.filter(r => r.referenceName.startsWith('hdl:signal:')).map(r => r.referenceName)).toEqual(['hdl:signal:x']); + }); +}); diff --git a/codegraph-kernel/src/ccpp/mod.rs b/codegraph-kernel/src/ccpp/mod.rs index e82715d76..8be43e143 100644 --- a/codegraph-kernel/src/ccpp/mod.rs +++ b/codegraph-kernel/src/ccpp/mod.rs @@ -489,7 +489,11 @@ impl<'t> Walker<'t> { return None; } let start_line = self.line_of(node); - let id = ids::node_id(self.file_path, kind, name, start_line); + let id = if self.variant == Variant::Cpp { + ids::node_id_at_column(self.file_path, kind, name, start_line, Some(self.col_of(node))) + } else { + ids::node_id(self.file_path, kind, name, start_line) + }; // (c/cpp define no resolveBody hook, so createNode's endLine extension // for sibling-body grammars never fires — endLine is the node's own.) let end_line = node.end_position().row as u32 + 1; @@ -599,6 +603,14 @@ impl<'t> Walker<'t> { } } if let Some(name_node) = node.child_by_field_name("declarator") { + if self.variant == Variant::C && node.kind() == "function_definition" + && name_node.kind() == "parenthesized_declarator" && name_node.named_child_count() == 1 + && self.single_parameter_macro_names_function(node, node.child_by_field_name("type").map(|n| self.text(n)).unwrap_or("")) + { + if let Some(name) = name_node.named_child(0).filter(|n| n.kind() == "identifier") { + return self.text(name).to_string(); + } + } let mut resolved = name_node; // Unwrap pointer/reference declarators (`int* f()`, `T& f()`). while matches!(resolved.kind(), "pointer_declarator" | "reference_declarator") { @@ -651,8 +663,33 @@ impl<'t> Walker<'t> { } /// recoverCppMacroDefinedName (languages/c-cpp.ts:49). + fn single_parameter_macro_names_function(&self, node: Node, macro_name: &str) -> bool { + let mut root = node; + while let Some(parent) = root.parent() { root = parent; } + let mut definition: Option = None; + let mut pending = vec![root]; + while let Some(candidate) = pending.pop() { + if candidate.start_byte() >= node.start_byte() { continue; } + if candidate.kind() == "preproc_function_def" + && candidate.child_by_field_name("name").map(|n| self.text(n)) == Some(macro_name) + && definition.map(|d| candidate.start_byte() > d.start_byte()).unwrap_or(true) + { definition = Some(candidate); } + for i in 0..candidate.named_child_count() { + if let Some(child) = candidate.named_child(i) { pending.push(child); } + } + } + let Some(definition) = definition else { return false }; + let Some(params) = definition.child_by_field_name("parameters") else { return false }; + let Some(value) = definition.child_by_field_name("value") else { return false }; + let Some(parameter) = params.named_child(0) else { return false }; + if params.named_child_count() != 1 || parameter.kind() != "identifier" { return false; } + let head = self.text(value).split('(').next().unwrap_or("").trim(); + let pattern = regex::Regex::new(r"^(?:[A-Za-z_]\w*(?:::\w+)*(?:[ \t]+|[*&][ \t]*))+([A-Za-z_]\w*)$").unwrap(); + pattern.captures(head).and_then(|c| c.get(1)).map(|m| m.as_str()) == Some(self.text(parameter)) + } + fn recover_cpp_macro_defined_name(&self, node: Node) -> Option { - if node.kind() != "function_definition" { + if node.kind() != "function_definition" || node.child_by_field_name("type").is_some() { return None; } let declarator = node.child_by_field_name("declarator")?; @@ -664,11 +701,21 @@ impl<'t> Walker<'t> { return None; } let macro_name = self.text(inner); + let mut owner = node.parent(); + while let Some(parent) = owner { + if matches!(parent.kind(), "class_specifier" | "struct_specifier") { + if parent.child_by_field_name("name").map(|n| strip_cpp_template_args(self.text(n))) == Some(macro_name.to_string()) { + return None; + } + break; + } + owner = parent.parent(); + } if !macro_shaped_re().is_match(macro_name) { return None; } let params = declarator.child_by_field_name("parameters")?; - if params.named_child_count() < 2 { + if params.named_child_count() < 1 { return None; } let lone_ident_text = |p: Node| -> Option<&'t str> { @@ -685,6 +732,9 @@ impl<'t> Walker<'t> { if !has_lower_re().is_match(name) { return None; } + if params.named_child_count() == 1 { + if !self.single_parameter_macro_names_function(node, macro_name) { return None; } + } for i in 1..params.named_child_count() { if let Some(p) = params.named_child(i) { if lone_ident_text(p).is_some() { @@ -818,6 +868,15 @@ impl<'t> Walker<'t> { let kind = node.kind(); let mut skip_children = false; + if kind == "preproc_function_def" { + if let Some(name_node) = node.child_by_field_name("name") { + let name = self.text(name_node).to_string(); + let signature = Some(self.text(node).trim().to_string()); + self.create_node("constant", &name, node, Extra { signature, ..Extra::default() }); + } + return; + } + // C++ namespace blocks: prefix-only, no node (#1291/#1093). Anonymous // namespaces fall through to the generic walk. if self.variant == Variant::Cpp && kind == "namespace_definition" { @@ -962,6 +1021,10 @@ impl<'t> Walker<'t> { let extra = Extra { docstring: preceding_docstring(node, self.src), + signature: if self.variant == Variant::Cpp && node.child_by_field_name("type").is_none() + && self.recover_cpp_macro_defined_name(node).is_none() { + node.child_by_field_name("declarator").and_then(|d| d.child_by_field_name("parameters")).map(|p| self.text(p).to_string()) + } else { None }, visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None }, is_abstract: if self.variant == Variant::Cpp && self.is_cpp_pure_virtual_method_decl(node) { Some(true) @@ -1455,26 +1518,37 @@ impl<'t> Walker<'t> { } /// isCppStackConstruction (#1035). - fn is_cpp_stack_construction(&self, node: Node) -> bool { - let Some(type_node) = node.child_by_field_name("type") else { return false }; + fn cpp_stack_constructions(&self, node: Node<'t>) -> Vec<(Node<'t>, usize)> { + for i in 0..node.named_child_count() { + if let Some(child) = node.named_child(i) { + if child.kind() == "storage_class_specifier" && self.text(child) == "extern" { return Vec::new(); } + } + } + let Some(type_node) = node.child_by_field_name("type") else { return Vec::new() }; if !matches!( type_node.kind(), "type_identifier" | "template_type" | "qualified_identifier" ) { - return false; + return Vec::new(); } + let mut constructions = Vec::new(); for i in 0..node.named_child_count() { let Some(child) = node.named_child(i) else { continue }; + if child.kind() == "identifier" { constructions.push((child, 0)); continue; } if child.kind() != "init_declarator" { continue; } + let Some(declarator) = child.child_by_field_name("declarator") else { continue }; + if !matches!(declarator.kind(), "identifier" | "array_declarator") { continue; } if let Some(value) = child.child_by_field_name("value") { if matches!(value.kind(), "argument_list" | "initializer_list") { - return true; + if declarator.kind() == "array_declarator" && value.named_child_count() != 0 { continue; } + let count = (0..value.named_child_count()).filter(|&i| value.named_child(i).map(|n| n.kind() != "comment").unwrap_or(false)).count(); + constructions.push((child, count)); } } } - false + constructions } /// recordCppFnPtrBinding (tree-sitter.ts:5089). @@ -1570,6 +1644,11 @@ impl<'t> Walker<'t> { fn visit_for_calls_and_structure(&mut self, node: Node<'t>) { stack_guard!(); let kind = node.kind(); + if kind == "preproc_function_def" { + self.visit_node(node); + return; + } + self.maybe_capture_fn_refs(node); if kind == "call_expression" { @@ -1579,11 +1658,17 @@ impl<'t> Walker<'t> { } // C++ stack construction `Calculator calc(0)` / `Widget w{1,2}` (#1035). - if kind == "declaration" - && self.variant == Variant::Cpp - && self.is_cpp_stack_construction(node) - { - self.extract_instantiation(node); + if kind == "declaration" && self.variant == Variant::Cpp { + let constructions = self.cpp_stack_constructions(node); + if !constructions.is_empty() { self.extract_instantiation(node); } + if let (Some(type_node), Some(from)) = (node.child_by_field_name("type"), self.stack.last().map(|s| s.row)) { + let class_name = strip_cpp_template_args(self.text(type_node)); + if let Some(name) = class_name.split("::").filter(|s| !s.is_empty()).last() { + for (_, arity) in constructions { + self.push_ref_at(from, &format!("{class_name}::{name}/{arity}"), edge_kind_index("calls").unwrap(), node); + } + } + } } // C++ local fn-pointer bindings: declarations and branch reassignments. diff --git a/codegraph-kernel/src/dart.rs b/codegraph-kernel/src/dart.rs index 748410b10..7753e5858 100644 --- a/codegraph-kernel/src/dart.rs +++ b/codegraph-kernel/src/dart.rs @@ -398,7 +398,7 @@ impl<'t> Walker<'t> { while let Some(parent) = p { if matches!( parent.kind(), - "class_definition" | "mixin_declaration" | "extension_declaration" | "enum_declaration" + "class_definition" | "mixin_declaration" | "extension_declaration" | "extension_type_declaration" | "enum_declaration" ) { return parent.child_by_field_name("name").map(|n| self.text(n)); } @@ -630,7 +630,7 @@ impl<'t> Walker<'t> { self.extract_function(node); return; } - "class_definition" | "mixin_declaration" | "extension_declaration" => { + "class_definition" | "mixin_declaration" | "extension_declaration" | "extension_type_declaration" => { self.extract_class(node); return; } diff --git a/codegraph-kernel/src/ids.rs b/codegraph-kernel/src/ids.rs index 23b3aeb1c..062605ff8 100644 --- a/codegraph-kernel/src/ids.rs +++ b/codegraph-kernel/src/ids.rs @@ -3,6 +3,9 @@ //! //! `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex[0..32]}` //! +//! TS/JS and C++ use `node_id_at_column`, appending `:${utf16Column}` before hashing +//! so same-line declarations have distinct identities. +//! //! and the file-node special case in `TreeSitterExtractor.extract()`: //! //! `file:${filePath}` @@ -14,6 +17,10 @@ use sha2::{Digest, Sha256}; pub fn node_id(file_path: &str, kind: &str, name: &str, line: u32) -> String { + node_id_at_column(file_path, kind, name, line, None) +} + +pub fn node_id_at_column(file_path: &str, kind: &str, name: &str, line: u32, column: Option) -> String { let mut hasher = Sha256::new(); hasher.update(file_path.as_bytes()); hasher.update(b":"); @@ -22,6 +29,10 @@ pub fn node_id(file_path: &str, kind: &str, name: &str, line: u32) -> String { hasher.update(name.as_bytes()); hasher.update(b":"); hasher.update(line.to_string().as_bytes()); + if let Some(column) = column { + hasher.update(b":"); + hasher.update(column.to_string().as_bytes()); + } let digest = hasher.finalize(); // 32 hex chars = first 16 bytes. let mut hex = String::with_capacity(kind.len() + 1 + 32); diff --git a/codegraph-kernel/src/tsjs/extractors.rs b/codegraph-kernel/src/tsjs/extractors.rs index 83f070b76..249a46c84 100644 --- a/codegraph-kernel/src/tsjs/extractors.rs +++ b/codegraph-kernel/src/tsjs/extractors.rs @@ -1130,10 +1130,9 @@ impl<'t> Walker<'t> { // --- extractCall (TS/JS generic tail) ------------------------------------------------- - /// Identifier-rooted member chains have no inferred property type (#1566), - /// including host API chains (#1707). Keep the existing window namespace - /// escape; call-result and `this` receivers are outside this guard. - fn is_unresolved_member_chain(&self, receiver: Node<'t>) -> bool { + /// Identifier-rooted nested receivers retain their full call-site text. + /// Preserve the existing window namespace escape (#1794, #1566). + fn is_identifier_chain(&self, receiver: Node<'t>) -> bool { let mut cur = receiver; if !matches!(cur.kind(), "member_expression" | "subscript_expression") { return false; @@ -1174,14 +1173,6 @@ impl<'t> Walker<'t> { if is_literal_receiver(r.kind()) { return; } - // `holder.values.get()` has no inferred property type - // (#1566). Dropping the receiver or merely preserving it - // would allow unrelated same-name method guesses. Emit - // nothing, as for host chains (#1707); argument calls are - // visited independently. Mirrors extractCall in TS. - if self.is_unresolved_member_chain(r) { - return; - } } let recv_ident = receiver.filter(|r| { matches!(r.kind(), "identifier" | "simple_identifier" | "field_identifier") @@ -1205,6 +1196,10 @@ impl<'t> Walker<'t> { // TreeSitterExtractor.extractCall. let Some(inner) = self.plain_inner_callee(r) else { return }; callee_name = format!("{inner}().{method_name}"); + } else if let Some(r) = receiver.filter(|r| self.is_identifier_chain(*r)) { + // Frameworks and Steps need the call site even when + // generic resolution cannot prove a target (#1794). + callee_name = format!("{}.{method_name}", self.text(r)); } else { callee_name = method_name.to_string(); } diff --git a/codegraph-kernel/src/tsjs/mod.rs b/codegraph-kernel/src/tsjs/mod.rs index d9932ad6f..3118be3fc 100644 --- a/codegraph-kernel/src/tsjs/mod.rs +++ b/codegraph-kernel/src/tsjs/mod.rs @@ -348,7 +348,7 @@ impl<'t> Walker<'t> { return None; } let start_line = self.line_of(node); - let id = ids::node_id(self.file_path, kind, name, start_line); + let id = ids::node_id_at_column(self.file_path, kind, name, start_line, Some(self.col_of(node))); // endLine body extension: resolveBody only (TS/JS: function-valued // class fields whose body nests in the arrow / HOF-wrapped arrow). diff --git a/docs/hdl-axi-build-validation.json b/docs/hdl-axi-build-validation.json new file mode 100644 index 000000000..d60be7816 --- /dev/null +++ b/docs/hdl-axi-build-validation.json @@ -0,0 +1,222 @@ +{ + "axiRevision": "da8793b0e3f14c9186c94fac9ed6ec06375c254e", + "benderLockSha256": "6e91c73d8fdb6f6a2280e433dc4a3879adc0bb09969c09049d65564c0c50d223", + "tools": { + "bender": "bender 0.32.1", + "verilator": "Verilator 5.051 devel rev v5.050-260-g1c9a9e0cc (mod)" + }, + "dependencies": { + "common_cells": { + "revision": "03d98106aa19952a10360d2230def85144a0008b", + "path": ".bender/git/checkouts/common_cells-3e2fcccecd7aee7b" + }, + "common_verification": { + "revision": "fb1885f48ea46164a10568aeff51884389f67ae3", + "path": ".bender/git/checkouts/common_verification-5b9bacfe2f79eb87" + }, + "tech_cells_generic": { + "revision": "3a3de73632a06826b1bd9c65a0a2e92b32016845", + "path": ".bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b" + } + }, + "profile": { + "target": "synth_test", + "top": "synth_axi_lite_xbar", + "parameterValues": { + "NoSlvMst": [ + 1, + 2, + 4 + ] + }, + "defines": [ + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR" + ], + "includeDirs": [ + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include" + ], + "filelistSha256": "89377169cba1b548529c3fd2a54a5b20fd66c925ccb94e64f005d343945a37f3" + }, + "cases": [ + { + "name": "demux-missing-include-control", + "argv": [ + "${VERILATOR}", + "-E", + "-P", + "-Iinclude", + "src/axi_demux_simple.sv" + ], + "exit": 1, + "seconds": 0.072, + "warnings": {}, + "errors": 3 + }, + { + "name": "demux-preprocess-default", + "argv": [ + "${VERILATOR}", + "-E", + "-P", + "-Iinclude", + "-I${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "src/axi_demux_simple.sv" + ], + "exit": 0, + "seconds": 0.072, + "warnings": {}, + "errors": 0 + }, + { + "name": "demux-preprocess-VCS", + "argv": [ + "${VERILATOR}", + "-E", + "-P", + "-Iinclude", + "-I${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+define+VCS", + "src/axi_demux_simple.sv" + ], + "exit": 0, + "seconds": 0.071, + "warnings": {}, + "errors": 0 + }, + { + "name": "xbar-lint-1", + "argv": [ + "${VERILATOR}", + "--lint-only", + "--assert", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "-GNoSlvMst=1", + "--Mdir", + "${AXI}/build/final-controls/obj-1", + "-f", + "${AXI}/build/final-controls/verilator.f" + ], + "exit": 0, + "seconds": 0.611, + "warnings": { + "PINMISSING": 3, + "WIDTHEXPAND": 3, + "WIDTHTRUNC": 3 + }, + "errors": 0 + }, + { + "name": "xbar-lint-2", + "argv": [ + "${VERILATOR}", + "--lint-only", + "--assert", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "-GNoSlvMst=2", + "--Mdir", + "${AXI}/build/final-controls/obj-2", + "-f", + "${AXI}/build/final-controls/verilator.f" + ], + "exit": 0, + "seconds": 0.773, + "warnings": { + "PINMISSING": 3, + "SPLITVAR": 3, + "UNOPTFLAT": 1, + "WIDTHEXPAND": 3, + "WIDTHTRUNC": 4 + }, + "errors": 0 + }, + { + "name": "xbar-lint-4", + "argv": [ + "${VERILATOR}", + "--lint-only", + "--assert", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "-GNoSlvMst=4", + "--Mdir", + "${AXI}/build/final-controls/obj-4", + "-f", + "${AXI}/build/final-controls/verilator.f" + ], + "exit": 0, + "seconds": 1.183, + "warnings": { + "PINMISSING": 3, + "UNOPTFLAT": 2, + "WIDTHEXPAND": 3, + "WIDTHTRUNC": 4 + }, + "errors": 0 + } + ], + "trackedInputCount": 152, + "trackedInputsUnchanged": true, + "limitations": [ + "Warnings retained with -Wno-fatal; not a warning-free build", + "Lint/elaboration controls only, no simulation, synthesis or FPGA validation", + "Not the complete axi_synth_bench and not all tops" + ], + "toolDistribution": { + "benderRelease": "https://github.com/pulp-platform/bender/releases/tag/v0.32.1", + "benderArchiveSha256": "59a36723b056a06b266dc68d4ceedcd0aa17a1c096e8c2ea512af264ff6a13f6", + "benderArchiveChecksumVerified": true, + "ossCadSuite": "20260825" + }, + "negativeEnvironmentControl": { + "BENDER_DIR": "/no/such/project", + "result": "Explicit --dir pins checkout; all six controls passed" + } +} diff --git a/docs/hdl-corpus-manifest.json b/docs/hdl-corpus-manifest.json new file mode 100644 index 000000000..22f5ceab7 --- /dev/null +++ b/docs/hdl-corpus-manifest.json @@ -0,0 +1,2745 @@ +{ + "schema_version": 1, + "purpose": "Воспроизводимые source snapshots для проверки CodeGraph HDL; исходники не распространяются.", + "hash_algorithm": "sha256", + "corpora": [ + { + "id": "dual-uart", + "provenance": { + "remote": "https://github.com/danusha2345/2uart_to_1uart.git", + "revision": "afce8bf3392da11ed76dacd122e32b1fdb1aee6f", + "revision_matches_all_captured_sources": true, + "sources_not_matching_revision": [], + "working_tree_matches_all_captured_sources": true, + "sources_not_matching_origin_working_tree": [], + "note": "revision — доказательство только совпавших blobs; остальные файлы закреплены отдельными SHA-256" + }, + "license": { + "status": "unknown; license file отсутствует в исходном repo root", + "evidence": [], + "source_spdx": [] + }, + "source_files": [ + { + "path": "src/fifo.v", + "sha256": "c65ecffaec1fca5e7af55e4ae58071ec79299bff504c22fd54d4a87dc0660641", + "bytes": 2366 + }, + { + "path": "src/top.v", + "sha256": "1868c1182f158533c439804d084a65cda98b6d56d6c25783f4f226a62762ab2c", + "bytes": 7818 + }, + { + "path": "src/uart_arbiter.v", + "sha256": "61255fb82edcfd2b8ce5b72a175fcdecc7682c772d9247d6a379b74e8e242aaf", + "bytes": 5505 + }, + { + "path": "src/uart_rx.v", + "sha256": "19c2a62de05955a6f6efefab307d7f5d38d15e14f6bf7d72a43bbb66840d7a17", + "bytes": 4458 + }, + { + "path": "src/uart_rx_autobaud.v", + "sha256": "ea8bbd813e26807e33f7ec3151041b757063e79523196e077253aeaf2aa74838", + "bytes": 10553 + }, + { + "path": "src/uart_tx.v", + "sha256": "67a1c828fc32fb81ed8c763c7230ff0c495662003d5d2796a9c1c0253d09cbdb", + "bytes": 3248 + }, + { + "path": "tb/tb_fifo.v", + "sha256": "8f4c07e1c8aa2f16104faebe806747fda0fe9339c0ef7d3cdc9bab7e7e0b008f", + "bytes": 11580 + }, + { + "path": "tb/tb_top.v", + "sha256": "a9634eea196f2959f17f918d657a1829ac73b8259fad9b74b9dce7e92c2dc7ee", + "bytes": 13085 + }, + { + "path": "tb/tb_uart_arbiter.v", + "sha256": "310be4640f501fe0be8471245e233dd8f2bfe4c6a36a61e642408eed1771cc1a", + "bytes": 12202 + }, + { + "path": "tb/tb_uart_rx.v", + "sha256": "c55dfe4809564da5debcaf14f90d87553c9c27841dc11cd0cbdbf4a029884e02", + "bytes": 12179 + }, + { + "path": "tb/tb_uart_tx.v", + "sha256": "ca2311337d7d403b4e5d246e662045d712316816fedd6f7ea2390b9eff1145ac", + "bytes": 8715 + } + ], + "source_inventory_sha256": "a71a77bfc592f9ed723de5fd50edad8e3d7e0bca5e71016d68bc62f491cc5adc", + "build_evidence": [ + { + "path": "build.tcl", + "sha256": "fc60c215a602884aeb12a62affaede928d8d3e73d27eee4f4d4aaf4062517130", + "bytes": 829, + "command_evidence": [ + { + "line": 8, + "text": "add_file -type verilog \"src/top.v\"" + }, + { + "line": 9, + "text": "add_file -type verilog \"src/uart_rx.v\"" + }, + { + "line": 10, + "text": "add_file -type verilog \"src/uart_tx.v\"" + }, + { + "line": 11, + "text": "add_file -type verilog \"src/fifo.v\"" + }, + { + "line": 12, + "text": "add_file -type verilog \"src/uart_arbiter.v\"" + }, + { + "line": 15, + "text": "add_file -type cst \"src/tangprimer25k.cst\"" + }, + { + "line": 16, + "text": "add_file -type sdc \"src/fpga_project.sdc\"" + }, + { + "line": 19, + "text": "set_option -top_module top" + } + ], + "declared_files": [ + "src/top.v", + "src/uart_rx.v", + "src/uart_tx.v", + "src/fifo.v", + "src/uart_arbiter.v", + "src/tangprimer25k.cst", + "src/fpga_project.sdc" + ], + "matches_revision": true + }, + { + "path": "fpga_primer.gprj", + "sha256": "febe843273bf64f873911a108fc9e02f5e69afa9cf34cecb508c73201a924c7a", + "bytes": 873, + "command_evidence": [], + "declared_files": [ + "src/fifo.v", + "src/top.v", + "src/uart_arbiter.v", + "src/uart_rx.v", + "src/uart_rx_autobaud.v", + "src/uart_tx.v", + "src/tangprimer25k.cst", + "src/fpga_project.sdc" + ], + "matches_revision": true + }, + { + "path": "tb/Makefile", + "sha256": "87026066eb3e5f371c0cb9047ab65b73c49f9f5c830a4d6ea28eb3c02a0e1bb3", + "bytes": 991, + "command_evidence": [ + { + "line": 19, + "text": "$(IV) -o $(SIMDIR)/tb_fifo tb_fifo.v $(SRCDIR)/fifo.v" + }, + { + "line": 23, + "text": "$(IV) -o $(SIMDIR)/tb_uart_rx tb_uart_rx.v $(SRCDIR)/uart_rx.v" + }, + { + "line": 27, + "text": "$(IV) -o $(SIMDIR)/tb_uart_tx tb_uart_tx.v $(SRCDIR)/uart_tx.v" + }, + { + "line": 31, + "text": "$(IV) -o $(SIMDIR)/tb_uart_arbiter tb_uart_arbiter.v $(SRCDIR)/uart_arbiter.v" + }, + { + "line": 35, + "text": "$(IV) -o $(SIMDIR)/tb_top tb_top.v $(SRCS)" + } + ], + "matches_revision": true + } + ], + "validation_profiles": [ + { + "name": "synth-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "src/fifo.v", + "src/top.v", + "src/uart_arbiter.v", + "src/uart_rx.v", + "src/uart_rx_autobaud.v", + "src/uart_tx.v" + ], + "top_modules": [ + "top" + ], + "top_evidence": [ + { + "kind": "module", + "name": "top", + "path": "src/top.v", + "line": 20 + } + ], + "include_dirs": [], + "defines": {} + }, + { + "name": "sim-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "src/fifo.v", + "src/top.v", + "src/uart_arbiter.v", + "src/uart_rx.v", + "src/uart_rx_autobaud.v", + "src/uart_tx.v", + "tb/tb_fifo.v", + "tb/tb_top.v", + "tb/tb_uart_arbiter.v", + "tb/tb_uart_rx.v", + "tb/tb_uart_tx.v" + ], + "top_modules": [ + "tb_top" + ], + "top_evidence": [ + { + "kind": "module", + "name": "tb_top", + "path": "tb/tb_top.v", + "line": 3 + } + ], + "include_dirs": [], + "defines": {} + } + ], + "literal_include_references": [], + "missing_dependencies": { + "literal_includes": [], + "imported_packages_without_declaration": [], + "symbols": [], + "locked_external_packages": [], + "build_recipe_files_not_in_corpus": [ + { + "path": "src/tangprimer25k.cst", + "evidence": "build.tcl" + }, + { + "path": "src/fpga_project.sdc", + "evidence": "build.tcl" + }, + { + "path": "src/tangprimer25k.cst", + "evidence": "fpga_primer.gprj" + }, + { + "path": "src/fpga_project.sdc", + "evidence": "fpga_primer.gprj" + } + ], + "scope": "Только наблюдаемые literal includes/imports и перечисленные primitives; conditional branches не вычисляются. Не доказательство полной build closure." + } + }, + { + "id": "card-reader", + "provenance": { + "remote": "https://github.com/danusha2345/fpga_tang_nano_9k_card_reader.git", + "revision": "dac04f8ec82869d3e358d3c4eefb7c168d42546f", + "revision_matches_all_captured_sources": false, + "sources_not_matching_revision": [ + "sim/tb_uart_bridge_debug.v" + ], + "working_tree_matches_all_captured_sources": true, + "sources_not_matching_origin_working_tree": [], + "note": "revision — доказательство только совпавших blobs; остальные файлы закреплены отдельными SHA-256" + }, + "license": { + "status": "unknown; license file отсутствует в исходном repo root", + "evidence": [], + "source_spdx": [] + }, + "source_files": [ + { + "path": "sim/pll_stub.v", + "sha256": "0192791254f9c3215e1471afe211df1c199d2fa25bec674022a615405b3e06b9", + "bytes": 381 + }, + { + "path": "sim/tb_crc16.v", + "sha256": "553ebf3f32407ea54a53e471aa61ec5a7ee1e1a15918c765e12e9e22b4fa142c", + "bytes": 3093 + }, + { + "path": "sim/tb_crc7.v", + "sha256": "24d01ad415f665fdc7eac4fd4791728d0f5914d387b2388f6b261836d6115308", + "bytes": 3704 + }, + { + "path": "sim/tb_crc8.v", + "sha256": "91955aa6b6b7eecbb9973968f44c4b99b91c0c86a76d14d98de18d808916d0b0", + "bytes": 3760 + }, + { + "path": "sim/tb_emmc_cmd.v", + "sha256": "75e9d48be8d4baf8cea3a431e5a3670fa6a2c47ff80e8b1a5d5c7ce816cd0c89", + "bytes": 11899 + }, + { + "path": "sim/tb_emmc_cmd_crc_verify.v", + "sha256": "2498a5dd9d8c396421639c770124dd20f6e222c118a03582396fb222bcf946e2", + "bytes": 9231 + }, + { + "path": "sim/tb_emmc_controller.v", + "sha256": "7981f493e0292155e1de00b81ccf4988b280541234de20a9a0531290e6a28e0b", + "bytes": 123856 + }, + { + "path": "sim/tb_emmc_dat.v", + "sha256": "bda8e0aa2a2d5e1bf00418019b7a33274227c74eac8fd9f8aba786629a8fcd22", + "bytes": 23900 + }, + { + "path": "sim/tb_emmc_init.v", + "sha256": "d081bded8352a2571c996b1793dea2a7a6e7704bc5fd0c3a177d959787eef55d", + "bytes": 13724 + }, + { + "path": "sim/tb_led_status.v", + "sha256": "a147c7fe76605ab3b9de7694aeabb4599981cf096f648cd379025fd87f08346d", + "bytes": 6202 + }, + { + "path": "sim/tb_sector_buf.v", + "sha256": "91317266d09fb3d9f04716f50d4ab0094803089959d115d8c02c0f7fd087cb82", + "bytes": 5479 + }, + { + "path": "sim/tb_sector_buf_wr.v", + "sha256": "c824a359034bae978cbb79601c1f187b699dd44e51483046af7e63144f1fe850", + "bytes": 6581 + }, + { + "path": "sim/tb_top_integration.v", + "sha256": "0a18c66bd1470a1f8ab6d06debc0ba041247e19a5043e7ec994eff02580201e6", + "bytes": 27724 + }, + { + "path": "sim/tb_uart_bridge.v", + "sha256": "e0a3da7e2dd07f032e6853a68850e66b79532775cb85af35a1f73211bc7dd4b8", + "bytes": 97128 + }, + { + "path": "sim/tb_uart_bridge_debug.v", + "sha256": "fb613fbdf492ec1078d0e4c370f27d18d23089d62b4a6523197b51e4205ab959", + "bytes": 56 + }, + { + "path": "sim/tb_uart_loopback.v", + "sha256": "b3bc3ccfbf1027283caf4c38d1c64d881cd905eb690c172e94a36b8d54c2636f", + "bytes": 4755 + }, + { + "path": "sim/tb_uart_rx.v", + "sha256": "b2ca6aee7434d78b70ada6c1509e6d3c3f768b62e0f136575c9ad10f7baed2f5", + "bytes": 7493 + }, + { + "path": "sim/tb_uart_tx.v", + "sha256": "7f1694bde8defdaa081425495eb9cfa08e98bb27104143b2725f97065d354bd6", + "bytes": 6631 + }, + { + "path": "src/crc8.v", + "sha256": "fd17a37ac67e64efd43e77223da726bd1709a5bb4569e5ba86ef82dd80ebb5f7", + "bytes": 1359 + }, + { + "path": "src/emmc_cmd.v", + "sha256": "ae1b940eca8b5d0544d88d396bfb5d08b769515d2d43433807e432da97c7339a", + "bytes": 10333 + }, + { + "path": "src/emmc_controller.v", + "sha256": "0cfd8305dea92c4092e1f104d0d30afbb3ec897be4ae56d3a3b841da536d2596", + "bytes": 56612 + }, + { + "path": "src/emmc_crc16.v", + "sha256": "ecec53c91b36f19bfa14ab812cc64124e138dda9fee9ce880ea02f51ba4094dd", + "bytes": 1283 + }, + { + "path": "src/emmc_crc7.v", + "sha256": "02dfdf52d5d31a5ae3fa42f5259e985367eeb8d4356a09621173ae9f954e8bc8", + "bytes": 876 + }, + { + "path": "src/emmc_dat.v", + "sha256": "a3115db1e40b3dfdbfb38576b1b313fe7cc798f05a6f61cf33e5df565d7de1f3", + "bytes": 14746 + }, + { + "path": "src/emmc_init.v", + "sha256": "4e40f0e6c0ba032572f1c23080b828815b19e0758af81af4f47ca39a6e2bd659", + "bytes": 12607 + }, + { + "path": "src/led_status.v", + "sha256": "2a985147d102321e2e6d4f38416454789bc990d76afb95e63e66c4621239c10f", + "bytes": 2592 + }, + { + "path": "src/pll.v", + "sha256": "93beb7e1d457ea058d9c36b18f0a0f9b74610065e7790e620b86804a60b97a4b", + "bytes": 1657 + }, + { + "path": "src/sector_buf.v", + "sha256": "81f15d0757f1b5a44d580182df54b5e64acd5b46986e57ab7827384dd2c0cc5e", + "bytes": 1825 + }, + { + "path": "src/sector_buf_wr.v", + "sha256": "df3a8f302fdfcbfdc8e3508b5a43e42a609582d970befc46623dca09c17b00cd", + "bytes": 1978 + }, + { + "path": "src/top.v", + "sha256": "c4ae822abf6d2f0606c3212d3ce0a07fe0f6e94641ca0031f20fbe13c05b6e47", + "bytes": 7984 + }, + { + "path": "src/uart_bridge.v", + "sha256": "c271dd7a7d319c56060e142c9d9a4f714b03c042b040bda911a5f568af944481", + "bytes": 44080 + }, + { + "path": "src/uart_rx.v", + "sha256": "143a1d6075ba1ee0cff545a10e9fe481344be8a33d4805e87298269ed4703fc9", + "bytes": 3724 + }, + { + "path": "src/uart_tx.v", + "sha256": "4cc042e5556e61766a586ab7ce1315a673bbc0abdf4586158dbf1f6542266086", + "bytes": 2940 + } + ], + "source_inventory_sha256": "99ddbc05f3e9045f39ab3f94bda8586fd3fd1e6a71f5ba36c05bd554d2fbb1d9", + "build_evidence": [ + { + "path": "build.tcl", + "sha256": "63d49dee2411d6e198808e0fa78f6ba606a3c7266f568caebd82368aa79bf7bf", + "bytes": 213, + "command_evidence": [], + "declared_files": [], + "matches_revision": true + }, + { + "path": "card_reader.gprj", + "sha256": "30547212c14daa7ac56f47c4aaf80ab538998508822a4c13968ce6144e90499d", + "bytes": 1428, + "command_evidence": [], + "declared_files": [ + "src/top.v", + "src/pll.v", + "src/led_status.v", + "src/uart_rx.v", + "src/uart_tx.v", + "src/uart_bridge.v", + "src/crc8.v", + "src/emmc_crc7.v", + "src/emmc_crc16.v", + "src/emmc_cmd.v", + "src/emmc_dat.v", + "src/emmc_init.v", + "src/emmc_controller.v", + "src/sector_buf.v", + "src/sector_buf_wr.v", + "src/tangnano9k.cst", + "src/tangnano9k.sdc" + ], + "matches_revision": true + }, + { + "path": "sim/Makefile", + "sha256": "cf03029a0536957b62eebb764a87b62562b8d11d04f75570a00fbd9d98244f6d", + "bytes": 3887, + "command_evidence": [ + { + "line": 4, + "text": "IFLAGS = -I $(SRCDIR) -g2005-sv" + }, + { + "line": 36, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_crc8 tb_crc8.v $(SRC_CRC8) && $(VVP) sim_crc8" + }, + { + "line": 39, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_crc7 tb_crc7.v $(SRC_CRC7) && $(VVP) sim_crc7" + }, + { + "line": 42, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_crc16 tb_crc16.v $(SRC_CRC16) && $(VVP) sim_crc16" + }, + { + "line": 45, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_uart_tx tb_uart_tx.v $(SRC_UART_TX) && $(VVP) sim_uart_tx" + }, + { + "line": 48, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_uart_rx tb_uart_rx.v $(SRC_UART_RX) && $(VVP) sim_uart_rx" + }, + { + "line": 51, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_uart_loopback tb_uart_loopback.v $(SRC_UART_TX) $(SRC_UART_RX) && $(VVP) sim_uart_loopback" + }, + { + "line": 54, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_sector_buf tb_sector_buf.v $(SRC_SECTOR_BUF) && $(VVP) sim_sector_buf" + }, + { + "line": 57, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_sector_buf_wr tb_sector_buf_wr.v $(SRC_SECTOR_BUF_WR) && $(VVP) sim_sector_buf_wr" + }, + { + "line": 60, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_emmc_cmd tb_emmc_cmd.v $(SRC_EMMC_CMD) && $(VVP) sim_emmc_cmd" + }, + { + "line": 63, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_emmc_cmd_crc_verify tb_emmc_cmd_crc_verify.v $(SRC_EMMC_CMD) && $(VVP) sim_emmc_cmd_crc_verify" + }, + { + "line": 66, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_uart_bridge tb_uart_bridge.v $(SRC_UART_BRIDGE) && $(VVP) sim_uart_bridge" + }, + { + "line": 69, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_emmc_controller tb_emmc_controller.v $(SRC_EMMC_CTRL) && $(VVP) sim_emmc_controller" + }, + { + "line": 72, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_emmc_dat tb_emmc_dat.v $(SRC_EMMC_DAT) && $(VVP) sim_emmc_dat" + }, + { + "line": 75, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_emmc_init tb_emmc_init.v $(SRC_EMMC_INIT) && $(VVP) sim_emmc_init" + }, + { + "line": 78, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_led_status tb_led_status.v $(SRC_LED_STATUS) && $(VVP) sim_led_status" + }, + { + "line": 81, + "text": "@$(IVERILOG) $(IFLAGS) -o sim_top_integration tb_top_integration.v $(SRC_TOP_INTEGRATION) && $(VVP) sim_top_integration" + }, + { + "line": 84, + "text": "waves: IFLAGS += -DVCD_DUMP" + } + ], + "matches_revision": true + } + ], + "validation_profiles": [ + { + "name": "synth-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "src/crc8.v", + "src/emmc_cmd.v", + "src/emmc_controller.v", + "src/emmc_crc16.v", + "src/emmc_crc7.v", + "src/emmc_dat.v", + "src/emmc_init.v", + "src/led_status.v", + "src/pll.v", + "src/sector_buf.v", + "src/sector_buf_wr.v", + "src/top.v", + "src/uart_bridge.v", + "src/uart_rx.v", + "src/uart_tx.v" + ], + "top_modules": [ + "top" + ], + "top_evidence": [ + { + "kind": "module", + "name": "top", + "path": "src/top.v", + "line": 4 + } + ], + "include_dirs": [], + "defines": {} + }, + { + "name": "sim-integration-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "sim/pll_stub.v", + "sim/tb_crc16.v", + "sim/tb_crc7.v", + "sim/tb_crc8.v", + "sim/tb_emmc_cmd.v", + "sim/tb_emmc_cmd_crc_verify.v", + "sim/tb_emmc_controller.v", + "sim/tb_emmc_dat.v", + "sim/tb_emmc_init.v", + "sim/tb_led_status.v", + "sim/tb_sector_buf.v", + "sim/tb_sector_buf_wr.v", + "sim/tb_top_integration.v", + "sim/tb_uart_bridge.v", + "sim/tb_uart_bridge_debug.v", + "sim/tb_uart_loopback.v", + "sim/tb_uart_rx.v", + "sim/tb_uart_tx.v", + "src/crc8.v", + "src/emmc_cmd.v", + "src/emmc_controller.v", + "src/emmc_crc16.v", + "src/emmc_crc7.v", + "src/emmc_dat.v", + "src/emmc_init.v", + "src/led_status.v", + "src/pll.v", + "src/sector_buf.v", + "src/sector_buf_wr.v", + "src/top.v", + "src/uart_bridge.v", + "src/uart_rx.v", + "src/uart_tx.v" + ], + "top_modules": [ + "tb_top_integration" + ], + "top_evidence": [ + { + "kind": "module", + "name": "tb_top_integration", + "path": "sim/tb_top_integration.v", + "line": 7 + } + ], + "include_dirs": [ + "src" + ], + "defines": {} + } + ], + "literal_include_references": [], + "missing_dependencies": { + "literal_includes": [], + "imported_packages_without_declaration": [], + "symbols": [ + { + "name": "rPLL", + "kind": "vendor primitive", + "path": "src/pll.v", + "line": 13, + "evidence": "rPLL #( ... ); declaration отсутствует в corpus" + } + ], + "locked_external_packages": [], + "build_recipe_files_not_in_corpus": [ + { + "path": "src/tangnano9k.cst", + "evidence": "card_reader.gprj" + }, + { + "path": "src/tangnano9k.sdc", + "evidence": "card_reader.gprj" + } + ], + "scope": "Только наблюдаемые literal includes/imports и перечисленные primitives; conditional branches не вычисляются. Не доказательство полной build closure." + } + }, + { + "id": "axi", + "provenance": { + "remote": "https://github.com/pulp-platform/axi.git", + "revision": "da8793b0e3f14c9186c94fac9ed6ec06375c254e", + "revision_matches_all_captured_sources": true, + "sources_not_matching_revision": [], + "working_tree_matches_all_captured_sources": true, + "sources_not_matching_origin_working_tree": [], + "note": "revision — доказательство только совпавших blobs; остальные файлы закреплены отдельными SHA-256" + }, + "license": { + "status": "evidence-found", + "evidence": [ + { + "path": "LICENSE", + "sha256": "6527a46225891b976fa94f634f6ee0cd22e1c7129d6f11c5c98c997627625fc7", + "bytes": 10329, + "first_line": "SOLDERPAD HARDWARE LICENSE version 0.51", + "matches_revision": true + } + ], + "source_spdx": [ + { + "path": "src/axi_burst_splitter_gran.sv", + "line": 3, + "identifier": "SHL-0.51" + }, + { + "path": "src/axi_inval_filter.sv", + "line": 3, + "identifier": "SHL-0.51" + }, + { + "path": "src/axi_sim_mem.sv", + "line": 2, + "identifier": "SHL-0.51" + }, + { + "path": "src/axi_throttle.sv", + "line": 3, + "identifier": "SHL-0.51" + }, + { + "path": "test/tb_axi_bus_compare.sv", + "line": 2, + "identifier": "SHL-0.51" + }, + { + "path": "test/tb_axi_sim_mem.sv", + "line": 2, + "identifier": "SHL-0.51" + }, + { + "path": "test/tb_axi_slave_compare.sv", + "line": 2, + "identifier": "SHL-0.51" + } + ] + }, + "source_files": [ + { + "path": "include/axi/assign.svh", + "sha256": "8d6aa6ecc736515db75bb691e377b64753310c442e107757b99caabdf043b6cd", + "bytes": 45647 + }, + { + "path": "include/axi/port.svh", + "sha256": "3fa0ccc81d775c439e47d2c01dd62be79a9cfd8f2f84a05a1ef90b9cf3fb43d1", + "bytes": 6805 + }, + { + "path": "include/axi/typedef.svh", + "sha256": "63f095042c9183f977e95307df14ce0baa06b71121491a380af289f8b309482c", + "bytes": 14574 + }, + { + "path": "src/axi_atop_filter.sv", + "sha256": "06b31710ae8a80ecbba58a75b13d1441f9d14b26d5d425a52adaa94ff7f2b156", + "bytes": 16551 + }, + { + "path": "src/axi_burst_splitter.sv", + "sha256": "02de2c32cd5b1c933c2ee8700470664af60fb0916c6844962d307d575edce961", + "bytes": 3759 + }, + { + "path": "src/axi_burst_splitter_gran.sv", + "sha256": "0e265a8358a2b1a3eef0c564d29308c9e5cd4bacd33dee17475acd5dfb7fc44b", + "bytes": 25804 + }, + { + "path": "src/axi_burst_unwrap.sv", + "sha256": "d6e57c5a5c4c0c53a89074983cbb4aad82cf4987d3a181b0455ba2e8ecf58922", + "bytes": 23938 + }, + { + "path": "src/axi_bus_compare.sv", + "sha256": "588aabe3bf1fd0741adcdb64e0e8fffc3f0a2292070a7b2b09e7db7393a65d59", + "bytes": 30822 + }, + { + "path": "src/axi_cdc.sv", + "sha256": "060f775eb544a092bb4c8d0b5168a6cb7b0cd6128f1e4a35477918426c6307d2", + "bytes": 9778 + }, + { + "path": "src/axi_cdc_dst.sv", + "sha256": "19e0b9500ab384588bbcea09516e5cccb66c20fefef9cacc93e4213b19f67bdf", + "bytes": 11399 + }, + { + "path": "src/axi_cdc_src.sv", + "sha256": "3cdf744af8a126660dc5e57372112c09b86f162740b61d00041d1a27d95fcd6a", + "bytes": 11409 + }, + { + "path": "src/axi_chan_compare.sv", + "sha256": "94b632f82a8201357c838cd46837a861dc90a14f247035477cc3516e774c8688", + "bytes": 13598 + }, + { + "path": "src/axi_cut.sv", + "sha256": "a455ac4f0ca4d88c289579bdcb37d75c6e76c665d3aace6b8d5cd9f2272f1696", + "bytes": 9826 + }, + { + "path": "src/axi_delayer.sv", + "sha256": "84acca0fe4cbdddf39dce37f5bc65a308d7b98291398c2a2006db8f2217eee03", + "bytes": 6822 + }, + { + "path": "src/axi_demux.sv", + "sha256": "db364dfa2ce938cfb1f88c5eb6887fedfed16a1f2021446b7890dc625a864b09", + "bytes": 11244 + }, + { + "path": "src/axi_demux_id_counters.sv", + "sha256": "05a5b6b1cc6c481f8e3c8b1e785a4082831f0a3ab242cf266fd390447d018c0e", + "bytes": 6086 + }, + { + "path": "src/axi_demux_simple.sv", + "sha256": "f0beea644e9b4721046e27fb1179e9a1f481a1767a52092fe5ee7a1b3d287d08", + "bytes": 22427 + }, + { + "path": "src/axi_dumper.sv", + "sha256": "972066185a4cf52e0a53eef13b237306763451b8c98a6fb63fe49c01bb1b0d3f", + "bytes": 7872 + }, + { + "path": "src/axi_dw_converter.sv", + "sha256": "d3abfa0da05869740f6d8cfac03b36fd6719ac2eb9d46e6b9836346cbb1d00f1", + "bytes": 8249 + }, + { + "path": "src/axi_dw_downsizer.sv", + "sha256": "abf1d10e0cbf2441cd77c00673d80d475298f2f30bedd55851deace9fac34c71", + "bytes": 35248 + }, + { + "path": "src/axi_dw_upsizer.sv", + "sha256": "f728f9236365d1851149b6eeaa72aa51d5aa74a14499ef44689f495c72ecb220", + "bytes": 26506 + }, + { + "path": "src/axi_err_slv.sv", + "sha256": "1dedd614d1404367d1d8e555877d0421e3747ec9bca2f644d6b3587f9e37579d", + "bytes": 8461 + }, + { + "path": "src/axi_fifo.sv", + "sha256": "3d22b3749fd1328936544656765af3535eff751681dae26196fb54b1647d28dd", + "bytes": 8576 + }, + { + "path": "src/axi_fifo_delay_dyn.sv", + "sha256": "3c13123f7551d1c756eba5245ae4e3ba10c470d592e64a4fc2d5aa12e620aff7", + "bytes": 17017 + }, + { + "path": "src/axi_from_mem.sv", + "sha256": "6a176c4ae7af3fa0abc307e34273c9a09534975ca1ef096ebbee60dd5768476f", + "bytes": 5080 + }, + { + "path": "src/axi_id_prepend.sv", + "sha256": "190b2f3fd55ab509dc25d4495694c3f5c4e91cbed8b79727b5543ef017a3348e", + "bytes": 8634 + }, + { + "path": "src/axi_id_remap.sv", + "sha256": "50e88ebc9c439a0e9dda1674235f256c216d3db687d22a9bab2b66447275bd36", + "bytes": 32201 + }, + { + "path": "src/axi_id_serialize.sv", + "sha256": "f500626fb5d99694756adca33a79dedf8dd849840858ad086077097acb984ebe", + "bytes": 19672 + }, + { + "path": "src/axi_interleaved_xbar.sv", + "sha256": "c3673eab4a89a5c9395d75630e959c5fda383653a21d0970d2347a25f80c8959", + "bytes": 19157 + }, + { + "path": "src/axi_intf.sv", + "sha256": "85dea3129f2f63e6a06d1ee210fa1b76a689694e9f9fb5c489948d7eefd1c0ac", + "bytes": 21878 + }, + { + "path": "src/axi_inval_filter.sv", + "sha256": "1c452951b0d7c7821a788f156495d924d396df6c865b7f9bd93f74ae6970840a", + "bytes": 4375 + }, + { + "path": "src/axi_isolate.sv", + "sha256": "f92d7c6933e171d3ca216a591f6423d9f50aa05bed2c1bde508b76c5957bd39b", + "bytes": 17062 + }, + { + "path": "src/axi_iw_converter.sv", + "sha256": "f41dc7d8f462e247f8fcdfb0a2950852514ab649d5d397a7ced611efd3fc58d5", + "bytes": 16599 + }, + { + "path": "src/axi_join.sv", + "sha256": "e57c5738000347f3e0d2e912712ccb54e06643a5ea7b011d065817a48683f145", + "bytes": 1264 + }, + { + "path": "src/axi_lfsr.sv", + "sha256": "b88cf99560cf7c14374921417fd2415d4bb2d9b32cc20f0696e56d005067030f", + "bytes": 4130 + }, + { + "path": "src/axi_lite_demux.sv", + "sha256": "e20cca5d5268ff3388b436f491a4c373d1fa9e2523efd57f6b73e9788ea91e2d", + "bytes": 22980 + }, + { + "path": "src/axi_lite_dw_converter.sv", + "sha256": "99ecbc98581388c3d3ebb99abc71cb3bf4ff3cc0b6de09cdd14b4e81885d115a", + "bytes": 24607 + }, + { + "path": "src/axi_lite_from_mem.sv", + "sha256": "664abf80248bb45350241f8643abe9e50b0b574a0a80a01f4cfa902d26a1d8d2", + "bytes": 10765 + }, + { + "path": "src/axi_lite_join.sv", + "sha256": "17a0027d23a5255986f9fd11d215d1c1a47fe721a38b08429de29ef75154ff2b", + "bytes": 1175 + }, + { + "path": "src/axi_lite_lfsr.sv", + "sha256": "6ef7dc962291bafec0eed8e6566672fdf582ef89ff165530e40b5b1851e8db55", + "bytes": 7586 + }, + { + "path": "src/axi_lite_mailbox.sv", + "sha256": "c675e0a57dc8c8a72b72d2d1391a1ad83f26db4eabd5a816bc07ef6f49660c5e", + "bytes": 25943 + }, + { + "path": "src/axi_lite_mux.sv", + "sha256": "99b9a4a22c525f04ed10a06d9f9e59b80588226d9847976db6be5ded27e02f42", + "bytes": 20356 + }, + { + "path": "src/axi_lite_regs.sv", + "sha256": "7ba5e7228a8fca0057db362ae615b7c2c94fb384b5cd791746b0c38f3a7fef08", + "bytes": 20729 + }, + { + "path": "src/axi_lite_to_apb.sv", + "sha256": "41331a2316772913895d766d16c3ff77b2afc925f4090e857f9f76052be60f65", + "bytes": 20598 + }, + { + "path": "src/axi_lite_to_axi.sv", + "sha256": "f35ec83cc2168898f880e5edfcdf3a4ef323b11216f540233062cd27a692cb86", + "bytes": 4846 + }, + { + "path": "src/axi_lite_xbar.sv", + "sha256": "8aeb4125cef42a9ab0840bf26d57b356f4da1bdf617ce2702d92efa1058a7978", + "bytes": 13522 + }, + { + "path": "src/axi_modify_address.sv", + "sha256": "22515ac952bd92a0e81851609bc22cefd3889e1f264147bac0533d4a0adf18a9", + "bytes": 5597 + }, + { + "path": "src/axi_multicut.sv", + "sha256": "2dd21cd472c3c448a35814b8614ad68eb7c8ab7a00ed89935371a2f5de5bec68", + "bytes": 8070 + }, + { + "path": "src/axi_mux.sv", + "sha256": "d1635b12affe48d6e2c6f4c9fc7eebcd92e21c523e33162c98b3b21c2df49416", + "bytes": 25317 + }, + { + "path": "src/axi_pkg.sv", + "sha256": "244bd5cb3c5bc58d786f2ed2f5eeefd910c4fe03a175f4a2888c6a2ea1c430dc", + "bytes": 26577 + }, + { + "path": "src/axi_rw_join.sv", + "sha256": "b455707ff04a857c639f4b4887cda412a6c3f40d111d1ee5ffe5f5c8aa74fc04", + "bytes": 3821 + }, + { + "path": "src/axi_rw_split.sv", + "sha256": "d195730fde3fac4a84e090ff585ca584a21f8f905c3a10809ddb378ba28a4554", + "bytes": 3787 + }, + { + "path": "src/axi_serializer.sv", + "sha256": "b7fa3204c22220ab78873e9f52a91610502aeeeb3c8c2999f528c1b337d66a6d", + "bytes": 11184 + }, + { + "path": "src/axi_sim_mem.sv", + "sha256": "d88c7ef7ecbec85400ba16085e82172b180425ebe6827da41d18f5b045ea53a1", + "bytes": 19882 + }, + { + "path": "src/axi_slave_compare.sv", + "sha256": "75d1db7a303de753378443356cbc9eb07e248635744e6e28727cdaa642eddf0b", + "bytes": 6743 + }, + { + "path": "src/axi_test.sv", + "sha256": "d3ae3f15f0d674bdb322f9059a527b40e016b3733d7fd15f79414b87206c3648", + "bytes": 98306 + }, + { + "path": "src/axi_throttle.sv", + "sha256": "6597927f36422027a0f0d4d40d0a43721323b5f9e2428fc4ee903226e6491471", + "bytes": 3959 + }, + { + "path": "src/axi_to_axi_lite.sv", + "sha256": "1bc5aa798bac4b2b45718f899f3f78bfc5cc8146940051548ab402603de9290a", + "bytes": 12810 + }, + { + "path": "src/axi_to_detailed_mem.sv", + "sha256": "3e0d8a4407a9d6d2ca16dedfee68e9602493e4461d4bc0c09e10d02ec7137730", + "bytes": 39381 + }, + { + "path": "src/axi_to_mem.sv", + "sha256": "e6a282e8ce5c1d0e22a9e5971bbcc4d993d93fcdacbceb1b747473c9e8c35f32", + "bytes": 9034 + }, + { + "path": "src/axi_to_mem_banked.sv", + "sha256": "0ab535d90af7db007b02659f58a5f6fd6f13ca04cc89490f9b8856e39f923000", + "bytes": 18128 + }, + { + "path": "src/axi_to_mem_interleaved.sv", + "sha256": "60e8e75ec95f823f938ac09e803240cdfffd08a14203471e664c5ee847c02512", + "bytes": 15304 + }, + { + "path": "src/axi_to_mem_split.sv", + "sha256": "108a3cfc207c8627784770e08c705039832987b38f94938d27d6c64cced5ee35", + "bytes": 11641 + }, + { + "path": "src/axi_xbar.sv", + "sha256": "05987e199be28cf5716b934d3466a8deeee737ae6361d3a1394d64784e8cc78c", + "bytes": 11764 + }, + { + "path": "src/axi_xbar_unmuxed.sv", + "sha256": "2d5e1d4671f32520870360eb6e591856329f7ca02aa2f47dc76d6dc0a4b008bc", + "bytes": 12733 + }, + { + "path": "src/axi_xp.sv", + "sha256": "69d0fa49e07da874ef2b01ddc4545fab4a95f63b45dd0e1adb4bc666d75474b2", + "bytes": 10783 + }, + { + "path": "src/axi_zero_mem.sv", + "sha256": "c538ca77f2ec568ca6ea13d73ce29666891639ba368b43a1529143486e62a2ae", + "bytes": 4450 + }, + { + "path": "test/axi_synth_bench.sv", + "sha256": "02eecbcad3eeeb9ed8b37331ebb787a144cdcf5c1b93ab3af07fa7ff929d73ab", + "bytes": 25391 + }, + { + "path": "test/tb_axi_addr_test.sv", + "sha256": "315d3cbe9bf26134a73b41b8dbbef31a2f80fb2862d890b2be0968225cf577f5", + "bytes": 9058 + }, + { + "path": "test/tb_axi_atop_filter.sv", + "sha256": "a806df31dc31a8746121b9688983ae8ec0b1e94e731aaab81bb64ef39834ccdb", + "bytes": 15180 + }, + { + "path": "test/tb_axi_bus_compare.sv", + "sha256": "c0a42c7dda5a2fc8d51608b0b2e9a426ea3c34c93049c4ff4f7b658f6289fdee", + "bytes": 9500 + }, + { + "path": "test/tb_axi_cdc.sv", + "sha256": "bc36d363ceddd3156ae8a7fce4c46a4dac49ecbf19b2bf10559a90c1116e69a5", + "bytes": 8103 + }, + { + "path": "test/tb_axi_delayer.sv", + "sha256": "5ce2ad513fc8c76d9a2a4098900058049ad40491047c36a3833eb497f97eca14", + "bytes": 3836 + }, + { + "path": "test/tb_axi_dw_downsizer.sv", + "sha256": "2f80754761bb47d955b9e4912add2f01a9c2dd0abbaa0278c14ed08549cd83f3", + "bytes": 6642 + }, + { + "path": "test/tb_axi_dw_pkg.sv", + "sha256": "f45f79f43af06dcb71c5d2de6a2ae4038099fd4e11e2b77d15fb0dff333b1f91", + "bytes": 58508 + }, + { + "path": "test/tb_axi_dw_upsizer.sv", + "sha256": "80acb9719529c8534eef6ca1e681cb85ae89e66d17a8edd7b614986807f8e337", + "bytes": 5264 + }, + { + "path": "test/tb_axi_fifo.sv", + "sha256": "7bd8ec77c32dfaecc48f0475626b5ad8e429aac082b69c273eb693cebf7f6643", + "bytes": 6405 + }, + { + "path": "test/tb_axi_isolate.sv", + "sha256": "f9418ccd6f3a0dcf781f10a7924cfb886eebaac16a767524281a9e3fe6fa6f4a", + "bytes": 7293 + }, + { + "path": "test/tb_axi_iw_converter.sv", + "sha256": "921cdecc94fa443998e3c7154877ebc76ad628d0bae4b80e5a30e08a737cb415", + "bytes": 18357 + }, + { + "path": "test/tb_axi_lite_dw_converter.sv", + "sha256": "31b322a59a38e4390ad4edd46ab599f57ba96f938ce59603abe41c0efe5a2e06", + "bytes": 16953 + }, + { + "path": "test/tb_axi_lite_mailbox.sv", + "sha256": "5e5834904ee4f714f7c89d2794efd757c92e511193491ccfcd6df69da59563ea", + "bytes": 24929 + }, + { + "path": "test/tb_axi_lite_regs.sv", + "sha256": "e2a424715aba1f8f947f2fdb032be91fe470987aa971c8edf9da3a087585eecb", + "bytes": 13642 + }, + { + "path": "test/tb_axi_lite_to_apb.sv", + "sha256": "24744a16f55c6a922ecaa76f825a2d3df38fb8300c1f8eb3ef3a7c8e772470f7", + "bytes": 8825 + }, + { + "path": "test/tb_axi_lite_to_axi.sv", + "sha256": "6035670ef97817b351436f55473bcc72adb09da021955995db28f26a9f54ee90", + "bytes": 2943 + }, + { + "path": "test/tb_axi_lite_xbar.sv", + "sha256": "47bd0677132c4fa71b87c6e7f4aefb12c5f435fa74a24f3be18ce1188ee95260", + "bytes": 7021 + }, + { + "path": "test/tb_axi_modify_address.sv", + "sha256": "65cccd135d5b7db41adb37f47d8880b229570d40bf58b5717fa7d25f38f39479", + "bytes": 7662 + }, + { + "path": "test/tb_axi_serializer.sv", + "sha256": "afab1d86fd257ca9df3ebd9fc529af422f4f7e7d8879a463db0f7e7b09e1be17", + "bytes": 9850 + }, + { + "path": "test/tb_axi_sim_mem.sv", + "sha256": "7dc4f46e6d7400a89f311290726f69b2777884c175ecda2914ee4cf1a9c7df9a", + "bytes": 5276 + }, + { + "path": "test/tb_axi_slave_compare.sv", + "sha256": "40bf717f48bd8da7d7eea51b88ac573218aecb40b1a2fa43d3075e2ecebe4ac4", + "bytes": 7162 + }, + { + "path": "test/tb_axi_to_axi_lite.sv", + "sha256": "cdfaa127ba98f5a3ea385dbf4a56c9c093fb62e8ad752c9210c6ff1c5ee62985", + "bytes": 4853 + }, + { + "path": "test/tb_axi_to_mem_banked.sv", + "sha256": "3d8e72be993d42d53d4133c07d3a55560389e2122430feb417a1b66c96825df7", + "bytes": 13881 + }, + { + "path": "test/tb_axi_xbar.sv", + "sha256": "50c6cb7221ae88732dbcd43d7c68808693bf8e4e4a6863847ec476d9f8c2236f", + "bytes": 21907 + }, + { + "path": "test/tb_axi_xbar_pkg.sv", + "sha256": "a5476a02f5942ce8dd46efd54f9e58c34ac8834e14e37a822cc5fc565f92ad50", + "bytes": 20617 + } + ], + "source_inventory_sha256": "12c988383b5ca8496055c17a0f9b5818ca19f3446051b1de0d5b6427909d8a0d", + "build_evidence": [ + { + "path": "Bender.yml", + "sha256": "106b0e6b9306d0ea329ba09dce792c022a857acc76c72c480e80a00016148a52", + "bytes": 4193, + "command_evidence": [], + "matches_revision": true + }, + { + "path": "Bender.lock", + "sha256": "6e91c73d8fdb6f6a2280e433dc4a3879adc0bb09969c09049d65564c0c50d223", + "bytes": 659, + "command_evidence": [], + "matches_revision": true + }, + { + "path": "Makefile", + "sha256": "78b15816eb04acb7acc355fb1d5faff9239b11aca91b580968a4c94f270d6956", + "bytes": 2578, + "command_evidence": [], + "matches_revision": true + }, + { + "path": "src_files.yml", + "sha256": "31ac5d14288b6a2bb628631282d8913463e76cfd54dd3c8e0e443c86dbf5a080", + "bytes": 2265, + "command_evidence": [], + "matches_revision": true + }, + { + "path": "axi.core", + "sha256": "f139a36b04f07f92119a5a0be2ae117998ded05d3fb77aee7da822f6e3b4cdf0", + "bytes": 7862, + "command_evidence": [], + "matches_revision": true + } + ], + "validation_profiles": [ + { + "name": "normal-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "src/axi_pkg.sv", + "src/axi_interleaved_xbar.sv" + ], + "top_modules": [ + "axi_interleaved_xbar" + ], + "top_evidence": [ + { + "kind": "module", + "name": "axi_interleaved_xbar", + "path": "src/axi_interleaved_xbar.sv", + "line": 19 + } + ], + "include_dirs": [ + "include" + ], + "defines": {} + }, + { + "name": "vcs-source", + "purpose": "source-graph validation; не compiler/elaboration closure", + "files": [ + "src/axi_pkg.sv", + "src/axi_interleaved_xbar.sv" + ], + "top_modules": [ + "axi_interleaved_xbar" + ], + "top_evidence": [ + { + "kind": "module", + "name": "axi_interleaved_xbar", + "path": "src/axi_interleaved_xbar.sv", + "line": 19 + } + ], + "include_dirs": [ + "include" + ], + "defines": { + "VCS": "1" + } + } + ], + "literal_include_references": [ + { + "path": "src/axi_atop_filter.sv", + "line": 373, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_atop_filter.sv", + "line": 375, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_burst_splitter.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_burst_splitter_gran.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_burst_splitter_gran.sv", + "line": 11, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_burst_unwrap.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_burst_unwrap.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_bus_compare.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cdc.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cdc.sv", + "line": 125, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cdc.sv", + "line": 127, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_cdc_dst.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cdc_dst.sv", + "line": 17, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_cdc_src.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cdc_src.sv", + "line": 17, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_cut.sv", + "line": 124, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_cut.sv", + "line": 126, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_delayer.sv", + "line": 127, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_delayer.sv", + "line": 129, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_demux.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux.sv", + "line": 217, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_demux.sv", + "line": 220, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_demux_id_counters.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_simple.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_simple.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_simple.sv", + "line": 19, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_dumper.sv", + "line": 158, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_dumper.sv", + "line": 161, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_dw_converter.sv", + "line": 112, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_dw_converter.sv", + "line": 116, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_fifo.sv", + "line": 161, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_fifo.sv", + "line": 163, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_fifo_delay_dyn.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_fifo_delay_dyn.sv", + "line": 15, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_fifo_delay_dyn.sv", + "line": 16, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_from_mem.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_id_remap.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_id_remap.sv", + "line": 650, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_id_remap.sv", + "line": 653, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_id_serialize.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_id_serialize.sv", + "line": 17, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_interleaved_xbar.sv", + "line": 311, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_interleaved_xbar.sv", + "line": 313, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_intf.sv", + "line": 354, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_inval_filter.sv", + "line": 39, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_inval_filter.sv", + "line": 42, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_isolate.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_isolate.sv", + "line": 17, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_isolate.sv", + "line": 411, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_iw_converter.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_iw_converter.sv", + "line": 256, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_join.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lfsr.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_demux.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_demux.sv", + "line": 490, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_demux.sv", + "line": 492, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_dw_converter.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_dw_converter.sv", + "line": 491, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_dw_converter.sv", + "line": 494, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_from_mem.sv", + "line": 92, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_join.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_lfsr.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_mailbox.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_mailbox.sv", + "line": 196, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_mailbox.sv", + "line": 553, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_mux.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_mux.sv", + "line": 463, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_mux.sv", + "line": 466, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_regs.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_regs.sv", + "line": 16, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_regs.sv", + "line": 411, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_to_apb.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_to_apb.sv", + "line": 383, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_to_apb.sv", + "line": 385, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_lite_xbar.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_lite_xbar.sv", + "line": 237, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_modify_address.sv", + "line": 83, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_modify_address.sv", + "line": 86, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_multicut.sv", + "line": 91, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_multicut.sv", + "line": 93, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_mux.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_mux.sv", + "line": 26, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_mux.sv", + "line": 505, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_mux.sv", + "line": 508, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_rw_join.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_rw_join.sv", + "line": 16, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_rw_split.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_rw_split.sv", + "line": 16, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_serializer.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_serializer.sv", + "line": 221, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_serializer.sv", + "line": 223, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_sim_mem.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_sim_mem.sv", + "line": 351, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_slave_compare.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_axi_lite.sv", + "line": 249, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_axi_lite.sv", + "line": 252, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_to_detailed_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_to_detailed_mem.sv", + "line": 601, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_detailed_mem.sv", + "line": 604, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_to_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_to_mem.sv", + "line": 116, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_mem.sv", + "line": 119, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_to_mem_banked.sv", + "line": 308, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_to_mem_banked.sv", + "line": 311, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_mem_interleaved.sv", + "line": 259, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_to_mem_interleaved.sv", + "line": 261, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_mem_split.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_to_mem_split.sv", + "line": 164, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_xbar.sv", + "line": 154, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_xbar.sv", + "line": 156, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_xp.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_xp.sv", + "line": 166, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "src/axi_xp.sv", + "line": 168, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "src/axi_zero_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "test/axi_synth_bench.sv", + "line": 294, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/axi_synth_bench.sv", + "line": 402, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_addr_test.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_atop_filter.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_bus_compare.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_bus_compare.sv", + "line": 8, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_cdc.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_cdc.sv", + "line": 16, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_delayer.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_dw_downsizer.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_dw_upsizer.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_fifo.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_fifo.sv", + "line": 15, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_isolate.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_isolate.sv", + "line": 16, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_iw_converter.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_iw_converter.sv", + "line": 16, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_lite_dw_converter.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_lite_dw_converter.sv", + "line": 17, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_lite_mailbox.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_lite_mailbox.sv", + "line": 34, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_lite_regs.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_lite_to_apb.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_lite_to_apb.sv", + "line": 20, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_lite_to_axi.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_lite_xbar.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_lite_xbar.sv", + "line": 22, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_modify_address.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_modify_address.sv", + "line": 15, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_serializer.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_serializer.sv", + "line": 16, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_sim_mem.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_sim_mem.sv", + "line": 9, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_slave_compare.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_slave_compare.sv", + "line": 10, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_to_axi_lite.sv", + "line": 1, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_to_mem_banked.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_to_mem_banked.sv", + "line": 16, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + }, + { + "path": "test/tb_axi_to_mem_banked.sv", + "line": 17, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "test/tb_axi_xbar.sv", + "line": 1, + "request": "axi/typedef.svh", + "resolved_candidates": [ + "include/axi/typedef.svh" + ] + }, + { + "path": "test/tb_axi_xbar.sv", + "line": 24, + "request": "axi/assign.svh", + "resolved_candidates": [ + "include/axi/assign.svh" + ] + } + ], + "missing_dependencies": { + "literal_includes": [ + { + "path": "src/axi_burst_splitter_gran.sv", + "line": 11, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_burst_unwrap.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_id_counters.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_simple.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_demux_simple.sv", + "line": 18, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_fifo_delay_dyn.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_id_remap.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_inval_filter.sv", + "line": 42, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_isolate.sv", + "line": 17, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_demux.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_dw_converter.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_from_mem.sv", + "line": 92, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_lfsr.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_mailbox.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_mux.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_regs.sv", + "line": 16, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_lite_to_apb.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_mux.sv", + "line": 1, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_mux.sv", + "line": 26, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_rw_join.sv", + "line": 16, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_rw_split.sv", + "line": 16, + "request": "common_cells/assertions.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_serializer.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_to_detailed_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_to_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "src/axi_zero_mem.sv", + "line": 1, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + }, + { + "path": "test/tb_axi_to_mem_banked.sv", + "line": 17, + "request": "common_cells/registers.svh", + "resolved_candidates": [] + } + ], + "imported_packages_without_declaration": [ + { + "package": "cc_pkg", + "path": "src/axi_dw_downsizer.sv", + "line": 54 + }, + { + "package": "cc_pkg", + "path": "src/axi_dw_upsizer.sv", + "line": 56 + }, + { + "package": "cc_pkg", + "path": "src/axi_interleaved_xbar.sv", + "line": 20 + }, + { + "package": "cc_pkg", + "path": "src/axi_interleaved_xbar.sv", + "line": 316 + }, + { + "package": "cc_pkg", + "path": "src/axi_inval_filter.sv", + "line": 38 + }, + { + "package": "cc_pkg", + "path": "src/axi_xbar.sv", + "line": 19 + }, + { + "package": "cc_pkg", + "path": "src/axi_xbar.sv", + "line": 159 + }, + { + "package": "cc_pkg", + "path": "src/axi_xbar_unmuxed.sv", + "line": 19 + }, + { + "package": "cc_pkg", + "path": "src/axi_xp.sv", + "line": 171 + }, + { + "package": "rand_id_queue_pkg", + "path": "test/tb_axi_atop_filter.sv", + "line": 45 + }, + { + "package": "rand_verif_pkg", + "path": "test/tb_axi_atop_filter.sv", + "line": 46 + } + ], + "symbols": [], + "locked_external_packages": [ + { + "name": "common_cells", + "revision": "03d98106aa19952a10360d2230def85144a0008b", + "remote": "https://github.com/pulp-platform/common_cells.git", + "status": "external; не включён в source inventory", + "evidence": "Bender.lock" + }, + { + "name": "common_verification", + "revision": "fb1885f48ea46164a10568aeff51884389f67ae3", + "remote": "https://github.com/pulp-platform/common_verification.git", + "status": "external; не включён в source inventory", + "evidence": "Bender.lock" + }, + { + "name": "tech_cells_generic", + "revision": "3a3de73632a06826b1bd9c65a0a2e92b32016845", + "remote": "https://github.com/pulp-platform/tech_cells_generic.git", + "status": "external; не включён в source inventory", + "evidence": "Bender.lock" + } + ], + "build_recipe_files_not_in_corpus": [], + "scope": "Только наблюдаемые literal includes/imports и перечисленные primitives; conditional branches не вычисляются. Не доказательство полной build closure." + } + } + ] +} diff --git a/docs/hdl-frontend-comparison.json b/docs/hdl-frontend-comparison.json new file mode 100644 index 000000000..b0ff3872e --- /dev/null +++ b/docs/hdl-frontend-comparison.json @@ -0,0 +1,4179 @@ +{ + "tools": { + "slang": "slang version 11.0.448+e222e7dc0", + "verilator": "Verilator 5.051 devel rev v5.050-260-g1c9a9e0cc (mod)" + }, + "cases": [ + { + "case": "uart-strict", + "tool": "slang", + "iteration": 0, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.02, + "wrapperSeconds": 0.032, + "peakRssKiB": 14296, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart-strict-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/uart-strict-slang-0/diagnostics.json", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3108, + "astBytes": 1829408, + "expectedOutcome": true, + "errorLines": 9, + "warningLines": 0 + }, + { + "case": "uart-strict", + "tool": "slang", + "iteration": 1, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.02, + "wrapperSeconds": 0.032, + "peakRssKiB": 14300, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart-strict-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/uart-strict-slang-1/diagnostics.json", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3108, + "astBytes": 1829408, + "expectedOutcome": true, + "errorLines": 9, + "warningLines": 0 + }, + { + "case": "uart-strict", + "tool": "verilator", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 15596, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/uart-strict-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/uart-strict-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/uart-strict-verilator-0/obj", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3335, + "astBytes": 209577, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 6, + "VAR": 112, + "CONST": 191, + "INITIALSTATIC": 2, + "ASSIGN": 5, + "VARREF": 319, + "ALWAYS": 18, + "SENTREE": 12, + "SENITEM": 23, + "BEGIN": 89, + "ASSIGNDLY": 126, + "CONCAT": 7, + "SEL": 16, + "ASSIGNW": 5, + "NOT": 11, + "COND": 2, + "ADD": 12, + "IF": 48, + "LT": 6, + "SUB": 4, + "AND": 9, + "CELL": 6, + "PIN": 47, + "ARRAYSEL": 2, + "EQ": 4, + "NEQ": 1, + "CASE": 3, + "EXTEND": 1, + "CASEITEM": 19, + "GT": 7, + "TYPETABLE": 1, + "BASICDTYPE": 22, + "VOIDDTYPE": 1, + "UNPACKARRAYDTYPE": 1, + "RANGE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 7 + }, + { + "case": "uart-strict", + "tool": "verilator", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 15596, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/uart-strict-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/uart-strict-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/uart-strict-verilator-1/obj", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3335, + "astBytes": 209577, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 6, + "VAR": 112, + "CONST": 191, + "INITIALSTATIC": 2, + "ASSIGN": 5, + "VARREF": 319, + "ALWAYS": 18, + "SENTREE": 12, + "SENITEM": 23, + "BEGIN": 89, + "ASSIGNDLY": 126, + "CONCAT": 7, + "SEL": 16, + "ASSIGNW": 5, + "NOT": 11, + "COND": 2, + "ADD": 12, + "IF": 48, + "LT": 6, + "SUB": 4, + "AND": 9, + "CELL": 6, + "PIN": 47, + "ARRAYSEL": 2, + "EQ": 4, + "NEQ": 1, + "CASE": 3, + "EXTEND": 1, + "CASEITEM": 19, + "GT": 7, + "TYPETABLE": 1, + "BASICDTYPE": 22, + "VOIDDTYPE": 1, + "UNPACKARRAYDTYPE": 1, + "RANGE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 7 + }, + { + "case": "uart-compatible", + "tool": "slang", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.02, + "wrapperSeconds": 0.032, + "peakRssKiB": 14300, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart-compatible-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/uart-compatible-slang-0/diagnostics.json", + "--allow-use-before-declare", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 0, + "astBytes": 1807877, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Instance": 7, + "InstanceBody": 5, + "Port": 86, + "ScalarType": 1214, + "Net": 49, + "NetType": 49, + "PackedArrayType": 693, + "Variable": 36, + "Parameter": 27, + "Conversion": 266, + "IntegerLiteral": 147, + "PredefinedIntegerType": 124, + "ProceduralBlock": 13, + "Timed": 13, + "SignalEvent": 23, + "NamedValue": 390, + "Block": 89, + "List": 35, + "ExpressionStatement": 136, + "Assignment": 159, + "Concatenation": 3, + "ElementSelect": 19, + "UnaryOp": 22, + "EventList": 11, + "Conditional": 50, + "BinaryOp": 61, + "ImplicitEvent": 1, + "Case": 3, + "EmptyArgument": 20, + "FixedSizeUnpackedArrayType": 3, + "RangeSelect": 3, + "Replication": 1, + "ContinuousAssign": 3, + "Definition": 6 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "uart-compatible", + "tool": "slang", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.02, + "wrapperSeconds": 0.032, + "peakRssKiB": 14280, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart-compatible-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/uart-compatible-slang-1/diagnostics.json", + "--allow-use-before-declare", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 0, + "astBytes": 1807877, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Instance": 7, + "InstanceBody": 5, + "Port": 86, + "ScalarType": 1214, + "Net": 49, + "NetType": 49, + "PackedArrayType": 693, + "Variable": 36, + "Parameter": 27, + "Conversion": 266, + "IntegerLiteral": 147, + "PredefinedIntegerType": 124, + "ProceduralBlock": 13, + "Timed": 13, + "SignalEvent": 23, + "NamedValue": 390, + "Block": 89, + "List": 35, + "ExpressionStatement": 136, + "Assignment": 159, + "Concatenation": 3, + "ElementSelect": 19, + "UnaryOp": 22, + "EventList": 11, + "Conditional": 50, + "BinaryOp": 61, + "ImplicitEvent": 1, + "Case": 3, + "EmptyArgument": 20, + "FixedSizeUnpackedArrayType": 3, + "RangeSelect": 3, + "Replication": 1, + "ContinuousAssign": 3, + "Definition": 6 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "uart-compatible", + "tool": "verilator", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 15468, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/uart-compatible-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/uart-compatible-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/uart-compatible-verilator-0/obj", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3335, + "astBytes": 209577, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 6, + "VAR": 112, + "CONST": 191, + "INITIALSTATIC": 2, + "ASSIGN": 5, + "VARREF": 319, + "ALWAYS": 18, + "SENTREE": 12, + "SENITEM": 23, + "BEGIN": 89, + "ASSIGNDLY": 126, + "CONCAT": 7, + "SEL": 16, + "ASSIGNW": 5, + "NOT": 11, + "COND": 2, + "ADD": 12, + "IF": 48, + "LT": 6, + "SUB": 4, + "AND": 9, + "CELL": 6, + "PIN": 47, + "ARRAYSEL": 2, + "EQ": 4, + "NEQ": 1, + "CASE": 3, + "EXTEND": 1, + "CASEITEM": 19, + "GT": 7, + "TYPETABLE": 1, + "BASICDTYPE": 22, + "VOIDDTYPE": 1, + "UNPACKARRAYDTYPE": 1, + "RANGE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 7 + }, + { + "case": "uart-compatible", + "tool": "verilator", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.08, + "wrapperSeconds": 0.114, + "peakRssKiB": 15504, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/uart-compatible-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/uart-compatible-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/uart-compatible-verilator-1/obj", + "${CORPORA}/dual-uart/src/fifo.v", + "${CORPORA}/dual-uart/src/top.v", + "${CORPORA}/dual-uart/src/uart_arbiter.v", + "${CORPORA}/dual-uart/src/uart_rx.v", + "${CORPORA}/dual-uart/src/uart_rx_autobaud.v", + "${CORPORA}/dual-uart/src/uart_tx.v" + ], + "stderrBytes": 3335, + "astBytes": 209577, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 6, + "VAR": 112, + "CONST": 191, + "INITIALSTATIC": 2, + "ASSIGN": 5, + "VARREF": 319, + "ALWAYS": 18, + "SENTREE": 12, + "SENITEM": 23, + "BEGIN": 89, + "ASSIGNDLY": 126, + "CONCAT": 7, + "SEL": 16, + "ASSIGNW": 5, + "NOT": 11, + "COND": 2, + "ADD": 12, + "IF": 48, + "LT": 6, + "SUB": 4, + "AND": 9, + "CELL": 6, + "PIN": 47, + "ARRAYSEL": 2, + "EQ": 4, + "NEQ": 1, + "CASE": 3, + "EXTEND": 1, + "CASEITEM": 19, + "GT": 7, + "TYPETABLE": 1, + "BASICDTYPE": 22, + "VOIDDTYPE": 1, + "UNPACKARRAYDTYPE": 1, + "RANGE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "CLK_FREQ", + "valueOrWidth": 50000000, + "source": "${CORPORA}/dual-uart/src/top.v", + "line": 35, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 7 + }, + { + "case": "crc16", + "tool": "slang", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.01, + "wrapperSeconds": 0.032, + "peakRssKiB": 10888, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "emmc_crc16", + "--ast-json", + "${OUTPUT}/crc16-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/crc16-slang-0/diagnostics.json", + "${CORPORA}/card-reader/src/emmc_crc16.v" + ], + "stderrBytes": 0, + "astBytes": 175143, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Instance": 1, + "InstanceBody": 1, + "Port": 6, + "ScalarType": 125, + "Net": 7, + "NetType": 7, + "PackedArrayType": 46, + "Variable": 1, + "BinaryOp": 3, + "ElementSelect": 32, + "NamedValue": 45, + "IntegerLiteral": 34, + "PredefinedIntegerType": 32, + "ProceduralBlock": 1, + "Timed": 1, + "EventList": 1, + "SignalEvent": 2, + "Block": 2, + "Conditional": 3, + "UnaryOp": 1, + "ExpressionStatement": 18, + "Assignment": 19, + "Conversion": 2, + "List": 1, + "ContinuousAssign": 1, + "Definition": 1 + }, + "semanticControl": { + "symbol": "crc_out", + "valueOrWidth": 16, + "source": "${CORPORA}/card-reader/src/emmc_crc16.v", + "line": 12, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "crc16", + "tool": "slang", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.01, + "wrapperSeconds": 0.032, + "peakRssKiB": 10856, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "emmc_crc16", + "--ast-json", + "${OUTPUT}/crc16-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/crc16-slang-1/diagnostics.json", + "${CORPORA}/card-reader/src/emmc_crc16.v" + ], + "stderrBytes": 0, + "astBytes": 175143, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Instance": 1, + "InstanceBody": 1, + "Port": 6, + "ScalarType": 125, + "Net": 7, + "NetType": 7, + "PackedArrayType": 46, + "Variable": 1, + "BinaryOp": 3, + "ElementSelect": 32, + "NamedValue": 45, + "IntegerLiteral": 34, + "PredefinedIntegerType": 32, + "ProceduralBlock": 1, + "Timed": 1, + "EventList": 1, + "SignalEvent": 2, + "Block": 2, + "Conditional": 3, + "UnaryOp": 1, + "ExpressionStatement": 18, + "Assignment": 19, + "Conversion": 2, + "List": 1, + "ContinuousAssign": 1, + "Definition": 1 + }, + "semanticControl": { + "symbol": "crc_out", + "valueOrWidth": 16, + "source": "${CORPORA}/card-reader/src/emmc_crc16.v", + "line": 12, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "crc16", + "tool": "verilator", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 14280, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "emmc_crc16", + "--json-only-output", + "${OUTPUT}/crc16-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/crc16-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/crc16-verilator-0/obj", + "${CORPORA}/card-reader/src/emmc_crc16.v" + ], + "stderrBytes": 0, + "astBytes": 30389, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 2, + "VAR": 8, + "ALWAYS": 3, + "ASSIGNW": 2, + "XOR": 3, + "SEL": 32, + "VARREF": 46, + "CONST": 34, + "SENTREE": 1, + "SENITEM": 2, + "BEGIN": 2, + "IF": 3, + "ASSIGNDLY": 18, + "TYPETABLE": 1, + "BASICDTYPE": 4, + "VOIDDTYPE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "crc_out", + "valueOrWidth": 16, + "source": "${CORPORA}/card-reader/src/emmc_crc16.v", + "line": 12, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "crc16", + "tool": "verilator", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 14508, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "emmc_crc16", + "--json-only-output", + "${OUTPUT}/crc16-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/crc16-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/crc16-verilator-1/obj", + "${CORPORA}/card-reader/src/emmc_crc16.v" + ], + "stderrBytes": 0, + "astBytes": 30389, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 2, + "VAR": 8, + "ALWAYS": 3, + "ASSIGNW": 2, + "XOR": 3, + "SEL": 32, + "VARREF": 46, + "CONST": 34, + "SENTREE": 1, + "SENITEM": 2, + "BEGIN": 2, + "IF": 3, + "ASSIGNDLY": 18, + "TYPETABLE": 1, + "BASICDTYPE": 4, + "VOIDDTYPE": 1, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "crc_out", + "valueOrWidth": 16, + "source": "${CORPORA}/card-reader/src/emmc_crc16.v", + "line": 12, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 0 + }, + { + "case": "axi2", + "tool": "slang", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.14, + "wrapperSeconds": 0.164, + "peakRssKiB": 58716, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi2-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/axi2-slang-0/diagnostics.json", + "-GNoSlvMst=2", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 307, + "astBytes": 12529230, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Package": 6, + "Subroutine": 36, + "FormalArgument": 152, + "PredefinedIntegerType": 1496, + "Variable": 383, + "Conversion": 682, + "PackedArrayType": 2150, + "ScalarType": 5181, + "IntegerLiteral": 557, + "List": 74, + "VariableDeclaration": 10, + "Conditional": 200, + "BinaryOp": 638, + "NamedValue": 2714, + "Block": 446, + "ExpressionStatement": 403, + "Call": 149, + "VoidType": 68, + "StringLiteral": 180, + "Assignment": 632, + "UnaryOp": 273, + "Return": 71, + "TypeAlias": 51, + "PackedStructType": 21, + "Field": 101, + "Parameter": 190, + "SimpleAssignmentPattern": 1, + "StructuredAssignmentPattern": 11, + "WhileLoop": 1, + "EnumType": 6, + "EnumValue": 31, + "ImmediateAssertion": 20, + "Case": 4, + "UnbasedUnsizedIntegerLiteral": 162, + "ConditionalOp": 39, + "Instance": 46, + "InstanceBody": 29, + "Port": 641, + "Net": 171, + "NetType": 171, + "TypeParameter": 54, + "MemberAccess": 266, + "GenerateBlockArray": 21, + "Genvar": 21, + "GenerateBlock": 71, + "StatementBlock": 70, + "ProceduralBlock": 118, + "ForLoop": 3, + "ElementSelect": 265, + "StringType": 13, + "EmptyArgument": 116, + "ContinuousAssign": 183, + "ConcurrentAssertion": 39, + "Clocking": 39, + "SignalEvent": 141, + "Binary": 40, + "Simple": 79, + "Concatenation": 16, + "Timed": 51, + "EventList": 51, + "DisableIff": 24, + "RangeSelect": 16, + "Replication": 12, + "defaultDisable": 4, + "DataType": 1, + "WildcardImport": 1, + "Definition": 364, + "Attribute": 33 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 2, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 1 + }, + { + "case": "axi2", + "tool": "slang", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.15, + "wrapperSeconds": 0.165, + "peakRssKiB": 56772, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi2-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/axi2-slang-1/diagnostics.json", + "-GNoSlvMst=2", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 307, + "astBytes": 12529230, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Package": 6, + "Subroutine": 36, + "FormalArgument": 152, + "PredefinedIntegerType": 1496, + "Variable": 383, + "Conversion": 682, + "PackedArrayType": 2150, + "ScalarType": 5181, + "IntegerLiteral": 557, + "List": 74, + "VariableDeclaration": 10, + "Conditional": 200, + "BinaryOp": 638, + "NamedValue": 2714, + "Block": 446, + "ExpressionStatement": 403, + "Call": 149, + "VoidType": 68, + "StringLiteral": 180, + "Assignment": 632, + "UnaryOp": 273, + "Return": 71, + "TypeAlias": 51, + "PackedStructType": 21, + "Field": 101, + "Parameter": 190, + "SimpleAssignmentPattern": 1, + "StructuredAssignmentPattern": 11, + "WhileLoop": 1, + "EnumType": 6, + "EnumValue": 31, + "ImmediateAssertion": 20, + "Case": 4, + "UnbasedUnsizedIntegerLiteral": 162, + "ConditionalOp": 39, + "Instance": 46, + "InstanceBody": 29, + "Port": 641, + "Net": 171, + "NetType": 171, + "TypeParameter": 54, + "MemberAccess": 266, + "GenerateBlockArray": 21, + "Genvar": 21, + "GenerateBlock": 71, + "StatementBlock": 70, + "ProceduralBlock": 118, + "ForLoop": 3, + "ElementSelect": 265, + "StringType": 13, + "EmptyArgument": 116, + "ContinuousAssign": 183, + "ConcurrentAssertion": 39, + "Clocking": 39, + "SignalEvent": 141, + "Binary": 40, + "Simple": 79, + "Concatenation": 16, + "Timed": 51, + "EventList": 51, + "DisableIff": 24, + "RangeSelect": 16, + "Replication": 12, + "defaultDisable": 4, + "DataType": 1, + "WildcardImport": 1, + "Definition": 364, + "Attribute": 33 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 2, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 1 + }, + { + "case": "axi2", + "tool": "verilator", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.59, + "wrapperSeconds": 0.615, + "peakRssKiB": 91508, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "--json-only-output", + "${OUTPUT}/axi2-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/axi2-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/axi2-verilator-0/obj", + "-GNoSlvMst=2", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 5977, + "astBytes": 1624749, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 34, + "VAR": 768, + "CONST": 982, + "TYPEDEF": 49, + "CELL": 49, + "PIN": 413, + "VARREF": 2087, + "PACKAGE": 5, + "GENBLOCK": 107, + "SEL": 338, + "ALWAYS": 320, + "ASSIGNW": 209, + "COND": 26, + "EXTEND": 68, + "CONCAT": 23, + "BEGIN": 445, + "ASSIGN": 156, + "IF": 354, + "AND": 170, + "NOT": 262, + "SENTREE": 97, + "SENITEM": 155, + "ASSIGNDLY": 177, + "EQ": 34, + "ADD": 41, + "MUL": 38, + "LOOP": 3, + "LOOPTEST": 3, + "GT": 7, + "GTE": 1, + "OR": 112, + "LT": 4, + "INITIAL": 11, + "CEXPR": 94, + "TEXT": 94, + "FINAL": 1, + "LOGOR": 1, + "ONEHOT0": 3, + "TESTPLUSARGS": 1, + "DISPLAY": 36, + "SFORMATF": 38, + "TIME": 36, + "SCOPENAME": 36, + "STOP": 36, + "ISUNKNOWN": 1, + "SFORMATARG": 2, + "EQCASE": 37, + "SAMPLED": 84, + "LTE": 2, + "REDOR": 8, + "SHIFTL": 2, + "SUB": 6, + "TYPETABLE": 1, + "VOIDDTYPE": 1, + "BASICDTYPE": 145, + "REFDTYPE": 311, + "PACKARRAYDTYPE": 48, + "RANGE": 48, + "STRUCTDTYPE": 21, + "MEMBERDTYPE": 101, + "ENUMDTYPE": 6, + "ENUMITEM": 31, + "PARAMTYPEDTYPE": 61, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 2, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 10 + }, + { + "case": "axi2", + "tool": "verilator", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.61, + "wrapperSeconds": 0.666, + "peakRssKiB": 91748, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "--json-only-output", + "${OUTPUT}/axi2-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/axi2-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/axi2-verilator-1/obj", + "-GNoSlvMst=2", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 5977, + "astBytes": 1624749, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 34, + "VAR": 768, + "CONST": 982, + "TYPEDEF": 49, + "CELL": 49, + "PIN": 413, + "VARREF": 2087, + "PACKAGE": 5, + "GENBLOCK": 107, + "SEL": 338, + "ALWAYS": 320, + "ASSIGNW": 209, + "COND": 26, + "EXTEND": 68, + "CONCAT": 23, + "BEGIN": 445, + "ASSIGN": 156, + "IF": 354, + "AND": 170, + "NOT": 262, + "SENTREE": 97, + "SENITEM": 155, + "ASSIGNDLY": 177, + "EQ": 34, + "ADD": 41, + "MUL": 38, + "LOOP": 3, + "LOOPTEST": 3, + "GT": 7, + "GTE": 1, + "OR": 112, + "LT": 4, + "INITIAL": 11, + "CEXPR": 94, + "TEXT": 94, + "FINAL": 1, + "LOGOR": 1, + "ONEHOT0": 3, + "TESTPLUSARGS": 1, + "DISPLAY": 36, + "SFORMATF": 38, + "TIME": 36, + "SCOPENAME": 36, + "STOP": 36, + "ISUNKNOWN": 1, + "SFORMATARG": 2, + "EQCASE": 37, + "SAMPLED": 84, + "LTE": 2, + "REDOR": 8, + "SHIFTL": 2, + "SUB": 6, + "TYPETABLE": 1, + "VOIDDTYPE": 1, + "BASICDTYPE": 145, + "REFDTYPE": 311, + "PACKARRAYDTYPE": 48, + "RANGE": 48, + "STRUCTDTYPE": 21, + "MEMBERDTYPE": 101, + "ENUMDTYPE": 6, + "ENUMITEM": 31, + "PARAMTYPEDTYPE": 61, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 2, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 10 + }, + { + "case": "axi4", + "tool": "slang", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.16, + "wrapperSeconds": 0.215, + "peakRssKiB": 65848, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi4-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/axi4-slang-0/diagnostics.json", + "-GNoSlvMst=4", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 307, + "astBytes": 15189248, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Package": 6, + "Subroutine": 36, + "FormalArgument": 152, + "PredefinedIntegerType": 2079, + "Variable": 401, + "Conversion": 839, + "PackedArrayType": 2719, + "ScalarType": 5839, + "IntegerLiteral": 667, + "List": 74, + "VariableDeclaration": 10, + "Conditional": 200, + "BinaryOp": 824, + "NamedValue": 3407, + "Block": 454, + "ExpressionStatement": 411, + "Call": 173, + "VoidType": 76, + "StringLiteral": 188, + "Assignment": 744, + "UnaryOp": 309, + "Return": 71, + "TypeAlias": 51, + "PackedStructType": 21, + "Field": 101, + "Parameter": 242, + "SimpleAssignmentPattern": 1, + "StructuredAssignmentPattern": 11, + "WhileLoop": 1, + "EnumType": 6, + "EnumValue": 31, + "ImmediateAssertion": 20, + "Case": 4, + "UnbasedUnsizedIntegerLiteral": 162, + "ConditionalOp": 52, + "Instance": 58, + "InstanceBody": 29, + "Port": 717, + "Net": 171, + "NetType": 171, + "TypeParameter": 54, + "MemberAccess": 346, + "GenerateBlockArray": 25, + "Genvar": 25, + "GenerateBlock": 126, + "StatementBlock": 78, + "ProceduralBlock": 126, + "ForLoop": 3, + "ElementSelect": 533, + "StringType": 21, + "EmptyArgument": 138, + "ContinuousAssign": 273, + "ConcurrentAssertion": 47, + "Clocking": 47, + "SignalEvent": 149, + "Binary": 48, + "Simple": 95, + "Concatenation": 18, + "Timed": 51, + "EventList": 51, + "DisableIff": 24, + "RangeSelect": 18, + "Replication": 12, + "defaultDisable": 6, + "DataType": 1, + "WildcardImport": 1, + "Definition": 364, + "Attribute": 33 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 4, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 1 + }, + { + "case": "axi4", + "tool": "slang", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.16, + "wrapperSeconds": 0.215, + "peakRssKiB": 67664, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi4-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/axi4-slang-1/diagnostics.json", + "-GNoSlvMst=4", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 307, + "astBytes": 15189248, + "nodeKinds": { + "Root": 1, + "CompilationUnit": 1, + "Package": 6, + "Subroutine": 36, + "FormalArgument": 152, + "PredefinedIntegerType": 2079, + "Variable": 401, + "Conversion": 839, + "PackedArrayType": 2719, + "ScalarType": 5839, + "IntegerLiteral": 667, + "List": 74, + "VariableDeclaration": 10, + "Conditional": 200, + "BinaryOp": 824, + "NamedValue": 3407, + "Block": 454, + "ExpressionStatement": 411, + "Call": 173, + "VoidType": 76, + "StringLiteral": 188, + "Assignment": 744, + "UnaryOp": 309, + "Return": 71, + "TypeAlias": 51, + "PackedStructType": 21, + "Field": 101, + "Parameter": 242, + "SimpleAssignmentPattern": 1, + "StructuredAssignmentPattern": 11, + "WhileLoop": 1, + "EnumType": 6, + "EnumValue": 31, + "ImmediateAssertion": 20, + "Case": 4, + "UnbasedUnsizedIntegerLiteral": 162, + "ConditionalOp": 52, + "Instance": 58, + "InstanceBody": 29, + "Port": 717, + "Net": 171, + "NetType": 171, + "TypeParameter": 54, + "MemberAccess": 346, + "GenerateBlockArray": 25, + "Genvar": 25, + "GenerateBlock": 126, + "StatementBlock": 78, + "ProceduralBlock": 126, + "ForLoop": 3, + "ElementSelect": 533, + "StringType": 21, + "EmptyArgument": 138, + "ContinuousAssign": 273, + "ConcurrentAssertion": 47, + "Clocking": 47, + "SignalEvent": 149, + "Binary": 48, + "Simple": 95, + "Concatenation": 18, + "Timed": 51, + "EventList": 51, + "DisableIff": 24, + "RangeSelect": 18, + "Replication": 12, + "defaultDisable": 6, + "DataType": 1, + "WildcardImport": 1, + "Definition": 364, + "Attribute": 33 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 4, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 1 + }, + { + "case": "axi4", + "tool": "verilator", + "iteration": 0, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.6, + "wrapperSeconds": 0.615, + "peakRssKiB": 93172, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "--json-only-output", + "${OUTPUT}/axi4-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/axi4-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/axi4-verilator-0/obj", + "-GNoSlvMst=4", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 5977, + "astBytes": 1884692, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 34, + "VAR": 801, + "CONST": 1289, + "TYPEDEF": 49, + "CELL": 61, + "PIN": 489, + "VARREF": 2447, + "PACKAGE": 5, + "GENBLOCK": 177, + "SEL": 594, + "ALWAYS": 426, + "ASSIGNW": 315, + "COND": 39, + "EXTEND": 80, + "CONCAT": 25, + "BEGIN": 445, + "ASSIGN": 156, + "IF": 354, + "AND": 221, + "NOT": 285, + "SENTREE": 97, + "SENITEM": 155, + "ASSIGNDLY": 177, + "EQ": 54, + "ADD": 41, + "MUL": 40, + "LOOP": 3, + "LOOPTEST": 3, + "GT": 11, + "GTE": 1, + "OR": 122, + "LT": 4, + "INITIAL": 11, + "CEXPR": 94, + "TEXT": 94, + "FINAL": 1, + "LOGOR": 1, + "ONEHOT0": 3, + "TESTPLUSARGS": 1, + "DISPLAY": 36, + "SFORMATF": 38, + "TIME": 36, + "SCOPENAME": 36, + "STOP": 36, + "ISUNKNOWN": 1, + "SFORMATARG": 2, + "EQCASE": 37, + "SAMPLED": 84, + "LTE": 6, + "REDOR": 8, + "SUB": 6, + "SHIFTL": 2, + "TYPETABLE": 1, + "VOIDDTYPE": 1, + "BASICDTYPE": 147, + "REFDTYPE": 319, + "PACKARRAYDTYPE": 48, + "RANGE": 48, + "STRUCTDTYPE": 21, + "MEMBERDTYPE": 101, + "ENUMDTYPE": 6, + "ENUMITEM": 31, + "PARAMTYPEDTYPE": 61, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 4, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 10 + }, + { + "case": "axi4", + "tool": "verilator", + "iteration": 1, + "exit": 0, + "expectedSuccess": true, + "seconds": 0.64, + "wrapperSeconds": 0.666, + "peakRssKiB": 92844, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "synth_axi_lite_xbar", + "--json-only-output", + "${OUTPUT}/axi4-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/axi4-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/axi4-verilator-1/obj", + "-GNoSlvMst=4", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/clk_rst_gen.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/stream_watchdog.sv", + "${AXI}/.bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/signal_highlighter.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sram_impl.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_clk.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/rtl/tc_sync.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clock_gating_async.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/cluster_clk_cells.sv", + "${AXI}/.bender/git/checkouts/tech_cells_generic-6e6736c6cf5dbb6b/src/deprecated/pulp_clk_cells.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/assert_rpt_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_binary_to_gray.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_shift_register_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_multiaddr_decode.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_majority_vote_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/cc_mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_cdc_2phase_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_id_queue_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_stream_arbiter_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_ecc_synth.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/test/cc_synth_bench.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cf_math_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr_pkg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_intf.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/binary_to_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/boxcar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cb_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_4phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_2phase.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_int_div_static.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/clk_mux_glitch_free.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/credit_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/delta_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ecc_encode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_detect.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_tx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_rx.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator_ack.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/edge_propagator.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/exp_backoff.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fifo_v3.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/gray_to_binary.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/heaviside.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_4phase_handshake.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/isochronous_spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_16bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lfsr_8bit.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lossy_valid_to_stream.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/lzc.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/onehot_to_bin.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/passthrough_stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/plru_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/popcount.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/read.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/ring_buffer.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rr_arb_tree.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen_bypass.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/rstgen.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/serial_deglitch.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/shift_reg_gated.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/spill_register_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sub_per_hash.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/sync_wedge.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/unread.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/fall_through_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/max_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mv_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/trip_counter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_dync.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/addr_decode_napot.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/multiaddr_decode.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_reset_ctrlr.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_demux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_mux.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_throttle.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_2phase_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/cdc_fifo_gray_clearable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/id_queue.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter_flushable.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_delay.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fifo_optimal_wrap.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_filter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_fork_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_join_dynamic.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_register.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_to_mem.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_xbar.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks_detailed.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_arbiter.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/stream_omega_net.sv", + "${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/src/deprecated/mem_to_banks.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/src/axi_pkg.sv", + "${AXI}/src/axi_demux_id_counters.sv", + "${AXI}/src/axi_intf.sv", + "${AXI}/src/axi_atop_filter.sv", + "${AXI}/src/axi_burst_splitter_gran.sv", + "${AXI}/src/axi_burst_unwrap.sv", + "${AXI}/src/axi_bus_compare.sv", + "${AXI}/src/axi_cdc_dst.sv", + "${AXI}/src/axi_cdc_src.sv", + "${AXI}/src/axi_cut.sv", + "${AXI}/src/axi_delayer.sv", + "${AXI}/src/axi_demux_simple.sv", + "${AXI}/src/axi_dw_downsizer.sv", + "${AXI}/src/axi_dw_upsizer.sv", + "${AXI}/src/axi_fifo.sv", + "${AXI}/src/axi_fifo_delay_dyn.sv", + "${AXI}/src/axi_id_remap.sv", + "${AXI}/src/axi_id_prepend.sv", + "${AXI}/src/axi_inval_filter.sv", + "${AXI}/src/axi_isolate.sv", + "${AXI}/src/axi_join.sv", + "${AXI}/src/axi_lite_demux.sv", + "${AXI}/src/axi_lite_dw_converter.sv", + "${AXI}/src/axi_lite_from_mem.sv", + "${AXI}/src/axi_lite_join.sv", + "${AXI}/src/axi_lite_lfsr.sv", + "${AXI}/src/axi_lite_mailbox.sv", + "${AXI}/src/axi_lite_mux.sv", + "${AXI}/src/axi_lite_regs.sv", + "${AXI}/src/axi_lite_to_apb.sv", + "${AXI}/src/axi_lite_to_axi.sv", + "${AXI}/src/axi_modify_address.sv", + "${AXI}/src/axi_mux.sv", + "${AXI}/src/axi_rw_join.sv", + "${AXI}/src/axi_rw_split.sv", + "${AXI}/src/axi_serializer.sv", + "${AXI}/src/axi_slave_compare.sv", + "${AXI}/src/axi_throttle.sv", + "${AXI}/src/axi_to_detailed_mem.sv", + "${AXI}/src/axi_burst_splitter.sv", + "${AXI}/src/axi_cdc.sv", + "${AXI}/src/axi_demux.sv", + "${AXI}/src/axi_err_slv.sv", + "${AXI}/src/axi_dw_converter.sv", + "${AXI}/src/axi_from_mem.sv", + "${AXI}/src/axi_id_serialize.sv", + "${AXI}/src/axi_lfsr.sv", + "${AXI}/src/axi_multicut.sv", + "${AXI}/src/axi_to_axi_lite.sv", + "${AXI}/src/axi_to_mem.sv", + "${AXI}/src/axi_zero_mem.sv", + "${AXI}/src/axi_interleaved_xbar.sv", + "${AXI}/src/axi_iw_converter.sv", + "${AXI}/src/axi_lite_xbar.sv", + "${AXI}/src/axi_xbar_unmuxed.sv", + "${AXI}/src/axi_to_mem_banked.sv", + "${AXI}/src/axi_to_mem_interleaved.sv", + "${AXI}/src/axi_to_mem_split.sv", + "${AXI}/src/axi_xbar.sv", + "${AXI}/src/axi_xp.sv", + "+define+TARGET_SYNTHESIS", + "+define+TARGET_SYNTH_TEST", + "+define+TARGET_VERILATOR", + "+incdir+${AXI}/.bender/git/checkouts/common_cells-3e2fcccecd7aee7b/include", + "+incdir+${AXI}/include", + "${AXI}/test/axi_synth_bench.sv" + ], + "stderrBytes": 5977, + "astBytes": 1884692, + "nodeKinds": { + "NETLIST": 1, + "MODULE": 34, + "VAR": 801, + "CONST": 1289, + "TYPEDEF": 49, + "CELL": 61, + "PIN": 489, + "VARREF": 2447, + "PACKAGE": 5, + "GENBLOCK": 177, + "SEL": 594, + "ALWAYS": 426, + "ASSIGNW": 315, + "COND": 39, + "EXTEND": 80, + "CONCAT": 25, + "BEGIN": 445, + "ASSIGN": 156, + "IF": 354, + "AND": 221, + "NOT": 285, + "SENTREE": 97, + "SENITEM": 155, + "ASSIGNDLY": 177, + "EQ": 54, + "ADD": 41, + "MUL": 40, + "LOOP": 3, + "LOOPTEST": 3, + "GT": 11, + "GTE": 1, + "OR": 122, + "LT": 4, + "INITIAL": 11, + "CEXPR": 94, + "TEXT": 94, + "FINAL": 1, + "LOGOR": 1, + "ONEHOT0": 3, + "TESTPLUSARGS": 1, + "DISPLAY": 36, + "SFORMATF": 38, + "TIME": 36, + "SCOPENAME": 36, + "STOP": 36, + "ISUNKNOWN": 1, + "SFORMATARG": 2, + "EQCASE": 37, + "SAMPLED": 84, + "LTE": 6, + "REDOR": 8, + "SUB": 6, + "SHIFTL": 2, + "TYPETABLE": 1, + "VOIDDTYPE": 1, + "BASICDTYPE": 147, + "REFDTYPE": 319, + "PACKARRAYDTYPE": 48, + "RANGE": 48, + "STRUCTDTYPE": 21, + "MEMBERDTYPE": 101, + "ENUMDTYPE": 6, + "ENUMITEM": 31, + "PARAMTYPEDTYPE": 61, + "CONSTPOOL": 1, + "SCOPE": 1 + }, + "semanticControl": { + "symbol": "NoSlvMst", + "valueOrWidth": 4, + "source": "${AXI}/test/axi_synth_bench.sv", + "line": 406, + "sourceLineVerified": true + }, + "expectedOutcome": true, + "errorLines": 0, + "warningLines": 10 + }, + { + "case": "board-missing-pll", + "tool": "slang", + "iteration": 0, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.08, + "wrapperSeconds": 0.114, + "peakRssKiB": 38008, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/board-missing-pll-slang-0/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/board-missing-pll-slang-0/diagnostics.json", + "${CORPORA}/card-reader/src/crc8.v", + "${CORPORA}/card-reader/src/emmc_cmd.v", + "${CORPORA}/card-reader/src/emmc_controller.v", + "${CORPORA}/card-reader/src/emmc_crc16.v", + "${CORPORA}/card-reader/src/emmc_crc7.v", + "${CORPORA}/card-reader/src/emmc_dat.v", + "${CORPORA}/card-reader/src/emmc_init.v", + "${CORPORA}/card-reader/src/led_status.v", + "${CORPORA}/card-reader/src/pll.v", + "${CORPORA}/card-reader/src/sector_buf.v", + "${CORPORA}/card-reader/src/sector_buf_wr.v", + "${CORPORA}/card-reader/src/top.v", + "${CORPORA}/card-reader/src/uart_bridge.v", + "${CORPORA}/card-reader/src/uart_rx.v", + "${CORPORA}/card-reader/src/uart_tx.v" + ], + "stderrBytes": 1582, + "astBytes": 14214653, + "expectedOutcome": true, + "errorLines": 5, + "warningLines": 0 + }, + { + "case": "board-missing-pll", + "tool": "slang", + "iteration": 1, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.07, + "wrapperSeconds": 0.114, + "peakRssKiB": 37996, + "argv": [ + "${SLANG}", + "--std", + "1800-2023", + "--single-unit", + "--top", + "top", + "--ast-json", + "${OUTPUT}/board-missing-pll-slang-1/ast.json", + "--ast-json-source-info", + "--ast-json-detailed-types", + "--diag-json", + "${OUTPUT}/board-missing-pll-slang-1/diagnostics.json", + "${CORPORA}/card-reader/src/crc8.v", + "${CORPORA}/card-reader/src/emmc_cmd.v", + "${CORPORA}/card-reader/src/emmc_controller.v", + "${CORPORA}/card-reader/src/emmc_crc16.v", + "${CORPORA}/card-reader/src/emmc_crc7.v", + "${CORPORA}/card-reader/src/emmc_dat.v", + "${CORPORA}/card-reader/src/emmc_init.v", + "${CORPORA}/card-reader/src/led_status.v", + "${CORPORA}/card-reader/src/pll.v", + "${CORPORA}/card-reader/src/sector_buf.v", + "${CORPORA}/card-reader/src/sector_buf_wr.v", + "${CORPORA}/card-reader/src/top.v", + "${CORPORA}/card-reader/src/uart_bridge.v", + "${CORPORA}/card-reader/src/uart_rx.v", + "${CORPORA}/card-reader/src/uart_tx.v" + ], + "stderrBytes": 1582, + "astBytes": 14214653, + "expectedOutcome": true, + "errorLines": 5, + "warningLines": 0 + }, + { + "case": "board-missing-pll", + "tool": "verilator", + "iteration": 0, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.08, + "wrapperSeconds": 0.114, + "peakRssKiB": 18832, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/board-missing-pll-verilator-0/ast.json", + "--json-only-meta-output", + "${OUTPUT}/board-missing-pll-verilator-0/meta.json", + "--Mdir", + "${OUTPUT}/board-missing-pll-verilator-0/obj", + "${CORPORA}/card-reader/src/crc8.v", + "${CORPORA}/card-reader/src/emmc_cmd.v", + "${CORPORA}/card-reader/src/emmc_controller.v", + "${CORPORA}/card-reader/src/emmc_crc16.v", + "${CORPORA}/card-reader/src/emmc_crc7.v", + "${CORPORA}/card-reader/src/emmc_dat.v", + "${CORPORA}/card-reader/src/emmc_init.v", + "${CORPORA}/card-reader/src/led_status.v", + "${CORPORA}/card-reader/src/pll.v", + "${CORPORA}/card-reader/src/sector_buf.v", + "${CORPORA}/card-reader/src/sector_buf_wr.v", + "${CORPORA}/card-reader/src/top.v", + "${CORPORA}/card-reader/src/uart_bridge.v", + "${CORPORA}/card-reader/src/uart_rx.v", + "${CORPORA}/card-reader/src/uart_tx.v" + ], + "stderrBytes": 2223, + "astBytes": 0, + "expectedOutcome": true, + "errorLines": 2, + "warningLines": 2 + }, + { + "case": "board-missing-pll", + "tool": "verilator", + "iteration": 1, + "exit": 1, + "expectedSuccess": false, + "seconds": 0.09, + "wrapperSeconds": 0.114, + "peakRssKiB": 19280, + "argv": [ + "${VERILATOR}", + "--language", + "1800-2023", + "--json-only", + "-Wno-fatal", + "--top-module", + "top", + "--json-only-output", + "${OUTPUT}/board-missing-pll-verilator-1/ast.json", + "--json-only-meta-output", + "${OUTPUT}/board-missing-pll-verilator-1/meta.json", + "--Mdir", + "${OUTPUT}/board-missing-pll-verilator-1/obj", + "${CORPORA}/card-reader/src/crc8.v", + "${CORPORA}/card-reader/src/emmc_cmd.v", + "${CORPORA}/card-reader/src/emmc_controller.v", + "${CORPORA}/card-reader/src/emmc_crc16.v", + "${CORPORA}/card-reader/src/emmc_crc7.v", + "${CORPORA}/card-reader/src/emmc_dat.v", + "${CORPORA}/card-reader/src/emmc_init.v", + "${CORPORA}/card-reader/src/led_status.v", + "${CORPORA}/card-reader/src/pll.v", + "${CORPORA}/card-reader/src/sector_buf.v", + "${CORPORA}/card-reader/src/sector_buf_wr.v", + "${CORPORA}/card-reader/src/top.v", + "${CORPORA}/card-reader/src/uart_bridge.v", + "${CORPORA}/card-reader/src/uart_rx.v", + "${CORPORA}/card-reader/src/uart_tx.v" + ], + "stderrBytes": 2223, + "astBytes": 0, + "expectedOutcome": true, + "errorLines": 2, + "warningLines": 2 + } + ], + "inputsUnchanged": true, + "inputSnapshotSha256": "22bb25e326bb7a1779925a2983168431ea0b47a627034aa351f95b594849e77c", + "measurement": "Linux GNU time peak RSS; sequential export runs, not cold-cache benchmark; repetitions not independent machines", + "profile": "Explicit arguments identical per case; tools retain their own implicit predefines and language semantics", + "allExpectedOutcomes": true +} diff --git a/docs/hdl-frontend-decision.md b/docs/hdl-frontend-decision.md new file mode 100644 index 000000000..40ce4dcba --- /dev/null +++ b/docs/hdl-frontend-decision.md @@ -0,0 +1,154 @@ +# Выбор HDL frontend: slang для первого semantic adapter + +Дата: 2026-09-12. Пакет A3–A6 трекера. База CodeGraph: `ced3a063`, extraction +stamp 38. Это решение о следующей реализации; compiler semantics ещё не +подключена к production graph. + +## Решение + +Первый адаптер строить поверх **slang**, как необязательного отдельного процесса. +Начальная вертикаль: один profile/top, evaluated parameters и тип/ширина порта +со ссылкой на исходное объявление. Verilator сохранить как независимый compiler +control. Второй production adapter пока не писать. + +Основание — реальные UART, eMMC CRC и восстановленный AXI, одинаковые явные +source lists/defines/top/parameter overrides для сравниваемых инструментов. +Slang экспортирует Parameter.value, структурированные types и elaborated Instance/ +GenerateBlockArray; из них проверяемо восстанавливаются instance paths. На этих +локальных controls slang использовал меньше CPU wall time и peak RSS, но его +подробный JSON больше. Это аргумент для ограниченного прототипа, не обещание +скорости/охвата на любом проекте. + +[Проверка официальных версий, лицензий и платформ](hdl-frontend-source-review.md) +содержит первичные ссылки. Измерены установленные slang `11.0.448+e222e7dc0` и +Verilator `5.051 devel v5.050-260-g1c9a9e0cc (mod)` из OSS CAD Suite 20260825. +Они не равны опубликованным releases; в частности, актуальный Verilator 5.052 +в этом benchmark не запускался. Перед поддержкой release artifact повторить +contract tests на его точной версии. + +Surelog/UHDM изучен по официальной модели/API: потребует отдельной установки, +согласованных writer/reader versions и адаптации elaborated topModules/VPI. +В проверенной среде binary отсутствует, runtime сравнение не проводилось. +Первый адаптер не требует его для подтверждённых задач. Yosys отдельно не +измерялся: текущая цель — source semantics SystemVerilog, синтез будет другой задачей. + +## Метод и результаты + +Скрипт `scripts/hdl-frontend-compare.py`: шесть controls × два frontend × два +последовательных запуска = **24**. GNU time измеряет время процесса и peak RSS; +таблица показывает медиану времени и максимальный RSS двух запусков. Это +локальный warm-cache smoke benchmark, без очистки filesystem cache, не статистический +вывод для всех проектов. Время имеет точность GNU time 0.01 с. AST sizes — полные +JSON dumps с разными schema/настройками, не нормализованные semantic payloads. + +| Контроль | slang exit / время / RSS | Verilator exit / время / RSS | +| --- | --- | --- | +| UART top, strict | 1 / 0.02 с / 14 300 KiB | 0 / 0.07 с / 15 596 KiB | +| UART top, explicit compatible | 0 / 0.02 с / 14 300 KiB | 0 / 0.075 с / 15 504 KiB | +| eMMC CRC16 | 0 / 0.01 с / 10 888 KiB | 0 / 0.07 с / 14 508 KiB | +| AXI xbar, NoSlvMst=2 | 0 / 0.145 с / 58 716 KiB | 0 / 0.60 с / 91 748 KiB | +| AXI xbar, NoSlvMst=4 | 0 / 0.16 с / 67 664 KiB | 0 / 0.62 с / 93 172 KiB | +| Card-reader board, missing rPLL | 1 / 0.075 с / 38 008 KiB | 1 / 0.085 с / 19 280 KiB | + +UART strict: slang сообщает 9 use-before-declaration errors. В отдельном +compatible control только slang получает явно записанный `--allow-use-before-declare`; +RTL не исправляется и strict failure не скрывается. Это отличается от автоматического +ослабления всех проверок. Board control намеренно неполон: нет vendor primitive rPLL; +оба frontend отвергают его. Slang дополнительно сообщает другие исходные ошибки. + +На AXI оба получают один filelist Bender из A2, включая TARGET_SYNTHESIS, +TARGET_SYNTH_TEST, TARGET_VERILATOR. Их встроенные predefines остаются различными. +Дополнительный probe slang с VERILATOR=1 меняет macro branch и предупреждения; +в основной таблице это послабление не добавлялось. Нельзя считать одинаковые +явные arguments доказательством одинакового compiler environment. + +Все expected outcomes совпали. На AXI slang оставляет 1 warning, Verilator 10; +UART compatible Verilator оставляет 7. Использован Verilator `-Wno-fatal`. +Это JSON export/elaboration controls; результаты не означают simulation, synthesis, +отсутствие RTL warnings или работоспособность FPGA. + +## Проверенные данные для адаптера + +- У обоих tools автоматические assertions подтвердили UART CLK_FREQ=50000000, + наличие u_rx1/u_rx2, CRC crc_out `[15:0]`, AXI NoSlvMst=2/4. Source locations + сверены с реальной строкой объявления, а не только наличием поля в JSON. +- Slang probe восстановил 7 UART instances, including top, параметры обоих + UART receivers и CLKS_PER_BIT=54. Для AXI — 92/182 reachable instances и + generate indices 0..1 / 0..3. Instance body может быть ссылкой на ранее + сериализованный addr; нужна общая таблица объектов, а не только рекурсивный + обход inline body. +- GenerateBlockArray содержит и genvar, и анонимные blocks. Нужно учитывать + implicit iteration parameter и исключать isUninstantiated blocks. +- Slang JSON source info даёт declaration coordinates и ranges выражений; + в проверенном statement control конец диапазона exclusive, coordinates 1-based. + Это не доказательство полного macro expansion source map. Byte/UTF16 conversion + и цепочки macro/include provenance требуют отдельных contract fixtures в B2. + Реальные macro-generated AXI typedefs указывают invocation line 415–421, но + column=0: это unknown column, не первая колонка. JSON не дал точную expansion/ + spelling chain; для B2 оценить небольшой exporter через slang SourceManager, + если CLI metadata недостаточно. До этого не заявлять полный source mapping. +- Verilator JSON требует `.meta.json` для расшифровки loc file IDs, dtypep/modp + references — отдельной адресной таблицы. Адреса обоих AST не являются stable IDs. +- Slang создаёт JSON даже при exit 1: failed/partial artifact не публиковать как + актуальный semantic graph. В benchmark поля такого AST не считаются успешными. +- AXI2 detailed slang AST ≈12.5 MB, Verilator ≈1.6 MB; AXI4 ≈15.2/1.9 MB. + Подробный dump нельзя без ограничений включать в MCP-ответ или source Git. + +## Контракт следующего адаптера + +1. Вход: canonical project root, source snapshot hashes, явный ordered filelist, + includeDirs, defines, dialect, top и overrides. Default строгий; compatibility + flags только явно и входят в fingerprint. Не исполнять произвольные флаги из + чужого build script и не менять конфигурацию при compiler errors. +2. Запуск: отдельный process, разрешённый executable/version, timeout/cancel, + пределы stdout/JSON/RSS. При отсутствии tool остаётся source graph. Никаких + обязательных native/npm dependencies и автоматического скачивания frontend. +3. Импорт: validate schema/version, exit status и diagnostics; разрешить object + references, сохранить declaration и instance identity отдельно. Store только + необходимые нормализованные facts, raw dump — вне Git/индекса. +4. Координаты: source bytes и revision должны совпасть с snapshot. Проверить + CRLF/Unicode, macro definitions/invocations и generated ranges; unresolved + mapping показывать явно. Не назначать строку expansion другому source file. +5. Provenance: exact executable version/hash, semantic schema version, profile, + implicit/explicit compiler flags, sources/dependency digest и top/overrides. + Unsupported tool/schema → понятный отказ semantic layer, source API работает. +6. Публикация: новый semantic context становится видимым целиком после успешного + импорта. Failure/cancel сохраняет source graph; старый semantic context stale. +7. Приёмка минимальной вертикали: params/width query на UART/CRC/AXI, два instances + с разными values, profile/parameter/include change и sync/clean parity, + missing frontend и invalid compiler output controls. + +## Воспроизведение + +Сначала A1 manifest check и A2 restoration из +`validation-hdl-corpora-build-2026-09-12.md`. Использовать новый пустой output: + +```sh +python3 scripts/hdl-frontend-compare.py \ + --corpus-root /absolute/path/to/corpora \ + --axi /absolute/path/to/axi-isolated \ + --filelist /absolute/path/to/axi-isolated/build/controls/verilator.f \ + --slang /absolute/path/to/slang \ + --verilator /absolute/path/to/verilator \ + --output /absolute/path/to/new-empty-output --runs 2 +``` + +Runner проверяет pinned revisions, A2 filelist hash, tracked inputs и отсутствие +untracked/include shadow files. Перед/после snapshot должен совпасть. Output +каталог должен быть пуст, чтобы не принять старый JSON. При timeout/interrupt +убивается собственная process group. Raw AST/logs сохраняются в output; +машиночитаемый итог этой проверки — `hdl-frontend-comparison.json`, дополнительный +semantic probe — `hdl-slang-semantic-probe.json`. + +Проверка выполнена на Linux; Windows/macOS availability изучена по официальным +источникам, реальные запуски этих ОС не выполнялись. GNU time runner — Linux +validation helper, не production cross-platform subprocess layer. + +Следующее действие: B1/B2 + C1/C2 минимальная вертикаль slang. Полный preprocessing, +source mapping macros, API integration и автоматическая semantic invalidation +этим сравнением не объявлены реализованными. + +Независимое review выявило и помогло исправить cleanup при прерывании и +untracked include shadowing. Повторные controls подтвердили завершение child +process и отклонение untracked/symlink headers. Engine не изменялся; полные +native/WASM suites не повторялись. Python syntax/JSON/diff проверены. diff --git a/docs/hdl-frontend-source-review.md b/docs/hdl-frontend-source-review.md new file mode 100644 index 000000000..2906d6d7d --- /dev/null +++ b/docs/hdl-frontend-source-review.md @@ -0,0 +1,124 @@ +# HDL frontend: проверка первичных источников + +Дата проверки: **2026-09-12**. Это входные данные для A3/A5, а не выбор победителя: +качество и стоимость на наших проектах определяет отдельный benchmark. + +## Версии и платформы + +| Frontend | Официальный релиз на дату проверки | Локальное наблюдение Linux | +| --- | --- | --- | +| slang | [v11.0, 2026-05-15](https://github.com/MikePopoloski/slang/releases/tag/v11.0) | `slang version 11.0.448+e222e7dc0`: development build, не чистый release artifact v11.0 | +| Verilator | [v5.052, 2026-09-05](https://verilator.org/guide/latest/changes.html#verilator-5-052-2026-09-05) | `Verilator 5.051 devel rev v5.050-260-g1c9a9e0cc (mod)`: другая, более ранняя development/vendor build | +| Surelog + UHDM | [Surelog v1.87](https://github.com/chipsalliance/Surelog/releases/tag/v1.87) и [UHDM v1.87](https://github.com/chipsalliance/UHDM/releases/tag/v1.87), оба опубликованы 2026-08-24 | Исполняемые `surelog`/`uhdm-dump` не найдены в PATH и проверенном OSS CAD Suite; runtime не проверен | + +Версии и help двух установленных инструментов повторно прочитаны из +`/home/danik/storage/toolchains/oss-cad-suite-20260825/bin/`. +Локальный help подтверждает slang `--ast-json`, `--ast-json-source-info`, `--cst-json` +и Verilator `--json-only` с отдельным meta-output. Эта проверка **не** подтверждает +совместимость всего API или результаты elaboration. Новые сборки, установки, +Windows/macOS и симуляция в рамках этого обзора не запускались. + +Официальный релиз slang v11.0 содержит ровно три бинарных assets: +`slang-linux-x86_64.tar.gz`, `slang-macos-arm64.tar.gz`, +`slang-windows-x86_64.zip`. Linux ARM64/macOS x86_64 в этом перечне отсутствуют. +[Release assets](https://github.com/MikePopoloski/slang/releases/tag/v11.0). +Текущие build docs описывают GCC/Clang/Xcode/MSVC и C++20; это документация текущего +дерева, её требования нельзя автоматически переносить на установленный snapshot. +[Сборка slang](https://sv-lang.com/building.html). + +Verilator документирует основной Linux workflow и дополнительные платформы +macOS/Windows MSVC; FAQ отдельно рекомендует WSL2 и слабее обещает проверку других +Windows-вариантов. Это поддержка проекта, а не проверенный нами Windows package. +[Installation](https://verilator.org/guide/latest/install.html#os-requirements), +[Windows FAQ](https://verilator.org/guide/latest/faq.html#does-verilator-run-under-windows). +Surelog/UHDM заявляют Linux, Windows MSYS2/MSVC и macOS; в проверенных releases v1.87 +нет загруженных авторами бинарных assets, только стандартные source archives. +[Surelog](https://github.com/chipsalliance/Surelog), +[UHDM](https://github.com/chipsalliance/UHDM). + +## Контракт данных и координаты + +**slang.** CLI предлагает compiled AST JSON, выбор scope, source-info, detailed types +и CST JSON с токенами/trivia. Source-info обещает файл и строку; это не обещание +полной macro-provenance в каждом JSON node. +[CLI JSON output](https://sv-lang.com/command-line-ref.html#json-output). +Библиотечный `SourceManager` хранит отдельные source/macro buffers, origin и место +раскрытия; `SourceLocation` содержит BufferID и **byte offset**. SourceManager должен +жить дольше объектов, использующих его память. Для CodeGraph потребуется проверенная +конвертация byte→UTF-16 и явное различение spelling/expansion locations. +[Source Management](https://sv-lang.com/sourcemanagement.html). + +Стабильная межверсионная JSON schema/ABI в просмотренной документации slang не +обещана. У v11.0 есть перечисленные breaking changes C++/Python API, включая +ASTVisitor и организацию pyslang imports. Вывод для адаптера: зафиксировать +бинарную версию/хеш и golden fixtures; не объявлять любой `11.x` совместимым без +проверки. [Release notes v11.0](https://github.com/MikePopoloski/slang/releases/tag/v11.0). + +**Verilator.** Официально предназначенный для downstream инструментов `--json-only` +отключает часть агрессивных преобразований, но отдаёт конечную стадию AST; это не +обещание неизменённого CST. Документация прямо предупреждает, что JSON формат +эволюционирует. [CLI contract](https://verilator.org/guide/latest/exe_verilator.html#cmdoption-json-only). +Нужно читать **оба** файла: `.tree.json` и `.tree.meta.json`; `loc` кодирует file-id, +начальную/конечную строку и колонку с исключённой правой границей. Meta разрешает +file-id в filename/realpath и содержит pointer metadata. Отсутствующий boolean +не следует путать с отсутствующим обязательным объектом: false-поля не печатаются. +Поля узлов описаны исходниками `V3AstNodes.cpp`, а не независимой неизменяемой schema. +[Официальный internals.rst](https://github.com/verilator/verilator/blob/master/docs/internals.rst#treejson-output). + +**Surelog/UHDM.** Основной interchange — сериализованный `surelog.uhdm` и VPI/C++ API; +`uhdm-dump` — человекочитаемый dump, не документированный стабильный JSON protocol. +Нельзя просмотреть только `allModules` и заявить полную схему: folded model разделяет +definitions и elaborated `topModules`; generate logic и окончательные типы находятся +в elaborated view. Есть дополнительная full elaboration/uniquification. +[Модель и навигация UHDM](https://github.com/chipsalliance/UHDM#model-concepts). +Surelog Python AST API не является post-elaboration API; для последнего используется +UHDM/VPI, включая отдельные UHDM Python bindings. +[Surelog API](https://github.com/chipsalliance/Surelog#python-api). + +UHDM `BaseClass` имеет file/start/end line и column поля; columns в просмотренном +шаблоне — `uint16_t`. Гарантию всех macro expansion цепочек из этих полей вывести +нельзя; длинные строки и generated/include provenance требуют отдельных fixtures. +[BaseClass](https://github.com/chipsalliance/UHDM/blob/master/templates/BaseClass.h). +Surelog v1.87 явно согласует номер UHDM и pkg-config dependency; совместимость +произвольных writer/reader snapshots этим не гарантируется. Адаптер должен +фиксировать **пару** Surelog/UHDM и проверять restore на своих fixtures. +[Связанные версии](https://github.com/chipsalliance/Surelog/releases/tag/v1.87). + +## Лицензии и упаковка + +| Компонент | Документированная лицензия | Практическое ограничение redistribution | +| --- | --- | --- | +| slang | [MIT](https://github.com/MikePopoloski/slang/blob/v11.0/LICENSE) | Сохранять copyright и текст разрешения; MIT самого проекта не заменяет notices всех включённых зависимостей. | +| Verilator | [LGPL-3.0-only OR Artistic-2.0](https://github.com/verilator/verilator/blob/v5.052/README.rst#open-license) | Выбрать и выполнить условия одной лицензии; нельзя перелицензировать сам frontend в MIT. Для распространяемых модификаций upstream FAQ требует открытый исходный код. [FAQ](https://verilator.org/guide/latest/faq.html#will-verilator-output-remain-under-my-own-license-copyright). | +| Surelog и UHDM | [Surelog Apache-2.0](https://github.com/chipsalliance/Surelog/blob/master/LICENSE), [UHDM Apache-2.0](https://github.com/chipsalliance/UHDM/blob/master/LICENSE) | Передавать LICENSE, сохранять применимые notices, отмечать изменённые файлы и передавать NOTICE, если он есть в распространяемой работе; условия третьих сторон проверяются отдельно. | + +У Verilator верхний `LICENSE` содержит LGPL; альтернативная Artistic-лицензия +подтверждается README/SPDX и отдельным +[LICENSES/Artistic-2.0.txt](https://github.com/verilator/verilator/blob/v5.052/LICENSES/Artistic-2.0.txt). +Локальная пометка `(mod)` не раскрывает перечень vendor patches. Обзор не является +аудитом полного redistribution комплекта OSS CAD Suite; текущий suite целиком +не выбран для включения в CodeGraph. + +## Ограничения будущего адаптера + +Это инженерные выводы из проверенных контрактов, не обещания готовой интеграции: + +- Нормализовать в собственный versioned graph contract, сохраняя frontend version, + hash/profile, diagnostics и evidence level; не хранить compiler memory addresses + как устойчивые CodeGraph IDs. +- Проверять каждый frontend на одинаковом профиле: порядок источников, compilation + units, defines, includes, tops, language mode и неизвестные модули. У slang по + умолчанию отдельные compilation units; `--single-unit` меняет семантику. + [Compilation flags](https://sv-lang.com/command-line-ref.html). +- Передавать сформированный разрешённый argv, а не произвольный исходный command + file. Например, Surelog документирует `-exe` и Python evaluation/listener hooks; + это исполняемые действия. Для ограниченного runner учитывать `-nopython` и запрет + таких опций. [Surelog CLI](https://github.com/chipsalliance/Surelog#surelog-commands). +- Отдельно измерить wall time, peak RSS, объём AST, startup, source-location fidelity, + точность hierarchy/port bindings и deterministic повтор. В этом документе нет + сравнительных измерений и нет решения, какой frontend станет основным. + +Сохранённые локальные справочные результаты: `.scratch/hdl-frontend-sources/` +(release REST readbacks и local versions), `.scratch/hdl-source-slang-help.txt`, +`.scratch/hdl-source-verilator-help.txt`. Проверенные ссылки выше — первичные +репозитории и документация авторов; страницы `/latest`/`master` могут измениться. diff --git a/docs/hdl-macro-source-map.md b/docs/hdl-macro-source-map.md new file mode 100644 index 000000000..ddd0fe725 --- /dev/null +++ b/docs/hdl-macro-source-map.md @@ -0,0 +1,92 @@ +# Происхождение HDL-макросов через pyslang + +Для точек исходного написания и диапазонов вызовов добавлен отдельный optional +backend. Он использует SourceManager из `pyslang==11.0.0`, не ищет определения +макросов по регулярным выражениям и не исправляет неизвестные колонки догадками. + +## Запуск + +Понадобится отдельный Python >=3.11 с установленным pyslang 11.0.0. +В этом пакете проверен Python 3.14. CodeGraph не ставит +его автоматически и не меняет Python dependencies проекта. Пример из корня +уже инициализированного проекта с active HDL profile: + +```sh +codegraph hdl-semantic data --python /absolute/path/to/venv/bin/python +``` + +В API: + +```ts +const result = await cg.getHdlSemantics({ + pythonExecutable: '/absolute/path/to/venv/bin/python', + query: 'top.data', +}); +``` + +Выбрать ровно один вариант: `--python` / pythonExecutable либо прежний +`--slang` / executable. Python backend сам выполняет compilation и экспорт +фактов; результаты разных запусков compiler не склеиваются. Сохраняются +active profile, overrides, separate compilation units, limits и snapshot checks. + +## Что возвращается + +Имена объявлений parameter/port/typedef сохраняют прежние sourceOrigin и +macroExpansion. Макрос в initializer или типе не превращает само имя в +macro-origin: ссылка на прямое source declaration остаётся допустимой. + +Дополнительно возвращаются `expressionOrigins` — уникальные compiler macro-token +origins с ролями `initializer`, `declared-initializer` и `type`. Для `type` +проверяется доступный объявленный type syntax, включая поддержанные dimensions. +Фактический initializer не смешивается с default из объявления после override. +Это прямые macro tokens соответствующего syntax, не транзитивный анализ всех +констант, от которых зависит результат. + +`expressionOriginCoverage` отдельно описывает initializer, declaredInitializer +и type: `checked`, `not-applicable` или `unavailable`; initializer также может +иметь `command-line`. Пустой список при unavailable не доказывает отсутствия +макросов. `truncated=true` означает достижение лимита выдачи. На факт возвращается +не больше 32 expression origins; полнота отдельных macro frames остаётся в +macroExpansionComplete. + +- `provenance.frontend = pyslang`, версия протокола/exporter и хеши Python, + exporter и native pyslang module. Запуск идёт через Python `-I`; путь venv + сохраняется, чтобы не выбрать site-packages другого interpreter. +- `sourceOrigin`: direct или macro. Поле source — физическая точка исходника + либо доступная точка вызова, а не автоматически полный диапазон объявления. +- `macroExpansion`: ограниченный набор provenance frames. Для каждого доступны + имя макроса, признак argument, точка spelling и compiler invocation range, + когда frontend смог их сопоставить. +- Каждый point содержит project-relative file, физические line/column и + `byteOffset`. Колонки отсчитываются с 1 в UTF-8 байтах, offset — с 0; + это не UTF-16 колонка CodeGraph. Координаты проверяются по snapshot bytes. +- `macroExpansionComplete` явно сообщает, удалось ли сопоставить все frames. + У collapsed/неизвестных ranges значение false. Отсутствующая часть не + восстанавливается эвристически. + +Frames — не линейный стек. SourceManager имеет разные связи original и expansion; +для nested macros нужно обходить обе. Spelling point внутри аргумента или macro +body не означает, что всё полученное имя записано там непрерывно. Особенно это +касается token concatenation: точка `bus` не является диапазоном имени `bus_data`. + +У macro-origin фактов sourceNodeId не выдаётся, даже при точной spelling column: +она может указывать на аргумент вызова или definition body, а не на исходное +объявление порта. Переходы доступны через macroExpansion. Прямые факты сохраняют +проверенные source node links. + +## Проверяемость и ограничения + +Exporter — небольшой поставляемый Python helper; используется compiler API, +а не собственный HDL-preprocessor. Нормализованный JSON имеет отдельную версию +`codegraphSemanticVersion: 1`, проверяется до выдачи фактов. Ошибка compiler, +неизвестная версия библиотеки, повреждённый payload, source path вне snapshot, +несогласованные координаты и изменения inputs во время запуска отвергаются. + +Computed include paths, files вне root и остальные ограничения snapshot runner +сохраняются. Это не OS sandbox для запуска недоверенного compiler. Нет +persistent cache, транзитивной карты зависимостей всех expressions, полного +source-map для всех constructed tokens или automatic +MCP compilation. Windows backend пока отключён; macOS runtime не проверялся. + +Поведение SourceManager сверено с [официальным API](https://www.sv-lang.com/classslang_1_1_source_manager.html). +Подробные результаты пакета: `validation-hdl-macro-source-map-2026-09-12.md`. diff --git a/docs/hdl-profiles.md b/docs/hdl-profiles.md new file mode 100644 index 000000000..e0144dae4 --- /dev/null +++ b/docs/hdl-profiles.md @@ -0,0 +1,91 @@ +# Профили HDL-сборки + +Профиль выбирает HDL-источники и контекст условной компиляции. В одном индексе +хранится один активный профиль; остальные языки проекта сохраняют обычный scope. + +## Конфигурация + +```json +{ + "hdl": { + "activeProfile": "synth", + "profiles": { + "synth": { + "filelists": ["profiles/synth.f"], + "includeDirs": ["rtl/include"], + "defines": {"SYNTHESIS": "1"}, + "topModules": ["top"], + "languageMode": "systemverilog" + }, + "sim": { + "files": ["rtl/top.sv", "tb/top_tb.sv"], + "includeDirs": ["rtl/include"], + "defines": {}, + "topModules": ["top_tb"] + } + } + } +} +``` + +Файл — codegraph.json в корне проекта. files и filelists складываются; +пустые/отсутствующие оба списка означают пустую HDL-выборку, а не все файлы. +Явные HDL sources могут находиться в gitignored/generated каталогах. Пути +должны оставаться внутри проекта; VCS и хранилища CodeGraph исключены. + +Filelist поддерживает пути, quotes/comments, +incdir+ и +define+, вложенные +-f/-F. Для .f/-f относительные пути считаются от корня проекта; для .F/-F — +от каталога содержащего списка. Неизвестные флаги, циклы и конфликтующие +значения defines отклоняются, а не интерпретируются предположительно. +Значения defines задаются строками. В текущем препроцессоре используется +определённость макроса; значение "0" тоже означает defined. + +## Работа с индексом + +Для уже инициализированного проекта после добавления профиля: + +```sh +codegraph index +codegraph status --json +``` + +Сменить hdl.activeProfile, затем выполнить codegraph sync. Работающий watcher +наблюдает конфигурацию, filelists и include dependencies, включая неизвестные +расширения заголовков и явно выбранные ignored paths. Смена контекста требует +пересчёта всей HDL-выборки даже при scoped sync; частичный indexFiles не +разрешает сменить контекст без полного sync/index. + +CLI/MCP отдельно показывают configured и indexed profile, fingerprint, +диагностику и ограничения. Изменение include учитывается по содержимому, +включая сохранённые размер/mtime. В fingerprint входят selected config, +filelists, include snapshots и extraction version. При некорректном профиле +индексация прекращается до изменения графа. + +Единицы разбираются из подготовленного snapshot; stored hashes относятся к +исходным байтам, не к замаскированному тексту. Правки во время прохода остаются +pending для следующего sync. Частичные операции обновляют диагностику только +действительно обработанных единиц. + +## Поддержанный препроцессинг и ограничения + +- ifdef/ifndef/elsif/else/endif и define/undef/undefineall. +- Include вносит macro state и зависимость. Он не вставляет содержимое header + в source unit; header можно явно перечислить как отдельный источник. +- Неактивный текст заменяется пробелами с сохранением UTF-16 offsets и CR/LF. + Показанные snippets остаются оригинальным исходником и могут содержать + неактивные ветки; граф учитывает выбранный профиль. +- Полного macro expansion нет. Непрозрачный макровызов может изменить defines: + дальнейшая неизвестная условная ветка не выбирается, а диагностируется. + Неподдержанные конструкции и incomplete state явно видны в status. +- Каждая listed source unit начинает с defines профиля; порядок файлов не + используется как неявный общий macro environment нескольких compilation units. +- languageMode записывает intended dialect; parser остаётся SystemVerilog и не + доказывает соответствие только Verilog. topModules — записанные roots; + reachability pruning/elaboration не выполняются. +- Пределы текущего snapshot режима: 1 MiB на source/header, 64 MiB суммарно; + loader ограничивает размер и глубину filelists. Диагностика ограничена по + объёму без прекращения обработки macro state. + +Полное раскрытие include/macro с compiler source mapping и semantic frontend +остаётся следующим отдельным этапом. Профиль не доказывает synthesis, simulation, +width compatibility, timing или CDC. diff --git a/docs/hdl-roadmap.md b/docs/hdl-roadmap.md new file mode 100644 index 000000000..d2274adc3 --- /dev/null +++ b/docs/hdl-roadmap.md @@ -0,0 +1,143 @@ +# План развития Verilog/SystemVerilog + +Актуальная очередь, критерии готовности и журнал выполнения: [VERILOG_TRACKER.md](../VERILOG_TRACKER.md). Ниже сохранена история исходных этапов. + +## Реализованный пакет после ce13487b + +1. Ссылки на основания и индексы LHS bit/part selects и concatenations. + Приёмка: crc[15], bus[i +: W], {a,b} сохраняют сигналы; hierarchical access, + package qualifiers и shadowing не создают ложных локальных целей. +2. Именованные подключения instance → formal port целевого модуля. + Приёмка: .rx(rx1) даёт проверяемый переход к rx нужного uart_rx; + отсутствующий/чужой порт и неоднозначный module не угадываются. + Source signature и прежние instance→local references сохраняются. +3. Regression tests, реальные Dual UART/eMMC/AXI, повторная индексация, + build, проверки backend, push трёх remote и обновление draft PR #1845. + +## Реализованный пакет после 7d0321ae + +- Позиционные подключения: source-order ports, включая non-ANSI headers, + пустые позиции и проверку числа формальных портов. +- Wildcard .*: раскрытие только при известном module и доступном локальном + сигнале; explicit connections имеют приоритет, unknown остаётся unknown. +- tree-sitter-systemverilog 0.4.0: проверка ABI, реальных корпусов и современных конструкций. + +## План после 4d97bb29 + +База: grammar 0.4.0, extraction stamp 35, named/positional/wildcard bindings. +Статус этапов отмечен ниже. Очерёдность учитывает +зависимости: scopes нужны до read/write, build context — до elaboration. + +### 1. Уточнить текущие ограничения и закрыть локальные scopes — реализовано + +Польза: имя i/x ведёт к правильному объявлению; локальная переменная во +вложенном блоке не скрывает одноимённый сигнал во всём always. + +- Разобрать 7 оставшихся ERROR на 137 HDL-файлах: синтаксическая ошибка, + отсутствие preprocessing context или пробел grammar/extractor. Сохранить + минимальные воспроизведения; исправлять только подтверждённые дефекты. +- Отдельные scopes для begin/end, fork/join, for/foreach и function/task; + различать объявления в соседних блоках и время их видимости. +- Genvar и соответствующее имя внутри generate-body представить как + декларации шаблона, без подстановки выдуманных iteration values. +- Убрать подавление имени на весь process там, где доступна точная область. + +Приёмка: nested/sibling scopes, shadow внутри/снаружи цикла, escaped names, +same-line declarations и stable IDs; sync после перемещения объявления даёт +те же связи, что clean rebuild. Реальные UART/CRC/AXI-control examples. + +### 2. Разделить синтаксические reads, writes и control dependencies — реализовано + +Польза: ответить, какой блок читает сигнал и где код присваивает ему значение. + +- LHS/RHS blocking и nonblocking assignments; assign, compound assignments, + increment/decrement, concatenations и bit/part selects. +- В x[i] = y запись относится к x; чтения — к i и y. Условия if/case/loops, + RHS и event controls сохраняются раздельно. +- Function/task аргументы классифицировать по известным направлениям; + при неизвестной сигнатуре сохранять обычную reference. +- Добавить фильтры/объяснения в существующий query/MCP путь и синхронизировать + server instructions. Форма хранения (edge metadata или новые kinds) + выбирается после проверки API/wire/schema совместимости. + +Приёмка: ручная таблица ожидаемых доступов для небольших настоящих блоков +UART и eMMC CRC; отрицательные controls для package/member/индексов; запросы +по сигналам показывают исходные строки и тип использования. Не называть это +доказательством электрического драйвера, latch или race. + +### 3. Явный контекст HDL-сборки — реализован ограниченный source-режим + +Польза: различать synthesis/testbench и условные варианты без выбора по имени +директории там, где проект предоставляет точный состав сборки. + +- Именованные profiles: source files/filelists, include directories, defines, + top modules и language mode. Определить минимальный поддерживаемый формат. +- Отразить происхождение профиля и активный профиль в status/query. +- Учитывать include dependencies и условную компиляцию с mapping к исходным + файлам; unsupported directives возвращают явную неизвестность. +- Изменение define/include/filelist инвалидирует зависимую часть графа. + Зафиксировать профиль в fingerprint индекса, чтобы состояния не смешивались. + +Приёмка: один RTL-корпус под двумя profiles с разными ветками и одинаковыми +именами модулей; смена профиля и изменение включаемого header сходятся с +clean rebuild. Измерить время/объём повторной индексации. + +### 4. Семантический слой через HDL-компилятор + +Польза: точные parameter overrides, widths/types и elaborated instances. + +- Сначала сравнительный прототип доступных HDL frontends на наших корпусах: + AST/semantic export, source mapping, охват конструкций, стоимость запуска. +- Выбрать адаптер по результатам; source graph сохраняется независимо от + доступности и успешности компилятора. +- Начальный объём: parameters/localparams, packed/unpacked dimensions, + typedef/struct/enum и generate conditions/iterations. +- Различать instance-specific значения и исходную декларацию. Результат + должен содержать provenance: frontend/version/profile/source revision. + +Приёмка: сверка типов, ширин и instance paths с выводом frontend на UART, +CRC и параметризованном AXI; отдельные случаи unknown/external definitions. +Изменение параметра через sync не оставляет старый elaborated result. +Не строить собственный полный HDL-компилятор внутри extractor. + +### 5. Сквозные пути сигналов и полезные инженерные запросы + +Зависит от этапов 2–4. + +- Путь signal → expression/port → instance → formal signal с направлениями, + подтверждёнными декларациями и профилем. +- Учитывать slices/concatenations, interface/modport и instance-specific + параметры в пределах подтверждённой семантики. +- Показывать неизвестный участок пути и доказательства каждого перехода. +- Примеры приёмки: UART RX до FIFO, вход CRC до crc_out, AXI valid/ready + через wrapper. Сравнить ответы с ручной трассировкой реальных исходников. + +Timing, CDC/RDC, гонки и аппаратную корректность не выводить из такого графа: +для них нужен отдельный анализ и отдельные проверяемые требования. + +### 6. Verification-конструкции SystemVerilog + +После RTL-навигации: property/sequence/assert/cover, clocking blocks, +bind и связи DUT↔testbench. Затем оценить необходимость class/UVM dispatch +по реальным проектам, а не по общему списку возможностей стандарта. + +Приёмка: находить assertion/coverage, относящиеся к выбранному RTL-сигналу, +со ссылками на исходные конструкции. Не заявлять formal proof по AST. + +## Проверки и порядок поставки + +- Этап 1 реализован; отчёты validation-hdl-scopes-2026-09-12.md и + validation-hdl-parse-gaps-2026-09-12.md. Этап 2 реализован: validation-hdl-access-2026-09-12.md. + Этап 3 реализован в ограниченном source-режиме: hdl-profiles.md и + validation-hdl-profiles-2026-09-12.md. Следующий пакет — этап 4, compiler frontend. +- Для каждого пакета: failing regression → исправление → независимое review + → focused tests → реальные corpus queries и sync/rebuild comparison. +- Полные native/WASM suites при изменениях общих extraction/resolution/storage + путей; build и проверка доставки assets. Не повторять широкий прогон после + успешного результата без новой причины. +- Обновлять extraction stamp и используемые индексы, когда меняется граф; + фиксировать проверки и ограничения в отдельном validation report. +- Поставлять логическими коммитами; push/PR update — в рамках согласованной + реализации. Проверять одинаковый SHA во всех запрошенных remote. +- Для новых возможностей стандарта вести таблицу: parse / extract / resolve / + query / проверенный пример. Версия grammar не равна полному охвату стандарта. diff --git a/docs/hdl-semantics.md b/docs/hdl-semantics.md new file mode 100644 index 000000000..f9b3097b8 --- /dev/null +++ b/docs/hdl-semantics.md @@ -0,0 +1,82 @@ +# Вычисленные параметры и ширины HDL через slang + +Команда `hdl-semantic` и API `CodeGraph.getHdlSemantics()` запускают установленный +slang по явному запросу. Нужны инициализированный проект и активный HDL build +profile в `codegraph.json`. Факты возвращаются отдельно от source graph; данные +графа не перезаписываются. Slang автоматически не устанавливается. Для compiler macro provenance доступен +[отдельный pyslang backend](hdl-macro-source-map.md) через --python; выбрать ровно +один frontend executable. + +## CLI + +```sh +codegraph hdl-semantic top.W --path /path/to/project --slang /path/to/slang +codegraph hdl-semantic crc_out --path /path/to/project --slang /path/to/slang +codegraph hdl-semantic NoSlvMst --path /path/to/project --slang /path/to/slang \ + --top synth_axi_lite_xbar --parameter NoSlvMst=4 +``` + +Ответ — JSON с facts и provenance. Query выбирает точное имя, instance path или +`instancePath.name`; без query возвращается первая ограниченная выборка. +`--limit` задаёт 1..1000 фактов (по умолчанию 100), totalMatches/truncated +показывают обрезку. Для нескольких overrides перечислить NAME=VALUE после +`--parameter`. Имена/значения передаются отдельными arguments без shell. + +Если slang отклоняет UART из-за use-before-declare, совместимость можно включить +явно через `--allow-use-before-declare`. Она входит в provenance/fingerprint и +не включается автоматически после compiler error. + +## API + +```ts +const result = await cg.getHdlSemantics({ + executable: '/path/to/slang', + top: 'top', + parameters: { W: '16' }, + query: 'top.data', + limit: 20, + signal: abortController.signal, +}); +``` + +Параметры возвращаются как строки evaluated values, чтобы не терять большие +целые и неизвестные разряды. Port width возвращается только для типов, +которые importer умеет вычислить достоверно; отсутствие width не означает 0. +Instance-specific facts не сливаются между экземплярами одного module. + +Каждый ответ фиксирует slang version, executable hash, active profile, +configuration и semantic fingerprints, top, overrides, language standard, +separate compilation-unit mode, runner version, compiler limits и compatibility mode. +`sourceNodeId` добавляется только при совпадении профиля, raw hash файла и +единственном совпадении имени/строки source declaration. При устаревшем или +другом source graph вычисленные facts доступны без предполагаемых node links. + +Source location сохраняет frontend coordinates; это не обещание UTF16 offsets +CodeGraph. Column null означает неизвестную колонку, например у macro-generated +объявления. Macro invocation line не выдаётся за точный expanded token range. + +## Границы первой реализации + +- Вычисление по запросу, без persistent semantic cache и автоматического запуска + при index/sync/explore. Следующий запрос использует новый snapshot. +- Поддержанный slang contract проверяется runner/importer; неизвестная версия + или форма AST отвергается. AST, записанный при ошибке compiler, не принимается. +- Snapshot: 1 MiB на input, 64 MiB суммарно; AST максимум 64 MiB, stdout/stderr + суммарно 1 MiB. Фиксированы compiler limits для hierarchy/generate/constant + evaluation; это не жёсткий OS RSS limit. Временные данные хранятся в + `~/storage/codegraph-semantic-runs` (override `CODEGRAPH_SEMANTIC_TMPDIR` вне + project root) и удаляются после запроса. +- Snapshot включает выбранные inputs и literal include dependencies. + Некорректные/выходящие за project root пути и неподдержанные формы include + отклоняются. Это не sandbox для запуска недоверенного compiler executable. +- Full macro expansion выполняет compiler в допустимом snapshot context; + точная цепочка expansion/spelling locations ещё не экспортируется. +- Ошибка, отсутствие slang или отмена не изменяют source graph. Windows runner + пока не включён; процессная отмена требует отдельной проверки на этой ОС. +- Это параметры/порты и их compiler context, не simulation, synthesis, timing, + CDC или доказательство аппаратной корректности. Semantics не добавлена в + обычный MCP explore; для явного запроса использовать CLI/API выше. + +Проверенные версии, реальные корпуса и результаты — в +`validation-hdl-semantics-2026-09-12.md`. План дальнейшей интеграции — +`../VERILOG_TRACKER.md`. diff --git a/docs/hdl-slang-semantic-probe.json b/docs/hdl-slang-semantic-probe.json new file mode 100644 index 000000000..06be6cc13 --- /dev/null +++ b/docs/hdl-slang-semantic-probe.json @@ -0,0 +1,473 @@ +{ + "schemaVersion": 1, + "toolVersion": "slang version 11.0.448+e222e7dc0", + "standard": "1800-2023", + "sourceInventory": "docs/hdl-corpus-manifest.json", + "all137ManifestSourceHashesStillMatch": true, + "cases": [ + { + "name": "uart", + "exit": 1, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/uart.diagnostics.json", + "src/fifo.v", + "src/top.v", + "src/uart_arbiter.v", + "src/uart_rx.v", + "src/uart_rx_autobaud.v", + "src/uart_tx.v" + ], + "cwd": "${CORPUS}/dual-uart", + "astBytes": 1089429, + "diagnosticsBySeverity": { + "error": 9, + "note": 9 + }, + "diagnosticExamples": [ + { + "severity": "error", + "message": "identifier 'rx1_dv' used before its declaration", + "location": "src/top.v:68:17", + "symbolPath": "top" + }, + { + "severity": "note", + "message": "declared here", + "location": "src/top.v:142:16" + }, + { + "severity": "error", + "message": "identifier 'rx2_dv' used before its declaration", + "location": "src/top.v:77:17", + "symbolPath": "top" + } + ] + }, + { + "name": "board", + "exit": 1, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "top", + "--ast-json", + "${OUTPUT}/board.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/board.diagnostics.json", + "src/crc8.v", + "src/emmc_cmd.v", + "src/emmc_controller.v", + "src/emmc_crc16.v", + "src/emmc_crc7.v", + "src/emmc_dat.v", + "src/emmc_init.v", + "src/led_status.v", + "src/pll.v", + "src/sector_buf.v", + "src/sector_buf_wr.v", + "src/top.v", + "src/uart_bridge.v", + "src/uart_rx.v", + "src/uart_tx.v" + ], + "cwd": "${CORPUS}/card-reader", + "astBytes": 8437037, + "diagnosticsBySeverity": { + "error": 5, + "note": 4 + }, + "diagnosticExamples": [ + { + "severity": "error", + "message": "identifier 'state' used before its declaration", + "location": "src/emmc_cmd.v:49:24", + "symbolPath": "top.u_emmc.u_cmd" + }, + { + "severity": "note", + "message": "declared here", + "location": "src/emmc_cmd.v:51:17" + }, + { + "severity": "error", + "message": "identifier 'wr_done_timeout' used before its declaration", + "location": "src/emmc_controller.v:466:36", + "symbolPath": "top.u_emmc" + } + ] + }, + { + "name": "crc-std", + "exit": 0, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "emmc_crc16", + "--ast-json", + "${OUTPUT}/crc-std.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/crc-std.diagnostics.json", + "src/emmc_crc16.v" + ], + "cwd": "${CORPUS}/card-reader", + "astBytes": 107639, + "diagnosticsBySeverity": {}, + "diagnosticExamples": [] + }, + { + "name": "axi-2-exact", + "exit": 0, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi-2-exact.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/axi-2-exact.diagnostics.json", + "-f", + "${AXI}/build/final-controls/verilator.f", + "-GNoSlvMst=2" + ], + "cwd": "${AXI}", + "astBytes": 9382003, + "diagnosticsBySeverity": { + "warning": 1 + }, + "diagnosticExamples": [ + { + "severity": "warning", + "message": "finish argument must have value of 0, 1, or 2", + "optionName": "finish-num", + "location": ".bender/git/checkouts/common_verification-5b9bacfe2f79eb87/src/sim_timeout.sv:24:21", + "symbolPath": ".validate_params" + } + ] + }, + { + "name": "axi-2", + "exit": 0, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi-2.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/axi-2.diagnostics.json", + "-f", + "${AXI}/build/final-controls/verilator.f", + "-GNoSlvMst=2", + "-DVERILATOR=1" + ], + "cwd": "${AXI}", + "astBytes": 9030294, + "diagnosticsBySeverity": {}, + "diagnosticExamples": [] + }, + { + "name": "axi-4", + "exit": 0, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "synth_axi_lite_xbar", + "--ast-json", + "${OUTPUT}/axi-4.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/axi-4.diagnostics.json", + "-f", + "${AXI}/build/final-controls/verilator.f", + "-GNoSlvMst=4", + "-DVERILATOR=1" + ], + "cwd": "${AXI}", + "astBytes": 10796211, + "diagnosticsBySeverity": {}, + "diagnosticExamples": [] + }, + { + "name": "uart-compat", + "exit": 0, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "top", + "--ast-json", + "${OUTPUT}/uart-compat.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/uart-compat.diagnostics.json", + "src/fifo.v", + "src/top.v", + "src/uart_arbiter.v", + "src/uart_rx.v", + "src/uart_rx_autobaud.v", + "src/uart_tx.v", + "--allow-use-before-declare" + ], + "cwd": "${CORPUS}/dual-uart", + "astBytes": 1075539, + "diagnosticsBySeverity": {}, + "diagnosticExamples": [] + }, + { + "name": "board-compat", + "exit": 1, + "argv": [ + "${SLANG}", + "--single-unit", + "--std", + "1800-2023", + "--top", + "top", + "--ast-json", + "${OUTPUT}/board-compat.json", + "--ast-json-source-info", + "--diag-json", + "${OUTPUT}/board-compat.diagnostics.json", + "src/crc8.v", + "src/emmc_cmd.v", + "src/emmc_controller.v", + "src/emmc_crc16.v", + "src/emmc_crc7.v", + "src/emmc_dat.v", + "src/emmc_init.v", + "src/led_status.v", + "src/pll.v", + "src/sector_buf.v", + "src/sector_buf_wr.v", + "src/top.v", + "src/uart_bridge.v", + "src/uart_rx.v", + "src/uart_tx.v", + "--allow-use-before-declare" + ], + "cwd": "${CORPUS}/card-reader", + "astBytes": 8402802, + "diagnosticsBySeverity": { + "error": 1 + }, + "diagnosticExamples": [ + { + "severity": "error", + "message": "unknown module 'rPLL'", + "location": "src/pll.v:13:5", + "symbolPath": "top.u_pll" + } + ] + } + ], + "examples": { + "crc-std": { + "reachableInstancesIncludingTop": 1, + "parametersAtRoot": [], + "missingBodyReferences": [], + "crcOut": { + "name": "crc_out", + "type": "logic[15:0]", + "direction": "Out" + } + }, + "uart-compat": { + "reachableInstancesIncludingTop": 7, + "parametersAtRoot": [ + { + "path": "top.CLK_FREQ", + "value": "50000000", + "type": "logic signed[31:0]", + "source": { + "source_file": "src/top.v", + "source_line": 35, + "source_column": 16 + } + }, + { + "path": "top.BAUD_RATE", + "value": "921600", + "type": "logic signed[31:0]", + "source": { + "source_file": "src/top.v", + "source_line": 36, + "source_column": 16 + } + }, + { + "path": "top.BLINK_DURATION", + "value": "24'd5000000", + "type": "logic[23:0]", + "source": { + "source_file": "src/top.v", + "source_line": 62, + "source_column": 16 + } + } + ], + "missingBodyReferences": [], + "rxParameters": [ + { + "path": "top.u_rx1.CLK_FREQ", + "value": "50000000", + "type": "logic signed[31:0]", + "source": { + "source_file": "src/uart_rx.v", + "source_line": 11, + "source_column": 15 + } + }, + { + "path": "top.u_rx1.CLKS_PER_BIT", + "value": "54", + "type": "logic signed[31:0]", + "source": { + "source_file": "src/uart_rx.v", + "source_line": 22, + "source_column": 16 + } + }, + { + "path": "top.u_rx2.CLK_FREQ", + "value": "50000000", + "type": "logic signed[31:0]", + "source": { + "source_file": "src/uart_rx.v", + "source_line": 11, + "source_column": 15 + } + } + ] + }, + "axi-2": { + "reachableInstancesIncludingTop": 92, + "parametersAtRoot": [ + { + "path": "synth_axi_lite_xbar.NoSlvMst", + "value": "32'd2", + "type": "int unsigned", + "source": { + "source_file": "test/axi_synth_bench.sv", + "source_line": 406, + "source_column": 26 + } + }, + { + "path": "synth_axi_lite_xbar.XbarCfg", + "value": "332'h2000000020000000500000005ffe000000000000000000000000000000200000002000000002", + "type": "axi_pkg::xbar_cfg_t", + "source": { + "source_file": "test/axi_synth_bench.sv", + "source_line": 422, + "source_column": 34 + } + } + ], + "missingBodyReferences": [], + "slaveGenerateIndices": { + "path": "synth_axi_lite_xbar.i_xbar_dut.gen_slv_port_demux", + "loopVariable": "i", + "indices": [ + "0", + "1" + ] + } + }, + "axi-4": { + "reachableInstancesIncludingTop": 182, + "parametersAtRoot": [ + { + "path": "synth_axi_lite_xbar.NoSlvMst", + "value": "32'd4", + "type": "int unsigned", + "source": { + "source_file": "test/axi_synth_bench.sv", + "source_line": 406, + "source_column": 26 + } + }, + { + "path": "synth_axi_lite_xbar.XbarCfg", + "value": "332'h4000000040000000500000005ffe000000000000000000000000000000200000002000000004", + "type": "axi_pkg::xbar_cfg_t", + "source": { + "source_file": "test/axi_synth_bench.sv", + "source_line": 422, + "source_column": 34 + } + } + ], + "missingBodyReferences": [], + "slaveGenerateIndices": { + "path": "synth_axi_lite_xbar.i_xbar_dut.gen_slv_port_demux", + "loopVariable": "i", + "indices": [ + "0", + "1", + "2", + "3" + ] + } + } + }, + "astSchema": { + "rootInstances": "design.members entries with kind=Instance", + "instanceBody": "Instance.body is an object OR decimal-addr-prefixed reference string; resolve with global addr catalog", + "parameter": "InstanceBody.members Parameter: value (SV constant string), type, isLocal/isPort/isBody, source_file/source_line/source_column", + "types": "Default type is printable string, sometimes addr-prefixed reference. --ast-json-detailed-types emits PackedArrayType.range and elementType, isSigned, aliases and members. Strip addr from portable results.", + "source": "Ordinary symbols have source_file/source_line/source_column (declaration-name location); statements/expressions have source_*_start and source_*_end ranges. Ordinary positive columns are 1-based; tested literal range end is exclusive. Macro-expanded symbols can have column 0 at invocation line, not an exact token location.", + "generate": "GenerateBlockArray.members include Genvar and GenerateBlock. Each block has implicit local Parameter value for iteration index; skip isUninstantiated. Derive path array[index] then descend body members; follow shared InstanceBody references for each instance path." + }, + "limitations": [ + "Elaborated AST/type/constant probes only: no simulation, synthesis or timing validation.", + "AST JSON is emitted even on failed compilations; gate acceptance on compiler exit/diagnostics.", + "Strict Slang rejects UART use-before-declaration; success requires explicitly recorded --allow-use-before-declare compatibility option.", + "Board top still fails on missing rPLL even with declaration-order compatibility; no vendor-primitive substitution was introduced.", + "Generated TARGET_VERILATOR does not equal compiler builtin VERILATOR. Explicit -DVERILATOR=1 matches relevant Verilator guarded branches; exact-filelist and explicit-macro outcomes kept separately.", + "addr values and addr-prefixed strings are process-dependent and must not enter portable identity hashes.", + "Macro-origin AST locations can be coarse (column 0 at invocation); JSON alone did not provide a complete macro expansion/spelling source map." + ], + "macroSourceMapping": { + "example": { + "symbol": "synth_axi_lite_xbar.aw_chan_t", + "kind": "TypeAlias", + "source_file": "test/axi_synth_bench.sv", + "source_line": 415, + "source_column": 0, + "rawSource": " `AXI_LITE_TYPEDEF_AW_CHAN_T(aw_chan_t, addr_t)" + }, + "observation": "Macro-generated type symbols identify the invocation file/line but use column 0. This is not an exact original token position. No spelling/expansion stack was present in the probed AST JSON.", + "requiredBoundary": "Treat column 0 as unavailable/coarse macro origin; do not turn it into a valid 1-based column or claim exact declaration source ranges." + } +} diff --git a/docs/validation-call-evidence-index-2026-09-11.md b/docs/validation-call-evidence-index-2026-09-11.md new file mode 100644 index 000000000..35635be18 --- /dev/null +++ b/docs/validation-call-evidence-index-2026-09-11.md @@ -0,0 +1,34 @@ +# Проверка связей вызовов и актуальности индекса — 11 сентября 2026 + +Ветка: `fix/call-evidence-index-freshness`. База upstream: `3ed73bc127323e63153bf6ec8354afa82ce36aaf`. + +## Состав + +- #1760, `2b126b6b`: повторное связывание действия store не считается собственной локальной реализацией. +- #1844, `95dccf90`: сохранение вложенных вызовов без угадывания их целей; extraction version 27. +- #1843, `a5709f47`: сравнение файлов с индексом после commit, включая добавления, удаления и переименования. +- #1842, `28ca635b`: явный CLI `sync` отклоняет устаревший extraction stamp, в том числе с `--quiet`. +- Дополнительное исправление: сигнатуры методов внутри TypeScript interface/type alias исключены из проверки единственности реализации действия store. Реальные методы классов остаются кандидатами, поэтому неоднозначность не маскируется. +- Тест MCP truncation использует две настоящие функции в разных модулях и требует отдельный маркер обрезания для каждой. Файл с тем же именем не является второй реализацией функции. + +## Проверки + +Linux x64, Node 24.15.0, зависимости установлены через `npm ci` из lock-файла ветки. +Native-ядро собрано через `npm run build:kernel`; debug-вывод подтвердил загрузку именно бинарника этого checkout. + +- `npm run build`: успешно, включая viewer и проверку копирования 29 WASM-грамматик. +- `tsc --noEmit`, `git diff --check`: успешно. +- Итоговый профильный native-прогон: **427 passed**, 16 файлов. +- Итоговый профильный WASM-прогон (`CODEGRAPH_KERNEL=0`): **406 passed**, 15 файлов; native parity в этот прогон не включён. +- Полный native-прогон: **4630 passed, 4 failed, 9 skipped**, 268 файлов. Все четыре отказа относятся к `kernel-dart-parity.test.ts`: `torture.dart` и `TortureCtors.dart`, каждый в LF и CRLF. +- Контроль на чистом upstream `3ed73bc`: те же **4 failed / 14 passed** в Dart parity с тем же собранным бинарником. Dart-исходники и fixtures идентичны upstream. Это исходное расхождение, а не новый отказ от этой интеграции; полный suite не объявляется зелёным. + +Профильный набор: `kernel-tsjs-parity`, `object-literal-methods`, `bare-call-no-method`, `resolution`, `ts-chained-receiver`, `ui-steps-api`, `ui-steps-cross-tier`, `ui-steps-api-servers`, `nextjs`, `react-native-bridge`, `sync`, `cli-sync-stale-index`, `include-ignored-config`, `git-changed-untracked-dir`, `extraction-old-git`, `mcp-callers-truncation`. + +Первичная связка PR оставляла `hardReset → reset` неразрешённым: индекс содержал и `S::reset` (сигнатуру интерфейса), и функцию `reset`. После дополнительного исправления проходят внешний `useStore.getState().reset()` и внутренний `get().reset()`. Негативные проверки сохраняют отсутствие связи при одной лишь сигнатуре и при двух настоящих реализациях. + +## Ограничения + +Windows и macOS не запускались. Полный WASM-suite не запускался. Полное хеширование inventory увеличивает стоимость `status`; новый замер на большом репозитории в этой проверке не выполнялся. Перед использованием новых результатов существующий индекс нужно пересобрать через `codegraph index`. + +Установка глобального CLI, перезапуск пользовательских MCP-процессов и обновление старых индексов в эту задачу не входят. Проверка и сборка выполнены в отдельном checkout под `~/storage`. diff --git a/docs/validation-hdl-access-2026-09-12.md b/docs/validation-hdl-access-2026-09-12.md new file mode 100644 index 000000000..19d4854f8 --- /dev/null +++ b/docs/validation-hdl-access-2026-09-12.md @@ -0,0 +1,77 @@ +# HDL: чтение, запись и контекст использования сигналов + +База 545b083f; extraction stamp 37. Linux x64 / Node 24.15.0; +grammar tree-sitter-systemverilog 0.4.0. Реализован этап 2 roadmap. + +## Использование + +Из каталога проиндексированного проекта: + +```sh +codegraph explore 'top::q' --hdl-access write +codegraph explore 'top::q' --hdl-access read +codegraph explore 'top::clk' --hdl-access event +``` + +MCP: `codegraph_explore({query:"top::q", hdlAccess:"write"})`. +Доступны read/write/readwrite/control/event/all. Qualified names с :: или +точками поддерживаются; одинаковые bare names выводятся отдельно по scopes. +Обычный explore без hdlAccess сохраняет прежнее поведение. + +Read включает readwrite/control/event; write включает readwrite. Вывод содержит +точные file:line:column, source snippets и evidence formal direction для +аргументов известных подпрограмм. Перед показом исходника проверяется SHA256, +в том числе при сохранённых размере и mtime. Unknown references не выдаются за +доступ; пустой результат не доказывает отсутствия использования. + +## Классификация + +- Blocking/nonblocking/continuous assignments; compound и increment/decrement. + x[i]=y означает x:write, i/y:read. Позиции одной строки хранятся отдельно + существующим SQLite identity, поэтому x=x+1 сохраняет обе occurrences. +- Условия if/case/loops — control; события — event с posedge/negedge evidence. + if(i++) сохраняет readwrite и control одновременно. Return и delays — read. +- Net/data initializer даёт self-reference write и отдельные RHSreads; + bare declarations, dimensions и localparam не получают предполагаемых writes. +- Полная однозначная сигнатура function/task: input/const ref read, output + write, inout/ref readwrite. Индексы выходного actual читаются; явный i++ + сохраняет побочный эффект. Unsupported/unknown signatures и недопустимые + expression shapes оставляют обычные references. +- Formal direction определяется при resolution, не по глобальному имени. + При HDL-изменениях sync переклассифицирует argument sites в неизменённых + caller files; при удалении сигнатуры старая классификация снимается. + Поиск ограничен HDL sources и candidate argument edges, не всем графом. + +Это синтаксические и обусловленные декларацией роли доступа, а не доказательство +исполнения, электрического драйвера, latch/race, timing или CDC. + +## Проверки + +30 focused tests прошли, включая публичные MCP/CLI, negative controls, +неизменённый non-HDL explore и source-drift guard. Постоянный sync-тест меняет +output→input, удаляет и возвращает task как inout при неизменном top; результат +совпадает с clean rebuild и не оставляет прежних access tags. + +Реальные запросы через ToolHandler: + +| Корпус | Проверенные запросы | +|---|---| +| Dual UART (11 файлов) | top::hb_cnt/write, top::clk/event | +| eMMC reader (33 файла) | crc/write, bit_in/read, feedback/write, enable/control внутри emmc_crc16 | +| AXI (104 файла) | axi_atop_filter::clk_i/event | + +Отдельная копия AXI: изменение axi_pkg.sv, sync 366 мс; 6981 access/argument +edges полностью совпали с clean rebuild, включая source positions и metadata. +Исходные пользовательские RTL не изменялись. Это измерение одного локального +контроля, не общая гарантия производительности. + +Полный native: 295 test files, 4839 passed, 9 skipped, 0 failed. +Полный WASM: 294 test files passed, 1 skipped; 4829 passed, 19 skipped, 0 failed. +Build с viewer/30 grammar assets и TypeScript прошли. Все 10 активных индексов: +stamp 37, complete, 0 pending changes. + +Первый native-прогон выявил округление mtime в новой тестовой fixture на 1 мс; +фиксированное целое значение timestamp устранило нестабильность, затем прогон +повторён. Проверка SHA256 исходника не ослаблялась. +Артефакты: .scratch/hdl-access-complete-focused.log, hdl-access-real-results.json, +hdl-access-real-sync-results.json, hdl-access-*.txt и hdl-access-final-*.log. diff --git a/docs/validation-hdl-corpora-build-2026-09-12.md b/docs/validation-hdl-corpora-build-2026-09-12.md new file mode 100644 index 000000000..2c2758175 --- /dev/null +++ b/docs/validation-hdl-corpora-build-2026-09-12.md @@ -0,0 +1,115 @@ +# HDL: manifest корпусов и восстановленный AXI build context + +Дата: 2026-09-12. Пакет A1/A2 из `VERILOG_TRACKER.md`. +CodeGraph до этого пакета: `d8921aa7`, extraction stamp 38. +Runtime extractor и формат индекса этим пакетом не меняются. + +## A1. Manifest корпусов + +`hdl-corpus-manifest.json` фиксирует UART, eMMC/CRC и AXI: происхождение, +revision, состав источников, SHA256, evidence build profiles и ограничения. +Скрипт `scripts/hdl-corpus-manifest.py` позволяет проверить копию корпуса; +проверено 137 HDL-файлов: UART 11, eMMC/CRC 33, AXI 93. +Проверка всех трёх корпусов и build/license evidence дала 0 расхождений. +Отрицательные controls change/add/remove и возврат к исходному состоянию прошли. +Сами RTL-корпуса в CodeGraph Git не добавлены. + +UART и AXI целиком совпадают с указанными Git revisions. У card-reader 32 из +33 файлов совпадают с revision; `sim/tb_uart_bridge_debug.v` взят из локального +working tree и закреплён отдельным SHA256. Одной Git revision недостаточно, +чтобы восстановить этот дополнительный файл; frozen corpus сохранён отдельно. +Лицензия UART/card-reader не установлена по файлам репозиториев. Для AXI +зафиксированы LICENSE Solderpad 0.51 и исходные SPDX markers. + +Пример проверки сохранённого corpus (без исполнения его build recipes): + +```sh +python3 scripts/hdl-corpus-manifest.py check \ + --corpus-root /absolute/path/to/corpora \ + --manifest docs/hdl-corpus-manifest.json +``` + +Для проверки оригинальных build/license files добавить `--origin dual-uart=PATH`, +`--origin card-reader=PATH`, `--origin axi=PATH`. При generate те же arguments +фиксируют provenance. Профили manifest предназначены для source-graph validation; +они не объявлены полной compiler closure. Literal dependencies собраны отдельно +от результатов preprocessing/elaboration. + +## A2. Восстановление AXI + +Использован upstream AXI `da8793b0e3f14c9186c94fac9ed6ec06375c254e`. +В отдельной копии `bender checkout` восстановил зависимости из существующего +`Bender.lock`; команда `bender update` не применялась. + +| Dependency | Точная revision | +| --- | --- | +| common_cells | `03d98106aa19952a10360d2230def85144a0008b` | +| common_verification | `fb1885f48ea46164a10568aeff51884389f67ae3` | +| tech_cells_generic | `3a3de73632a06826b1bd9c65a0a2e92b32016845` | + +Bender **0.32.1**, release binary x86_64 Linux проверен по SHA256 asset. +Verilator **5.051 devel, v5.050-260-g1c9a9e0cc (mod)** из установленного +OSS CAD Suite 20260825. Это точная локальная версия, не утверждение о latest. + +[Официальный workflow Bender](https://github.com/pulp-platform/bender): checkout +использует lockfile, script генерирует входные файлы для инструмента. +[Использованный release Bender](https://github.com/pulp-platform/bender/releases/tag/v0.32.1). + +### Воспроизведение + +Понадобятся Git, Python >=3.9, Bender и Verilator. Корпус и generated output размещать +в каталоге данных, например `~/storage`. Использовать новый isolated checkout: + +```sh +git clone https://github.com/pulp-platform/axi.git "$HOME/storage/axi-hdl-control" +git -C "$HOME/storage/axi-hdl-control" checkout --detach da8793b0e3f14c9186c94fac9ed6ec06375c254e +python3 scripts/hdl-axi-build-check.py \ + --axi "$HOME/storage/axi-hdl-control" \ + --bender /absolute/path/to/bender \ + --verilator /absolute/path/to/verilator \ + --output "$HOME/storage/axi-hdl-control/build/controls" +``` + +Скрипт отклоняет другую revision, правки tracked sources и `Bender.local` +overrides, проверяет dependency revisions и tracked cleanliness. Bender получает явный +`--dir`; generated source/include paths должны оставаться внутри checkout. +Контроль с `BENDER_DIR=/no/such/project` прошёл все шесть сценариев. Сохраняет +команды, tool versions, нормализованный filelist/hash, stdout/stderr каждого +прохода и JSON-отчёт. Снимок всех 152 tracked AXI inputs до/после должен совпасть. +Содержимое locks и исходников script не исправляет. + +### Compiler controls + +Filelist сгенерирован `bender script verilator -t synth_test`; он включает +upstream sources и transitive dependencies. Defines от Bender фиксируются +в отчёте. Это compiler control, не подмена CodeGraph profile и не выбор +production semantic adapter. + +| Контроль | Результат | +| --- | --- | +| `axi_demux_simple` preprocessing без common_cells include path | Ожидаемый exit 1, отсутствуют common_cells headers | +| Тот же файл с восстановленным include path, default | Exit 0, исходный module присутствует, register macros раскрыты | +| Тот же файл, define VCS | Exit 0, macro/include preprocessing выполнен | +| Upstream `synth_axi_lite_xbar`, `NoSlvMst=1` | Lint/elaboration exit 0, 9 warnings | +| Тот же top, `NoSlvMst=2` | Lint/elaboration exit 0, 14 warnings | +| Тот же top, `NoSlvMst=4` | Lint/elaboration exit 0, 12 warnings | + +Предупреждения сохранены: PINMISSING, WIDTHEXPAND, WIDTHTRUNC, а в отдельных +конфигурациях SPLITVAR/UNOPTFLAT. Использован `-Wno-fatal`, поэтому exit 0 +не означает отсутствие предупреждений или доказанную корректность RTL. +Включён `--assert`; simulation, synthesis, весь `axi_synth_bench` и аппаратная +проверка FPGA не выполнялись. RTL upstream не исправлялся. + +Полный машиночитаемый результат: `hdl-axi-build-validation.json`. +Следующий этап A3–A6: сравнение frontends на общей manifest-базе. Наличие +успешного Verilator control не закрывает выбор frontend или интеграцию semantics. + +## Проверки пакета + +Manifest generate повторён с byte-identical результатом; check всех трёх +корпусов и original build/license evidence — exit 0. В независимой копии +clean/change/add/remove/restore дали ожидаемые exits 0/1/1/1/0. +Python syntax, JSON parsing и whitespace проверены. Независимое review нашло +и помогло исправить перенаправление Bender через окружение; повторный control +подтвердил исправление. Полные native/WASM suites не повторялись: этот пакет +добавляет внешние validation scripts, manifest и docs, без изменений engine. diff --git a/docs/validation-hdl-expression-origins-2026-09-12.md b/docs/validation-hdl-expression-origins-2026-09-12.md new file mode 100644 index 000000000..4dcbc7116 --- /dev/null +++ b/docs/validation-hdl-expression-origins-2026-09-12.md @@ -0,0 +1,102 @@ +# Проверка macro origins выражений HDL + +Дата: 2026-09-12. База перед пакетом: `13df92be`. Source extraction stamp +остаётся 38; обновлён optional pyslang exporter, нормализация ответа и проверки. + +## Новое поведение + +`expressionOrigins` хранит уникальные compiler macro-token origins с ролями +initializer, declared-initializer и type. Имя объявления сохраняет собственный +sourceOrigin: прямое имя с macro initializer может иметь корректный sourceNodeId. +Type origins включают доступный declared type syntax и dimensions. + +`expressionOriginCoverage` различает checked/not-applicable/unavailable и +command-line для effective initializer. Default после override не выдаётся за +использованное выражение. В частности, pyslang isOverridden недостаточен для -G: +сравниваются нативные syntax ranges и подтверждается synthetic command-line buffer. + +Внутренние collapsed macro ranges остаются incomplete. Лимит — 32 origins на +факт, общий budget frames и syntax visits; truncated сообщает ограничение. +Кэш syntax сохраняет wrapper, чтобы повторное использование Python id не +подменило origins. Нормализованные дубликаты удаляются даже после исчерпания +frame budget. Это прямые syntax tokens, не транзитивный анализ всех констант. + +## Реальный AXI и отдельный diagnostic wrapper + +Исходный `axi_from_mem.sv` revision `da8793b0e3f14c9186c94fac9ed6ec06375c254e` +не изменялся. Для его допустимой elaboration создан отдельный wrapper с корректными +request/response struct types; это новый control, а не исходный AXI test. +Использованы 244 исходные единицы из восстановленного A2 filelist плюс wrapper. +Compilation завершилась без ошибок, с предупреждениями о неподключённых портах. + +Через **CodeGraph.getHdlSemantics** проверены: + +| Query внутри cg_axi_from_mem_control.dut | Width | Macro range expression spelling в src/axi_from_mem.sv | +| --- | ---: | --- | +| axi_lite_aw_chan_t | 35 | AxiAddrWidth-1, строка 81, колонка 42 | +| axi_lite_w_chan_t | 36 | DataWidth-1 и DataWidth/8-1, 81:68 и 81:91 | +| axi_lite_r_chan_t | 34 | DataWidth-1, 81:68 | + +Все три имеют coverage.type=checked, без truncation: 19/26/19 expression origins. +На compiled CLI повторён axi_lite_w_chan_t: width36 и type origins подтверждены. +Compiler frames связывают аргументы с `include/axi/typedef.svh` и внешним вызовом +AXI_LITE_TYPEDEF_ALL на строке 81. В обычных UART/CRC tops нет соответствующих +macro range expressions; они не выдавались за положительные случаи. + +Wrapper для воспроизведения в независимой копии A2 (добавить его в profile files, +задать top cg_axi_from_mem_control): + +```systemverilog +`include "axi/typedef.svh" +module cg_axi_from_mem_control; + typedef logic [31:0] addr_t; + typedef logic [31:0] data_t; + typedef logic [3:0] strb_t; + typedef logic [0:0] id_t; + typedef logic [0:0] user_t; + `AXI_TYPEDEF_ALL(bus, addr_t, id_t, data_t, strb_t, user_t) + axi_from_mem #(.MemAddrWidth(32), .AxiAddrWidth(32), .DataWidth(32), + .MaxRequests(2), .axi_req_t(bus_req_t), .axi_rsp_t(bus_resp_t)) dut(); +endmodule +``` + +## Override, изменение header и восстановление графа + +Дополнительный небольшой RTL control: leaf с P=`DEF, экземпляр a переопределяет +P через `OVR, экземпляр b оставляет default. + +- Сначала значения 11/7; у a initializer указывает OVR, declared-initializer — DEF. +- После смены header значения 13/9; старые source node links не выдаются до sync. +- После scoped sync и затем recreate+indexAll совпадают facts, sourceNodeId, + expression origins и semantic fingerprint. + +В отдельных реальных helper tests проверен -G: effective initializer имеет +coverage command-line, исходный macro default отдельно declaredInitializer. +Необнаруживаемые compiler syntax данные остаются unavailable. + +## Проверки и границы + +Целевые TypeScript tests: 40 passed, включая 9 новых expression-origin regressions, +оба CLI selectors, coordinate validation и доставку Python asset. Review нашло +и помогло исправить coercion coverage: JSON array не принимается как enum string. +Неполное/противоречивое покрытие, ложные paths/offsets, duplicate origins и budgets +проверены отрицательными controls. + +Реальные Python tests запускать установленным pyslang 11.0.0: + +```sh +/path/to/venv/bin/python -I scripts/verify-hdl-source-map-export.py --work-root /path/to/storage +``` + +Дополнительная проверка non-ANSI портов выявила отдельный случай: dimensions +находятся в internalSymbol.syntax, а не в header PortReference. Это исправлено +и проверено отдельным regression: ANSI и non-ANSI дают один и тот же macro origin +без дубликатов; при неизвестном declarator coverage не объявляется checked. + +15/15 проверок с настоящим pyslang прошли после исправления non-ANSI. +Финальный native: 310 test files, 4956 passed, 9 skipped, 0 failed. +Финальный WASM: 309 test files passed, 1 skipped; 4946 passed, 19 skipped, 0 failed. +Матрица выполнена последовательно с двумя workers. +Production build и real API controls прошли; originals не изменялись. +Persistent cache, полная C3 type model, simulation/synthesis/timing и аппаратная +корректность не заявляются. Windows runner остаётся отключён. diff --git a/docs/validation-hdl-macro-source-map-2026-09-12.md b/docs/validation-hdl-macro-source-map-2026-09-12.md new file mode 100644 index 000000000..b02984b97 --- /dev/null +++ b/docs/validation-hdl-macro-source-map-2026-09-12.md @@ -0,0 +1,80 @@ +# Проверка compiler macro provenance + +Дата: 2026-09-12. База CodeGraph до пакета: `d1d25f02`; source extraction stamp +остаётся 38. Добавлен optional pyslang backend для `hdl-semantic --python`, +отдельный нормализованный протокол exporter и проверка координат по snapshot. +Инструкция: `hdl-macro-source-map.md`. + +## Реализация и доказательства + +Использован `pyslang==11.0.0` в отдельном Python 3.14 venv. В production runtime +зависимость сама не устанавливается; script поставляется вместе с dist. +Backend выполняет собственную compilation через Driver/SourceManager и получает +значения и provenance в одном процессе. JSON от отдельного slang CLI не +склеивается с координатами другой compilation. + +SourceManager даёт две связи: original и expansion. Exporter обходит обе, +сохраняет bounded set frames и не выдаёт их за линейный стек. Points отражают +физические UTF-8 byte offsets и line/column, включая BOM/CRLF и `line` directives. +Macro-origin никогда не получает предполагаемый sourceNodeId. Прямые parameter, +port и typedef факты могут связаться с подходящим source node при совпадении +профиля, raw hash, имени, строки и вида узла. + +Объём — происхождение имени объявления. Macro uses только внутри initializer +или выражения ширины этим пакетом полностью не представлены. Token concatenation +даёт spelling point префикса, а не диапазон всего склеенного имени. + +## Реальный AXI + +В восстановленной копии A2 через новое API и compiled CLI проверен +`synth_axi_lite_xbar.aw_chan_t` при `NoSlvMst=4`: + +- `kind=type`, `width=35`, `sourceOrigin=macro`. +- Точка аргумента: `test/axi_synth_bench.sv:415:31`, byteOffset 13097. +- Точка исходного текста в теле определения: `include/axi/typedef.svh:178:44`, + byteOffset 9228. +- Внешний invocation range: `test/axi_synth_bench.sv:415:3–49`, + byteOffsets 13069..13115. +- Внутренний argument range у SourceManager сворачивается в одну точку: + `macroExpansionComplete=false`. Диапазон не дорисовывается по строке. +- sourceNodeId отсутствует: аргумент/тело macro не подменяет literal declaration. + +Отдельно через новый backend повторены UART CLK_FREQ=50000000, CRC width=16 +и AXI NoSlvMst=4. В custom nested/concat control с UTF-8 и CRLF проверены +parameter P=7 и macro-generated bus_data width=4. Все API assertions прошли. +Original UART/CRC/AXI корпуса не изменялись. + +## Автоматические проверки + +53 focused tests прошли: 20 runner, 10 прежнего importer, 4 API, 8 прежнего +review, 1 compiled CLI, 4 normalized importer, 5 source-map review, 1 asset. +Проверены ровно один selector, venv launch path, native/helper hashes, timeout, +cancel, source changes, malformed envelope, forged sourceNodeId, byte-offset +расхождения, UTF-8 continuation, граница CRLF, incomplete/collapsed ranges и limits. + +Шесть отдельных проверок с настоящим pyslang: + +```sh +/path/to/venv/bin/python -I scripts/verify-hdl-source-map-export.py \ + --work-root /path/to/storage +``` + +Они покрывают nested/include/default macros, обе parent relations, quoted +filename/arguments, BOM/UTF-8/CRLF, physical `line` coordinates, token concatenation, +ноль/пустую строку/XZ, shared instance bodies/generate, compiler error gating и +macro typedef. В первой live-интеграции найдена и исправлена ошибка чтения +ConstantValue: методы empty/isContainer нельзя трактовать как boolean свойства; +валидность и container state теперь проверяются правильно. + +Production build прошёл; helper в dist побайтно совпадает с source и присутствует +в `npm pack --dry-run` (11889 bytes на этой базе). Ничего в npm не опубликовано. +Полный native: 309 test files, 4947 passed, 9 skipped, 0 failed. +Полный WASM: 308 test files passed, 1 skipped; 4937 passed, 19 skipped, 0 failed. +Матрица выполнена последовательно с двумя workers. + +## Границы + +Нет полной origin map выражений, persistent cache, автоматической MCP compilation, +simulation, synthesis или аппаратных проверок. Windows runner отключён; +проверен Linux, macOS runtime не запускался. Пределы snapshot/процесса сохранены. +Версия normal slang CLI не заменяется и старый selector остаётся доступен. diff --git a/docs/validation-hdl-navigation-2026-09-12.md b/docs/validation-hdl-navigation-2026-09-12.md new file mode 100644 index 000000000..e5bdfa4bf --- /dev/null +++ b/docs/validation-hdl-navigation-2026-09-12.md @@ -0,0 +1,71 @@ +# Практическая навигация по HDL — 12 сентября 2026 + +База e174e684; Linux x64, Node 24.15.0. Extraction stamp 33. +Verilog/SystemVerilog использует WASM grammar и при включённом native kernel. + +## Что теперь можно спросить у графа + +- `top.u_rx1 top.u_rx2`: разные экземпляры одного модуля, тип, параметры, + исходные named/positional/shorthand подключения и локальные сигналы. +- `emmc_crc16.crc emmc_crc16.crc_out`: объявления, исходные always/assign + и синтаксические references; initializer feedback связан с bit_in. +- `AXI_BUS.Master AXI_BUS.Slave`: отдельные modports, направления в source + signature, ссылки на сигналы интерфейса. Interface-порт связан через type_of + с конкретным modport. +- Package calls сохраняют pkg::func. Bare calls требуют lexical/import + evidence; одинаковые функции разных packages не выбираются произвольно. + +Именованные generate blocks образуют scope. Для безымянных используется +позиция исходника. Массив instances представлен одной декларацией с исходным +диапазоном; одинаковые имена на одной строке различаются колонкой ID. +Module→module instantiates сохранён для прежних flow queries; дополнительно +есть instance→type. Новых schema kinds и Rust wire indexes не добавлено. + +## Реальные проекты + +Неизменённые копии исходников находятся вне Git в +`~/storage/codegraph-real-issues-20260912/`. AXI — pulp-platform/axi, +ревизия da8793b0e3f14c9186c94fac9ed6ec06375c254e. + +| Корпус | Файлы индекса | Поля/сигналы | Instances | Always/assign/initial blocks | Время index + query | +|---|---:|---:|---:|---:|---:| +| Dual UART | 11 | 226 | 11 | 36 | 0.620 с | +| eMMC card reader | 33 | 1266 | 35 | 180 | 1.069 с | +| AXI | 104 | 3333 | 494 | 905 | 1.755 с | + +До изменения первые два корпуса вообще не имели port/signal/instance nodes; +AXI также не имел этих nodes. Все три запроса выше выполнены через настоящий +ToolHandler codegraph_explore; ответы содержат соответствующие исходники. +SQLite readback подтвердил u_rx1→rx1, u_rx2→rx2, feedback→bit_in. +В отдельном probe трёх AXI файлов: 18 modports, 534 references от modports, +точные mst/slv→AXI_BUS::Master/Slave. Дубликатов ID в трёх RTL-корпусах нет. + +## Проверки и границы + +Полный native: 283 test files, 4745 passed, 9 skipped, 0 failed. +Полный WASM: 282 test files passed, 1 skipped; 4735 passed, 19 skipped, 0 failed. +TypeScript и build с viewer/30 grammar assets прошли. + + +22 focused HDL tests и 8 extraction tests прошли. Проверяются generate scopes, +повторная индексация, массивы, wildcard без придуманных connections, named/ +positional/shorthand references, non-ANSI redeclaration, port direction, +shadowing (включая genvar), package qualifiers не становятся сигналами, конфликтующие imports и explicit package calls. + +Первый полный запуск не засчитан: агент удалил временный probe из __tests__ +после обнаружения его Vitest, получился ENOENT при загрузке suite. Все проверки +повторены после фиксации набора файлов; временные probes не входят в commit. + +References означают синтаксическое использование, без классификации read/write. +Procedural locals подавляются консервативно при shadowing; hierarchical signal +access и LHS part-select не дают предполагаемую локальную связь. `.*`, generate +iterations, preprocessing/filelists, widths/types expressions не elaborated. +Нет timing, synthesis, CDC, UVM class-method dispatch или аппаратной проверки. +Windows/macOS и новый agent A/B не запускались. + +Все 10 активных индексов пересобраны до stamp 33; readback: complete и 0 pending changes. +Другим ранее созданным HDL-индексам нужен rebuild до stamp 33. Уже запущенный MCP-процесс +загружает новый extractor после перезапуска. + +Артефакты: `.scratch/hdl-usefulness-results.json`, `hdl-explore-*.txt`, +`hdl-instances-corpus-results.json`, `hdl-package-real.log`, `hdl-final-*.log`. diff --git a/docs/validation-hdl-parse-gaps-2026-09-12.md b/docs/validation-hdl-parse-gaps-2026-09-12.md new file mode 100644 index 000000000..5749246c4 --- /dev/null +++ b/docs/validation-hdl-parse-gaps-2026-09-12.md @@ -0,0 +1,132 @@ +# Остаточные ошибки разбора HDL: 2026-09-12 + +Все **7 `ERROR` в 2 файлах** из корпуса 137 HDL-файлов связаны с обработкой +исходного текста до препроцессора. Невалидность RTL этими ошибками не доказана. +Шесть ошибок исчезают после настоящего препроцессора на полном исходном файле; +седьмая воспроизведена и устранена препроцессором на минимальном валидном примере. +Полная сборка AXI и аппаратная проверка в этот аудит не входят. + +## Объект проверки + +- Корпус: `dual-uart` — 11 файлов, `card-reader` — 33, `axi` — 93. + В первых двух проектах `ERROR` и `MISSING` отсутствуют по сохранённому сравнению + `.scratch/grammar-v040/compare.json` от 2026-09-12. +- AXI revision: `da8793b0e3f14c9186c94fac9ed6ec06375c254e`. +- Грамматика: `tree-sitter-systemverilog` v0.4.0, release commit + `aa09b9004478cea0f46d877608910dc153b277b6`, ABI 15. +- SHA-256 WASM: `52e470eacf2a87af5056fe054c081d4495d1da72f5d0a9bb0d431e111ee8f235`. +- Node `v24.15.0`, `web-tree-sitter` `0.25.10`, запуск с `--liftoff-only`. +- Компилятор контрольных примеров: `Verilator 5.051 devel rev v5.050-260-g1c9a9e0cc (mod)` + из OSS CAD Suite 20260825. + +Старая и новая грамматики дают одинаковые 7 `ERROR`, `MISSING` — 0. +В этом аудите повторно разобраны оба проблемных исходника новой грамматикой; +их SHA-256 совпадают с исходным сравнением: + +| Файл AXI | SHA-256 | +| --- | --- | +| `src/axi_demux_simple.sv` | `f0beea644e9b4721046e27fb1179e9a1f481a1767a52092fe5ee7a1b3d287d08` | +| `src/axi_interleaved_xbar.sv` | `c3673eab4a89a5c9395d75630e959c5fda383653a21d0970d2347a25f80c8959` | + +## Классификация всех семи ERROR + +Номера строк — 1-based, в исходном AXI checkout. + +| Файл и строки | Ошибочный фрагмент AST | Причина и проверка | +| --- | --- | --- | +| `axi_demux_simple.sv:510` | `>` в `ASSUME(... |-> ...)` | Макроаргумент содержит property expression. Грамматика пытается разобрать его до раскрытия макроса. Минимальный пример ниже точно повторяет `ERROR >`; после `-E` AST чистый, Verilator lint проходит. | +| `axi_interleaved_xbar.sv:54–55` | директива `else` перед `input logic` | Альтернативные ANSI-порты внутри `ifdef VCS`. Обе отдельные конфигурации полного файла успешно препроцессируются и разбираются. | +| `axi_interleaved_xbar.sv:55–56` | `default_mst_port_i` и `endif` | Продолжение той же условной декларации порта; отдельного дефекта RTL не установлено. | +| `axi_interleaved_xbar.sv:323` | `ifdef VCS` | Условное добавление `localparam` в parameter port list. Обе конфигурации после препроцессора дают чистый AST. | +| `axi_interleaved_xbar.sv:325–326` | тернарное выражение и `endif` | Продолжение условного параметра, а не отдельная ошибка `$clog2` или cast. Минимальный пример отдельно принят компилятором. | +| `axi_interleaved_xbar.sv:337–338` | директива `else` перед `input logic` | Повтор условного ANSI-порта в interface wrapper. Обе конфигурации полного файла проверены. | +| `axi_interleaved_xbar.sv:338–339` | `default_mst_port_i` и `endif` | Продолжение той же условной декларации interface wrapper. | + +Это ограничения грамматики на непрепроцессированном SystemVerilog и отсутствие +выбранного preprocessing profile в текущем индексаторе. Отсутствующий файл include +не создаёт эти шесть ошибок xbar: для полного `axi_interleaved_xbar.sv` достаточно +существующего `axi/include`, и `Verilator -E` завершился с кодом 0 как без `VCS`, +так и с `+define+VCS`. Полученные два полных текста имеют **0 ERROR / 0 MISSING**. +Это проверка preprocessing + parsing, не полная elaboration зависимого проекта. + +Для полного `axi_demux_simple.sv` препроцессор остановился на отсутствующих в данном +корпусе `common_cells/assertions.svh` и `common_cells/registers.svh` (exit 1). +Нельзя считать этот полный файл проверенным компилятором или подставлять заглушки +и выдавать результат за проверку настоящей конфигурации. Класс ошибки на строке 510 +подтверждён отдельным самодостаточным примером. + +## Самодостаточные контрольные примеры + +Примеры ниже созданы для диагностики; это не скопированные AXI модули. + +Макроаргумент с property implication, до preprocessing — ровно `ERROR >`: + +```systemverilog +`define ASSUME(name, expression) name: assume property (@(posedge clk) expression); +module top(input clk, input Support, input valid, input [5:0] atop); + `ASSUME(no_error, !Support && valid |-> atop == '0) +endmodule +``` + +Альтернативные ANSI-порты, до preprocessing — два `ERROR`: + +```systemverilog +module top( +`ifdef VCS + input logic [3:0] port_i +`else + input logic [7:0] port_i +`endif +); +endmodule +``` + +Условный параметр с разделяющей запятой, до preprocessing — два `ERROR`: + +```systemverilog +module top #( + parameter int A = 2 +`ifdef VCS + , localparam int Width = (A == 1) ? 1 : unsigned'($clog2(A)) +`endif +)(input logic clk); +endmodule +``` + +Для каждого из трёх примеров выполнены обе конфигурации, без define и с +`+define+VCS`: `verilator -E -P` — exit 0; +`verilator --lint-only --assert -Wno-fatal` — exit 0, stderr пустой; +повторный tree-sitter parse раскрытого текста — 0 ERROR / 0 MISSING. +Итого шесть успешных compile/lint controls. Симуляция не выполнялась. + +Воспроизведение на сохранённых диагностических файлах из корня checkout: + +```bash +node --liftoff-only .scratch/hdl-parse-gaps/check.cjs +python3 .scratch/hdl-parse-gaps/full-preprocess.py +``` + +Основные локальные доказательства: `.scratch/hdl-parse-gaps/results.json`, +`full-preprocess.json`, `expanded-parse.json`, три `.sv` и раскрытые `.expanded.sv`. +Они исключены из Git; исходники минимальных примеров приведены в этом документе. + +## Безопасные следующие шаги + +1. Для макровызовов — исправление upstream grammar, которое воспринимает аргументы + как сбалансированный непрозрачный текст, включая property operators, строки, + комментарии и вложенные скобки. Нельзя интерпретировать макроаргумент как обычное + expression и терять часть `|->`. +2. Для условных header lists — поддержка conditional directive nodes в грамматике + либо явный preprocessing profile с include paths и defines. Пока ветка не выбрана, + порядок и состав портов неопределённы; named/positional/wildcard связывание не должно + превращать несовместимые ветки в один достоверный интерфейс. +3. Если добавлять внешний препроцессор, сохранить mapping раскрытого текста к исходным + файлам, fingerprint профиля и зависимостей include/define для incremental invalidation. + Без этого улучшенный parse будет показывать неверные строки и устаревшие связи. +4. Отдельно отображать parser recovery и отсутствие preprocessing context, не называть + `ERROR` доказательством дефекта RTL. Простое удаление строк `ifdef/else/endif` + объединит взаимоисключающие ветки; такой обход здесь не применялся. + +Правки грамматики, parser/extractor и постоянные regression tests в этом аудите +не выполнялись. Установлен воспроизводимый preprocessing/grammar gap, а не новый +дефект обработки уже корректного AST экстрактором. diff --git a/docs/validation-hdl-port-bindings-2026-09-12.md b/docs/validation-hdl-port-bindings-2026-09-12.md new file mode 100644 index 000000000..955ec6399 --- /dev/null +++ b/docs/validation-hdl-port-bindings-2026-09-12.md @@ -0,0 +1,58 @@ +# HDL: именованные порты и выбранные биты + +База ce13487b; extraction stamp 34. Linux x64 / Node 24.15.0. + +## Изменения + +- Отдельный property node для каждого named connection: top::u_rx1::rx. + Его signature сохраняет .rx(rx1), а references показывают локальный rx1 и + formal uart_rx::rx. Formal edge имеет metadata binding=hdl-named-port. +- Shorthand .rx поддерживается, explicit empty .rx() имеет только formal + endpoint. Массив экземпляров остаётся одной source declaration. +- Module selection согласован с прежним instantiates (synthesis/testbench), + но formal port binding строже: равнозначные definitions, duplicate module + в одном файле и отсутствующий порт остаются unresolved. Чужой одноимённый + порт не подставляется. Повторная индексация удаляет исчезнувшую цель. Обычный sync также + удаляет и восстанавливает formal endpoint при неизменном top; failed refs + повторно ищутся по имени порта из структурированного reference token. +- LHS bit/part selects и concatenations дают references на основания, + индексы и границы AST. Package/hierarchical members не превращаются в + одноимённые local signals. For/foreach locals подавляются при shadowing. + +Это source correspondence. Нет bit-level netlist, drive direction, evaluation +диапазонов или выбора generate branch. Wildcard/positional connections пока +сохраняются исходным текстом. Дальнейшие этапы описаны в hdl-roadmap.md. + +## Проверки + +Постоянные regressions: 7 port binding tests и 12 signal tests прошли. +Независимое ревью воспроизвело и затем подтвердило исправление p::x слева +от procedural assignment и справа с индексом; корректные a/n/y сохранены. +Два исходных квалифицированных примера проходят Verilator lint-only. + +На неизменённых копиях Dual UART/eMMC reader/AXI выполнены настоящие +codegraph_explore queries: top.u_rx1.rx top.u_rx2.rx, emmc_crc16.crc, +axi_atop_filter_intf.mst. SQLite показывает две стороны подключений UART: +top::u_rx1::rx → uart_rx::rx и top::rx1; аналогично u_rx2/rx2. +Для реального emmc_crc16 AST подтверждает crc на LHS выбранного бита +(позиция строки/колонки), а SQLite — сохранённую ссылку на crc на этой строке. + +| Корпус | Файлы | Named connections | Связи от connections | Index + query | +|---|---:|---:|---:|---:| +| Dual UART | 11 | 86 | 172 | 0.683 с | +| eMMC reader | 33 | 560 | 1070 | 1.261 с | +| AXI | 104 | 3411 | 4023 | 1.958 с | + +Количество связей не обязано равняться удвоенному числу подключений: +пустые порты, сложные выражения и отсутствующие внешние модули различаются. + +Выполнено source review с отрицательными controls swap .a(b)/.b(a), +package constants/casts, missing/fuzzy/duplicate module, scope shadows. +Полный native: 284 test files passed, 4756 passed, 9 skipped, 0 failed. +Полный WASM: 283 test files passed, 1 skipped; 4746 passed, 19 skipped, 0 failed. +TypeScript и сборка с viewer/30 grammar assets прошли. +Все 10 активных индексов пересобраны до stamp 34; readback complete, 0 pending. + +Артефакты: .scratch/hdl-ports-real-results.json, hdl-ports-explore-*.txt, +hdl-port-binding-sync-final-tests.log, hdl-review-fixed.log, +hdl-ports-final-*.log. Копии реальных RTL вне Git в ~/storage. diff --git a/docs/validation-hdl-profiles-2026-09-12.md b/docs/validation-hdl-profiles-2026-09-12.md new file mode 100644 index 000000000..df4f86b5a --- /dev/null +++ b/docs/validation-hdl-profiles-2026-09-12.md @@ -0,0 +1,71 @@ +# Проверка HDL profiles — 12 сентября 2026 + +База 5f903355; extraction stamp 38. Linux x64, Node 24.15.0, +tree-sitter-systemverilog 0.4.0. Конфигурация и границы — hdl-profiles.md. + +## Проверенные сценарии + +- Загрузчик: .f/.F, вложенные -f/-F, quotes/comments, incdir/define, + fingerprint, неизвестные флаги, циклы/конфликты и недопустимые пути. +- Выбор веток: offsets CRLF/UTF16, вложенные includes, guards/cycles, + отсутствующие заголовки и неизвестные эффекты непрозрачных макросов. +- Index: смена профиля удаляет предыдущий HDL-вариант, сохраняет остальные + языки и raw hashes, пересчитывает общие units при изменениях контекста. +- Compiled CLI index/sync/status/explore: выбранный source проходит через + worker threads; configured/indexed profiles разделены. +- Настоящий fs.watch: ignored filelists, заголовки с произвольным расширением, + переключение на ранее не наблюдавшиеся каталоги и callback dependencies. +- Include с сохранёнными размером/mtime меняет fingerprint; scoped sync + выбирает новую ветку. Правка во время прохода сохраняет согласованный старый + snapshot и подхватывается следующим sync вместо записи пустого marker. +- Частичные indexFiles/scoped sync сохраняют диагностику необработанных source units. + +Ревью нашло и помогло исправить два интеграционных дефекта: несогласованный +snapshot/raw hash и устаревшую диагностику частичных обновлений. Также исправлен +выбор неверного else после непрозрачного макровызова; compiler controls +подтверждают необходимость состояния unknown. + +## Реальные корпуса + +Filelists для независимых копий сгенерированы явно; это не исходные build +files авторов. Исходные 11 UART и 67 AXI source/include файлов проверены +по SHA256 до и после; originals и copies не изменились. + +| Корпус / профиль | Файлы | Узлы | Рёбра | Проверка | +| --- | ---: | ---: | ---: | --- | +| Dual UART synth | 6 | 396 | 1159 | tb_top отсутствует | +| Dual UART sim | 11 | 853 | 2978 | tb_top присутствует | +| AXI normal | 2 | 301 | 660 | default_mst_port_i использует idx_width(Cfg.NoMstPorts) | +| AXI VCS | 2 | 303 | 662 | default_mst_port_i использует MstPortsIdxWidth | + +Для обоих корпусов выполнено переключение туда и обратно через scoped sync +и сравнение всех nodes/edges с CodeGraph.recreate: полное совпадение, +включая metadata. Возврат UART synth восстановил исходный hash графа. +Все проходы вместе заняли 3.85 с на этой машине; это локальная проверка, +не обещание производительности на иных проектах. + +UART sim отмечен incomplete: 5 сохранённых, но не интерпретированных timescale. +Оба AXI-профиля отмечены incomplete: 17 диагностик macro expansion. +Эти результаты подтверждают выбор условных исходников, но не elaboration, +полный preprocessing, синтез или аппаратную корректность. + +## Обновление активных проектов + +Все 10 ранее выбранных активных проектов переиндексированы установленным CLI: +extraction stamp 38, state complete, reindexRecommended false, pending changes 0. +Это индексы ADRC (три checkout), FreeFCC, Wails, bot, CodeGraph, +CSQTT android-server/desktop и Skylab Hub. Уже работающие MCP-сессии +нужно перезапустить, чтобы они загрузили новую реализацию. + +## Автоматические проверки + +- 55 профильных tests: loader 22, preprocessor 14, status 10, index 4, + настоящий fs.watch 4 и compiled CLI/worker 1 — все прошли. +- Полный native: 301 suite, 4894 passed, 9 skipped, 0 failed. +- TypeScript и production build с viewer/30 grammar assets прошли. +- Полный WASM: 300 suites passed, 1 skipped; 4884 passed, 19 skipped, 0 failed. +- Полная матрица выполнена последовательно с двумя workers. +- Уточнена прежняя MCP-проверка: запрещается сообщение о неактивном сервере, + а не слово inactive в справке об условных ветках HDL. + +Windows/macOS в этом пакете не проверялись. diff --git a/docs/validation-hdl-scopes-2026-09-12.md b/docs/validation-hdl-scopes-2026-09-12.md new file mode 100644 index 000000000..5ff47e8be --- /dev/null +++ b/docs/validation-hdl-scopes-2026-09-12.md @@ -0,0 +1,59 @@ +# HDL: лексические scopes и generate declarations + +База 4d97bb29; extraction stamp 36. Linux x64, Node 24.15.0, +tree-sitter-systemverilog 0.4.0. Реализован этап 1 из hdl-roadmap.md. + +## Изменения + +- Named/anonymous begin/end, fork/join, for/foreach получают отдельные scopes. + Локальный x во вложенном блоке больше не подавляет ссылки на внешний x + во всём process; соседние блоки сохраняют собственные объявления. +- Function/task formal ports — отдельные поля в scope подпрограммы. + Сохраняются зависимости defaults и вызовы в for initializers. +- Inline/predeclared genvar и generate-body parameters представлены + декларациями шаблона. Значения итераций и elaborated instances не придуманы. +- Signal references принадлежат точному lexical block. Calls сохраняют + function/process owner и дополнительный lexical context для imports. +- Внутренний package import перекрывает внешнюю функцию/импорт; конфликт + на одном уровне остаётся unresolved. Context сохраняется через SQLite, + повторное разрешение и удаление/восстановление package function. +- Cross-file HDL edges пересчитываются по исходной qualified reference, + а не переносятся на произвольный символ того же короткого имени. + +При parser recovery внутри procedural региона сигнал не привязывается к +предполагаемому внешнему объявлению. Исходные узлы и независимые корректные +процессы остаются доступны; неполный регион может иметь меньше references. + +## Проверки + +32 focused tests прошли: scopes/signals/generate/packages/call scope. Review +воспроизвело пропуски defaults/initializers и неверный приоритет imports; +после исправлений повторены 10 независимых scratch controls. Выбор внутреннего +q::f подтверждён также результатом HDL-компилятора на минимальном примере. + +Постоянный тест меняет package-файл с одноимёнными p::run/q::run, удаляет и +восстанавливает функцию, сохраняя top без изменений: calls совпадают с clean +rebuild. Отдельные tests проверяют удаление локального shadow и generate sync. + +Реальный axi_xbar.sv: 3 generate template declarations, 8 occurrence references; +sync совпадает с clean index. Реальный emmc_crc16.v также проверен через индекс. +Повторены полные Dual UART/eMMC/AXI индексы и настоящие queries по UART ports, +CRC и AXI modport. В AXI сохранились 32 positional и 14 wildcard clauses с +28 formal/actual pairs; контрольная positional-копия UART также прошла. +Все 10 активных индексов: stamp 36, complete, 0 pending changes. +Полный native: 291 test files, 4809 passed, 9 skipped, 0 failed. +Полный WASM: 290 test files passed, 1 skipped; 4799 passed, 19 skipped, 0 failed. +TypeScript и build с viewer/30 grammar assets прошли. + +## Остаточные parse errors + +Все 7 ERROR классифицированы в validation-hdl-parse-gaps-2026-09-12.md. +Шесть raw conditional-header ошибок исчезают после препроцессинга полного +axi_interleaved_xbar в двух конфигурациях. Assertion macro проверен минимальным +воспроизведением; полный demux требует отсутствующих common_cells includes. +Это подтверждённая граница preprocessing context, а не повод удалять директивы +из исходника. Конфигурации сборки остаются отдельным этапом плана. + +Артефакты: .scratch/hdl-scope-final-focused.log, +hdl-generate-real-axi-results.json, hdl-lexical-review-fixed.log, +hdl-lexical-corpus.log, hdl-lexical-real-bindings.log, hdl-lexical-final-*.log. diff --git a/docs/validation-hdl-semantics-2026-09-12.md b/docs/validation-hdl-semantics-2026-09-12.md new file mode 100644 index 000000000..65fcfc8a7 --- /dev/null +++ b/docs/validation-hdl-semantics-2026-09-12.md @@ -0,0 +1,73 @@ +# Проверка первой интеграции HDL semantics + +Дата: 2026-09-12. База до пакета `11b3b44b`; extraction stamp остаётся 38: +source extraction/schema не меняется. Добавлены on-demand API +`CodeGraph.getHdlSemantics` и CLI `hdl-semantic`, optional slang runner и importer. +Инструкция и ограничения: `hdl-semantics.md`. + +## Проверки интеграции + +38 focused tests прошли: importer 10, runner 15, API 4, review controls 8, +compiled CLI 1. Покрыты shared bodies и отдельные instance parameters, +generate indices/instance arrays, malformed refs/cycles/oversized AST, +сложные unknown types и source coordinates. Runner проверен настоящими +subprocesses: timeout, AbortSignal, остановка descendants, cleanup, input/output +limits, BOM, source/snapshot modifications, missing-header creation и options +mutation. CLI передаёт overrides и отвергает duplicate parameters/invalid limits. + +Review помог исправить происхождение параметров при мутации caller options, +типовую совместимость sourceNodeId и проверку диапазона source column. Повторные +controls подтвердили отсутствие ложной связи macro port с одноимённым method. +Сложные неизвестные widths не преобразуются в 0; null column остаётся unknown. + +## Реальные корпуса через новую API, затем compiled CLI + +Использованы отдельные копии frozen UART/CRC и AXI с восстановленными dependency +revisions A2. Originals не изменялись. Slang: `11.0.448+e222e7dc0` из установленного +OSS CAD Suite 20260825; проверен существующий wrapper executable и его hash. +Это hash launcher-файла, не fingerprint всех связанных библиотек/toolchain. + +| Корпус / запрос | Проверенный результат | Время одного локального запроса | +| --- | --- | ---: | +| UART / top.CLK_FREQ, explicit compatible | value 50000000, width 32, sourceNodeId | 98 мс | +| CRC / crc_out | width 16, sourceNodeId | 70 мс | +| AXI / NoSlvMst=2 | value 32'd2, width 32, sourceNodeId | 566 мс | +| AXI / NoSlvMst=4 | value 32'd4, width 32, sourceNodeId | 638 мс | + +Это одиночные measurements для smoke validation, не performance guarantee. +Fingerprint AXI меняется между overrides. Строгий UART возвращает compiler error; +compatibility включается только явно. Отсутствующий executable возвращает ошибку, +source graph statistics остаются прежними (динамический getStats.lastUpdated +исключён из сравнения). На реальном compiled CLI повторён AXI override 4: +проверены value, sourceNodeId и compilationUnitMode=separate. + +## Include change, два экземпляра и sync/rebuild + +Отдельный небольшой RTL control использует leaf с parameter W из header, +два экземпляра `top.a` и `top.b`, у второго override W=16. + +1. Начальный header WIDTH=8: widths 8/16. +2. Header изменён на WIDTH=12: следующий on-demand запрос возвращает 12/16, + fingerprint меняется, sourceGraphProfileMatches=false, старые node links не выдаются. +3. Scoped sync include-файла: sourceGraphProfileMatches=true, ссылки доступны. +4. CodeGraph.recreate и полная indexAll: facts, sourceNodeId и semantic fingerprint + совпадают с результатом после sync. + +Это подтверждает первую вертикаль вычислений; persistent semantic cache, +автоматическая elaboration при sync и semantic edges в SQLite не добавлены. + +## Build и полная матрица + +Production build с viewer и 30 grammar assets прошёл. +Полный native: 306 test files, 4932 passed, 9 skipped, 0 failed. +Полный WASM: 305 test files passed, 1 skipped; 4922 passed, 19 skipped, 0 failed. +Матрица выполнена последовательно с двумя workers; 38 focused tests также прошли. + +## Оставшиеся границы + +Macro-generated source locations могут иметь column 0 (в API null); точная +expansion/spelling chain остаётся B2. Macro-computed include paths и пути вне +project snapshot пока отклоняются. Нет полного type/struct/enum/query coverage, +сохранённого semantic cache и автоматического MCP compiler query. Windows runner +явно отключён до проверки process-tree cancellation; реальные macOS запуски +не выполнялись. Simulation, synthesis, timing/CDC и аппаратные проверки не заявлены. diff --git a/docs/validation-hdl-wildcard-2026-09-12.md b/docs/validation-hdl-wildcard-2026-09-12.md new file mode 100644 index 000000000..a8ede086a --- /dev/null +++ b/docs/validation-hdl-wildcard-2026-09-12.md @@ -0,0 +1,47 @@ +# HDL: positional, wildcard и актуальная grammar + +База 7d0321ae, extraction stamp 35. Linux x64, Node 24.15.0. + +## Версии + +На 12 сентября 2026 активный стандарт — [IEEE 1800-2023 SystemVerilog](https://standards.ieee.org/ieee/1800/7743/), опубликованный 28 февраля 2024. Старый Verilog IEEE 1364 входит в объединённое семейство SystemVerilog. + +Grammar обновлена с 0.3.1 до [tree-sitter-systemverilog 0.4.0](https://github.com/gmlarumbe/tree-sitter-systemverilog/releases/tag/v0.4.0), релиз 17 июля 2026. Upstream commit aa09b9004478cea0f46d877608910dc153b277b6, ABI 15, web-tree-sitter 0.25.10. Официальный release WASM SHA256: 52e470eacf2a87af5056fe054c081d4495d1da72f5d0a9bb0d431e111ee8f235 (21,802,386 bytes). + +Изолированное сравнение 137 HDL-файлов (11 Dual UART, 33 eMMC reader, 93 AXI): одинаковые 7 ERROR/0 missing и их позиции. Хотя AST 19 файлов изменился, результаты nodes/refs/edges совпали полностью после исключения timestamps. Дополнительные method_call wrappers вокруг field accesses не создали calls. Отдельно подтверждены улучшения class timeunits, generate inside, keywords в аргументах макросов. Это не сертификация полной поддержки IEEE 1800-2023. + +## Связи + +- Порядок портов берётся из заголовка, включая non-ANSI с другим порядком body declarations. Unknown/complex headers не угадываются. +- Позиционные подключения представлены port[0], port[1] и т.д. Пустые позиции сохраняются; extra/mixed/error inputs не получают formal bindings. +- Wildcard clause имеет узел *. Только однозначные formal/local пары получают endpoints. Explicit .a и .a() имеют приоритет; genvar, параметры и другие shadow declarations не подменяются внешним сигналом. +- Каждое вычисляемое подключение сохраняет зависимость от целевого модуля. Это позволяет обновить результат после изменения заголовка, даже когда прежние ID портов не изменились. +- Sync заменяет всю группу производных endpoints; surviving actual edges не остаются после удаления formal. Failed bindings восстанавливаются после удаления дублирующего модуля, включая deletion-only sync. + +Это синтаксическое соответствие имён. Width compatibility, elaboration, generate expansion, timing/CDC не вычисляются. + +## Проверки + +Постоянные focused tests: порядок заголовков, пустые позиции/комментарии, +unknown headers, explicit precedence, escaped names, genvar/parameter shadows, +восстановление после удаления duplicate, удаление последнего модуля и старых +actual endpoints. Добавлены 6 parser regression cases для grammar 0.4.0. + +Реальные копии: Dual UART 11 файлов/86 named; eMMC reader 33/560 named; +AXI 104 файла, 3411 named, 32 positional, 14 wildcard clauses. +В AXI получены 28 formal/actual wildcard pairs: например +axi_synth_bench::generate@28:33::s::* связывает synth_slice::clk_i с +axi_synth_bench::clk_i и аналогично rst_ni. Для дополнительного positive control +создана отдельная копия Dual UART: u_rx1 переписан из named в 6 positional +аргументов с комментариями. Port[2] сохранил uart_rx::rx и top::rx1. +Исходные RTL-проекты не изменялись. + +Полный native: 288 test files, 4796 passed, 9 skipped, 0 failed. +Полный WASM: 287 test files passed, 1 skipped; 4786 passed, 19 skipped, 0 failed. +Все 10 активных индексов пересобраны до stamp 35: complete, 0 pending changes. + +Build копирует точный проверенный WASM: source/dist SHA256 совпадают. + +Артефакты: .scratch/grammar-v040/manifest.json, compare.json, +extraction-deltas.json, .scratch/hdl-expanded-real-results.json и +hdl-expanded-final-*.log. Полные RTL/scratch snapshots вне Git. diff --git a/docs/validation-index-fast-path-2026-09-12.md b/docs/validation-index-fast-path-2026-09-12.md new file mode 100644 index 000000000..327ba6f7f --- /dev/null +++ b/docs/validation-index-fast-path-2026-09-12.md @@ -0,0 +1,100 @@ +# Пересмотр проверки актуальности индекса — 12 сентября 2026 + +## Решение и источник + +В checkout `/home/danik/storage/codegraph-fix-20260911` поверх `086e577b` +проверка полного inventory из #1843 заменена подходом #1848 +(`496f1f4eef7501e737033452fe74526eee543325`). + +- Обсуждение: https://github.com/colbymchenry/codegraph/pull/1843 +- Альтернатива: https://github.com/colbymchenry/codegraph/pull/1848 + +На момент проверки #1848 открыт. Автор #1843 закрыл свой PR в пользу +альтернативы; это не подтверждение слияния или решения владельца upstream. + +Индекс хранит `indexed_at_commit`. Проверка объединяет рабочие изменения +Git с diff между сохранённым коммитом и HEAD и хеширует только кандидатов. +Если отметка отсутствует или недоступна, используется полный inventory. +Полный sync обновляет отметку; частичный sync её сохраняет. +Сам filesystem reconcile внутри sync сохраняется. + +## Дополнения к #1848 + +- HEAD для полной индексации фиксируется до начала работы, чтобы коммит во + время индексации не считался автоматически обработанным. +- Ошибка committed diff переводит проверку на полный inventory. +- NUL-разделители committed diff сохраняют имена с кириллицей и табуляцией. +- Удалённый коммитом, но восстановленный на диске файл проверяется по + текущему содержимому; его символы не удаляются из-за устаревшего статуса D. +- Fallback сохраняет проверку существования файла для unstaged удаления. + +Исправления extraction/store и CLI guard из предыдущей сборки сохранены. + +## Проверки + +Linux x64, Node 24.15.0. + +- WASM (`CODEGRAPH_KERNEL=0`): 73 passed, 4 файла — sync, + include-ignored-config, git-changed-untracked-dir, extraction-old-git. +- Native (`CODEGRAPH_KERNEL_EXPECT=1`): тот же набор, 73 passed. +- Native: cli-sync-stale-index и object-literal-methods, 15 passed. +- `npx tsc --noEmit`: успешно. +- `npm run build`: успешно, viewer и 29 WASM-грамматик проверены. +- `git diff --check`: успешно. + +Добавлены девять тестов upstream и пять дополнительных регрессий: частичный +sync, восстановление удалённого файла, Unicode, fallback удаления и коммит +во время полной индексации. Старые тесты committed rename/no-op сохранены. + +Полный suite, Windows/macOS не запускались. Предыдущие результаты полного +suite относятся к `086e577b`, а не к этим изменениям. + +## Локальное сравнение скорости + +Синтетический Git-репозиторий: 2000 TypeScript-файлов, около 8.87 MB. +Два прогрева, 10 измерений каждого варианта, порядок чередуется. +Сравнивались метод из `086e577b` и новая сборка; одинаковые записи хешей +в памяти, без SQLite и без построения графа. Каждый набор изменений +проверялся assertion. В таблице верхняя медиана десяти измерений. + +| Состояние | Полный inventory, мс | Git + committed diff, мс | +|---|---:|---:| +| Чистое | 57.60 | 19.88 | +| Незакоммиченная правка | 56.64 | 20.09 | +| Та же правка после commit | 57.32 | 24.24 | + +Это тёплый in-process microbenchmark, не end-to-end CLI и не реальный +крупный проект. Результаты и скрипт: `.scratch/freshness-benchmark.json` +и `.scratch/freshness-bench.cjs` (локальные, исключены из VCS). + +Ограничение подхода upstream: изменения, скрытые через `assume-unchanged`, +могут отсутствовать в Git-кандидатах; полного сканирования на каждом status +больше нет. Для полного обновления остаётся полная индексация. + +## Состояние доставки + +Исходники и локальная сборка обновлены. Команда на PATH уже ссылается на +`dist/bin/codegraph.js` этого checkout. Существующие MCP-процессы не +перезапускались. Эта редакция подготовлена для публикации в ветке `fix/call-evidence-index-freshness` и обновления draft PR #1845; SHA публикации проверяется отдельно по remote. + +## Последующие исправления в той же локальной редакции + +- Перенесён quiet stderr из #1842 (`cb8dbdc3`): одинарная строка объяснения, + пустой stdout, exit 1 при старом или отсутствующем extraction stamp. +- Перенесён атомарный rebind #1849 (`e4c931a7`): удаление edges и запись + unresolved_refs объединены транзакцией. Тест с SQL-trigger RAISE(ABORT) + подтверждает сохранение исходной связи после неудачной записи. +- Исправлен repro #1840: локальный receiver, полученный через await простого + вызова, использует явно объявленный Promise разрешённой функции. + Примитивный тип не допускает fallback к постороннему методу; настоящий + Promise и new PaneManager сохраняют вызов split. + Это ограниченная инференция явной сигнатуры, не полный TypeScript type checker. +- Extraction stamp повышен с 27 до 28; уже построенным индексам нужен полный + rebuild. Автоматическую переиндексацию пользовательских проектов не запускали. + +Тест #1840 перед исправлением: 1 failed / 1 passed; после — 2 passed. +WASM: 292 passed / 6 файлов, включая resolution и sync-rebuild-convergence. +Native: 299 passed / 7 файлов, тот же набор плюс cli-sync-stale-index. +Сборка viewer/engine и TypeScript успешны. Реальный probe на dist показывает +только snapshot → listPaths; связи snapshot → PaneManager.split больше нет. +Полный suite и Windows/macOS не запускались. diff --git a/docs/validation-real-project-issues-2026-09-12.md b/docs/validation-real-project-issues-2026-09-12.md new file mode 100644 index 000000000..168e98de5 --- /dev/null +++ b/docs/validation-real-project-issues-2026-09-12.md @@ -0,0 +1,73 @@ +# Исправления по тестам на реальных проектах — 12 сентября 2026 + +База: `97de99bc`, Linux x64 / Node 24.15.0. Extraction stamp: 29. + +## Исправлено + +- Name matching для calls ограничен семейством языка. Межъязыковые связи + требуют отдельного bridge-resolver, а не совпадения имени. Method fallback + применяет ту же проверку. C/C++ enum_member не является целью вызова; + Rust enum constructors и Kotlin SAM-construction этим фильтром не запрещены. +- SyncResult сообщает skippedReason=locked при занятой блокировке. Watcher + сохраняет прежнюю возможность повторить sync; CLI возвращает exit 1 и + объяснение, включая одну строку stderr в quiet-режиме. +- QueryPool удаляет worker с ready(ok:false), ограниченно пытается заменить + его и завершает ожидающие запросы при исчерпании crash budget. Запрос к + неисправному pool больше не остаётся ждать worker бесконечно. +- Числовой legacy daemon.pid распознаётся. Живой PID без socket identity + нельзя удалить даже через allowLivePid; обычные структурированные записи + сохраняют механизм проверки PID reuse. + +## Тесты + +- Полный native-suite: **4655 passed, 4 failed, 9 skipped**, 270 файлов. + Все 4 отказа — kernel-dart-parity (torture.dart / TortureCtors.dart, + LF и CRLF), ранее воспроизведённые на upstream. Полный suite не зелёный; + новых отказов в этой редакции нет. +- Профильный WASM: **307 passed**, 7 файлов: resolution, query-pool, + real-project-regressions, sync, CLI stale/locked sync, awaited receiver, + sync-rebuild-convergence. +- TypeScript, build (viewer + 29 WASM-грамматик), diff whitespace: успешно. +- Windows/macOS не запускались. + +## Реальные проекты: контролируемый A/B + +Копии размещены в `/home/danik/storage/codegraph-real-issues-20260912`. +ADRC: 2069 исходных индексируемых файлов; CSQTT desktop: 51. +SQLite-копии получены backup API, исходные базы не использовались для fault +injection. A/B каждый раз создаёт БД заново на одном наборе исходников. +Baseline — name-matcher из 97de99bc, после — текущая реализация; остальные +модули одинаковы. Baseline выполняется без параллельного resolver, чтобы +worker не подменил проверяемую реализацию. Это resolver A/B, не полный +benchmark двух исторических сборок. + +| Проект | Уникальных calls до | После | Удалено | Добавлено | +|---|---:|---:|---:|---:| +| ADRC | 13923 | 13794 | 129 | 0 | +| CSQTT desktop | 381 | 380 | 1 | 0 | + +Все 129 удалённых ADRC-связей в сравнении — цели enum_member (MIN/MAX). +В прежнем аудите было 167 строк edges: здесь сравниваются уникальные +(source file/name, target file/name/kind), а не строки или call sites. +Другие callable-связи в контролируемом A/B не исчезли. +CSQTT потерял только ложную связь Go verifyUpdateCleanup → TS interface Size. + +На копии CSQTT: + +- Занятая блокировка, одна неиндексированная правка: exit 1, сообщение busy/lock, + pending.modified=1 сохраняется. Ложного Already up to date нет. +- QueryPool control: три успешных explore-запроса. +- Fault «БД не открывается»: healthy=false, ready=false, liveWorkers=0, + ожидающие запросы завершаются; все созданные worker threads остановлены. +- Legacy PID: тестовый живой PID декодируется и lockfile сохраняется. + +## Артефакты воспроизведения + +Локальные `.scratch/real-fix-compare.py`, `real-ab-index.cjs`, +`real-fix-compare.json`, `real-lock-probe.cjs`, `real-lock-results.json`, +`real-pool-probe.cjs`, `real-pool-results.json`, `real-fix-full-native.log`, +`real-fix-wasm-final.log`. Регрессионные тесты включены в исходники. + +Ограничение: это закрывает подтверждённые сценарии, а не все открытые issues +CodeGraph. В частности, отдельные macro-to-function коллизии, MCP projectPath +watch lifecycle и произвольная межъязыковая инференция здесь не реализованы. diff --git a/docs/validation-remaining-defects-2026-09-12.md b/docs/validation-remaining-defects-2026-09-12.md new file mode 100644 index 000000000..ff8de7c49 --- /dev/null +++ b/docs/validation-remaining-defects-2026-09-12.md @@ -0,0 +1,127 @@ +# Оставшиеся дефекты: исправления и реальная проверка — 12 сентября 2026 + +База работы: bc4a057d, Linux x64 / Node 24.15.0. Extraction stamp 31. +Работа разделена на C/C++, IDs/Dart и MCP lifecycle; callback и интеграция +проверялись отдельно. Новые дефекты, найденные в ходе реальных проверок, +закреплены регрессиями до окончательной проверки. + +## Callback и совместимость TS/TSX (#1355) + +- this.handler разрешается по лексическому owner через contains, затем по + extends для наследуемого метода. Одноимённый метод соседнего файла или + другого класса, в том числе на одной строке, не является кандидатом. +- Registrar/dispatcher с одноимённым полем связываются только внутри своего + класса. Сохранена регистрация this.handler.bind(this). +- Типизированный method lookup использует семейство языка: TSX/JSX вызов + метода TS-класса не отбрасывается из-за разных extension tags. + +Реальный Excalidraw afa3a653fc5d2b742adcbd5a6063187b056d2419: неизменённые +App.tsx, Scene.ts, barrel index.ts и tsconfig.json скопированы в storage. +App::componentDidMount → Scene::onUpdate (строка3818), Scene::triggerUpdate → +единственный App::triggerRender (строка5392). Добавление отдельного +искусственного Decoy.triggerRender не меняет target. Native/WASM результаты +совпадают; SHA исходников сохранены в real-excalidraw-callback-native.json. +Это настоящий observer path на выбранных файлах, не весь браузерный runtime. + +## C/C++ (#1838, #1839, #1373) + +- Function-like #define представлены невызываемыми constant nodes. Numeric/ + object-like defines не добавлены: они увеличивали ADRC-граф на ~57k узлов + без пользы для исправления вызовов. +- Макросы проверяются до name/import resolution. Учитываются порядок + define/undef, quoted sibling headers, include-root и однозначный suffix + fallback. Macro constant никогда не становится target обычного calls. +- Видимость вычисляется по завершённому обходу translation unit, а не по + зависимому от порядка обхода child-cache. Cache ограничен 128 root timelines. +- Локально решаемые if/ifdef/ifndef/elif/else и defined/числовые флаги + исключают неактивные ветки. Неизвестные build guards остаются возможными; + факты внутри одной hypothetical ветки сохраняют корреляцию. Внешние flags, + произвольные арифметические выражения и полный preprocessing не реализованы. +- Default/direct/brace construction дают calls к constructor method отдельно + от instantiates. Extern, pointer/reference, prototype и aggregate controls + не создают вызов произвольного конструктора. +- Одноаргументные объявления функций через макрос получают реальное имя + при подтверждённой форме определения, а не угадываются по любому аргументу. + +ADRC extraction native/WASM: 717 совпадений на 881 исходном файле src/main, +164 штатных parse-error deferrals в WASM, 0 diff. Это не означает, что каждый +из 881 файлов разобран native. Полная пересборка изолированной копии проверяет +отдельно ложные macro calls и 15 сохранённых настоящих вызовов, включая sin_approx. + +## IDs и Dart (#1349 и прежние parity failures) + +TS/JS/TSX/JSX IDs учитывают UTF-16 колонку: getter/setter одной строки сохраняют +разные строки SQLite и правильные исходящие вызовы. Формат IDs остальных +языков не менялся; это исправление подтверждённого TS/JS-сценария, не заявление +об отсутствии любых возможных коллизий в остальных языках. + +Dart extension_type_declaration распознаётся контейнером в native и WASM; +getter и method имеют корректного владельца и границы тела. Четыре старых +Dart parity failures устранены без удаления их assertions. + +Реальные проверки: 83 CodeGraph extraction/MCP/UI-файла — byte parity 83/83, +без deferral; dart-lang/shelf f36dd68654814e0498c30b9f7ccc4bb0aa2bc118 — +89/99 byte parity, 10 штатных deferrals, 0 diff. В shelf нет extension types: +они проверены отдельными fixtures, включая прежние LF/CRLF failures. + +## MCP projectPath и cleanup (#1835, #1782) + +- Explicit projectPath получает один catch-up gate, watcher и writer ownership. + Несколько запросов разделяют первоначальную синхронизацию; no-watch сохранён. + Новые индексы автоматически не создаются. +- Если root обслуживает другой writer, текущий engine остаётся читателем. + После ухода владельца следующий explicit-запрос берёт ownership и запускает + catch-up/watch. Фоновый takeover без следующего запроса не реализован. +- closeAsync прекращает watch и дожидается текущих записей. Daemon/direct/proxy + ожидают engine.stop. Поздний запрос не открывает повторно закрываемый cache. +- mcp-writer-lock test ждёт proxies, останавливает подтверждённый daemon своего + temp-проекта и проверяет удаление каталога без подавления ошибок cleanup. + +Один реальный stdio JSON-RPC MCP обслужил копии CSQTT desktop (51файл) и bot +(30файлов): catch-up до ответа, последующие правки обоих проектов дают +SHA256(source)==stored hash. Завершение exit0, writer locks удалены, shutdown +sync errors отсутствуют. Проверены также два engine в разных OS-процессах +с передачей writer ownership после следующего запроса. + +## Артефакты + +Локальные .scratch/cpp-real-adrc-results.json, cpp-real-adrc-validation.txt, +cpp-remaining-validation-20260912.md, cpp-adrc-parity-20260912.txt, +ids-real-ts-parity.log, dart-shelf-parity.log, real-excalidraw-callback*.json, +real-project-lifecycle-mcp-results.json и их воспроизводимые scripts. +Регрессионные tests включены в исходники. Windows/macOS не проверялись. + +Verilog остаётся структурным графом; полный elaboration/timing — отдельная +интеграция toolchain и не добавлен в этот пакет исправлений. + +## Финальная проверка + +Полный native-suite на замороженных исходниках: **4715 passed, 9 skipped**, +**278 test files passed**, 0 failed. Прежние четыре Dart parity failures +устранены. Команда: `CODEGRAPH_KERNEL_EXPECT=1 npm test -- --maxWorkers 2 --minWorkers 1`. + +Финальная ADRC-копия после recreate: 2021файл; 0 проверенных ложных macro calls; +15/15 настоящих контрольных calls сохранены; 32811nodes,70930edges,22663calls. +Полная длительность9.217с, resolver6.311с. 0filesErrored; 7 прежних parse warnings +в config headers. Это не обещание полного разбора всех условных C-конфигураций. + +Ошибочные промежуточные варианты (все numeric defines; child-cache при cyclic +includes; потеря локального FAST_MATH в unknown arm) не опубликованы. Они +выявлены реальными controls/review и закреплены дополнительными тестами. + +Native-only deep-nesting и raw-kernel transport tests теперь учитывают +CODEGRAPH_KERNEL=0. В WASM-arm они не могут требовать выключенный kernel; +в native-arm отдельно повторены: 10/10 passed. Это исправление условий +тестовой матрицы, а не удаление native-проверок. + +Полный WASM-suite: **4705 passed, 19 skipped, 0 failed**; 277 test files passed, +1 native-only suite skipped. Команда: `CODEGRAPH_KERNEL=0 npm test -- --maxWorkers 2 --minWorkers 1`. +Native-only suites дополнительно проверены 10/10 в native-режиме. +TypeScript, build viewer/30 грамматик и diff whitespace прошли. +Все 10 активных индексов пересозданы с extraction31; появившиеся во время +работы три изменения CSQTT desktop дополнительно поглощены sync. + +SHA256 собранного native kernel: +`a8acf04245b805c136a777ab8f8bd065c100bb2a7dd7b9bfd2915a45e241ca1b`. +Существующие чужие MCP-процессы не перезапускались; новый runtime загружается +после перезапуска соответствующей сессии. diff --git a/docs/validation-review-fixes-2026-09-12.md b/docs/validation-review-fixes-2026-09-12.md new file mode 100644 index 000000000..b025248c9 --- /dev/null +++ b/docs/validation-review-fixes-2026-09-12.md @@ -0,0 +1,65 @@ +# Исправления findings ревью 1a11de26 — 12 сентября 2026 + +База: 1a11de26, Linux x64 / Node 24.15.0. Extraction stamp 32. + +## Закрытые случаи + +1. C++ pointer/reference/function-pointer initialization не считается созданием + объекта. Проверяется вложенный declarator, а не только initializer list. +2. Constructor refs локальных объектов имеют формат Type::Type/N. Resolver + сначала ищет тип в лексическом namespace, затем выбирает единственный + constructor с совместимой арностью. Global/explicit namespace сохранены. + Same-arity overloads и initializer_list не угадываются; полного overload + resolution по типам аргументов нет. +3. C++ IDs учитывают UTF-16 колонку: constructors одной строки не перезаписывают + друг друга; тела и useDefault/useInt ссылаются на разные правильные IDs. + C и остальные ранее неизменённые языки сохраняют прежний формат IDs. +4. Macro recovery использует AST настоящего предшествующего #define; текст + комментария и noexcept(name()) не становятся доказательством имени функции. + Constructor не переименовывается как macro-generated function, включая + сценарий define/undef. Аргументы макроса не выдаются за параметры функции: + неподтверждённая signature у get_version отсутствует. +5. Macro visibility различает отсутствующий entry и existing unknown: + условный undef больше не превращает possible macro в definite absence. +6. Default/cache/lifecycle/catch-up используют canonical root. Symlink и real + path разделяют экземпляр, watcher и gate; позднее назначение default не + закрывает ранее открытый explicit-проект и его pending catch-up. + +## Подтверждение результатов + +Исходные review-примеры сохранены отдельно от результатов исправления. + +- 7 C++ fixtures компилируются как C++17; fixed native/WASM JSON совпадают. + Pointer/reference дают 0 ctor calls; default/int выбирают правильные defs; + namespace second не связывается с first; same-line constructors имеют разные + IDs; macro-comment сохраняет NATIVE_FN и настоящий invoke → NATIVE_FN. +- ADRC recreate: 2021файл, 0 проверенных ложных macro calls, 15/15 настоящих + контрольных calls сохранены. 32811nodes,70888edges,22663calls; 9.185с. + 0filesErrored, 7 прежних parse warnings. Это проверка графа, не firmware runtime. +- Alias probe: ровно1watcher; второй запрос не выходит до снятия barrier первого + catch-up; оба ответа содержат added.ts. Сохраняется общий timeout gate, + настройка бесконечного ожидания — CODEGRAPH_CATCHUP_GATE_TIMEOUT_MS=0. +- Реальный stdio MCP на копиях CSQTT (54файла) и bot (30файлов): source hashes + совпадают с SQLite после catch-up и live updates; exit0 и cleanup PASS. +- Repro unknown define/undef: possible macro=true, persisted calls=[]; + cc -E с ENABLE_TRACE=1,CLEAR_TRACE=0 подтверждает отсутствие вызова TRACE. + +## Финальная матрица + +- Native: 280 test files passed; 4729 passed, 9 skipped, 0 failed. +- WASM: 279 test files passed, 1 skipped; 4719 passed, 19 skipped, 0 failed. +- TypeScript и полная сборка с viewer и 30 grammar assets прошли. +- Прогоны выполнены последовательно с двумя workers и --liftoff-only. + +## Границы + +Полный C/C++ preprocessor и произвольные macro replacements не реализованы. +Выбор local constructors намеренно консервативен при неоднозначности. Полный +HDL elaboration/timing не добавлен. Windows/macOS не проверялись. + +## Артефакты + +.scratch/cpp-review-fixed-native.json, cpp-review-fixed-wasm.json, +cpp-real-adrc-results.json, fixed-lifecycle-alias-gate.cjs, +fixed-lifecycle-alias.cjs, real-project-lifecycle-mcp-results.json, +review-uncertain-undef.cjs, review-final-matrix.json и review-final-*.log. diff --git a/docs/validation-verilog-mcp-2026-09-12.md b/docs/validation-verilog-mcp-2026-09-12.md new file mode 100644 index 000000000..1d1c08a8a --- /dev/null +++ b/docs/validation-verilog-mcp-2026-09-12.md @@ -0,0 +1,70 @@ +# Verilog/SystemVerilog и общие исправления — 12 сентября 2026 + +## Восстановленная поддержка HDL + +Ветка, установленная после 11 сентября, не содержала Verilog, хотя предыдущая +интеграционная сборка его поддерживала. Перенесены extractor и grammar из +`eval/402-verilog` (`875880c`), включая include/define и simulation-stub +disambiguation. Первоначальный автор extractor — FHYQ-Dong, upstream PR #402 +(`26610932a9900d98df8802363581274b62b67262`); локальный порт — `e380bf7`, +дополнение include/define — `a87170a`. + +- Расширения: .v, .vh, .sv, .svh; используется tree-sitter-systemverilog 0.3.1. +- Модули/interfaces/packages, functions/tasks, constants/typedefs, imports/includes. +- Иерархия модулей через instantiates доступна в callers/callees и named flow. +- Flow следует instantiates только для Verilog; обычные new X() не превращаются + из-за этого в цепочки исполнения TypeScript. +- Порты/внутренние сигналы, timing и полный elaboration не моделируются. + Выбор по synthesis/simulation пути — эвристика, не замена filelist FPGA-tools. + +Грамматика: ABI 15, 20 повторных parse, 0 ошибок. SHA256: +`b07c23a12884f3bad0043ed77d246a7a3b04da70fc74d9193b67166481487801`. +WASM включён в version control и копируется в dist при build (30 грамматик). +Verilog разбирается WASM-путём, отдельного Rust extractor для него нет. + +## Реальные корпуса + +Использованы копии RTL/testbench-файлов наших проектов без дампов и bitstream, +а также clone pulp-platform/axi на `da8793b0e3f14c9186c94fac9ed6ec06375c254e`. + +| Корпус | HDL-файлы | Контейнеры class | Interfaces | Instantiates | Проверенный путь | +|---|---:|---:|---:|---:|---| +| 2uart_to_1uart | 11 | 11 | 0 | 11 | top → uart_rx | +| fpga_tang_nano_9k_card_reader | 33 | 32 | 0 | 34 | top → uart_bridge → uart_rx | +| pulp-platform/axi | 93 | 176 | 7 | 276 | axi_xbar → axi_xbar_unmuxed → axi_demux | + +AXI: всего проиндексировано 104 файла, из них 93 HDL; 587 typedef и 186 import. +Это новые детерминированные проверки extraction/retrieval. Старые agent A/B +из PR не переиспользуются как текущие. Нового платного agent A/B и timing/ +синтеза/прошивки FPGA не выполнялось. + +Артефакты: `.scratch/verilog-corpus.cjs`, `verilog-corpus-results.json`, +`verilog-corpus-final.log`; копии в `~/storage/codegraph-real-issues-20260912/`. + +## Общие исправления + +- #1356: закрывшийся во время hello socket не получает новую MCP session. + Проверяется реальным TCP connect → server hello → disconnect; clients=0. +- #1777: resolution tests сбрасывают ссылку cg перед тестом и удаляют tempDir + в finally после закрытия графа, а не только в альтернативной ветке else. +- #1832: prompt-hook игнорирует целый envelope task-notification до поиска + по проекту. Упоминание envelope в пользовательском вопросе не подавляется. + Реальный CLI на CodeGraph: notification — exit 0 / 0 bytes; обычный + structural prompt — exit 0 / 9223 bytes. + +Extraction stamp повышен до 30: ранее существовавшие HDL-файлы требуют rebuild. + +## Особенность тестового runtime + +Первый совместный запуск двух тяжёлых наборов без --liftoff-only завершился +V8 Fatal process out of memory: Zone; full native также имел таймауты под +нагрузкой. Эти прогоны не считаются успешными. npm test теперь запускает Vitest +с --liftoff-only, как рабочий CLI: это отключает дорогостоящую оптимизацию +тяжёлой WASM-грамматики. Контролируемый повтор выполняется последовательно, +с двумя workers. + +Итог controlled native: **4672 passed, 4 прежних Dart parity failures, +9 skipped**, 272 файла. Новых отказов нет. Профильный WASM с одним worker: +**945 passed**, 6 файлов (extraction, resolution, Verilog flow/resolution, +frontload-hook, daemon-client-liveness). TypeScript, build и whitespace +проверены. Windows/macOS не запускались. diff --git a/package.json b/package.json index d5a742420..6398acc0d 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,11 @@ "build:ui": "npm run build --workspace ui && node scripts/check-ui-build.mjs", "build:lib": "npm run build:lib --workspace ui", "preuninstall": "node dist/bin/uninstall.js", - "copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f))\"", + "copy-assets": "node -e \"const fs=require('fs');fs.mkdirSync('dist/db',{recursive:true});fs.copyFileSync('src/db/schema.sql','dist/db/schema.sql');fs.mkdirSync('dist/extraction/wasm',{recursive:true});fs.readdirSync('src/extraction/wasm').filter(f=>f.endsWith('.wasm')).forEach(f=>fs.copyFileSync('src/extraction/wasm/'+f,'dist/extraction/wasm/'+f));fs.mkdirSync('dist/hdl',{recursive:true});fs.copyFileSync('src/hdl/source-map-export.py','dist/hdl/source-map-export.py')\"", "dev": "tsc --watch", "build:kernel": "bash scripts/build-kernel.sh", "cli": "npm run build && node dist/bin/codegraph.js", - "test": "vitest run", + "test": "node --liftoff-only node_modules/vitest/vitest.mjs run", "test:watch": "vitest", "test:eval": "vitest run __tests__/evaluation/", "eval": "npm run build && npx tsx __tests__/evaluation/runner.ts", diff --git a/scripts/hdl-axi-build-check.py b/scripts/hdl-axi-build-check.py new file mode 100644 index 000000000..2a77af77c --- /dev/null +++ b/scripts/hdl-axi-build-check.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +"""Проверить pinned AXI dependencies и compiler controls без изменения RTL.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess +import time + +AXI_REV = 'da8793b0e3f14c9186c94fac9ed6ec06375c254e' +DEPENDENCIES = { + 'common_cells': '03d98106aa19952a10360d2230def85144a0008b', + 'common_verification': 'fb1885f48ea46164a10568aeff51884389f67ae3', + 'tech_cells_generic': '3a3de73632a06826b1bd9c65a0a2e92b32016845', +} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--axi', type=Path, required=True, help='Изолированный чистый checkout pinned AXI') + parser.add_argument('--bender', required=True) + parser.add_argument('--verilator', required=True) + parser.add_argument('--output', type=Path, required=True, help='Каталог логов вне source Git') + args = parser.parse_args() + root = args.axi.resolve() + out = args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + + def run(argv, timeout=180): + return subprocess.run(argv, cwd=root, capture_output=True, text=True, timeout=timeout) + + def git(*argv): + result = run(['git', *argv]) + if result.returncode: + raise RuntimeError(result.stderr) + return result.stdout.strip() + + if git('rev-parse', 'HEAD') != AXI_REV or git('status', '--porcelain', '--untracked-files=no'): + raise RuntimeError('Нужен чистый AXI checkout на зафиксированной revision') + # Hash tracked inputs before/after Bender and all compiler controls. + def snapshot(): + return {name: hashlib.sha256((root / name).read_bytes()).hexdigest() + for name in git('ls-files').splitlines() if (root / name).is_file()} + + if (root / 'Bender.local').exists(): + raise RuntimeError('Bender.local overrides are outside this pinned control') + initial = snapshot() + versions = {} + for name, executable in [('bender', args.bender), ('verilator', args.verilator)]: + result = run([executable, '--version']) + if result.returncode: + raise RuntimeError(result.stderr) + versions[name] = result.stdout.strip() + checkout = run([args.bender, '--dir', str(root), 'checkout']) + (out / 'checkout.log').write_text(checkout.stdout + checkout.stderr) + if checkout.returncode or snapshot() != initial: + raise RuntimeError('Checkout failed or modified tracked AXI inputs / lockfile') + deps = {} + for name, revision in DEPENDENCIES.items(): + candidates = list((root / '.bender/git/checkouts').glob(name + '-*')) + found = [] + for candidate in candidates: + result = run(['git', '-C', str(candidate), 'rev-parse', 'HEAD']) + if result.returncode == 0 and result.stdout.strip() == revision: + found.append(candidate) + if len(found) != 1: + raise RuntimeError(f'Expected one locked checkout for {name}@{revision}') + candidate = found[0] + if git('-C', str(candidate), 'status', '--porcelain', '--untracked-files=no'): + raise RuntimeError(f'Modified dependency: {name}') + deps[name] = {'revision': revision, 'path': str(candidate.relative_to(root))} + + generated = run([args.bender, '--dir', str(root), 'script', 'verilator', '-t', 'synth_test']) + if generated.returncode: + raise RuntimeError(generated.stderr) + for line in generated.stdout.splitlines(): + line = line.strip() + if not line or line.startswith('//') or line.startswith('+define+'): + continue + candidate = Path(line.removeprefix('+incdir+')) + if not candidate.is_absolute() or not candidate.resolve().is_relative_to(root): + raise RuntimeError('Generated filelist escapes the pinned AXI checkout: ' + line) + filelist = out / 'verilator.f' + filelist.write_text(generated.stdout) + portable = generated.stdout.replace(str(root), '${AXI}') + (out / 'verilator.portable.f').write_text(portable) + cases = [] + + def control(name, argv, expect_success=True): + start = time.monotonic() + result = run(argv) + (out / (name + '.stdout')).write_text(result.stdout) + (out / (name + '.stderr')).write_text(result.stderr) + warnings = re.findall(r'^%Warning-([^:]+):', result.stderr, re.M) + row = {'name': name, 'argv': [a.replace(str(root), '${AXI}').replace(str(out), '${OUTPUT}') + .replace(args.verilator, '${VERILATOR}') for a in argv], + 'exit': result.returncode, 'seconds': round(time.monotonic() - start, 3), + 'warnings': {w: warnings.count(w) for w in sorted(set(warnings))}, + 'errors': len(re.findall(r'^%Error', result.stderr, re.M))} + cases.append(row) + if (result.returncode == 0) != expect_success: + raise RuntimeError(f'{name}: unexpected exit {result.returncode}; see logs in {out}') + return result + + demux = 'src/axi_demux_simple.sv' + missing = control('demux-missing-include-control', [args.verilator, '-E', '-P', '-Iinclude', demux], False) + if 'Cannot find include file' not in missing.stderr or 'common_cells/assertions.svh' not in missing.stderr: + raise RuntimeError('Negative control failed for a reason other than missing dependency') + cc_include = str(root / deps['common_cells']['path'] / 'include') + for variant, defines in [('default', []), ('VCS', ['+define+VCS'])]: + selected = control('demux-preprocess-' + variant, + [args.verilator, '-E', '-P', '-Iinclude', '-I' + cc_include, *defines, demux]) + if 'module axi_demux_simple' not in selected.stdout or '`FF' in selected.stdout: + raise RuntimeError('Expected original module and expanded register macros') + for count in [1, 2, 4]: + control('xbar-lint-' + str(count), [args.verilator, '--lint-only', '--assert', '-Wno-fatal', + '--top-module', 'synth_axi_lite_xbar', '-GNoSlvMst=' + str(count), + '--Mdir', str(out / ('obj-' + str(count))), '-f', str(filelist)]) + if snapshot() != initial: + raise RuntimeError('Compiler controls changed tracked AXI files') + report = {'axiRevision': AXI_REV, 'benderLockSha256': initial['Bender.lock'], + 'tools': versions, 'dependencies': deps, + 'profile': {'target': 'synth_test', 'top': 'synth_axi_lite_xbar', + 'parameterValues': {'NoSlvMst': [1, 2, 4]}, + 'defines': [line for line in portable.splitlines() if line.startswith('+define+')], + 'includeDirs': [line for line in portable.splitlines() if line.startswith('+incdir+')], + 'filelistSha256': hashlib.sha256(portable.encode()).hexdigest()}, + 'cases': cases, 'trackedInputCount': len(initial), 'trackedInputsUnchanged': True, + 'limitations': ['Warnings retained with -Wno-fatal; not a warning-free build', + 'Lint/elaboration controls only, no simulation, synthesis or FPGA validation', + 'Not the complete axi_synth_bench and not all tops']} + (out / 'report.json').write_text(json.dumps(report, indent=2) + '\n') + print(json.dumps({'output': str(out), 'cases': len(cases), 'trackedInputsUnchanged': True})) + + +if __name__ == '__main__': + main() diff --git a/scripts/hdl-corpus-manifest.py b/scripts/hdl-corpus-manifest.py new file mode 100644 index 000000000..0fb17e9e1 --- /dev/null +++ b/scripts/hdl-corpus-manifest.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""Создать/проверить переносимый manifest HDL-корпусов без копирования исходников. + +Пример: generate --corpus-root /data/corpora --origin dual-uart=/repo/uart +--origin card-reader=/repo/reader --output docs/hdl-corpus-manifest.json +Проверка: check --corpus-root /data/corpora --manifest docs/hdl-corpus-manifest.json +Требуется только Python stdlib и локальный Git для доказательства revision. +Ни HDL, ни build recipes не исполняются; check не меняет corpus/index. +""" +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import xml.etree.ElementTree as ET +from pathlib import Path +from urllib.parse import urlsplit, urlunsplit + +IDS = ('dual-uart', 'card-reader', 'axi') +EXTENSIONS = {'.v', '.sv', '.vh', '.svh'} +EXCLUDE = {'.git', '.jj', '.codegraph', '.scratch', 'node_modules', 'target'} +BUILD_FILES = ('build.tcl', 'fpga_primer.gprj', 'card_reader.gprj', 'tb/Makefile', + 'sim/Makefile', 'Bender.yml', 'Bender.lock', 'Makefile', 'src_files.yml', 'axi.core') + + +def digest(data): + return hashlib.sha256(data).hexdigest() + + +def stable(value): + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':')).encode() + + +def inventory(root): + result = [] + candidates = [] + for directory, subdirectories, filenames in os.walk(root): + subdirectories[:] = sorted(d for d in subdirectories if d not in EXCLUDE and not d.startswith('.codegraph-')) + candidates.extend(Path(directory) / name for name in filenames if Path(name).suffix.lower() in EXTENSIONS) + for file in sorted(candidates): + relative = file.relative_to(root) + if not file.is_file(): + continue + if not file.resolve().is_relative_to(root.resolve()): + raise ValueError('HDL symlink выходит из corpus: ' + relative.as_posix()) + data = file.read_bytes() + result.append({'path': relative.as_posix(), 'sha256': digest(data), 'bytes': len(data)}) + return result + + +def git(root, *args, binary=False): + result = subprocess.run(['git', '-C', str(root), *args], capture_output=True, check=False) + return result.stdout if binary and result.returncode == 0 else ( + result.stdout.decode().strip() if result.returncode == 0 else None) + + +def portable_remote(value): + if not value: + return None + match = re.fullmatch(r'git@github\.com:(.+)', value) + if match: + return 'https://github.com/' + match[1] + parsed = urlsplit(value) + if parsed.scheme and parsed.hostname: + host = parsed.hostname + (':' + str(parsed.port) if parsed.port else '') + return urlunsplit((parsed.scheme, host, parsed.path, '', '')) + return value if not value.startswith(('/', '.', '~')) else None + + +def evidence(root, file): + data = (root / file).read_bytes() + return {'path': file, 'sha256': digest(data), 'bytes': len(data)} + + +def uncomment(text): + # Только для поиска literal evidence. Это не препроцессор и не elaboration. + return re.sub(r'/\*[\s\S]*?\*/|//[^\n]*', lambda m: '\n' * m[0].count('\n'), text) + + +def location(text, match): + return text.count('\n', 0, match.start()) + 1 + + +def analyze_sources(root, records): + modules, includes, imports = [], [], [] + for record in records: + file = record['path'] + text = uncomment((root / file).read_text(errors='replace')) + for match in re.finditer(r'\b(module|package)\s+(?:automatic\s+)?([A-Za-z_$][\w$]*)', text): + modules.append({'kind': match[1], 'name': match[2], 'path': file, 'line': location(text, match)}) + for match in re.finditer(r'^\s*`include\s+"([^"\n]+)"', text, re.M): + request = match[1] + candidates = [Path(file).parent / request, Path('include') / request, Path(request)] + resolved = [p.as_posix() for p in candidates if (root / p).is_file()] + includes.append({'path': file, 'line': location(text, match), 'request': request, + 'resolved_candidates': sorted(set(resolved))}) + for match in re.finditer(r'\bimport\s+([A-Za-z_$][\w$]*)::', text): + imports.append({'package': match[1], 'path': file, 'line': location(text, match)}) + packages = {m['name'] for m in modules if m['kind'] == 'package'} + return modules, includes, [i for i in imports if i['package'] not in packages] + + +def profile(name, files, tops, modules, includes=(), defines=None): + declarations = [m for m in modules if m['kind'] == 'module' and m['name'] in tops and m['path'] in files] + if {m['name'] for m in declarations} != set(tops): + raise ValueError('Нет точного source evidence для top: ' + name) + return {'name': name, 'purpose': 'source-graph validation; не compiler/elaboration closure', + 'files': files, 'top_modules': tops, 'top_evidence': declarations, + 'include_dirs': list(includes), 'defines': defines or {}} + + +def corpus_manifest(identifier, root, origin): + records = inventory(root) + if not records: + raise ValueError('Пустой HDL corpus: ' + identifier) + revision = git(origin, 'rev-parse', 'HEAD') + remote = portable_remote(git(origin, 'remote', 'get-url', 'origin')) + mismatched = [] + for record in records: + data = git(origin, 'show', revision + ':' + record['path'], binary=True) if revision else None + if data is None or digest(data) != record['sha256']: + mismatched.append(record['path']) + working_tree_mismatches = [r['path'] for r in records if not (origin / r['path']).is_file() + or digest((origin / r['path']).read_bytes()) != r['sha256']] + license_files = sorted(p.name for p in origin.iterdir() if p.is_file() and p.name.upper().startswith(('LICENSE', 'COPYING', 'UNLICENSE'))) + licenses = [{**evidence(origin, file), 'first_line': (origin / file).read_text(errors='replace').splitlines()[0]} + for file in license_files] + builds = [] + for file in BUILD_FILES: + if not (origin / file).is_file(): + continue + text = (origin / file).read_text(errors='replace') + item = evidence(origin, file) + item['command_evidence'] = [{'line': i, 'text': line.strip()} for i, line in enumerate(text.splitlines(), 1) + if re.search(r'add_file|top_module|\$\((?:IV|IVERILOG)\)|(?:^|\s)-[DIgs](?:\s|\w)', line) + and not line.lstrip().startswith('#')] + if file.endswith('.tcl'): + item['declared_files'] = re.findall(r'^\s*add_file\s+[^\n]*?"([^"]+)"', text, re.M) + if file.endswith('.gprj'): + item['declared_files'] = [n.attrib['path'] for n in ET.fromstring(text).iter('File') if 'path' in n.attrib] + builds.append(item) + for entry in builds + licenses: + blob = git(origin, 'show', revision + ':' + entry['path'], binary=True) if revision else None + entry['matches_revision'] = bool(blob is not None and digest(blob) == entry['sha256']) if revision else None + source_spdx = [] + for record in records: + for line, text in enumerate((root / record['path']).read_text(errors='replace').splitlines(), 1): + if 'SPDX-License-Identifier:' in text: + source_spdx.append({'path':record['path'],'line':line,'identifier':text.split('SPDX-License-Identifier:',1)[1].strip(' /*\t')}) + modules, include_refs, missing_packages = analyze_sources(root, records) + names = [r['path'] for r in records] + if identifier == 'dual-uart': + profiles = [profile('synth-source', [f for f in names if f.startswith('src/')], ['top'], modules), + profile('sim-source', names, ['tb_top'], modules)] + elif identifier == 'card-reader': + profiles = [profile('synth-source', [f for f in names if f.startswith('src/')], ['top'], modules), + profile('sim-integration-source', names, ['tb_top_integration'], modules, ['src'])] + else: + selected = ['src/axi_pkg.sv', 'src/axi_interleaved_xbar.sv'] + profiles = [profile('normal-source', selected, ['axi_interleaved_xbar'], modules, ['include']), + profile('vcs-source', selected, ['axi_interleaved_xbar'], modules, ['include'], {'VCS': '1'})] + locked = [] + lock = origin / 'Bender.lock' + if lock.is_file(): + # Ограниченный reader реальной flat Bender lock структуры; версии не вычисляются. + chunks = re.split(r'^ ([\w-]+):\s*$', lock.read_text(), flags=re.M) + for i in range(1, len(chunks), 2): + body = chunks[i + 1] + rev = re.search(r'^ revision: (\w+)$', body, re.M) + url = re.search(r'^ Git: (\S+)$', body, re.M) + if rev and url: + locked.append({'name': chunks[i], 'revision': rev[1], 'remote': url[1], + 'status': 'external; не включён в source inventory', 'evidence': 'Bender.lock'}) + missing_build_files = [{'path':file, 'evidence':build['path']} for build in builds + for file in build.get('declared_files', []) if not (root / file).is_file()] + missing_symbols = [] + if identifier == 'card-reader' and not any(m['name'] == 'rPLL' for m in modules): + text = uncomment((root / 'src/pll.v').read_text()) + match = re.search(r'\brPLL\s*#\s*\(', text) + if match: + missing_symbols.append({'name': 'rPLL', 'kind': 'vendor primitive', 'path': 'src/pll.v', + 'line': location(text, match), 'evidence': 'rPLL #( ... ); declaration отсутствует в corpus'}) + return {'id': identifier, 'provenance': {'remote': remote, 'revision': revision, + 'revision_matches_all_captured_sources': bool(revision) and not mismatched, + 'sources_not_matching_revision': mismatched, + 'working_tree_matches_all_captured_sources': not working_tree_mismatches, + 'sources_not_matching_origin_working_tree': working_tree_mismatches, + 'note': 'revision — доказательство только совпавших blobs; остальные файлы закреплены отдельными SHA-256'}, + 'license': {'status': 'evidence-found' if licenses else 'unknown; license file отсутствует в исходном repo root', 'evidence': licenses, 'source_spdx': source_spdx}, + 'source_files': records, 'source_inventory_sha256': digest(stable(records)), + 'build_evidence': builds, 'validation_profiles': profiles, + 'literal_include_references': include_refs, + 'missing_dependencies': {'literal_includes': [i for i in include_refs if not i['resolved_candidates']], + 'imported_packages_without_declaration': missing_packages, 'symbols': missing_symbols, 'locked_external_packages': locked, + 'build_recipe_files_not_in_corpus': missing_build_files, + 'scope': 'Только наблюдаемые literal includes/imports и перечисленные primitives; conditional branches не вычисляются. Не доказательство полной build closure.'}} + + +def parse_origins(values): + result = {} + for value in values: + key, separator, folder = value.partition('=') + if not separator or key not in IDS: + raise ValueError('--origin ожидает CORPUS_ID=PATH') + result[key] = Path(folder) + return result + + +def check(manifest, base, origins): + failures = [] + for corpus in manifest['corpora']: + identifier = corpus['id'] + actual = inventory(base / identifier) + expected_by_path = {r['path']: r for r in corpus['source_files']} + actual_by_path = {r['path']: r for r in actual} + changed = sorted(p for p in expected_by_path.keys() & actual_by_path.keys() if expected_by_path[p] != actual_by_path[p]) + added = sorted(actual_by_path.keys() - expected_by_path.keys()) + missing = sorted(expected_by_path.keys() - actual_by_path.keys()) + if changed or added or missing: + failures.append({'corpus': identifier, 'changed': changed, 'added': added, 'missing': missing}) + if identifier in origins: + origin = origins[identifier] + for entry in corpus['build_evidence'] + corpus['license']['evidence']: + file = origin / entry['path'] + if not file.resolve().is_relative_to(origin.resolve()) or not file.is_file() or digest(file.read_bytes()) != entry['sha256']: + failures.append({'corpus': identifier, 'origin_evidence_changed': entry['path']}) + print(json.dumps({'ok': not failures, 'checked_corpora': len(manifest['corpora']), 'failures': failures}, ensure_ascii=False, indent=2)) + return 1 if failures else 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('command', choices=('generate', 'check')) + parser.add_argument('--corpus-root', required=True, type=Path) + parser.add_argument('--origin', action='append', default=[], help='CORPUS_ID=local provenance repo; never stored as absolute path') + parser.add_argument('--output', type=Path, default=Path('docs/hdl-corpus-manifest.json')) + parser.add_argument('--manifest', type=Path, default=Path('docs/hdl-corpus-manifest.json')) + args = parser.parse_args() + origins = parse_origins(args.origin) + if args.command == 'check': + return check(json.loads(args.manifest.read_text()), args.corpus_root, origins) + result = {'schema_version': 1, 'purpose': 'Воспроизводимые source snapshots для проверки CodeGraph HDL; исходники не распространяются.', + 'hash_algorithm': 'sha256', 'corpora': [corpus_manifest(i, args.corpus_root / i, origins.get(i, args.corpus_root / i)) for i in IDS]} + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n') + print(json.dumps({'ok': True, 'corpora': len(IDS), 'output': str(args.output)}, ensure_ascii=False)) + return 0 + + +if __name__ == '__main__': + try: + sys.exit(main()) + except (OSError, ValueError, KeyError, ET.ParseError) as error: + print(str(error), file=sys.stderr) + sys.exit(2) diff --git a/scripts/hdl-frontend-compare.py b/scripts/hdl-frontend-compare.py new file mode 100644 index 000000000..919872d89 --- /dev/null +++ b/scripts/hdl-frontend-compare.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +"""Сравнить JSON export slang/Verilator на pinned HDL controls (Linux GNU time).""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import signal +import subprocess +import sys +import time + + +def walk(value): + if isinstance(value, dict): + yield value + for item in value.values(): + yield from walk(item) + elif isinstance(value, list): + for item in value: + yield from walk(item) + + + +def verify_semantic_control(tool, name, nodes, meta, cwd): + """Узкие assertions по реальному исходнику; не универсальный AST adapter.""" + by_addr = {str(n['addr']): n for n in nodes if 'addr' in n} + if name.startswith('axi'): + symbol, expected = 'NoSlvMst', int(name[-1]) + elif name == 'crc16': + symbol, expected = 'crc_out', 16 + else: + symbol, expected = 'CLK_FREQ', 50000000 + if tool == 'slang': + candidates = [n for n in nodes if n.get('name') == symbol and n.get('kind') in ('Port', 'Parameter')] + if symbol == 'CLK_FREQ': + candidates = [n for n in candidates if n.get('source_file', '').endswith('/top.v')] + node = candidates[0] + if symbol == 'crc_out': + assert node['type']['range'] == '[15:0]' + value = 16 + else: + raw = node['value'] + value = int(raw.split("'d")[-1]) + file, line = node['source_file'], node['source_line'] + else: + candidates = [n for n in nodes if n.get('name') == symbol and n.get('type') == 'VAR'] + files = json.loads(meta.read_text())['files'] + if symbol == 'CLK_FREQ': + candidates = [n for n in candidates if files[n['loc'].split(',')[0]]['filename'].endswith('/top.v')] + node = candidates[0] + if symbol == 'crc_out': + assert by_addr[node['dtypep']]['range'] == '15:0' + value = 16 + else: + raw = node['valuep'][0]['name'] + value = int(raw.split('h')[-1], 16) + location = node['loc'].split(',') + file, line = files[location[0]]['filename'], int(location[1].split(':')[0]) + assert value == expected, (symbol, value, expected) + source = (cwd / file).resolve() + assert symbol in source.read_text().splitlines()[line - 1] + if name.startswith('uart'): + kind_key, kind = ('kind', 'Instance') if tool == 'slang' else ('type', 'CELL') + assert {'u_rx1', 'u_rx2'} <= {n.get('name') for n in nodes if n.get(kind_key) == kind} + return {'symbol': symbol, 'valueOrWidth': value, 'source': str(source), 'line': line, + 'sourceLineVerified': True} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--corpus-root', type=Path, required=True) + parser.add_argument('--axi', type=Path, required=True) + parser.add_argument('--filelist', type=Path, required=True, help='A2 Bender Verilator filelist') + parser.add_argument('--slang', required=True) + parser.add_argument('--verilator', required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--runs', type=int, default=2) + args = parser.parse_args() + if not 1 <= args.runs <= 5: + parser.error('--runs must be 1..5') + repo = Path(__file__).resolve().parent.parent + base, axi, out = args.corpus_root.resolve(), args.axi.resolve(), args.output.resolve() + out.mkdir(parents=True, exist_ok=True) + if any(out.iterdir()): + parser.error('--output must be empty to prevent stale artifact reuse') + subprocess.run([sys.executable, str(repo / 'scripts/hdl-corpus-manifest.py'), 'check', + '--corpus-root', str(base), '--manifest', str(repo / 'docs/hdl-corpus-manifest.json')], check=True) + manifest = json.loads((repo / 'docs/hdl-corpus-manifest.json').read_text()) + def sources(identifier): + c = next(c for c in manifest['corpora'] if c['id'] == identifier) + return [str(base / identifier / f) for f in c['validation_profiles'][0]['files']] + shared = [line.strip() for line in args.filelist.read_text().splitlines() if line.strip()] + for line in shared: + if line.startswith('+define+'): + continue + file = Path(line.removeprefix('+incdir+')) + if not file.is_absolute() or not file.resolve().is_relative_to(axi): + raise ValueError('Filelist must use confined absolute AXI paths') + revision = subprocess.check_output(['git', '-C', str(axi), 'rev-parse', 'HEAD'], text=True).strip() + expected = json.loads((repo / 'docs/hdl-axi-build-validation.json').read_text()) + if revision != expected['axiRevision']: + raise ValueError('Wrong AXI revision') + portable_filelist = args.filelist.read_text().replace(str(axi), '${AXI}') + if hashlib.sha256(portable_filelist.encode()).hexdigest() != expected['profile']['filelistSha256']: + raise ValueError('Filelist differs from verified A2 input') + roots = [axi] + for info in expected['dependencies'].values(): + root = axi / info['path'] + if subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() != info['revision']: + raise ValueError('Dependency revision differs') + roots.append(root) + def snapshot(): + data = {} + for root in roots: + if subprocess.check_output(['git', '-C', str(root), 'status', '--porcelain', '--untracked-files=no'], text=True).strip(): + raise ValueError('Modified AXI/dependency tracked sources') + for name in subprocess.check_output(['git', '-C', str(root), 'ls-files'], text=True).splitlines(): + file = root / name + if file.is_file(): + data[str(file)] = hashlib.sha256(file.read_bytes()).hexdigest() + for line in shared: + if line.startswith('+define+'): + continue + file = Path(line.removeprefix('+incdir+')) + candidates = file.rglob('*') if line.startswith('+incdir+') else [file] + for candidate in candidates: + if candidate.is_symlink(): + raise ValueError('Symlink input is outside this pinned benchmark') + if candidate.is_file() and str(candidate) not in data: + raise ValueError('Untracked compiler input: ' + str(candidate)) + for c in manifest['corpora']: + for entry in c['source_files']: + file = base / c['id'] / entry['path'] + data[str(file)] = hashlib.sha256(file.read_bytes()).hexdigest() + return data + before = snapshot() + cases = [('uart-strict', 'top', sources('dual-uart'), None, True), + ('uart-compatible', 'top', sources('dual-uart'), None, True), + ('crc16', 'emmc_crc16', [str(base / 'card-reader/src/emmc_crc16.v')], None, True), + ('axi2', 'synth_axi_lite_xbar', shared, 'NoSlvMst=2', True), + ('axi4', 'synth_axi_lite_xbar', shared, 'NoSlvMst=4', True), + ('board-missing-pll', 'top', sources('card-reader'), None, False)] + versions = {tool: subprocess.check_output([getattr(args, tool), '--version'], text=True).strip() + for tool in ['slang', 'verilator']} + rows = [] + def portable(value): + return str(value).replace(str(out), '${OUTPUT}').replace(str(base), '${CORPORA}').replace(str(axi), '${AXI}') + for name, top, files, parameter, success in cases: + for tool in ['slang', 'verilator']: + for iteration in range(args.runs): + dest = out / (name + '-' + tool + '-' + str(iteration)) + dest.mkdir(exist_ok=True) + ast = dest / 'ast.json' + meta = dest / 'meta.json' + if tool == 'slang': + command = [args.slang, '--std', '1800-2023', '--single-unit', '--top', top, + '--ast-json', str(ast), '--ast-json-source-info', '--ast-json-detailed-types', + '--diag-json', str(dest / 'diagnostics.json')] + else: + command = [args.verilator, '--language', '1800-2023', '--json-only', '-Wno-fatal', + '--top-module', top, '--json-only-output', str(ast), + '--json-only-meta-output', str(meta), '--Mdir', str(dest / 'obj')] + if tool == 'slang' and name == 'uart-compatible': + command.append('--allow-use-before-declare') + if parameter: + command.append('-G' + parameter) + command += files + measurement = dest / 'time.txt' + timed = ['/usr/bin/time', '-f', '%e %M', '-o', str(measurement), *command] + start = time.monotonic() + with (dest / 'stdout').open('w') as stdout, (dest / 'stderr').open('w') as stderr: + process = subprocess.Popen(timed, cwd=axi, stdout=stdout, stderr=stderr, start_new_session=True) + try: + code = process.wait(timeout=120) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + code = -9 + except BaseException: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + process.wait() + raise + elapsed = round(time.monotonic() - start, 3) + metrics = measurement.read_text().splitlines()[-1].split() if measurement.exists() else [] + peak = int(metrics[1]) if len(metrics) == 2 and metrics[1].isdigit() else None + stderr_text = (dest / 'stderr').read_text() + stdout_text = (dest / 'stdout').read_text() + row = {'case': name, 'tool': tool, 'iteration': iteration, 'exit': code, + 'expectedSuccess': success and not (name == 'uart-strict' and tool == 'slang'), 'seconds': float(metrics[0]) if len(metrics) == 2 else None, 'wrapperSeconds': elapsed, 'peakRssKiB': peak, + 'argv': ['${' + tool.upper() + '}', *[portable(v) for v in command[1:]]], + 'stderrBytes': len(stderr_text.encode()), 'astBytes': ast.stat().st_size if ast.exists() else 0} + if code == 0 and ast.exists(): + nodes = list(walk(json.loads(ast.read_text()))) + key = 'kind' if tool == 'slang' else 'type' + kinds = {} + for node in nodes: + kind = node.get(key) + if isinstance(kind, str): + kinds[kind] = kinds.get(kind, 0) + 1 + row['nodeKinds'] = kinds + evidence = verify_semantic_control(tool, name, nodes, meta, axi) + evidence['source'] = portable(evidence['source']) + row['semanticControl'] = evidence + if name == 'uart-strict' and tool == 'slang': + row['expectedOutcome'] = code not in (0, -9) and 'used before its declaration' in stderr_text + else: + row['expectedOutcome'] = (code == 0 and ast.exists()) if success else (code not in (0, -9) and 'rPLL' in stderr_text + stdout_text) + row['errorLines'] = len(re.findall(r'(?:^%Error|: error:)', stderr_text, re.M)) + row['warningLines'] = len(re.findall(r'(?:^%Warning|: warning:)', stderr_text, re.M)) + rows.append(row) + (out / 'report.json').write_text(json.dumps({'tools': versions, 'cases': rows}, indent=2) + '\n') + print(name, tool, iteration, code, elapsed, peak, flush=True) + if snapshot() != before: + raise RuntimeError('Inputs changed during benchmark') + report = {'tools': versions, 'cases': rows, 'inputsUnchanged': True, + 'inputSnapshotSha256': hashlib.sha256(json.dumps({portable(k): v for k, v in before.items()}, sort_keys=True).encode()).hexdigest(), + 'measurement': 'Linux GNU time peak RSS; sequential export runs, not cold-cache benchmark; repetitions not independent machines', + 'profile': 'Explicit arguments identical per case; tools retain their own implicit predefines and language semantics', + 'allExpectedOutcomes': all(r['expectedOutcome'] for r in rows)} + (out / 'report.json').write_text(json.dumps(report, indent=2) + '\n') + return 0 if report['allExpectedOutcomes'] else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/verify-hdl-source-map-export.py b/scripts/verify-hdl-source-map-export.py new file mode 100644 index 000000000..c60f4dcc5 --- /dev/null +++ b/scripts/verify-hdl-source-map-export.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +"""Реальные bounded tests экспортера; запускать Python окружения pyslang 11.0.0. + +Не устанавливает зависимости. Создаёт только изолированные временные fixtures. +""" +import argparse +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +HELPER = Path(__file__).resolve().parents[1] / 'src/hdl/source-map-export.py' +WORK_ROOT = None + + +class SourceMapExportTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory(prefix='hdl-source-map-test-', dir=WORK_ROOT) + self.root = Path(self.directory.name) + + def tearDown(self): + self.directory.cleanup() + + def write(self, name, text, crlf=False, bom=False): + target = self.root / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((b'\xef\xbb\xbf' if bom else b'') + text.replace('\n', '\r\n' if crlf else '\n').encode('utf8')) + + def compile(self, source, extra=(), success=True): + out = self.root / 'facts.json' + result = subprocess.run([sys.executable, '-I', str(HELPER), '--ast-json', str(out), + '--ast-json-source-info', '--ast-json-detailed-types', '--std=1800-2023', + '--threads=1', '--error-limit=20', '--top', 'top', *extra, '--', source], + cwd=self.root, text=True, capture_output=True, timeout=60) + self.assertEqual(result.returncode == 0, success, result.stderr) + if not success: + self.assertFalse(out.exists(), 'failed frontend must not emit semantic facts') + return result + payload = json.loads(out.read_text()) + self.assertEqual(payload['codegraphSemanticVersion'], 1) + return payload['facts'] + + def assertPoint(self, point): + file = (self.root / point['file']).resolve() + self.assertTrue(file.is_relative_to(self.root)) + data = file.read_bytes() + offset = point['byteOffset'] + self.assertGreaterEqual(offset, 0) + self.assertLessEqual(offset, len(data)) + self.assertEqual(point['line'], data.count(b'\n', 0, offset) + 1) + self.assertEqual(point['column'], offset - data.rfind(b'\n', 0, offset)) + + def test_nested_arguments_include_utf8_crlf_and_four_state_value(self): + self.write('defs/inner.svh', '`define LEAF(name,value) localparam int name=value;\n', crlf=True) + self.write('defs/outer.svh', '`include "inner.svh"\n`define WRAP(name,value) `LEAF(name,value)\n', crlf=True) + source = "top space'quote.sv" + self.write(source, '`include "defs/outer.svh"\nmodule top #(parameter int N=1);\n /* 😀 */ `WRAP(P, 7)\n localparam logic [3:0] XZ=4\'b10xz;\nendmodule\n', crlf=True, bom=True) + facts = self.compile(source, ['-G', "N=32'd4"]) + byname = {fact['name']: fact for fact in facts} + self.assertEqual(byname['N']['value'], '4') + self.assertEqual(byname['P']['value'], '7') + self.assertEqual(byname['XZ']['value'].lower(), "4'b10xz") + p = byname['P'] + self.assertEqual(p['sourceOrigin'], 'macro') + self.assertFalse(p['macroExpansionComplete']) + frames = p['macroExpansion'] + self.assertEqual({frame['name'] for frame in frames}, {'LEAF', 'WRAP'}) + self.assertTrue(any(frame['name'] == 'LEAF' and frame['argument'] for frame in frames)) + self.assertTrue(any(frame['name'] == 'WRAP' and frame['argument'] for frame in frames)) + data = (self.root / source).read_bytes() + offset = data.index(b'P, 7') + self.assertEqual(p['source'], {'file': source, 'line': 3, 'column': offset - data.rfind(b'\n', 0, offset)}) + for frame in frames: + if 'spelling' in frame: + self.assertPoint(frame['spelling']) + if 'invocation' in frame: + self.assertPoint(frame['invocation']['start']) + self.assertPoint(frame['invocation']['end']) + self.assertTrue(any(frame.get('spelling', {}).get('file') == 'defs/inner.svh' for frame in frames)) + + def test_token_concat_is_prefix_point_not_a_generated_name_range(self): + self.write('defs.svh', '`define CAT(a,b) a``b\n`define DECL(p) localparam int `CAT(p,_param)=3;\n') + self.write('top.sv', '`include "defs.svh"\nmodule top;\n `DECL(bus)\nendmodule\n') + fact = next(f for f in self.compile('top.sv') if f['name'] == 'bus_param') + self.assertEqual(fact['value'], '3') + self.assertEqual(fact['sourceOrigin'], 'macro') + self.assertNotIn('nameRange', fact) + self.assertFalse(fact['macroExpansionComplete']) + data = (self.root / 'top.sv').read_bytes() + prefix = [frame['spelling'] for frame in fact['macroExpansion'] + if frame.get('spelling', {}).get('file') == 'top.sv' + and data[frame['spelling']['byteOffset']:].startswith(b'bus')] + self.assertTrue(prefix) + self.assertFalse(data[prefix[0]['byteOffset']:].startswith(b'bus_param')) + + def test_zero_empty_string_and_physical_line_directive_coordinates(self): + self.write('top.sv', '`line 700 "virtual.sv" 0\nmodule top; parameter int ZERO=0; parameter string EMPTY=""; endmodule\n') + facts = {f['name']: f for f in self.compile('top.sv')} + self.assertEqual(facts['ZERO']['value'], '0') + self.assertEqual(facts['EMPTY']['value'], '""') + for fact in facts.values(): + self.assertEqual(fact['sourceOrigin'], 'direct') + self.assertEqual(fact['source']['file'], 'top.sv') + self.assertEqual(fact['source']['line'], 2) + + def test_type_alias_facts_have_integral_width_and_declaration_origin(self): + self.write('types.svh', '`define DECLTYPE(name) typedef logic [7:0] name;\n') + self.write('top.sv', '`include "types.svh"\nmodule top;\n `DECLTYPE(byte_t)\n typedef logic [3:0] nibble_t;\nendmodule\n') + facts = {fact['name']: fact for fact in self.compile('top.sv')} + self.assertEqual(facts['byte_t']['kind'], 'type') + self.assertEqual(facts['byte_t']['width'], 8) + self.assertEqual(facts['byte_t']['sourceOrigin'], 'macro') + self.assertTrue(facts['byte_t']['macroExpansion']) + self.assertEqual(facts['nibble_t']['kind'], 'type') + self.assertEqual(facts['nibble_t']['width'], 4) + self.assertEqual(facts['nibble_t']['sourceOrigin'], 'direct') + self.assertNotIn('direction', facts['byte_t']) + + @staticmethod + def origin_names(fact, role): + return {frame['name'] for origin in fact['expressionOrigins'] if origin['role'] == role + for frame in origin.get('macroExpansion', [])} + + def test_literal_parameter_tracks_its_macro_initializer_not_declaration_name(self): + self.write('defs.svh', '`define VALUE 7\n') + self.write('top.sv', '`include "defs.svh"\nmodule top; localparam int P=`VALUE; endmodule\n') + fact = next(f for f in self.compile('top.sv') if f['name'] == 'P') + self.assertEqual(fact['sourceOrigin'], 'direct') + self.assertEqual(self.origin_names(fact, 'initializer'), {'VALUE'}) + self.assertEqual(fact['expressionOriginCoverage']['initializer'], 'checked') + self.assertEqual(fact['expressionOriginCoverage']['declaredInitializer'], 'not-applicable') + + def test_port_and_type_dimensions_have_compiler_macro_origins_with_physical_coordinates(self): + self.write('defs.svh', '`define WIDTH 8\n', crlf=True) + self.write('top.sv', '`include "defs.svh"\n/* 😀 */ module top(output logic [`WIDTH-1:0] data);\n typedef logic [`WIDTH-1:0] word_t;\nendmodule\n', crlf=True, bom=True) + facts = {f['name']: f for f in self.compile('top.sv')} + for name in ['data', 'word_t']: + fact = facts[name] + self.assertEqual(fact['sourceOrigin'], 'direct') + self.assertEqual(fact['width'], 8) + self.assertEqual(self.origin_names(fact, 'type'), {'WIDTH'}) + self.assertEqual(fact['expressionOriginCoverage']['type'], 'checked') + for origin in fact['expressionOrigins']: + for frame in origin.get('macroExpansion', []): + if 'spelling' in frame: + self.assertPoint(frame['spelling']) + if 'invocation' in frame: + self.assertPoint(frame['invocation']['start']) + self.assertPoint(frame['invocation']['end']) + + def test_nonansi_and_ansi_unpacked_port_dimensions_keep_macro_type_origins(self): + self.write('defs.svh', '`define W 4\n') + for declaration in ['module top(a); input logic a[`W]; endmodule', + 'module top(input logic a[`W]); endmodule']: + self.write('top.sv', '`include "defs.svh"\n' + declaration + '\n') + fact = next(f for f in self.compile('top.sv') if f['kind'] == 'port' and f['name'] == 'a') + self.assertEqual(self.origin_names(fact, 'type'), {'W'}) + self.assertEqual(fact['expressionOriginCoverage']['type'], 'checked') + self.assertFalse(fact['expressionOriginCoverage']['truncated']) + self.assertEqual(len(fact['expressionOrigins']), 1) + for frame in fact['expressionOrigins'][0]['macroExpansion']: + if 'spelling' in frame: + self.assertPoint(frame['spelling']) + if 'invocation' in frame: + self.assertPoint(frame['invocation']['start']) + self.assertPoint(frame['invocation']['end']) + + def test_instance_override_separates_actual_and_declared_default_macro_origins(self): + self.write('defs.svh', '`define DEFAULT 7\n`define ACTUAL 11\n') + self.write('top.sv', '`include "defs.svh"\nmodule leaf #(parameter int P=`DEFAULT); endmodule\nmodule top; leaf #(.P(`ACTUAL)) u(); endmodule\n') + fact = next(f for f in self.compile('top.sv') if f['instancePath'] == 'top.u' and f['name'] == 'P') + self.assertEqual(fact['value'], '11') + self.assertEqual(self.origin_names(fact, 'initializer'), {'ACTUAL'}) + self.assertEqual(self.origin_names(fact, 'declared-initializer'), {'DEFAULT'}) + self.assertEqual(fact['expressionOriginCoverage']['declaredInitializer'], 'checked') + + def test_command_line_override_never_reports_macro_default_as_effective(self): + self.write('top.sv', '`define DEFAULT 7\nmodule top #(parameter int P=`DEFAULT); endmodule\n') + fact = next(f for f in self.compile('top.sv', ['-G', 'P=11']) if f['name'] == 'P') + self.assertEqual(fact['value'], '11') + self.assertEqual(fact['expressionOriginCoverage']['initializer'], 'command-line') + self.assertFalse(self.origin_names(fact, 'initializer')) + self.assertEqual(self.origin_names(fact, 'declared-initializer'), {'DEFAULT'}) + + def test_checked_type_syntax_does_not_claim_transitive_typedef_dependencies(self): + self.write('top.sv', '`define WIDTH 8\ntypedef logic [`WIDTH-1:0] word_t;\nmodule top(output word_t data); endmodule\n') + fact = next(f for f in self.compile('top.sv') if f['name'] == 'data') + self.assertEqual(fact['width'], 8) + self.assertEqual(fact['expressionOriginCoverage']['type'], 'checked') + self.assertEqual(fact['expressionOrigins'], []) + + def test_expression_origin_budget_is_explicit_and_keeps_compiled_value(self): + self.write('top.sv', '`define V 1\nmodule top; localparam int P=' + '+'.join(['`V'] * 40) + '; endmodule\n') + fact = next(f for f in self.compile('top.sv') if f['name'] == 'P') + self.assertEqual(fact['value'], '40') + self.assertEqual(len(fact['expressionOrigins']), 32) + self.assertTrue(fact['expressionOriginCoverage']['truncated']) + + def test_normalized_origins_are_deduplicated_when_frame_budget_is_exhausted(self): + self.write('top.sv', '`define V 1\nmodule top; localparam int P=`V+`V; endmodule\n') + out = self.root / 'facts.json' + probe = """import importlib.util,sys +sys.dont_write_bytecode=True +spec=importlib.util.spec_from_file_location('test_exporter',sys.argv[1]) +module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) +module.MAX_PROVENANCE_FRAMES=0 +raise SystemExit(module.main(['--ast-json',sys.argv[2],'--std=1800-2023','--top','top','--','top.sv'])) +""" + result = subprocess.run([sys.executable, '-I', '-c', probe, str(HELPER), str(out)], + cwd=self.root, capture_output=True, text=True, timeout=60) + self.assertEqual(result.returncode, 0, result.stderr) + fact = next(f for f in json.loads(out.read_text())['facts'] if f['name'] == 'P') + self.assertEqual(fact['value'], '2') + self.assertEqual(len(fact['expressionOrigins']), 1) + self.assertTrue(fact['expressionOriginCoverage']['truncated']) + self.assertEqual(fact['expressionOrigins'][0]['macroExpansion'], []) + self.assertFalse(fact['expressionOrigins'][0]['macroExpansionComplete']) + + def test_line_directive_cannot_spoof_command_line_origin(self): + self.write('top.sv', '`define VALUE 7\n`line 700 "" 0\nmodule top; parameter int P=`VALUE; endmodule\n') + fact = next(f for f in self.compile('top.sv') if f['name'] == 'P') + self.assertEqual(fact['expressionOriginCoverage']['initializer'], 'checked') + self.assertEqual(self.origin_names(fact, 'initializer'), {'VALUE'}) + + def test_rejects_semantic_errors_instead_of_exporting_partial_ast(self): + self.write('top.sv', 'module top; missing_dependency u(); endmodule\n') + result = self.compile('top.sv', success=False) + self.assertIn('unknown module', result.stderr) + + def test_shared_bodies_and_generate_indices_have_instance_specific_paths(self): + self.write('top.sv', 'module leaf #(parameter int P=2); endmodule\nmodule top; leaf u1(); leaf u2(); for(genvar i=2;i<4;i++) begin : lanes leaf u(); end endmodule\n') + facts = self.compile('top.sv') + paths = {f['instancePath'] for f in facts if f['name'] == 'P'} + self.assertEqual(paths, {'top.u1', 'top.u2', 'top.lanes[2].u', 'top.lanes[3].u'}) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--helper', type=Path, default=HELPER) + parser.add_argument('--work-root', type=Path) + args = parser.parse_args() + HELPER = args.helper.resolve() + WORK_ROOT = args.work_root + unittest.main(argv=[sys.argv[0], '-v']) diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index a6fe54c98..219f0036c 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { formatHdlProfileStatus } from '../hdl/status'; /** * CodeGraph CLI * @@ -41,7 +42,7 @@ try { import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; -import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; +import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; import { extractProseCandidates } from '../search/identifier-segments'; import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree'; import { createShimmerProgress } from '../ui/shimmer-progress'; @@ -955,9 +956,20 @@ program const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); + if (cg.isIndexStale()) { + cg.destroy(); + const message = 'Index extraction is stale. Run "codegraph index" for a full rebuild before syncing.'; + if (options.quiet) { + process.stderr.write(`codegraph sync: ${message}\n`); + process.exit(1); + } + throw new Error(message); + } + if (options.quiet) { - await cg.sync(); + const result = await cg.sync(); cg.destroy(); + if (result.skippedReason === 'locked') throw new Error('Index is busy: another process holds the lock. Retry sync shortly.'); return; } @@ -972,6 +984,10 @@ program }); await progress.stop(); + if (result.skippedReason === 'locked') { + cg.destroy(); + throw new Error('Index is busy: another process holds the lock. Retry sync shortly.'); + } const totalChanges = result.filesAdded + result.filesModified + result.filesRemoved; @@ -989,9 +1005,9 @@ program clack.outro('Done'); cg.destroy(); } catch (err) { - if (!options.quiet) { - error(`Failed to sync: ${err instanceof Error ? err.message : String(err)}`); - } + const message = `Failed to sync: ${err instanceof Error ? err.message : String(err)}`; + if (options.quiet) process.stderr.write(`${message}\n`); + else error(message); process.exit(1); } }); @@ -1038,7 +1054,9 @@ program const journalMode = cg.getJournalMode(); const buildInfo = cg.getIndexBuildInfo(); - const reindexRecommended = cg.isIndexStale(); + const hdlProfile = cg.getHdlProfileStatus(); + const engineReindexRecommended = cg.isIndexStale(); + const reindexRecommended = engineReindexRecommended || !!hdlProfile?.reindexRecommended; const indexState = cg.getIndexState(); // Zero on a healthy index; non-zero at rest means a resolution pass was // interrupted, so some files' call edges are missing (#1187). @@ -1075,6 +1093,7 @@ program builtWithExtractionVersion: buildInfo.extractionVersion, currentExtractionVersion: EXTRACTION_VERSION, reindexRecommended, + hdlProfile, // 'complete' | 'partial' (files silently dropped) | 'indexing' // (a run was killed mid-index — the index is truncated) | // 'failed' | null (predates the marker). @@ -1175,6 +1194,10 @@ program console.log(` Removed: ${changes.removed.length} files`); } info('Run "codegraph sync" to update the index'); + } else if (hdlProfile?.state === 'configuration-error') { + warn('Source files are unchanged; HDL profile configuration requires attention'); + } else if (hdlProfile?.reindexRecommended) { + warn('Source files are unchanged; indexed HDL context needs a rebuild'); } else { success('Index is up to date'); } @@ -1182,7 +1205,12 @@ program // Re-index hint: the index was built by an older engine than the one now // running, so a rebuild would add data a migration can't backfill. - if (reindexRecommended) { + const profileNote = formatHdlProfileStatus(hdlProfile); + if (profileNote) { + console.log(profileNote); + console.log(); + } + if (engineReindexRecommended) { const builtWith = buildInfo.version ? `v${buildInfo.version.replace(/^v/, '')}` : 'an earlier version'; warn(`Index was built by ${builtWith}; re-index to pick up this engine's improvements.`); info('Run "codegraph index" (full rebuild) or "codegraph sync"'); @@ -1287,12 +1315,58 @@ program * subagents (which don't inherit MCP tools, #704) and non-MCP harnesses — * can reach the graph through a plain shell command. */ +program + .command('hdl-semantic [query]') + .description('Compute HDL parameters and port widths with optional slang (exact symbol or instance query)') + .option('--slang ', 'Path to the installed slang executable') + .option('--python ', 'Python with pinned pyslang for compiler macro source mapping (instead of --slang)') + .option('-p, --path ', 'Project path') + .option('--top ', 'Single top module (defaults to the active profile top)') + .option('--parameter ', 'Top-level parameter overrides') + .option('--allow-use-before-declare', 'Explicit slang compatibility mode') + .option('--limit ', 'Maximum facts (1..1000)', '100') + .action(async (query: string | undefined, options: { slang?: string; python?: string; path?: string; top?: string; + parameter?: string[]; allowUseBeforeDeclare?: boolean; limit: string }) => { + const projectPath = resolveProjectPath(options.path); + try { + if (!isInitialized(projectPath)) throw new Error('HDL semantic queries require an initialized CodeGraph project'); + const parameters: Record = Object.create(null); + for (const argument of options.parameter ?? []) { + const separator = argument.indexOf('='); + if (separator < 1 || !argument.slice(separator + 1)) throw new Error('Parameter must be NAME=VALUE'); + const name = argument.slice(0, separator); + if (Object.prototype.hasOwnProperty.call(parameters, name)) throw new Error('Duplicate HDL parameter override: ' + name); + parameters[name] = argument.slice(separator + 1); + } + const { default: CodeGraph } = await loadCodeGraph(); + const cg = await CodeGraph.open(projectPath); + const controller = new AbortController(); + const interrupt = () => controller.abort(); + process.once('SIGINT', interrupt); + process.once('SIGTERM', interrupt); + try { + const result = await cg.getHdlSemantics({ executable: options.slang, pythonExecutable: options.python, top: options.top, + parameters, allowUseBeforeDeclare: options.allowUseBeforeDeclare, query, limit: Number(options.limit), + signal: controller.signal }); + console.log(JSON.stringify(result)); + } finally { + process.removeListener('SIGINT', interrupt); + process.removeListener('SIGTERM', interrupt); + cg.close(); + } + } catch (err) { + error(`HDL semantics failed: ${err instanceof Error ? err.message : String(err)}`); + process.exitCode = 1; + } + }); + program .command('explore ') .description('Explore an area: relevant symbols\' source + call paths in one shot (same output as the codegraph_explore MCP tool)') .option('-p, --path ', 'Project path') .option('--max-files ', 'Maximum number of files to include source from') - .action(async (queryParts: string[], options: { path?: string; maxFiles?: string }) => { + .option('--hdl-access ', 'HDL signal access: read, write, readwrite, control, event, all (query is one exact signal name)') + .action(async (queryParts: string[], options: { path?: string; maxFiles?: string; hdlAccess?: string }) => { const projectPath = resolveProjectPath(options.path); try { @@ -1308,6 +1382,7 @@ program const args: Record = { query: queryParts.join(' ') }; if (options.maxFiles) args.maxFiles = parseInt(options.maxFiles, 10); + if (options.hdlAccess) args.hdlAccess = options.hdlAccess; const result = await handler.execute('codegraph_explore', args); console.log(result.content[0]?.text ?? ''); @@ -1413,6 +1488,7 @@ program let input: { prompt?: string; cwd?: string } = {}; try { input = JSON.parse(raw); } catch { return; } const prompt = String(input.prompt || ''); + if (isTaskNotification(prompt)) return; // Gate telemetry: how often each tier fires vs. no-ops — counter names // only, NEVER prompt content (see TELEMETRY.md). This is the data that diff --git a/src/db/queries.ts b/src/db/queries.ts index 37c078845..c4c9a2953 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -163,6 +163,22 @@ interface UnresolvedRefRow { * refs against newly-added node names. */ function referenceNameTail(referenceName: string): string { + // Named HDL connections carry structured module/port/instance identity. + // Retry on the formal port name when a previously removed port reappears. + for (const prefix of ['hdl:port-position:', 'hdl:wildcard:']) { + if (referenceName.startsWith(prefix)) { + try { + const parts: unknown = JSON.parse(referenceName.slice(prefix.length)); + if (Array.isArray(parts) && typeof parts[0] === 'string') return parts[0]; + } catch { /* Fall back for malformed internal references. */ } + } + } + if (referenceName.startsWith('hdl:port:')) { + try { + const parts: unknown = JSON.parse(referenceName.slice('hdl:port:'.length)); + if (Array.isArray(parts) && parts.length === 3 && typeof parts[1] === 'string') return parts[1]; + } catch { /* Malformed refs retain the generic tail behavior below. */ } + } // Erlang refs carry a written arity (`f/1`, `mod::fn/2` — #1610); the tail a // new symbol's plain name could match is the arity-less function name. const base = referenceName.replace(/\/\d{1,3}$/, '') || referenceName; @@ -3526,6 +3542,55 @@ export class QueryBuilder { return changed; } + /** + * Replace resolution edges with their original unresolved references as one + * transaction. If ref insertion fails, the edge deletion is rolled back. + */ + replaceResolutionEdgesWithUnresolvedRefs( + edgeIds: number[], + refs: UnresolvedReference[] + ): number { + return this.db.transaction(() => { + let changed = this.deleteEdgesByIds(edgeIds); + const seenHdl = new Set(); + const pending = refs.filter(ref => { + if (!ref.referenceName.startsWith('hdl:wildcard:') && !ref.referenceName.startsWith('hdl:port-position:')) return true; + const key = JSON.stringify([ref.fromNodeId, ref.referenceName, ref.line, ref.column]); + if (seenHdl.has(key)) return false; + seenHdl.add(key); + return true; + }); + // A wildcard/positional binding depends on the entire module header. + // Invalidate its complete fan-out, including actual signals in untouched + // files, before rebuilding it from one original reference. + for (const ref of pending) { + if (ref.referenceName.startsWith('hdl:wildcard:') || ref.referenceName.startsWith('hdl:port-position:')) { + changed += this.db.prepare("DELETE FROM edges WHERE source = ? AND json_extract(metadata, '$.refName') = ?") + .run(ref.fromNodeId, ref.referenceName).changes; + } + } + this.insertUnresolvedRefsBatch(pending); + return changed; + })(); + } + + hasLanguage(language: Language): boolean { + return !!this.db.prepare('SELECT 1 FROM nodes WHERE language = ? LIMIT 1').get(language); + } + + /** HDL call arguments may change access when a remote formal changes direction. + * Seek HDL sources first, then their indexed outgoing edges; ordinary projects + * have no rows here. Include unclassified arguments so newly indexed callees + * can provide direction evidence on the next sync. */ + getHdlCallArgumentEdges(): Array { + const rows = this.db.prepare(`SELECT e.*, src.file_path AS source_file_path, src.language AS source_language + FROM nodes src JOIN edges e ON e.source = src.id + WHERE src.language = 'verilog' AND e.kind = 'references' AND e.metadata LIKE '%hdl:call-arg:%'`) + .all() as Array; + return rows.map(row => ({ ...rowToEdge(row), edgeId: row.id, + sourceFilePath: row.source_file_path, sourceLanguage: row.source_language })); + } + /** * Distinct node names present in the given files — the symbol names a sync * pass uses to look up retryable failed refs after those files changed. diff --git a/src/directory.ts b/src/directory.ts index 3ada4c88c..bb8277fd0 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -899,3 +899,8 @@ export function validateDirectory(projectRoot: string): { errors, }; } + +/** Claude's complete task-notification envelope is not a user search (#1832). */ +export function isTaskNotification(prompt: string): boolean { + return /^\s*[\s\S]*<\/task-notification>\s*$/.test(prompt); +} diff --git a/src/extraction/extraction-version.ts b/src/extraction/extraction-version.ts index 8ddab13b8..0edf9fad5 100644 --- a/src/extraction/extraction-version.ts +++ b/src/extraction/extraction-version.ts @@ -21,4 +21,4 @@ * turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty * in the product is load-bearing"). */ -export const EXTRACTION_VERSION = 26; +export const EXTRACTION_VERSION = 38; diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index c7710f200..5ee46f617 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -23,6 +23,7 @@ const WASM_GRAMMAR_FILES: Record = { javascript: 'tree-sitter-javascript.wasm', jsx: 'tree-sitter-javascript.wasm', python: 'tree-sitter-python.wasm', + verilog: 'tree-sitter-systemverilog.wasm', go: 'tree-sitter-go.wasm', rust: 'tree-sitter-rust.wasm', java: 'tree-sitter-java.wasm', @@ -120,6 +121,11 @@ export const EXTENSION_MAP: Record = { '.scala': 'scala', '.sc': 'scala', '.lua': 'lua', + // Verilog / SystemVerilog (one grammar covers both) + '.v': 'verilog', + '.vh': 'verilog', + '.sv': 'verilog', + '.svh': 'verilog', '.luau': 'luau', '.m': 'objc', '.mm': 'objc', @@ -289,7 +295,7 @@ export async function initGrammars(): Promise { * the vendored wasm together. */ const VENDORED_WASM_LANGS: ReadonlySet = new Set([ - 'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery', + 'pascal', 'scala', 'lua', 'luau', 'verilog', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery', 'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix', 'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', // R7a (C/C++ kernel port prep): tree-sitter-c v0.24.2 (b780e47) + @@ -692,6 +698,7 @@ export function getLanguageDisplayName(language: Language): string { scala: 'Scala', lua: 'Lua', luau: 'Luau', + verilog: 'Verilog / SystemVerilog', objc: 'Objective-C', solidity: 'Solidity', nix: 'Nix', diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 8095ee5b5..ea2642030 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -5,6 +5,8 @@ */ import * as fs from 'fs'; +import { HdlIndexContext } from '../hdl/index-context'; +import { loadHdlProfile } from '../hdl/profile'; import * as fsp from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; @@ -119,6 +121,8 @@ export interface IndexResult { * Result of a sync operation */ export interface SyncResult { + /** No reconciliation ran because another process owns the index lock. */ + skippedReason?: 'locked'; filesChecked: number; filesAdded: number; filesModified: number; @@ -1287,20 +1291,97 @@ interface GitChanges { * case this cannot see (the child status that would report the deletions is gone * with it); a full `codegraph index` reconciles that. */ -export function getGitChangedFiles(rootDir: string): GitChanges | null { +export function getGitChangedFiles(rootDir: string, sinceCommit?: string | null): GitChanges | null { try { + // `git status` only ever describes the WORKING TREE, so a change that has + // been committed leaves no entry and never enters the candidate set — the + // hash comparison in getChangedFiles is correct but is never reached for + // it, and `pendingChanges` reads 0 while the index is genuinely behind + // (#1829). `sinceCommit` — the commit the index was last brought up to + // date at — adds the other half: what has been committed since. Callers + // that hold no such stamp still get exactly what they always did, the + // working-tree changes. const changes: GitChanges = { modified: [], added: [], deleted: [] }; // Custom extension → language overrides from the project's codegraph.json, // so change detection sees the same custom-extension files the full index does. const overrides = loadExtensionOverrides(rootDir); - collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir)); + collectGitStatus(rootDir, '', changes, overrides, loadIncludeIgnoredMatcher(rootDir), loadExcludeMatcher(rootDir), sinceCommit ?? undefined); return changes; } catch { return null; } } -function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record, includeIgnored: Ignore | null = null, exclude: Ignore | null = null): void { +/** + * Metadata key: the commit the index was last brought up to date at. Written by + * a full index AND by every successful sync — unlike the extraction stamp, which + * a sync must not advance because it only touches a subset of files. This one is + * about the TREE, and a sync does absorb the whole diff it computed. (#1829) + */ +export const INDEXED_AT_COMMIT_KEY = 'indexed_at_commit'; + +/** HEAD's commit sha, or null in a non-git repo or one with no commits yet. */ +export function getGitHeadSha(rootDir: string): string | null { + try { + return execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, + }).trim() || null; + } catch { + return null; + } +} + +/** + * `Apath` / `Mpath` / `Dpath` lines for every path committed + * between `sinceCommit` and HEAD. Empty when the stamp IS HEAD, which is the + * common case — one cheap git call on the hot path. + */ +function gitCommittedChangesSince(repoDir: string, sinceCommit: string): string[] { + // Let getGitChangedFiles return null on failure, selecting the full scan. + const out = execFileSync('git', ['diff', '--name-status', '--no-renames', '-z', sinceCommit, 'HEAD', '--'], { + cwd: repoDir, encoding: 'utf-8', timeout: 10000, maxBuffer: 50 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, + }); + const fields = out.split('\0'); + const lines: string[] = []; + for (let i = 0; i + 1 < fields.length; i += 2) { + lines.push(`${fields[i]}\t${fields[i + 1]}`); + } + return lines; +} + +/** + * Can an INDEX trust the git fast path, given the commit it was built at? + * + * False means "fall back to the full scan" — the expensive path that compares + * every file on disk against the DB, and the only correct read when git cannot + * say what happened between the stamp and now: + * + * - stamp present but unknown to this repo (rebase, gc, shallow clone, a stamp + * from a different checkout) — history moved under the index. + * - stamp absent while the repo HAS commits — an index built before stamping + * existed. One full scan; the next sync stamps it and the fast path returns. + * + * A repo with NO commits keeps the fast path with or without a stamp: every + * file is untracked, so `git status` already sees all of them. Callers with no + * index behind them (the exported `getGitChangedFiles`) never ask this — a + * working-tree diff is the whole of what they wanted. (#1829) + */ +export function canTrustGitFastPath(rootDir: string, sinceCommit?: string | null): boolean { + const head = getGitHeadSha(rootDir); + if (head == null) return true; // no commits (or not a git repo — caller handles that) + if (!sinceCommit) return false; + if (sinceCommit === head) return true; + try { + execFileSync('git', ['cat-file', '-e', `${sinceCommit}^{commit}`], { + cwd: rootDir, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, + }); + return true; + } catch { + return false; + } +} + +function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, overrides?: Record, includeIgnored: Ignore | null = null, exclude: Ignore | null = null, sinceCommit?: string): void { const output = execFileSync( 'git', // `-uall` lists individual untracked files instead of collapsing an @@ -1325,46 +1406,68 @@ function collectGitStatus(repoDir: string, prefix: string, out: GitChanges, over // parent's. (#766) const ig = buildDefaultIgnore(repoDir); - const untrackedDirs: string[] = []; - for (const line of output.split('\n')) { - if (line.length < 4) continue; // Minimum: "XY file" - - const statusCode = line.substring(0, 2); - const rel = normalizePath(line.substring(3)); - - // Untracked directory entries (trailing slash) may hide an embedded repo — - // collect for the recursion below instead of treating as a file. - if (statusCode === '??' && rel.endsWith('/')) { - untrackedDirs.push(rel); - continue; - } - + // One classifier for both candidate sources below, so a committed change is + // filtered by exactly the rules a working-tree change is (#766, #999, #1829). + const classify = (statusCode: string, rel: string): void => { const filePath = normalizePath(prefix + rel); - if (!isSourceFile(filePath, overrides)) continue; + if (!isSourceFile(filePath, overrides)) return; if (statusCode.includes('D')) { // Deletions stay unfiltered: getChangedFiles acts on one only when the // path is already tracked in the DB, where removal is always correct — and // that lets a newly-excluded dir's stale rows clean themselves up. (#766) out.deleted.push(filePath); - continue; + return; } // Added (`??`) / modified files inside an excluded dir must not enter the // index — match against the repo-relative path, same as the full scan. (#766) - if (ig.ignores(rel)) continue; + if (ig.ignores(rel)) return; // User `codegraph.json` `exclude` (#999) is project-root-relative, so it's // matched against the full path — sync must not re-add a tracked file the // full index now keeps out. Deletions above stay unfiltered so a file that // WAS indexed before an exclude was added still cleans itself out. - if (exclude && exclude.ignores(filePath)) continue; + if (exclude && exclude.ignores(filePath)) return; if (statusCode === '??') { out.added.push(filePath); } else { - // M, MM, AM, A (staged), etc. — treat as modified + // M, MM, AM, A (staged), etc. — treat as modified. getChangedFiles + // re-decides added-vs-modified from the DB, so a committed `A` that the + // index never saw still lands in `added`. out.modified.push(filePath); } + }; + + const untrackedDirs: string[] = []; + for (const line of output.split('\n')) { + if (line.length < 4) continue; // Minimum: "XY file" + + const statusCode = line.substring(0, 2); + const rel = normalizePath(line.substring(3)); + + // Untracked directory entries (trailing slash) may hide an embedded repo — + // collect for the recursion below instead of treating as a file. + if (statusCode === '??' && rel.endsWith('/')) { + untrackedDirs.push(rel); + continue; + } + + classify(statusCode, rel); + } + + // Committed but unindexed: everything between the commit this index was last + // brought up to date at and HEAD. `git status` cannot see these — committing + // is precisely what removes a file from its output — so without this pass a + // `git commit` makes a real pending change read as zero (#1829). The stamp + // belongs to the ROOT repo, so the embedded-repo recursion below passes none. + if (sinceCommit) { + for (const line of gitCommittedChangesSince(repoDir, sinceCommit)) { + const tab = line.indexOf('\t'); + if (tab < 1) continue; + // `A`/`M`/`D`/`T`… — padded to porcelain's two columns for `classify`. + classify(`${line.substring(0, tab).charAt(0)} `, normalizePath(line.substring(tab + 1))); + } } // Recurse embedded repos found under untracked dirs (at the dir itself or @@ -1643,6 +1746,9 @@ function resurrectRefFromDroppedEdge( fromNodeId: e.source, referenceName: refName, referenceKind: refKind, + ...(e.sourceLanguage === 'verilog' && Array.isArray(e.metadata?.refCandidates) + && e.metadata.refCandidates.every((c: unknown) => typeof c === 'string') + ? { candidates: e.metadata.refCandidates as string[] } : {}), line: e.line ?? 0, column: e.column ?? 0, filePath: e.sourceFilePath, @@ -1655,6 +1761,51 @@ function resurrectRefFromDroppedEdge( */ export class ExtractionOrchestrator { private rootDir: string; + private hdlContext: HdlIndexContext | null = null; + private hdlForce = false; + private hdlTouched = new Set(); + + private prepareHdlContext(): void { + this.hdlTouched.clear(); + const previous = this.queries.getMetadata('hdl_profile_fingerprint'); + this.hdlContext = new HdlIndexContext(this.rootDir, !!this.queries.getMetadata('hdl_profile_name')); + this.hdlForce = previous !== this.hdlContext.fingerprint && (!!this.hdlContext.profile || !!this.queries.getMetadata('hdl_profile_name')); + } + private forceHdlFile(file: string): boolean { return this.hdlForce && !!this.hdlContext?.isHdl(file); } + private hdlSource(file: string, content: string): string { + if (!this.hdlContext) this.prepareHdlContext(); + return this.hdlContext!.source(file, content); + } + commitHdlProfile(): void { + if (!this.hdlContext) return; + this.queries.setMetadata('hdl_profile_name', this.hdlContext.profile?.name ?? ''); + this.queries.setMetadata('hdl_profile_fingerprint', this.hdlContext.fingerprint); + const context = JSON.parse(this.hdlContext.context()); + if (this.hdlContext.profile) { + if (!this.hdlForce && this.hdlTouched.size < this.hdlContext.profile.files.length) { + const previous = JSON.parse(this.queries.getMetadata('hdl_profile_context') ?? '{}'); + const units = previous.unitDiagnostics ?? {}; + for (const file of this.hdlTouched) units[file] = context.unitDiagnostics[file] ?? []; + context.unitDiagnostics = units; + const combined = Object.values(units).flat(); + context.diagnosticCount = combined.length; + context.diagnostics = combined.slice(0, 200); + context.incomplete = context.diagnostics.length > 0 || (!previous.unitDiagnostics && previous.incomplete === true); + } + const parseIssues = this.hdlContext.profile.files.flatMap(file => { + const record = this.queries.getFileByPath(file); + return record ? (record.errors ?? []).map(error => ({ filePath: file, ...error })) + : [{ filePath: file, message: 'Profile source has not been indexed.' }]; + }); + if (parseIssues.length) { + context.incomplete = true; + context.diagnostics = [...context.diagnostics, ...parseIssues].slice(0, 200); + } + } + this.queries.setMetadata('hdl_profile_context', JSON.stringify(context)); + this.hdlForce = false; + } + private queries: QueryBuilder; /** * Names of frameworks detected for this project, populated by indexAll(). @@ -1817,6 +1968,7 @@ export class ExtractionOrchestrator { // Threaded into language detection so custom-extension files load the right // grammar and store under the mapped language. const overrides = loadExtensionOverrides(this.rootDir); + this.prepareHdlContext(); const log = verbose ? (msg: string) => { console.log(`[worker] ${msg}`); } @@ -1834,7 +1986,7 @@ export class ExtractionOrchestrator { // attributed — these labels settle scan vs framework-detect vs grammars. const tScan = Date.now(); const skipStats: ScanSkipStats = { unsupportedByExtension: new Map() }; - const files = await scanDirectoryAsync(this.rootDir, (current, file) => { + const scannedFiles = await scanDirectoryAsync(this.rootDir, (current, file) => { onProgress?.({ phase: 'scanning', current, @@ -1842,6 +1994,15 @@ export class ExtractionOrchestrator { currentFile: file, }); }, skipStats); + const files = this.hdlContext!.files(scannedFiles); + const allowed = new Set(files); + for (const tracked of this.queries.getAllFiles()) { + if (tracked.language !== 'verilog' || allowed.has(tracked.path)) continue; + const refs = this.queries.getCrossFileIncomingEdgesWithTarget(tracked.path) + .map(resurrectRefFromDroppedEdge).filter((ref): ref is UnresolvedReference => !!ref); + this.queries.deleteFile(tracked.path); + if (refs.length) this.queries.replaceResolutionEdgesWithUnresolvedRefs([], refs); + } if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] scan: ${Date.now() - tScan}ms (${files.length} files)`); /** Only meaningful when nothing was indexable — see IndexResult (#1502). */ const skipSummary = (): Pick => { @@ -1971,8 +2132,9 @@ export class ExtractionOrchestrator { */ const parseFile = (filePath: string, content: string): Promise => { const language = detectLanguage(filePath, content, overrides); - if (!pool) return Promise.resolve(extractFromSource(filePath, content, language, frameworkNames)); - return pool.requestParse({ filePath, content, language, frameworkNames }); + const selected = this.hdlSource(filePath, content); + if (!pool) return Promise.resolve(extractFromSource(filePath, selected, language, frameworkNames)); + return pool.requestParse({ filePath, content: selected, language, frameworkNames }); }; // --- Bounded rolling-window dispatch, ordered commit --- @@ -2002,6 +2164,7 @@ export class ExtractionOrchestrator { const storeResult = async (filePath: string, content: string, stats: fs.Stats, result: ExtractionResult): Promise => { processed++; + if (this.hdlContext?.profile && this.hdlContext.isHdl(filePath)) this.hdlTouched.add(filePath); // WAL hard-cap backstop: between files (never mid-transaction), pause // the store until the off-thread checkpoint catches up. Resolves to @@ -2149,6 +2312,8 @@ export class ExtractionOrchestrator { // Read files in parallel (with path validation before any I/O) const fileContents = await Promise.all( batch.map(async (fp) => { + const snapshot = this.hdlContext?.sources.get(fp); + if (snapshot) return { filePath: fp, content: snapshot.original, stats: snapshot.stats, error: null as Error | null }; try { // Indexing read: follow in-root symlinks the directory walk already // descended into (the `../` guard still applies) so files reached @@ -2361,6 +2526,7 @@ export class ExtractionOrchestrator { .map(line => /^\s*\/\//.test(line) ? '' : line) .join('\n'); + if (this.hdlContext?.profile && this.hdlContext.isHdl(filePath)) continue; let result: ExtractionResult; try { result = await parseFile(filePath, stripped); @@ -2415,6 +2581,11 @@ export class ExtractionOrchestrator { * Index specific files */ async indexFiles(filePaths: string[]): Promise { + this.prepareHdlContext(); + if (this.hdlForce && (this.hdlContext!.profile || this.queries.getMetadata('hdl_profile_name'))) { + throw new Error('HDL profile context changed; use sync or index to update the complete HDL selection'); + } + filePaths = filePaths.filter(file => this.hdlContext!.accepts(file)); const startTime = Date.now(); const errors: ExtractionError[] = []; let filesIndexed = 0; @@ -2475,6 +2646,9 @@ export class ExtractionOrchestrator { }; } + const snapshot = this.hdlContext?.sources.get(relativePath); + if (snapshot) return this.indexFileWithContent(relativePath, snapshot.original, snapshot.stats); + // Read file content and stats let content: string; let stats: fs.Stats; @@ -2560,7 +2734,7 @@ export class ExtractionOrchestrator { // otherwise detect on the spot so single-file re-index paths still emit // route nodes / middleware / etc. const frameworkNames = this.ensureDetectedFrameworks(); - const result = extractFromSource(relativePath, content, language, frameworkNames); + const result = extractFromSource(relativePath, this.hdlSource(relativePath, content), language, frameworkNames); // Store in database await this.storeExtractionResult(relativePath, content, language, stats, result, createYielder()); @@ -2612,6 +2786,7 @@ export class ExtractionOrchestrator { // storing — persisting the transport as-is records the file as having no // symbols at all (#1541). No-op for already-decoded results. result = materializeKernelResult(result, filePath, language); + if (this.hdlContext?.profile && language === 'verilog') this.hdlTouched.add(filePath); // Bulk inserts run in bounded sub-transactions with a yield between, so a // giant generated file (tens of thousands of symbols) can't block the @@ -2629,7 +2804,7 @@ export class ExtractionOrchestrator { // successful retry's symbols — a permanent empty file presented as // recovered (the #1541 wipe, reintroduced through the marker path). const existingFile = this.queries.getFileByPath(filePath); - if (existingFile && existingFile.contentHash === contentHash) { + if (existingFile && existingFile.contentHash === contentHash && !this.forceHdlFile(filePath)) { const existingIsMarker = existingFile.nodeCount === 0 && (existingFile.errors?.length ?? 0) > 0; const incomingHasContent = result.nodes.length > 0; @@ -2837,6 +3012,14 @@ export class ExtractionOrchestrator { const reinserted: Edge[] = []; const resurrected: UnresolvedReference[] = []; for (const e of crossFileIncomingEdges) { + const name = e.metadata?.refName; + // HDL members and ports often share short names within one file. + // Replay their qualified reference instead of reattaching by kind/name. + if (e.sourceLanguage === 'verilog' && typeof name === 'string') { + const ref = resurrectRefFromDroppedEdge(e); + if (ref) resurrected.push(ref); + continue; + } const newTargetId = newNodesByKindName.get(`${e.targetKind}\0${e.targetName}`); if (newTargetId) { reinserted.push({ source: e.source, target: newTargetId, kind: e.kind, metadata: e.metadata, line: e.line, column: e.column, provenance: e.provenance }); @@ -2849,7 +3032,7 @@ export class ExtractionOrchestrator { this.queries.insertEdges(reinserted); } if (resurrected.length > 0) { - this.queries.insertUnresolvedRefsBatch(resurrected); + this.queries.replaceResolutionEdgesWithUnresolvedRefs([], resurrected); } } @@ -2906,8 +3089,28 @@ export class ExtractionOrchestrator { // rebind to the same target is a clean no-op, but leaving the old row in // place for a rebind ELSEWHERE would keep both, turning drift into // duplication. - this.queries.deleteEdgesByIds(edgeIds); - this.queries.insertUnresolvedRefsBatch(refs); + this.queries.replaceResolutionEdgesWithUnresolvedRefs(edgeIds, refs); + return refs.length; + } + + /** Reclassify argument accesses against current function/task formals. + * Existing calls are dependencies; missing signatures also need retries when + * a callee appears, so retain and revisit their plain argument references. */ + resurrectHdlCallArgumentEdges(changedFilePaths: string[], removedFiles = false): number { + if (!changedFilePaths.length && !removedFiles) return 0; + const overrides = loadExtensionOverrides(this.rootDir); + // Removed files are absent from changedFilePaths and their language records + // have already cascaded; existing HDL argument sites still need declassification. + if (!removedFiles && !changedFilePaths.some(file => detectLanguage(file, undefined, overrides) === 'verilog')) return 0; + const fresh = new Set(changedFilePaths); + const candidates = this.queries.getHdlCallArgumentEdges().filter(e => !fresh.has(e.sourceFilePath)); + const ids: number[] = []; + const refs: UnresolvedReference[] = []; + for (const edge of candidates) { + const ref = resurrectRefFromDroppedEdge(edge); + if (ref) { ids.push(edge.edgeId); refs.push(ref); } + } + if (refs.length) this.queries.replaceResolutionEdgesWithUnresolvedRefs(ids, refs); return refs.length; } @@ -2938,6 +3141,8 @@ export class ExtractionOrchestrator { */ backpressure?: () => Promise | null ): Promise { + this.prepareHdlContext(); + if (this.hdlForce) scopedPaths = undefined; await initGrammars(); // Initialize WASM runtime (grammars loaded lazily below) const startTime = Date.now(); let filesChecked = 0; @@ -2991,7 +3196,7 @@ export class ExtractionOrchestrator { currentFiles = unique.filter( (p) => isSourceFile(p, overrides) && - !scope.ignores(p) && + (!scope.ignores(p) || this.hdlContext!.sources.has(p)) && fs.existsSync(path.join(this.rootDir, p)) ); trackedFiles = []; @@ -3018,6 +3223,9 @@ export class ExtractionOrchestrator { trackedFiles = this.queries.getAllFiles(); if (process.env.CODEGRAPH_SYNTH_TIMINGS) console.error(`[phase-timing] sync-tracked-load: ${Date.now() - tTracked}ms (${trackedFiles.length} tracked)`); } + currentFiles = scopedPaths?.length + ? currentFiles.filter(file => this.hdlContext!.accepts(file)) + : this.hdlContext!.files(currentFiles); const currentSet = new Set(currentFiles); const trackedMap = new Map(); for (const f of trackedFiles) { @@ -3048,7 +3256,7 @@ export class ExtractionOrchestrator { .map((e) => resurrectRefFromDroppedEdge(e)) .filter((r): r is UnresolvedReference => r !== null); if (resurrected.length > 0) { - this.queries.insertUnresolvedRefsBatch(resurrected); + this.queries.replaceResolutionEdgesWithUnresolvedRefs([], resurrected); } } this.queries.deleteFile(tracked.path); @@ -3075,7 +3283,7 @@ export class ExtractionOrchestrator { // change that preserves both exactly is the blind spot every mtime-based // incremental tool accepts; `index --force` is the escape hatch. Git bumps // mtime on every file it writes during checkout/merge, so pulls are caught.) - if (tracked) { + if (tracked && !this.forceHdlFile(filePath)) { try { const stat = fs.statSync(fullPath); if (stat.size === tracked.size && Math.floor(stat.mtimeMs) === Math.floor(tracked.modifiedAt)) { @@ -3101,7 +3309,7 @@ export class ExtractionOrchestrator { filesToIndex.push(filePath); changedFilePaths.push(filePath); filesAdded++; - } else if (tracked.contentHash !== contentHash) { + } else if (tracked.contentHash !== contentHash || this.forceHdlFile(filePath)) { filesToIndex.push(filePath); changedFilePaths.push(filePath); filesModified++; @@ -3176,7 +3384,20 @@ export class ExtractionOrchestrator { * Uses git status as a fast path when available, falling back to full scan. */ getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } { - const gitChanges = getGitChangedFiles(this.rootDir); + const profile = loadHdlProfile(this.rootDir).profile; + const profileFiles = profile ? new Set(profile.files) : null; + const profileOverrides = loadExtensionOverrides(this.rootDir); + const allowed = (file: string) => !profileFiles || detectLanguage(file, undefined, profileOverrides) !== 'verilog' || profileFiles.has(file); + + // The commit this index was last brought up to date at. Absent on an index + // built before stamping existed — getGitChangedFiles then declines the fast + // path and the full scan below answers correctly, once, until a sync or a + // full index writes the stamp. (#1829) + let sinceCommit: string | null = null; + try { sinceCommit = this.queries.getMetadata(INDEXED_AT_COMMIT_KEY) ?? null; } catch { /* advisory */ } + const gitChanges = canTrustGitFastPath(this.rootDir, sinceCommit) + ? getGitChangedFiles(this.rootDir, sinceCommit) + : null; if (gitChanges) { // === Git fast path === @@ -3184,10 +3405,21 @@ export class ExtractionOrchestrator { const modified: string[] = []; const removed: string[] = []; + // One file can now reach these lists from two candidate sources — the + // working tree AND the committed diff — when it was committed and then + // edited again. It is still one changed file: counting it twice would + // inflate `pendingChanges` and hand sync the same path twice. (#1829) + // A committed deletion can have been recreated on disk. Classify that + // path by its current content rather than deleting its indexed symbols. + const seenGone = new Set(); + const seenHere = new Set(); + // Deleted files — only report if tracked in DB for (const filePath of gitChanges.deleted) { + if (seenGone.has(filePath)) continue; + seenGone.add(filePath); const tracked = this.queries.getFileByPath(filePath); - if (tracked) { + if (tracked && !fs.existsSync(path.join(this.rootDir, filePath))) { removed.push(filePath); } } @@ -3196,8 +3428,19 @@ export class ExtractionOrchestrator { // files stay untracked in git even after indexing, so they must be // hash-compared like modified files instead of always counting as added — // otherwise status reports them as pending forever. (See issue #206.) - for (const filePath of [...gitChanges.modified, ...gitChanges.added]) { + for (const filePath of [...gitChanges.modified, ...gitChanges.added, ...gitChanges.deleted, ...(profile?.files ?? [])]) { + if (!allowed(filePath)) continue; + if (seenHere.has(filePath)) continue; + seenHere.add(filePath); const fullPath = path.join(this.rootDir, filePath); + if (profileFiles?.has(filePath)) { + try { + if (fs.statSync(fullPath).size > MAX_FILE_SIZE) { + (this.queries.getFileByPath(filePath) ? modified : added).push(filePath); + continue; + } + } catch { continue; } + } let content: string; try { content = fs.readFileSync(fullPath, 'utf-8'); @@ -3220,7 +3463,8 @@ export class ExtractionOrchestrator { } // === Fallback: full scan (non-git project or git failure) === - const currentFiles = new Set(scanDirectory(this.rootDir)); + const scanned = scanDirectory(this.rootDir).filter(allowed); + const currentFiles = new Set(profile ? [...scanned, ...profile.files] : scanned); const trackedFiles = this.queries.getAllFiles(); // Build Map for O(1) lookups @@ -3235,7 +3479,7 @@ export class ExtractionOrchestrator { // Find removed files for (const tracked of trackedFiles) { - if (!currentFiles.has(tracked.path)) { + if (!currentFiles.has(tracked.path) || !fs.existsSync(path.join(this.rootDir, tracked.path))) { removed.push(tracked.path); } } @@ -3243,6 +3487,14 @@ export class ExtractionOrchestrator { // Find added and modified files for (const filePath of currentFiles) { const fullPath = path.join(this.rootDir, filePath); + if (profileFiles?.has(filePath)) { + try { + if (fs.statSync(fullPath).size > MAX_FILE_SIZE) { + (trackedMap.has(filePath) ? modified : added).push(filePath); + continue; + } + } catch { continue; } + } let content: string; try { content = fs.readFileSync(fullPath, 'utf-8'); diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index afce3a014..462537ab1 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -41,21 +41,56 @@ function findDeclaratorQualifiedId(declarator: SyntaxNode): SyntaxNode | undefin * (`TEST` never matches; K&R C definitions have lowercase names); * - the first "parameter" is a LONE identifier (no type, no declarator) * containing a lowercase letter — the name being defined; - * - at least one more parameter follows and NONE of them is another lone - * identifier — a second bare arg means the first isn't the name (gtest's + * - additional parameters, if present, must not be lone identifiers; with + * only one argument, a same-file #define must declare that parameter as + * the function name. A second bare arg means the first isn't the name (gtest's * `TEST_F(Fixture, Name)`, `PYBIND11_MODULE(ext, m)`, * google-benchmark's `BENCHMARK_DEFINE_F(Fix, name)` all bail here). */ +function singleParameterMacroNamesFunction(node: SyntaxNode, macroName: string, source: string): boolean { + let root = node; + while (root.parent) root = root.parent; + let definition: SyntaxNode | undefined; + const pending = [root]; + while (pending.length) { + const candidate = pending.pop()!; + if (candidate.startIndex >= node.startIndex) continue; + if (candidate.type === 'preproc_function_def' && + getChildByField(candidate, 'name')?.text === macroName && + (!definition || candidate.startIndex > definition.startIndex)) definition = candidate; + pending.push(...candidate.namedChildren); + } + if (!definition) return false; + const params = getChildByField(definition, 'parameters'); + const value = getChildByField(definition, 'value'); + const parameter = params?.namedChild(0); + if (params?.namedChildCount !== 1 || parameter?.type !== 'identifier' || !value) return false; + // The first declarator's name must be the parameter. A later use inside + // noexcept(name()), an attribute or another expression is not a definition. + const head = getNodeText(value, source).split('(')[0]?.trim() ?? ''; + const match = head.match(/^(?:[A-Za-z_]\w*(?:::\w+)*(?:[ \t]+|[*&][ \t]*))+([A-Za-z_]\w*)$/); + return match?.[1] === getNodeText(parameter, source); +} + function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string | undefined { - if (node.type !== 'function_definition') return undefined; + if (node.type !== 'function_definition' || getChildByField(node, 'type')) return undefined; const declarator = getChildByField(node, 'declarator'); if (declarator?.type !== 'function_declarator') return undefined; const inner = getChildByField(declarator, 'declarator'); if (inner?.type !== 'identifier') return undefined; const macroName = getNodeText(inner, source); + // A real constructor has no return type too. Its owner is authoritative, + // including when a same-named macro appeared earlier and was undefined. + for (let owner = node.parent; owner; owner = owner.parent) { + if (owner.type === 'class_specifier' || owner.type === 'struct_specifier') { + const ownerName = getChildByField(owner, 'name'); + if (ownerName && stripCppTemplateArgs(getNodeText(ownerName, source)) === macroName) return undefined; + break; + } + } if (!/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/.test(macroName)) return undefined; const params = getChildByField(declarator, 'parameters'); - if (!params || params.namedChildCount < 2) return undefined; + if (!params || params.namedChildCount < 1) return undefined; const loneIdentText = (p: SyntaxNode): string | null => p.type === 'parameter_declaration' && p.namedChildCount === 1 && @@ -65,6 +100,11 @@ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string | const first = params.namedChild(0); const name = first ? loneIdentText(first) : null; if (!name || !/[a-z]/.test(name)) return undefined; + if (params.namedChildCount === 1) { + // A single bare argument is ambiguous unless this file defines the macro + // as a function declaration using its parameter as the function name. + if (!singleParameterMacroNamesFunction(node, macroName, source)) return undefined; + } for (let i = 1; i < params.namedChildCount; i++) { const p = params.namedChild(i); if (p && loneIdentText(p) !== null) return undefined; @@ -211,6 +251,15 @@ export const cExtractor: LanguageExtractor = { preParse: preParseCSource, // Universal net: recover a real name from any macro-mangled function name. recoverMangledName: recoverMangledCppName, + resolveName: (node, source) => { + const declarator = getChildByField(node, 'declarator'); + const name = declarator?.namedChild(0); + // C parses FN(name) { ... } as type FN + (name) declarator. + if (node.type === 'function_definition' && declarator?.type === 'parenthesized_declarator' && + declarator.namedChildCount === 1 && name?.type === 'identifier' && + singleParameterMacroNamesFunction(node, getChildByField(node, 'type')?.text ?? '', source)) return getNodeText(name, source); + return undefined; + }, functionTypes: ['function_definition'], classTypes: [], methodTypes: [], @@ -1728,6 +1777,20 @@ export const cppExtractor: LanguageExtractor = { resolveName: extractCppQualifiedMethodName, getReceiverType: extractCppReceiverType, getReturnType: extractCppReturnType, + getSignature: (node, source) => { + if (getChildByField(node, 'type') || recoverCppMacroDefinedName(node, source)) return undefined; + // Macro invocation parameters are not the generated function signature. + // Only method/constructor syntax contributes this parameter-only signature. + let isMethod = !!extractCppReceiverType(node, source); + for (let owner = node.parent; !isMethod && owner; owner = owner.parent) { + if (owner.type === 'field_declaration_list') { isMethod = true; break; } + if (owner.type === 'function_definition' || owner.type === 'compound_statement') break; + } + if (!isMethod) return undefined; + const declarator = getChildByField(node, 'declarator'); + const parameters = declarator && getChildByField(declarator, 'parameters'); + return parameters ? getNodeText(parameters, source) : undefined; + }, getVisibility: (node) => { // Check for access specifier in parent const parent = node.parent; diff --git a/src/extraction/languages/dart.ts b/src/extraction/languages/dart.ts index 8fc7bdc9f..db520e287 100644 --- a/src/extraction/languages/dart.ts +++ b/src/extraction/languages/dart.ts @@ -40,7 +40,7 @@ function dartEnclosingTypeName(node: SyntaxNode): string | undefined { while (p) { if ( p.type === 'class_definition' || p.type === 'mixin_declaration' || - p.type === 'extension_declaration' || p.type === 'enum_declaration' + p.type === 'extension_declaration' || p.type === 'extension_type_declaration' || p.type === 'enum_declaration' ) { return p.childForFieldName('name')?.text; } @@ -132,7 +132,7 @@ export const dartExtractor: LanguageExtractor = { importTypes: ['import_or_export'], callTypes: [], // Dart calls use identifier+selector, handled via extractBareCall variableTypes: [], - extraClassNodeTypes: ['mixin_declaration', 'extension_declaration'], + extraClassNodeTypes: ['mixin_declaration', 'extension_declaration', 'extension_type_declaration'], // A Dart `static_final_declaration` is exactly a top-level or class-`static` // `const`/`final` — the shared-constant idiom — so extract it as `constant` // for value-reference edges. Instance fields, `var`, and typed declarations diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 6b760b01d..ed11e1908 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -24,6 +24,7 @@ import { dartExtractor } from './dart'; import { pascalExtractor } from './pascal'; import { scalaExtractor } from './scala'; import { luaExtractor } from './lua'; +import { verilogExtractor } from './verilog'; import { rExtractor } from './r'; import { luauExtractor } from './luau'; import { objcExtractor } from './objc'; @@ -57,6 +58,7 @@ export const EXTRACTORS: Partial> = { pascal: pascalExtractor, scala: scalaExtractor, lua: luaExtractor, + verilog: verilogExtractor, r: rExtractor, luau: luauExtractor, objc: objcExtractor, diff --git a/src/extraction/languages/verilog-call-access.ts b/src/extraction/languages/verilog-call-access.ts new file mode 100644 index 000000000..7c5b374d5 --- /dev/null +++ b/src/extraction/languages/verilog-call-access.ts @@ -0,0 +1,80 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { ExtractorContext } from '../tree-sitter-types'; + +export const VERILOG_CALL_ARGUMENT_PREFIX = 'hdl:call-arg:'; +export interface VerilogCallArgument { + callee: string; + callableId: string; + scope: string; + argument: number | string; + slots: Array<{ key: number | string; omitted: boolean }>; + role: 'value' | 'index'; + writeTarget: boolean; + referenceTarget: boolean; +} + +function assignable(node: SyntaxNode, allowConcat = true): boolean { + if (node.type === 'expression') return node.namedChildCount === 1 && !!node.firstNamedChild && assignable(node.firstNamedChild, allowConcat); + if (node.type === 'primary') { + const first = node.firstNamedChild; + return !!first && assignable(first, allowConcat) && node.namedChildren.slice(1).every(n => n.type === 'select' + && !n.namedChildren.some(c => ['simple_identifier', 'escaped_identifier'].includes(c.type))); + } + if (node.type === 'hierarchical_identifier') return node.namedChildCount === 1 + && ['simple_identifier', 'escaped_identifier'].includes(node.firstNamedChild?.type ?? ''); + if (node.type === 'concatenation') return allowConcat && node.namedChildren.length > 0 && node.namedChildren.every(n => assignable(n, allowConcat)); + return false; +} + +/** Annotate an already discovered local signal occurrence; never emit a second edge. */ +export function getVerilogCallArgumentCandidates(id: SyntaxNode, ctx: ExtractorContext): string[] { + let argument: SyntaxNode = id; + let role: VerilogCallArgument['role'] = 'value'; + while (argument.parent && argument.parent.type !== 'list_of_arguments') { + argument = argument.parent; + if (['select', 'constant_select', 'bit_select', 'constant_bit_select', 'part_select_range'].includes(argument.type)) role = 'index'; + if (['function_declaration', 'task_declaration', 'module_declaration'].includes(argument.type)) return []; + } + const list = argument.parent; + if (!list || list.hasError || argument.type !== 'expression') return []; + const call = list.parent?.type === 'method_call_body' ? list.parent.parent : list.parent; + if (!call || call.hasError) return []; + let callee: string | undefined; + if (call.type === 'tf_call') callee = call.namedChildren.find(n => n.type === 'hierarchical_identifier')?.text.trim(); + if (call.type === 'method_call') { + const receiver = call.namedChildren.find(n => n.type === 'primary'); + const name = list.parent?.childForFieldName('name'); + if (receiver && name && /^\s*::\s*$/.test(ctx.source.slice(receiver.endIndex, name.startIndex))) callee = `${receiver.text.trim()}::${name.text.trim()}`; + } + if (!callee) return []; + const segments: SyntaxNode[][] = [[]]; + for (const child of list.children) { + if (child.type === ',') segments.push([]); + else if (!['comment', 'block_comment', 'one_line_comment'].includes(child.type)) segments[segments.length - 1]!.push(child); + } + const slots: VerilogCallArgument['slots'] = []; + let selected: number | string | undefined; + let namedSeen = false; + for (let index = 0; index < segments.length; index++) { + const segment = segments[index]!; + const named = segment[0]?.type === '.'; + const expression = segment.find(n => n.type === 'expression'); + let key: number | string = index; + if (named) { + namedSeen = true; + const name = segment.find(n => ['simple_identifier', 'escaped_identifier'].includes(n.type)); + if (!name) return []; + key = name.text.trim(); + } else if (namedSeen) return []; // A positional actual cannot follow named ones. + slots.push({ key, omitted: !expression }); + if (expression?.id === argument.id) selected = key; + } + if (selected === undefined) return []; + const lexical = ctx.nodes.find(n => n.id === ctx.nodeStack[ctx.nodeStack.length - 1]); + const owner = [...ctx.nodeStack].reverse().map(nodeId => ctx.nodes.find(n => n.id === nodeId)) + .find(n => n?.kind === 'function' || n?.kind === 'method') ?? lexical; + if (!owner || !lexical) return []; + const metadata: VerilogCallArgument = { callee, callableId: owner.id, scope: lexical.qualifiedName, + argument: selected, slots, role, writeTarget: assignable(argument), referenceTarget: assignable(argument, false) }; + return [`${VERILOG_CALL_ARGUMENT_PREFIX}${JSON.stringify(metadata)}`]; +} diff --git a/src/extraction/languages/verilog-generate.ts b/src/extraction/languages/verilog-generate.ts new file mode 100644 index 000000000..6a5c1f55c --- /dev/null +++ b/src/extraction/languages/verilog-generate.ts @@ -0,0 +1,33 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { ExtractorContext } from '../tree-sitter-types'; +import { getNodeText } from '../tree-sitter-helpers'; +import { addVerilogSignalReferences } from './verilog-signals'; + +function identifier(node: SyntaxNode): SyntaxNode | undefined { + return node.namedChildren.find(n => ['simple_identifier', 'escaped_identifier'].includes(n.type)); +} + +/** A generate iteration is a declaration template, never a guessed elaborated value. */ +export function initializeVerilogGenerateScope(block: SyntaxNode, ctx: ExtractorContext): void { + const loop = block.parent; + if (loop?.type !== 'loop_generate_construct') return; + const init = loop.namedChildren.find(n => n.type === 'genvar_initialization'); + const name = init && identifier(init); + if (!init || !name) return; + const binding = ctx.createNode('constant', getNodeText(name, ctx.source), init, { + signature: getNodeText(init, ctx.source), decorators: ['hdl:generate-parameter', 'hdl:template'], + }); + if (binding) addVerilogSignalReferences(init, ctx, binding.id); +} + +export function visitVerilogGenvarDeclaration(node: SyntaxNode, ctx: ExtractorContext): boolean { + if (node.type !== 'genvar_declaration') return false; + const list = node.namedChildren.find(n => n.type === 'list_of_genvar_identifiers'); + for (const name of list?.namedChildren ?? []) { + if (!['simple_identifier', 'escaped_identifier'].includes(name.type)) continue; + ctx.createNode('constant', getNodeText(name, ctx.source), name, { + signature: getNodeText(node, ctx.source), decorators: ['hdl:genvar', 'hdl:template'], + }); + } + return true; +} diff --git a/src/extraction/languages/verilog-packages.ts b/src/extraction/languages/verilog-packages.ts new file mode 100644 index 000000000..5440c479e --- /dev/null +++ b/src/extraction/languages/verilog-packages.ts @@ -0,0 +1,46 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { ExtractorContext } from '../tree-sitter-types'; + +/** Keep qualification: a package or hierarchical call must never become a bare global call. */ +export function getVerilogCallName(node: SyntaxNode, _source: string): string | undefined { + const callee = node.namedChildren.find(c => c.type === 'hierarchical_identifier' || c.type === 'simple_identifier'); + return callee?.text.trim(); +} + +/** Package calls use method_call in this grammar; modports are named interface views. */ +export function handleVerilogPackageNode(node: SyntaxNode, ctx: ExtractorContext): boolean { + if (node.type === 'modport_item') { + const name = node.namedChildren.find(c => c.type === 'simple_identifier'); + const view = name ? ctx.createNode('interface', name.text, node, { signature: `modport ${node.text.trim()}` }) : null; + if (view) { + const visit = (part: SyntaxNode): void => { + if (part.type === 'modport_simple_port' && part.namedChildCount === 1 && part.firstNamedChild?.type === 'simple_identifier') { + ctx.addUnresolvedReference({ fromNodeId: view.id, + referenceName: `hdl:signal:${part.firstNamedChild.text}`, referenceKind: 'references', + line: part.startPosition.row + 1, column: part.startPosition.column }); + } else for (const child of part.namedChildren) visit(child); + }; + visit(node); + } + return true; + } + if (node.type !== 'method_call') return false; + const primary = node.namedChildren.find(c => c.type === 'primary'); + const body = node.namedChildren.find(c => c.type === 'method_call_body'); + const name = body?.childForFieldName('name'); + // Require the literal package separator between AST nodes, excluding object methods. + if (primary && name && /^\s*::\s*$/.test(ctx.source.slice(primary.endIndex, name.startIndex))) { + const lexical = ctx.nodes.find(n => n.id === ctx.nodeStack[ctx.nodeStack.length - 1]); + const callable = [...ctx.nodeStack].reverse().map(id => ctx.nodes.find(n => n.id === id)) + .find(n => n && (n.kind === 'function' || n.kind === 'method')); + const fromNodeId = callable?.id ?? lexical?.id; + if (fromNodeId) ctx.addUnresolvedReference({ fromNodeId, + ...(lexical?.kind === 'namespace' && lexical.id !== fromNodeId + ? { candidates: [`hdl:scope:${lexical.qualifiedName}`] } : {}), + referenceName: `${primary.text.trim()}::${name.text}`, referenceKind: 'calls', + line: node.startPosition.row + 1, column: node.startPosition.column }); + for (const child of body!.namedChildren) if (child.id !== name.id) ctx.visitNode(child); + return true; + } + return false; +} diff --git a/src/extraction/languages/verilog-signals.ts b/src/extraction/languages/verilog-signals.ts new file mode 100644 index 000000000..fdfd40ff9 --- /dev/null +++ b/src/extraction/languages/verilog-signals.ts @@ -0,0 +1,387 @@ +import { getVerilogCallArgumentCandidates } from './verilog-call-access'; +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { ExtractorContext } from '../tree-sitter-types'; +import { getNodeText } from '../tree-sitter-helpers'; + +function children(node: SyntaxNode): SyntaxNode[] { + return node.namedChildren; +} +function identifier(node: SyntaxNode): SyntaxNode | undefined { + return children(node).find(n => n.type === 'simple_identifier' || n.type === 'escaped_identifier'); +} +function walkChildren(node: SyntaxNode, ctx: ExtractorContext): void { + for (const child of children(node)) ctx.visitNode(child); +} +function direction(node: SyntaxNode, ctx: ExtractorContext): string | undefined { + for (const child of children(node)) { + if (child.type === 'port_direction') return getNodeText(child, ctx.source); + const nested = direction(child, ctx); + if (nested) return nested; + } + return undefined; +} + +/** Authoritative source header order for positional instance connections. + * null means the syntax is incomplete or needs preprocessing/elaboration. + * [] explicitly describes a module with no ports. Body declaration order is + * never used: legacy non-ANSI declarations can legally be reordered. + */ +export function getVerilogPortOrder(node: SyntaxNode, source: string): string[] | null { + const headers = new Set(['module_ansi_header', 'module_nonansi_header', 'module_header', + 'interface_ansi_header', 'interface_nonansi_header', 'program_ansi_header', 'program_nonansi_header']); + const header = children(node).find(n => headers.has(n.type)); + if (!header || node.hasError) return null; + const uncertain = (current: SyntaxNode): boolean => { + if (current.type.endsWith('_comment') || current.type === 'comment') return false; + return current.isMissing || current.type.includes('directive') || current.type.includes('macro') + || children(current).some(uncertain); + }; + if (uncertain(header)) return null; + const list = children(header).find(n => ['list_of_ports', 'list_of_port_declarations'].includes(n.type)); + if (!list) return []; + const names: string[] = []; + const seen = new Set(); + for (const port of children(list)) { + if (port.type.endsWith('_comment') || port.type === 'comment') continue; + let id: SyntaxNode | null | undefined; + if (port.type === 'ansi_port_declaration') { + if (getNodeText(port, source).trimStart().startsWith('.')) return null; + id = port.childForFieldName('port_name'); + } else if (port.type === 'port') { + id = identifier(port); + if (!id || children(port).length !== 1 || getNodeText(port, source).trim() !== getNodeText(id, source).trim()) return null; + } else return null; + if (!id || !['simple_identifier', 'escaped_identifier'].includes(id.type)) return null; + const name = getNodeText(id, source).trim(); + // Escaping an otherwise simple name does not create a distinct HDL port. + const canonical = name.replace(/^\\/, ''); + if (!canonical || seen.has(canonical)) return null; + seen.add(canonical); + names.push(name); + } + return names; +} + +/** Generate iteration variables are implicit local parameters in their body. + * Do not let an identically named outer signal stand in for that binding. */ +export function isVerilogGenerateBinding(node: SyntaxNode, ctx: ExtractorContext, name: string): boolean { + for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) { + if (ancestor.type === 'loop_generate_construct') { + const init = children(ancestor).find(n => n.type === 'genvar_initialization'); + const id = init && identifier(init); + if (id && getNodeText(id, ctx.source) === name) { + for (let i = ctx.nodeStack.length - 1; i >= 0; i--) { + const scope = ctx.nodes.find(n => n.id === ctx.nodeStack[i]); + if (scope && ctx.nodes.some(n => n.qualifiedName === `${scope.qualifiedName}::${name}` && n.decorators?.includes('hdl:generate-parameter'))) return false; + } + return true; + } + } + if (['module_declaration', 'interface_declaration', 'program_declaration'].includes(ancestor.type)) break; + } + return false; +} + +/** Describe the syntactic role of an occurrence, not simulated drive/race behavior. + * Calls and other unknown constructs remain ordinary references unless a + * separate argument resolver can prove their formal direction. + */ +function signalAccess(id: SyntaxNode, source: string): string[] { + let lhs = false; + const role = (kind: string): string[] => { + const roles = [`hdl:access:${kind}`]; + if (kind === 'write' || kind === 'readwrite') { + for (let context: SyntaxNode | null = id; context; context = context.parent) { + if (['statement_item', 'list_of_arguments'].includes(context.type)) break; + if (['cond_predicate', 'case_expression', 'case_item_expression'].includes(context.type) + || (context.type === 'expression' && ['loop_statement', 'wait_statement'].includes(context.parent?.type ?? ''))) { + roles.push('hdl:access:control'); + break; + } + } + } + return roles; + }; + for (let current: SyntaxNode | null = id; current; current = current.parent) { + if (['select', 'constant_select', 'bit_select', 'constant_bit_select', 'constant_indexed_range', 'constant_range'].includes(current.type)) return role('read'); + if (['variable_lvalue', 'net_lvalue'].includes(current.type)) lhs = true; + if (current.type === 'list_of_arguments') return []; + if (current.type === 'inc_or_dec_expression') return lhs ? role('readwrite') : []; + if (['operator_assignment', 'blocking_assignment', 'nonblocking_assignment', 'net_assignment', 'variable_assignment'].includes(current.type)) { + const op = children(current).find(n => n.type === 'assignment_operator'); + return role(lhs ? (op && getNodeText(op, source).trim() !== '=' ? 'readwrite' : 'write') : 'read'); + } + if (current.type === 'delay_control') return role('read'); + if (current.type === 'jump_statement' && current.children.some(n => n.type === 'return')) return role('read'); + if (current.type === 'event_expression') { + const iff = current.children.find(n => n.type === 'iff'); + if (iff && id.startIndex > iff.endIndex) return role('control'); + const edge = children(current).find(n => n.type === 'edge_identifier'); + const text = edge && getNodeText(edge, source).trim(); + return [...role('event'), ...(['posedge', 'negedge'].includes(text ?? '') ? [`hdl:event:${text}`] : [])]; + } + if (['cond_predicate', 'case_expression', 'case_item_expression'].includes(current.type) + || (current.type === 'expression' && ['loop_statement', 'wait_statement'].includes(current.parent?.type ?? ''))) return role('control'); + if (['variable_decl_assignment', 'net_decl_assignment', 'for_variable_declaration', 'tf_port_item'].includes(current.type)) return role('read'); + if (['statement_item', 'function_body_declaration', 'task_body_declaration', 'module_declaration'].includes(current.type)) return []; + } + return []; +} + +function hasIncompleteProceduralScope(node: SyntaxNode): boolean { + for (let ancestor: SyntaxNode | null = node; ancestor; ancestor = ancestor.parent) { + if (['always_construct', 'initial_construct', 'final_construct', 'function_declaration', 'task_declaration', 'seq_block', 'par_block', 'loop_statement'].includes(ancestor.type) && ancestor.hasError) return true; + if (['module_declaration', 'interface_declaration', 'program_declaration'].includes(ancestor.type)) break; + } + return false; +} + +/** Emit exact syntactic occurrences and bounded source access roles. + * A dedicated resolver must bind these only in the lexical scope of the owner. + * Dotted hierarchical accesses are deliberately excluded: their root is not + * sufficient evidence that the terminal signal belongs to this module. + */ +export function addVerilogSignalReferences(node: SyntaxNode, ctx: ExtractorContext, ownerId: string, lexicalScope = false): void { + // Parser recovery can lose a declaration while retaining its later uses. + // Keep the source nodes, but do not bind names from an incomplete procedural + // region to outer declarations. An unrelated valid process remains usable. + if (hasIncompleteProceduralScope(node)) return; + + const skip = new Set(['data_declaration', 'net_declaration', 'for_variable_declaration', 'tf_port_list', 'tf_port_declaration', 'tf_call', 'ps_or_hierarchical_function_identifier']); + const emit = (id: SyntaxNode): void => { + const name = getNodeText(id, ctx.source).trim(); + if (isVerilogGenerateBinding(id, ctx, name)) return; + const syntax = signalAccess(id, ctx.source); + // i++ remains a read/write occurrence even when its resulting value is + // passed to an input formal. Formal direction cannot erase that side effect. + const explicitEffect = syntax.some(c => c === 'hdl:access:write' || c === 'hdl:access:readwrite'); + ctx.addUnresolvedReference({ fromNodeId: ownerId, referenceName: `hdl:signal:${name}`, + referenceKind: 'references', line: id.startPosition.row + 1, column: id.startPosition.column, + candidates: [...syntax, ...(explicitEffect ? [] : getVerilogCallArgumentCandidates(id, ctx))] }); + }; + const visit = (current: SyntaxNode): void => { + if (lexicalScope && current.id !== node.id && ['seq_block', 'par_block', 'loop_statement'].includes(current.type)) return; + // A package-qualified call is also parsed as method_call. Its receiver + // may name a package, never evidence for an identically named local port. + if (current.type === 'method_call') { + const body = children(current).find(n => n.type === 'method_call_body'); + const args = body?.namedChildren.find(n => n.type === 'list_of_arguments'); + if (args) visit(args); + return; + } + if (skip.has(current.type)) { + // Function arguments still contain ordinary signal expressions. + if (current.type === 'tf_call') { + for (const child of children(current)) if (child.type === 'list_of_arguments') visit(child); + } + return; + } + if (['variable_lvalue', 'primary', 'constant_primary'].includes(current.type) && children(current).some(n => n.type.endsWith('_scope') || n.type === 'implicit_class_handle')) { + // p::x (on either side) is a package variable, even though the grammar wraps x in a + // single hierarchical_identifier. Keep index expressions, not its name. + for (const child of children(current)) { + if (['select', 'constant_select', 'variable_lvalue'].includes(child.type)) visit(child); + } + return; + } + if (current.type === 'net_lvalue') { + const id = identifier(current); + const select = children(current).find(n => n.type === 'constant_select'); + // The grammar puts `.member` identifiers directly inside constant_select, + // whereas bit/part-select expressions have their own nested AST nodes. + const hasMember = select && children(select).some(n => ['simple_identifier', 'escaped_identifier'].includes(n.type)); + const hasQualifier = children(current).some(n => n.id !== id?.id && !['constant_select', 'net_lvalue'].includes(n.type)); + if (id && !hasMember && !hasQualifier) emit(id); + // Concatenations recurse through nested net_lvalue; bounds recurse through + // constant_select. Bare member identifiers are never emitted by this walk. + for (const child of children(current)) if (child.id !== id?.id) visit(child); + return; + } + if (current.type === 'hierarchical_identifier' || current.type === 'constant_primary' || current.type === 'delay_value') { + const id = identifier(current); + // A single AST identifier is local evidence; qualified/member expressions + // must not collapse to a terminal name merely because that signal exists. + if (id && children(current).length === 1 && getNodeText(id, ctx.source).trim() === getNodeText(current, ctx.source).trim()) { + emit(id); + return; + } + if (current.type === 'hierarchical_identifier') { + for (const child of children(current)) { + if (['constant_bit_select', 'bit_select', 'select'].includes(child.type)) visit(child); + } + return; + } + } + for (const child of children(current)) visit(child); + }; + visit(node); +} + +/** Source-order/direction evidence for function/task actual arguments. */ +function formalMetadata(node: SyntaxNode, id: SyntaxNode, source: string): string[] { + for (let ancestor: SyntaxNode | null = node; ancestor; ancestor = ancestor.parent) { + if (['function_declaration', 'task_declaration'].includes(ancestor.type)) { + if (ancestor.hasError) return []; + break; + } + } + const uncertain = (part: SyntaxNode): boolean => !part.type.endsWith('_comment') + && (part.type.includes('directive') || part.type.includes('macro') || children(part).some(uncertain)); + if (node.type === 'tf_port_item' && node.parent && uncertain(node.parent)) return []; + const declarations: SyntaxNode[] = []; + if (node.type === 'tf_port_item') { + declarations.push(...(node.parent?.namedChildren.filter(n => n.type === 'tf_port_item') ?? [])); + } else if (node.type === 'tf_port_declaration') { + const body = node.parent?.parent; + if (!body || !['task_body_declaration', 'function_body_declaration'].includes(body.type) || uncertain(body)) return []; + for (const item of children(body)) { + declarations.push(...children(item).filter(n => n.type === 'tf_port_declaration')); + } + } else return []; + let index = 0; + let inherited = 'input'; + for (const declaration of declarations) { + const directionNode = children(declaration).find(n => n.type === 'tf_port_direction'); + const direct = directionNode && getNodeText(directionNode, source).trim().replace(/\s+/g, '-'); + const direction = direct ?? (node.type === 'tf_port_item' ? inherited : 'input'); + inherited = direction; + const list = declaration.type === 'tf_port_declaration' + ? children(declaration).find(n => n.type === 'list_of_tf_variable_identifiers') : declaration; + for (const name of list ? children(list).filter(n => ['simple_identifier', 'escaped_identifier'].includes(n.type)) : []) { + if (name.id === id.id) { + const hasDefault = name.nextNamedSibling?.type === 'expression'; + return [`hdl:formal-index:${index}`, ...(['input', 'output', 'inout', 'ref', 'const-ref'].includes(direction) ? [`hdl:direction:${direction}`] : []), ...(hasDefault ? ['hdl:default'] : [])]; + } + index++; + } + } + return []; +} + +/** HDL declarations and executable blocks; called before generic recursion. */ +export function visitVerilogSignals(node: SyntaxNode, ctx: ExtractorContext): boolean { + if (['seq_block', 'par_block', 'loop_statement'].includes(node.type)) { + // Generate blocks have their own handler; these nodes are procedural scopes. + const keyword = node.children[0]?.text ?? 'block'; + const label = node.type === 'loop_statement' ? undefined : identifier(node); + const kind = node.type === 'loop_statement' ? keyword : node.type === 'par_block' ? 'fork' : 'block'; + const name = label ? getNodeText(label, ctx.source) : `${kind}@${node.startPosition.row + 1}:${node.startPosition.column}`; + const created = ctx.createNode('namespace', name, node, { decorators: ['hdl:procedural-scope'] }); + if (created) { + ctx.pushScope(created.id); + addVerilogSignalReferences(node, ctx, created.id, true); + } + walkChildren(node, ctx); + if (created) ctx.popScope(); + return true; + } + if (['tf_port_item', 'tf_port_declaration', 'for_variable_declaration', 'loop_variables'].includes(node.type)) { + const list = node.type === 'tf_port_declaration' + ? children(node).find(n => n.type === 'list_of_tf_variable_identifiers') : node; + const ids = list ? children(list).filter(n => ['simple_identifier', 'escaped_identifier'].includes(n.type)) : []; + for (const id of ids) { + const created = ctx.createNode('field', getNodeText(id, ctx.source), id, + { signature: getNodeText(node, ctx.source), decorators: [node.type.startsWith('tf_') ? 'hdl:formal' : 'hdl:signal', ...formalMetadata(node, id, ctx.source)] }); + // Declarator initializers belong to this declaration's lexical scope. + if (created) { + for (let next = id.nextNamedSibling; next && !ids.some(n => n.id === next!.id); next = next.nextNamedSibling) { + if (next.type === 'expression' || next.type === 'constant_expression') addVerilogSignalReferences(next, ctx, created.id, true); + } + } + } + // Visit each initializer once with the enclosing callable as call owner. + for (const child of list ? children(list) : []) { + if (child.type === 'expression' || child.type === 'constant_expression') ctx.visitNode(child); + } + return true; + } + + if (node.type === 'ansi_port_declaration') { + const name = node.childForFieldName('port_name'); + if (name) { + let dir = direction(node, ctx); + // ANSI comma continuations inherit direction from the preceding port. + let previous = node.previousNamedSibling; + const hasHeader = children(node).some(n => n.type.endsWith('_port_header')); + while (!dir && !hasHeader && previous?.type === 'ansi_port_declaration') { + dir = direction(previous, ctx); + if (children(previous).some(n => n.type.endsWith('_port_header'))) break; + previous = previous.previousNamedSibling; + } + const decorators = ['hdl:port', ...(dir ? [`hdl:${dir}`] : [])]; + const iface = children(node).find(n => n.type === 'interface_port_header'); + for (const [field, tag] of [['interface_name', 'interface'], ['modport_name', 'modport']] as const) { + const value = iface?.childForFieldName(field); + if (value) decorators.push(`hdl:${tag}:${getNodeText(value, ctx.source)}`); + } + const created = ctx.createNode('field', getNodeText(name, ctx.source), node, + { signature: getNodeText(node, ctx.source), decorators }); + const typeName = iface?.childForFieldName('interface_name'); + const modport = iface?.childForFieldName('modport_name'); + if (created && typeName) ctx.addUnresolvedReference({ + fromNodeId: created.id, referenceName: getNodeText(typeName, ctx.source) + (modport ? `.${getNodeText(modport, ctx.source)}` : ''), + referenceKind: 'type_of', line: node.startPosition.row + 1, column: node.startPosition.column, + }); + } + return true; + } + if (node.type === 'port_declaration') { + for (const decl of children(node)) { + const dir = decl.type.replace(/_declaration$/, ''); + for (const list of children(decl).filter(n => /list_of_.*port_identifiers/.test(n.type))) { + for (const id of children(list).filter(n => ['simple_identifier', 'escaped_identifier'].includes(n.type))) { + ctx.createNode('field', getNodeText(id, ctx.source), id, + { signature: getNodeText(node, ctx.source), decorators: ['hdl:port', `hdl:${dir}`] }); + } + } + } + return true; + } + if (node.type === 'net_declaration' || node.type === 'data_declaration') { + const lists = children(node).filter(n => ['list_of_net_decl_assignments', 'list_of_variable_decl_assignments'].includes(n.type)); + if (!lists.length) return false; // e.g. typedef: let its structural handler run. + for (const list of lists) for (const decl of children(list)) { + const name = decl.childForFieldName('name') ?? identifier(decl); + if (name) { + const text = getNodeText(name, ctx.source); + const parent = ctx.nodes.find(n => n.id === ctx.nodeStack[ctx.nodeStack.length - 1]); + const qualified = parent ? `${parent.qualifiedName}::${text}` : text; + // A non-ANSI output may legally be redeclared as reg in the body. + const port = ctx.nodes.find(n => n.qualifiedName === qualified && n.decorators?.includes('hdl:port')); + const created = port ?? ctx.createNode('field', text, decl, + { signature: getNodeText(node, ctx.source), decorators: ['hdl:signal'] }); + if (created) { + // The '=' token proves a source initialization; dimensions and bare + // declarations are dependencies/declarations, not writes to the name. + if (!decl.hasError && !hasIncompleteProceduralScope(decl) && decl.children.some(n => n.type === '=')) { + ctx.addUnresolvedReference({ fromNodeId: created.id, referenceName: `hdl:signal:${text}`, + referenceKind: 'references', line: name.startPosition.row + 1, column: name.startPosition.column, + candidates: ['hdl:access:write'] }); + } + for (const child of children(decl)) { + if (child.id !== name.id) addVerilogSignalReferences(child, ctx, created.id); + } + } + } + } + walkChildren(node, ctx); + return true; + } + if (['always_construct', 'initial_construct', 'final_construct', 'continuous_assign'].includes(node.type)) { + const keyword = children(node).find(n => n.type === 'always_keyword'); + const kind = node.type === 'always_construct' + ? (keyword ? getNodeText(keyword, ctx.source) : 'always') + : node.type === 'continuous_assign' ? 'assign' : node.type.replace(/_construct$/, ''); + const name = `${kind}@${node.startPosition.row + 1}:${node.startPosition.column}`; + const created = ctx.createNode('function', name, node, + { signature: getNodeText(node, ctx.source), decorators: ['hdl:process', `hdl:${kind}`] }); + if (created) { + ctx.pushScope(created.id); + addVerilogSignalReferences(node, ctx, created.id, true); + } + walkChildren(node, ctx); + if (created) ctx.popScope(); + return true; + } + return false; +} diff --git a/src/extraction/languages/verilog.ts b/src/extraction/languages/verilog.ts new file mode 100644 index 000000000..4bb471a49 --- /dev/null +++ b/src/extraction/languages/verilog.ts @@ -0,0 +1,444 @@ +import { initializeVerilogGenerateScope, visitVerilogGenvarDeclaration } from './verilog-generate'; +import { getVerilogPortOrder, visitVerilogSignals, addVerilogSignalReferences, isVerilogGenerateBinding } from './verilog-signals'; +import { handleVerilogPackageNode, getVerilogCallName } from './verilog-packages'; +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText, getChildByField } from '../tree-sitter-helpers'; +import type { LanguageExtractor, ExtractorContext } from '../tree-sitter-types'; + +/** + * Verilog / SystemVerilog extractor (grammar: tree-sitter-systemverilog, ABI 15). + * + * The SystemVerilog grammar nests declared names deep inside header/body + * wrapper nodes (`module_declaration → module_ansi_header → name`, + * `function_declaration → function_body_declaration → name`), so the generic + * field/first-identifier name resolution can't reach them. Everything + * structural is therefore handled in a custom `visitNode` hook that resolves + * names explicitly, manages scope, and drives its own child recursion. + * + * What the hook emits: + * - module / interface / program / package / class → container nodes (kind + * `class`/`interface`) that scope their members. + * - function / task → `function` nodes (their bodies are walked for calls). + * - module instances → named `variable` nodes with original connections and + * `instantiates` edges (parent module and instance → module type). + * - generate blocks → source scopes; arrays and loops are not elaborated. + * - parameter / localparam → `constant` nodes. + * - `define macro → `constant` node (a `.vh` header is mostly macros, and a + * `WIDTH` written as `` `DATA_W `` has to land somewhere). + * - typedef → `type_alias` node. + * - function/task subroutine calls (`tf_call`) → `calls` references. + * + * Package imports and `` `include `` directives go through the generic import + * path (`importTypes` + `extractImport`): each `package_import_item` so every + * package in a multi-import (`import a::*, b::*;`) gets its own node, and each + * `include_compiler_directive` with the quoted path as its module name — the + * file-path matcher resolves `"defs.vh"` / `"axi/typedef.svh"` to the indexed + * header closest to the including file, the same way a C `#include` lands. + * Ports, signals and executable blocks are handled by verilog-signals; source + * occurrences are distinct from elaborated connectivity or drive direction. + */ + +// Header wrappers that carry a module/interface/program name. +const HEADER_TYPES = [ + 'module_ansi_header', + 'module_nonansi_header', + 'module_header', + 'interface_ansi_header', + 'interface_nonansi_header', + 'program_ansi_header', + 'program_nonansi_header', +]; + +function firstChildOfType(node: SyntaxNode, types: string[]): SyntaxNode | null { + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c && types.includes(c.type)) return c; + } + return null; +} + +function firstSimpleIdentifier(node: SyntaxNode): SyntaxNode | null { + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c && c.type === 'simple_identifier') return c; + } + return null; +} + +/** + * Module type names are resolved against module declarations. Function calls + * retain their package or hierarchy qualification in verilog-packages. + */ +function trailingSegment(name: string): string { + const sep = Math.max(name.lastIndexOf('::'), name.lastIndexOf('.')); + return sep >= 0 ? name.slice(sep).replace(/^[:.]+/, '') : name; +} + +function visitNamedChildren(node: SyntaxNode, ctx: ExtractorContext): void { + for (let i = 0; i < node.namedChildCount; i++) { + const c = node.namedChild(i); + if (c) ctx.visitNode(c); + } +} + +/** + * Resolve a declared name that may sit directly on the node (package/class), + * on a header child (module/interface/program), or as the first identifier. + */ +function declName(node: SyntaxNode, source: string): string | undefined { + const direct = getChildByField(node, 'name'); + if (direct) return getNodeText(direct, source); + const header = firstChildOfType(node, HEADER_TYPES); + if (header) { + const hn = getChildByField(header, 'name') ?? firstSimpleIdentifier(header); + if (hn) return getNodeText(hn, source); + } + const si = firstSimpleIdentifier(node); + return si ? getNodeText(si, source) : undefined; +} + +function handleContainer( + node: SyntaxNode, + ctx: ExtractorContext, + kind: 'class' | 'interface' +): boolean { + const name = declName(node, ctx.source); + if (!name) { + visitNamedChildren(node, ctx); + return true; + } + const order = getVerilogPortOrder(node, ctx.source); + const created = ctx.createNode(kind, name, node, order === null ? undefined + : { decorators: [`hdl:port-order:${JSON.stringify(order)}`] }); + if (created) ctx.pushScope(created.id); + visitNamedChildren(node, ctx); + if (created) ctx.popScope(); + return true; +} + +function handleSubroutine(node: SyntaxNode, ctx: ExtractorContext): boolean { + const bodyDecl = firstChildOfType(node, [ + 'function_body_declaration', + 'task_body_declaration', + ]); + const nameNode = bodyDecl + ? getChildByField(bodyDecl, 'name') ?? firstSimpleIdentifier(bodyDecl) + : getChildByField(node, 'name'); + const name = nameNode ? getNodeText(nameNode, ctx.source) : undefined; + if (!name) { + visitNamedChildren(node, ctx); + return true; + } + + let signature: string | undefined; + if (bodyDecl) { + const ports = firstChildOfType(bodyDecl, ['tf_port_list']); + const ret = firstChildOfType(bodyDecl, ['data_type_or_void']); + const portText = ports ? getNodeText(ports, ctx.source) : ''; + const retText = ret ? getNodeText(ret, ctx.source).trim() : ''; + signature = `${retText} (${portText})`.trim(); + } + + const created = ctx.createNode('function', name, node, { signature }); + if (created) ctx.pushScope(created.id); + if (created && bodyDecl) addVerilogSignalReferences(bodyDecl, ctx, created.id, true); + visitNamedChildren(node, ctx); + if (created) ctx.popScope(); + return true; +} + +function handleGenerateBlock(node: SyntaxNode, ctx: ExtractorContext): boolean { + const nameNode = getChildByField(node, 'name'); + // Anonymous labels identify source scopes, not elaborated genblk numbers. + const name = nameNode ? getNodeText(nameNode, ctx.source) + : `generate@${node.startPosition.row + 1}:${node.startPosition.column}`; + const loop = node.parent?.type === 'loop_generate_construct' ? node.parent : null; + const created = ctx.createNode('namespace', name, node, { + decorators: ['hdl:generate-scope'], + ...(loop ? { signature: ctx.source.slice(loop.startIndex, node.startIndex).trim() } : {}), + }); + if (created) { + ctx.pushScope(created.id); + initializeVerilogGenerateScope(node, ctx); + } + visitNamedChildren(node, ctx); + if (created) ctx.popScope(); + return true; +} + +/** Direct AST commas delimit slots; nested expression commas stay inside their node. */ +function positionalConnections(node: SyntaxNode, ctx: ExtractorContext, leading: SyntaxNode[] = []): Array<{ position: SyntaxNode; text: string }> | null { + if (node.hasError || node.namedChildren.some(n => !['ordered_port_connection', 'comment', 'block_comment', 'one_line_comment'].includes(n.type))) return null; + const slots: Array<{ position: SyntaxNode; text: string }> = []; + let start = leading[0]?.startIndex ?? node.startIndex; + let position: SyntaxNode = leading[0] ?? node; + for (const child of [...leading.flatMap(n => n.children), ...node.children]) { + if (child.type === ',') { + slots.push({ position, text: ctx.source.slice(start, child.startIndex).trim() }); + start = child.endIndex; + position = child; + } else if (child.type === 'ordered_port_connection') position = child; + } + slots.push({ position, text: ctx.source.slice(start, node.endIndex).trim() }); + return slots; +} + +function handleInstantiation(node: SyntaxNode, ctx: ExtractorContext): boolean { + const typeNode = getChildByField(node, 'instance_type') ?? firstSimpleIdentifier(node); + if (!typeNode) { + visitNamedChildren(node, ctx); + return true; + } + const typeName = getNodeText(typeNode, ctx.source).trim(); + const moduleName = trailingSegment(typeName); + const addReference = (fromId: string, position: SyntaxNode): void => { + ctx.addUnresolvedReference({ + fromNodeId: fromId, + referenceName: moduleName, + referenceKind: 'instantiates', + line: position.startPosition.row + 1, + column: position.startPosition.column, + }); + }; + // Keep the module-to-module flow even when a generate scope contains this + // declaration. Individual instances additionally provide navigable evidence. + for (let i = ctx.nodeStack.length - 1; i >= 0; i--) { + const parent = ctx.nodes.find(n => n.id === ctx.nodeStack[i]); + if (parent && (parent.kind === 'class' || parent.kind === 'interface')) { + addReference(parent.id, node); + break; + } + } + const parameters = firstChildOfType(node, ['parameter_value_assignment']); + const prefix = `${typeName}${parameters ? ' ' + getNodeText(parameters, ctx.source) : ''}`; + for (let i = 0; i < node.namedChildCount; i++) { + const child = node.namedChild(i); + if (!child) continue; + if (child.type !== 'hierarchical_instance') { + ctx.visitNode(child); + continue; + } + const nameOfInstance = firstChildOfType(child, ['name_of_instance']); + const nameNode = nameOfInstance && (getChildByField(nameOfInstance, 'instance_name') + ?? firstSimpleIdentifier(nameOfInstance)); + const name = nameNode ? getNodeText(nameNode, ctx.source) : undefined; + const created = name ? ctx.createNode('variable', name, child, { + // Preserve arrays, named/ordered/shorthand connections and parameter + // overrides as source evidence; do not infer elaborated wire endpoints. + signature: `${prefix} ${getNodeText(child, ctx.source)}`, + }) : null; + if (created) { + addReference(created.id, child); + addVerilogSignalReferences(child, ctx, created.id); + ctx.pushScope(created.id); + const connections = firstChildOfType(child, ['list_of_port_connections']); + // This grammar wraps leading empty-slot commas in ERROR. Accept only + // those literal separators; all other parse errors/mixed lists stay opaque. + const leadingCommas = child.namedChildren.filter(n => n.type === 'ERROR' + && connections && n.endIndex <= connections.startIndex && /^\s*,+\s*$/.test(getNodeText(n, ctx.source))); + const allowedErrors = new Set(leadingCommas.map(n => n.id)); + const soundTree = (part: SyntaxNode): boolean => !part.isMissing && (part.type === 'ERROR' + ? allowedErrors.has(part.id) : part.namedChildren.every(soundTree)); + const validConnections = soundTree(node) + && child.namedChildren.filter(n => n.type === 'list_of_port_connections').length === 1; + const slots = validConnections && connections ? positionalConnections(connections, ctx, leadingCommas) : null; + if (slots) slots.forEach((slot, index) => { + const binding = ctx.createNode('property', `port[${index}]`, slot.position, { + signature: slot.text, decorators: ['hdl:connection', `hdl:position:${index}`], + }); + if (binding) { + ctx.addUnresolvedReference({ fromNodeId: binding.id, + referenceName: `hdl:port-position:${JSON.stringify([moduleName, index, slots.length, created.id])}`, + referenceKind: 'references', line: slot.position.startPosition.row + 1, + column: slot.position.startPosition.column }); + if (slot.position.type === 'ordered_port_connection') addVerilogSignalReferences(slot.position, ctx, binding.id); + } + }); + for (const connection of validConnections ? connections?.namedChildren ?? [] : []) { + if (connection.type !== 'named_port_connection') continue; + const port = getChildByField(connection, 'port_name'); + if (!port) { + if (!getNodeText(connection, ctx.source).includes('.*')) continue; + const excluded = (connections?.namedChildren ?? []).flatMap(n => { + const formal = getChildByField(n, 'port_name'); + return formal ? [getNodeText(formal, ctx.source)] : []; + }); + // An implicit generate parameter shadows an outer same-name signal. + for (let ancestor = connection.parent; ancestor; ancestor = ancestor.parent) { + if (ancestor.type === 'loop_generate_construct') { + const init = firstChildOfType(ancestor, ['genvar_initialization']); + const id = init && firstSimpleIdentifier(init); + if (id) excluded.push(getNodeText(id, ctx.source)); + } + if (['module_declaration', 'interface_declaration', 'program_declaration'].includes(ancestor.type)) break; + } + const wildcard = ctx.createNode('property', '*', connection, { + signature: getNodeText(connection, ctx.source), decorators: ['hdl:connection', 'hdl:wildcard'], + }); + if (wildcard) ctx.addUnresolvedReference({ fromNodeId: wildcard.id, + referenceName: `hdl:wildcard:${JSON.stringify([moduleName, created.id, excluded])}`, + referenceKind: 'references', line: connection.startPosition.row + 1, column: connection.startPosition.column }); + continue; + } + const portName = getNodeText(port, ctx.source); + const binding = ctx.createNode('property', portName, connection, { + signature: getNodeText(connection, ctx.source), decorators: ['hdl:connection', 'hdl:named'], + }); + if (binding) { + ctx.addUnresolvedReference({ fromNodeId: binding.id, + referenceName: `hdl:port:${JSON.stringify([moduleName, portName, created.id])}`, + referenceKind: 'references', line: port.startPosition.row + 1, column: port.startPosition.column }); + addVerilogSignalReferences(connection, ctx, binding.id); + } + // Shorthand .port connects the same local name. Empty .port() has a + // formal endpoint but no local expression; no drive direction inferred. + if (!isVerilogGenerateBinding(connection, ctx, portName) + && /^\s*\.\s*[^()\s]+\s*$/.test(getNodeText(connection, ctx.source))) { + for (const owner of [created, ...(binding ? [binding] : [])]) { + ctx.addUnresolvedReference({ fromNodeId: owner.id, + referenceName: `hdl:signal:${portName}`, referenceKind: 'references', + line: port.startPosition.row + 1, column: port.startPosition.column }); + } + } + } + } + visitNamedChildren(child, ctx); + if (created) ctx.popScope(); + } + return true; +} + +function handleParam(node: SyntaxNode, ctx: ExtractorContext): boolean { + const list = firstChildOfType(node, ['list_of_param_assignments']); + if (list) { + for (let i = 0; i < list.namedChildCount; i++) { + const assign = list.namedChild(i); + if (!assign || assign.type !== 'param_assignment') continue; + const id = firstSimpleIdentifier(assign); + if (id) ctx.createNode('constant', getNodeText(id, ctx.source), assign); + } + } + return true; +} + +function handleMacro(node: SyntaxNode, ctx: ExtractorContext): boolean { + const nameNode = firstChildOfType(node, ['text_macro_name']); + const id = nameNode ? firstSimpleIdentifier(nameNode) : null; + if (id) ctx.createNode('constant', getNodeText(id, ctx.source), node); + return true; +} + +function handleTypedef(node: SyntaxNode, ctx: ExtractorContext): boolean { + const nameNode = + getChildByField(node, 'type_name') ?? + getChildByField(node, 'name') ?? + firstSimpleIdentifier(node); + const name = nameNode ? getNodeText(nameNode, ctx.source) : undefined; + if (name) ctx.createNode('type_alias', name, node); + return true; +} + +function handleCall(node: SyntaxNode, ctx: ExtractorContext): boolean { + if (ctx.nodeStack.length > 0) { + const lexical = ctx.nodes.find(n => n.id === ctx.nodeStack[ctx.nodeStack.length - 1]); + const callable = [...ctx.nodeStack].reverse().map(id => ctx.nodes.find(n => n.id === id)) + .find(n => n && (n.kind === 'function' || n.kind === 'method')); + const fromId = callable?.id ?? lexical?.id; + const callee = firstChildOfType(node, ['hierarchical_identifier']) ?? firstSimpleIdentifier(node); + if (fromId && callee) { + const name = getVerilogCallName(node, ctx.source); + if (name) { + ctx.addUnresolvedReference({ + fromNodeId: fromId, + referenceName: name, + referenceKind: 'calls', + ...(lexical?.kind === 'namespace' && lexical.id !== fromId + ? { candidates: [`hdl:scope:${lexical.qualifiedName}`] } : {}), + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + } + } + // Walk arguments so nested calls (`caller(helper(x))`) get their own refs. + visitNamedChildren(node, ctx); + return true; +} + +export const verilogExtractor: LanguageExtractor = { + // The visitNode hook below owns dispatch for all structural constructs; + // these generic arrays stay empty except imports, which reuse the core path. + functionTypes: [], + classTypes: [], + methodTypes: [], + interfaceTypes: [], + structTypes: [], + enumTypes: [], + typeAliasTypes: [], + // Target the per-item node, not the whole declaration, so every package in a + // multi-import (`import a::*, b::*;`) is indexed — one import node per item. + importTypes: ['package_import_item', 'include_compiler_directive'], + callTypes: [], + variableTypes: [], + nameField: 'name', + bodyField: 'body', + paramsField: 'tf_port_list', + + visitNode: (node, ctx) => { + if (visitVerilogGenvarDeclaration(node, ctx) || visitVerilogSignals(node, ctx) || handleVerilogPackageNode(node, ctx)) return true; + switch (node.type) { + case 'module_declaration': + case 'program_declaration': + case 'package_declaration': + case 'class_declaration': + return handleContainer(node, ctx, 'class'); + case 'interface_declaration': + return handleContainer(node, ctx, 'interface'); + case 'function_declaration': + case 'task_declaration': + return handleSubroutine(node, ctx); + case 'generate_block': + return handleGenerateBlock(node, ctx); + case 'module_instantiation': + return handleInstantiation(node, ctx); + case 'parameter_declaration': + case 'local_parameter_declaration': + return handleParam(node, ctx); + case 'text_macro_definition': + return handleMacro(node, ctx); + case 'type_declaration': + return handleTypedef(node, ctx); + case 'tf_call': + return handleCall(node, ctx); + default: + return false; + } + }, + + extractImport: (node, source) => { + if (node.type === 'include_compiler_directive') { + // `include "defs.vh"` → the quoted path; `include ` → the + // bracketed one. The path is the module name, so the generic import ref + // resolves it to the header FILE by path suffix (name-matcher's + // file-path strategy), never to a symbol. + const quoted = firstChildOfType(node, ['quoted_string']); + const item = quoted ? firstChildOfType(quoted, ['quoted_string_item']) : null; + const system = firstChildOfType(node, ['system_lib_string']); + const target = item + ? getNodeText(item, source) + : system + ? getNodeText(system, source).replace(/^<|>$/g, '') + : ''; + if (!target.trim()) return null; + return { moduleName: target.trim(), signature: getNodeText(node, source).trim() }; + } + // `node` is a package_import_item (`pkg::*` / `pkg::name`); the package name + // is its first simple_identifier. One item → one import node. + const id = firstSimpleIdentifier(node); + if (!id) return null; + return { + moduleName: getNodeText(id, source), + signature: getNodeText(node, source).trim(), + }; + }, +}; diff --git a/src/extraction/tree-sitter-helpers.ts b/src/extraction/tree-sitter-helpers.ts index a6438e1b4..aabcd192e 100644 --- a/src/extraction/tree-sitter-helpers.ts +++ b/src/extraction/tree-sitter-helpers.ts @@ -13,17 +13,20 @@ import { NodeKind } from '../types'; * Generate a unique node ID * * Uses a 32-character (128-bit) hash to avoid collisions when indexing - * large codebases with many files containing similar symbols. + * large codebases with many files containing similar symbols. The optional + * UTF-16 column distinguishes same-line declarations (TS/JS, C++ and HDL); omitted columns + * preserve the existing identity format for other extraction paths. */ export function generateNodeId( filePath: string, kind: NodeKind, name: string, - line: number + line: number, + column?: number ): string { const hash = crypto .createHash('sha256') - .update(`${filePath}:${kind}:${name}:${line}`) + .update(`${filePath}:${kind}:${name}:${line}${column === undefined ? '' : `:${column}`}`) .digest('hex') .substring(0, 32); return `${kind}:${hash}`; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 20f5dbb26..003a854ff 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -411,12 +411,10 @@ const TS_JS_CHAIN_LANGUAGES = new Set(['typescript', 'tsx', 'javascript', 'jsx'] const TS_JS_CHAIN_RECEIVER_TYPES = new Set(['member_expression', 'subscript_expression']); /** - * Identifier-rooted member chains have no inferred property type (#1566), - * including host API chains (#1707). Keep the existing `window.MyNamespace` - * escape for project globals; call-result and `this` receivers have their own - * paths and are outside this guard. + * Nested identifier receivers retain their call-site text; window keeps its + * existing project-namespace behavior. Call-result and this paths are separate. */ -function isUnresolvedTsJsChain(node: SyntaxNode, source: string): boolean { +function isTsJsIdentifierChain(node: SyntaxNode, source: string): boolean { let cur: SyntaxNode | null = node; while (cur && TS_JS_CHAIN_RECEIVER_TYPES.has(cur.type)) { cur = getChildByField(cur, 'object'); @@ -1022,6 +1020,17 @@ export class TreeSitterExtractor { if (skipChildren) return; } + // Preprocessor definitions are values, never executable callees. Keeping + // the directive lets resolution recognize visible macros through includes. + if ((this.language === 'c' || this.language === 'cpp') && + nodeType === 'preproc_function_def') { + const name = getChildByField(node, 'name'); + if (name) this.createNode('constant', getNodeText(name, this.source), node, { + signature: getNodeText(node, this.source).trim(), + }); + return; + } + // C++ namespace blocks: carry the namespace name as a qualifiedName prefix // while walking the body, so `namespace flash { void compute_attn(); }` // indexes compute_attn with qualifiedName `flash::compute_attn` and a @@ -1390,7 +1399,11 @@ export class TreeSitterExtractor { return null; } - const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1); + // TS/JS accessors, C++ overloads and HDL declarations can share a name and source line. + // Include the UTF-16 column for these languages so their edges cannot alias. + const column = ['typescript', 'tsx', 'javascript', 'jsx', 'cpp', 'verilog'].includes(this.language) + ? node.startPosition.column : undefined; + const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1, column); // Some grammars (e.g. Dart) model a function/method body as a *sibling* of // the signature node, so the declaration node's own range is just the @@ -4868,16 +4881,12 @@ export class TreeSitterExtractor { TS_JS_CHAIN_LANGUAGES.has(this.language) && receiver && TS_JS_CHAIN_RECEIVER_TYPES.has(receiver.type) && - isUnresolvedTsJsChain(receiver, this.source) + isTsJsIdentifierChain(receiver, this.source) ) { - // `holder.values.get()` has no inferred property type (#1566). - // Emitting bare `get` exact-matches an unrelated project method; - // preserving the chain alone would still allow receiver guessing. - // Emit nothing until the property type can be established. This - // also covers host chains such as `chrome.storage.local.get()` - // (#1707). Calls inside arguments are visited independently. - // Mirrored in the kernel's extract_call (tsjs/extractors.rs). - return; + // Keep call-site evidence for framework resolution and Steps. + // Generic resolution must not guess a target from the last name + // when the nested receiver's type is unknown (#1794, #1566). + calleeName = `${getNodeText(receiver, this.source)}.${methodName}`; } else { calleeName = methodName; } @@ -5096,13 +5105,13 @@ export class TreeSitterExtractor { * `auto` (`placeholder_type_specifier` — that form always carries a real * `call_expression`, already handled), and sized specifiers are excluded — * they construct no class; and - * - a declarator carries constructor arguments: an `init_declarator` whose - * `value` is an `argument_list` (`(args)`) or `initializer_list` (`{args}`). - * This skips default construction `Calculator c;` (no value) and the - * most-vexing-parse `Calculator c();` (a bodyless `function_declarator`, - * a function decl — not a construction). + * - a bare identifier (default construction), or an `init_declarator` + * whose value is an `argument_list` / `initializer_list`. Pointer, + * reference, and function declarators are excluded, including the + * most-vexing-parse `Calculator c();` (a function declaration). */ - private isCppStackConstruction(node: SyntaxNode): boolean { + private cppStackConstructions(node: SyntaxNode): Array<{ node: SyntaxNode; arity: number }> { + if (node.namedChildren.some(c => c.type === 'storage_class_specifier' && c.text === 'extern')) return []; const typeNode = getChildByField(node, 'type'); if ( !typeNode || @@ -5110,17 +5119,23 @@ export class TreeSitterExtractor { typeNode.type !== 'template_type' && typeNode.type !== 'qualified_identifier') ) { - return false; + return []; } + const constructions: Array<{ node: SyntaxNode; arity: number }> = []; for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); + if (child?.type === 'identifier') { constructions.push({ node: child, arity: 0 }); continue; } if (child?.type !== 'init_declarator') continue; + const declarator = getChildByField(child, 'declarator'); + if (declarator?.type !== 'identifier' && declarator?.type !== 'array_declarator') continue; const value = getChildByField(child, 'value'); if (value && (value.type === 'argument_list' || value.type === 'initializer_list')) { - return true; + // Array initializer elements are objects, not constructor arguments. + if (declarator.type === 'array_declarator' && value.namedChildCount !== 0) continue; + constructions.push({ node: child, arity: value.namedChildren.filter(c => c.type !== 'comment').length }); } } - return false; + return constructions; } /** @@ -5596,6 +5611,12 @@ export class TreeSitterExtractor { const visitForCallsAndStructure = (node: SyntaxNode): void => { const nodeType = node.type; + if ((this.language === 'c' || this.language === 'cpp') && + nodeType === 'preproc_function_def') { + this.visitNode(node); + return; + } + // Function-as-value capture (#756) — function bodies are walked here, // not in visitNode, so the capture hook must fire in both walkers. @@ -5646,8 +5667,20 @@ export class TreeSitterExtractor { // (which strips template args / namespace and emits the `instantiates` // ref). Children still recurse below, so a nested ctor-arg call // (`Calculator calc(make())`) keeps its own `calls` ref. - if (nodeType === 'declaration' && this.language === 'cpp' && this.isCppStackConstruction(node)) { - this.extractInstantiation(node); + if (nodeType === 'declaration' && this.language === 'cpp') { + const constructions = this.cppStackConstructions(node); + if (constructions.length) this.extractInstantiation(node); + const type = getChildByField(node, 'type'); + const className = type ? stripCppTemplateArgs(getNodeText(type, this.source)) : ''; + const name = className.split('::').filter(Boolean).pop(); + const callerId = this.nodeStack[this.nodeStack.length - 1]; + if (name && callerId) for (const construction of constructions) this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: `${className}::${name}/${construction.arity}`, + referenceKind: 'calls', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); } // C++ local function-pointer bindings (see cppLocalFnPtrs): record diff --git a/src/extraction/wasm/tree-sitter-systemverilog.wasm b/src/extraction/wasm/tree-sitter-systemverilog.wasm new file mode 100644 index 000000000..ad8201395 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-systemverilog.wasm differ diff --git a/src/graph/named-symbol-flow.ts b/src/graph/named-symbol-flow.ts index a83edf2d8..02f6563b7 100644 --- a/src/graph/named-symbol-flow.ts +++ b/src/graph/named-symbol-flow.ts @@ -139,6 +139,15 @@ export const FLOW_CALLABLE_KINDS: ReadonlySet = new Set([ */ export const FLOW_EDGE_KINDS: ReadonlySet = new Set(['calls', 'navigates']); +function canSitOnFlow(n: { kind: string; language?: string }): boolean { + return FLOW_CALLABLE_KINDS.has(n.kind) || (n.kind === 'class' && n.language === 'verilog'); +} + +function carriesFlow(edge: { kind: string }, target: { language?: string }): boolean { + return FLOW_EDGE_KINDS.has(edge.kind) || (edge.kind === 'instantiates' && target.language === 'verilog'); +} + + /** * Node kinds that can be an endpoint of a SYNTHESIZED edge without being * callable. An RTK thunk is `const X = createAsyncThunk(...)`, so a thunk → @@ -309,7 +318,7 @@ export function resolveNamedTokens( for (const t of tokens) { const hits = findAllSymbols(cg, t).nodes; - const cands = hits.filter((n) => FLOW_CALLABLE_KINDS.has(n.kind)); + const cands = hits.filter((n) => canSitOnFlow(n)); out.tokenFamily.set(t, cands); // A qualified or otherwise-specific name (<=3 hits) keeps all of them. const specific = cands.length <= 3; @@ -343,7 +352,7 @@ export function resolveNamedTokens( if (out.dynNamed.size < 12) { let tokenDyn = 0; for (const n of hits) { - if (FLOW_CALLABLE_KINDS.has(n.kind) || !DYN_KINDS.has(n.kind) || out.dynNamed.has(n.id)) { + if (canSitOnFlow(n) || !DYN_KINDS.has(n.kind) || out.dynNamed.has(n.id)) { continue; } if (hasHeuristicEdge(n.id)) { @@ -410,7 +419,7 @@ function walkCalls( if (id !== seed.id && named.has(id)) reached.push(id); if (depth >= maxHops - 1) continue; for (const c of cg.getCallees(id)) { - if (!FLOW_EDGE_KINDS.has(c.edge.kind) || parent.has(c.node.id)) continue; + if (!carriesFlow(c.edge, c.node) || parent.has(c.node.id)) continue; // A route node is a connector, not a symbol the reader would have named: // crossing one costs no bridge budget. const newStreak = named.has(c.node.id) ? 0 : c.node.kind === 'route' ? streak : streak + 1; @@ -484,7 +493,7 @@ function walkBidirectional( const next: Node[] = []; for (const node of frontF) { for (const c of cg.getCallees(node.id)) { - if (!FLOW_EDGE_KINDS.has(c.edge.kind) || forward.has(c.node.id)) continue; + if (!carriesFlow(c.edge, c.node) || forward.has(c.node.id)) continue; forward.set(c.node.id, { prev: node.id, edge: c.edge, node: c.node }); next.push(c.node); } @@ -496,7 +505,7 @@ function walkBidirectional( const next: Node[] = []; for (const node of frontB) { for (const c of cg.getCallers(node.id)) { - if (!FLOW_EDGE_KINDS.has(c.edge.kind) || backward.has(c.node.id)) continue; + if (!carriesFlow(c.edge, c.node) || backward.has(c.node.id)) continue; backward.set(c.node.id, { next: node.id, edge: c.edge }); backNodes.set(c.node.id, c.node); next.push(c.node); diff --git a/src/hdl/index-context.ts b/src/hdl/index-context.ts new file mode 100644 index 000000000..d3e9acdda --- /dev/null +++ b/src/hdl/index-context.ts @@ -0,0 +1,132 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { createHash } from 'node:crypto'; +import { loadHdlProfile, type ActiveHdlProfile } from './profile'; +import { EXTRACTION_VERSION } from '../extraction/extraction-version'; +import { preprocessVerilog, type HdlPreprocessDiagnostic } from './preprocess'; +import { detectLanguage } from '../extraction/grammars'; +import { loadExtensionOverrides } from '../project-config'; + +export interface HdlDependency { path: string; sha256: string | null } +const hash = (text: string) => createHash('sha256').update(text).digest('hex'); +function inside(root: string, file: string): string { + const relative = path.relative(root, file); + if (relative.startsWith('..' + path.sep) || relative === '..' || path.isAbsolute(relative)) throw new Error('HDL path escapes project root'); + return relative.split(path.sep).join('/'); +} + +/** Immutable per-operation conditional source view. Original bytes remain file hashes/snippets. */ +export class HdlIndexContext { + readonly profile: ActiveHdlProfile | null; + readonly diagnostics: HdlPreprocessDiagnostic[] = []; + readonly dependencies: HdlDependency[]; + readonly fingerprint: string; + readonly sources = new Map(); + readonly unitDiagnostics = new Map(); + private readonly root: string; + private readonly overrides: ReturnType; + + constructor(root: string, previouslyProfiled: boolean) { + this.root = path.resolve(root); + this.overrides = loadExtensionOverrides(root); + const loaded = loadHdlProfile(root); + if (loaded.status === 'invalid-profile' || (loaded.status === 'invalid-config' && previouslyProfiled)) { + throw new Error(`Invalid HDL profile: ${loaded.diagnostics.join('; ')}`); + } + this.profile = loaded.profile; + if (!this.profile) { this.dependencies = []; this.fingerprint = 'raw'; return; } + this.root = fs.realpathSync(root); + const dependency = new Map(); + const bytes = new Map(); + const snapshots = new Map(); + let total = 0; + const read = (relative: string): string | null => { + const lexical = path.resolve(this.root, relative); + const normalized = inside(this.root, lexical); + if (normalized.split('/').some(p => ['.git', '.jj', '.codegraph'].includes(p) || p.startsWith('.codegraph-'))) throw new Error('HDL profile cannot read VCS/index storage'); + if (bytes.has(normalized)) return bytes.get(normalized)!; + try { + inside(this.root, fs.realpathSync(lexical)); + const stat = fs.statSync(lexical); + if (!stat.isFile() || stat.size > 1024 * 1024) throw new Error(`HDL input is not a regular file of at most 1 MiB: ${normalized}`); + const text = fs.readFileSync(lexical, 'utf8'); + const after = fs.statSync(lexical); + if (stat.size !== after.size || stat.mtimeMs !== after.mtimeMs || stat.ino !== after.ino) { + throw new Error(`HDL input changed while preparing snapshot; retry: ${normalized}`); + } + snapshots.set(normalized, after); + total += Buffer.byteLength(text); + if (total > 64 * 1024 * 1024) throw new Error('HDL profile source/dependency snapshot exceeds 64 MiB'); + bytes.set(normalized, text); + return text; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; + throw error; + } + }; + for (const file of this.profile.files) { + if (detectLanguage(file, undefined, this.overrides) !== 'verilog') throw new Error(`HDL profile lists a non-HDL source: ${file}`); + const source = read(file); + if (source === null) throw new Error(`HDL source missing: ${file}`); + const result = preprocessVerilog(source, { filePath: file, defines: this.profile.defines, includeDirs: this.profile.includeDirs, + readInclude: (request, from, dirs) => { + if (path.isAbsolute(request) || /^[A-Za-z]:[\\/]/.test(request)) throw new Error('Absolute HDL include paths are outside the supported project-relative profile'); + const candidates = [path.posix.join(path.posix.dirname(from), request), ...dirs.map(dir => path.posix.join(dir, request))]; + for (const candidate of [...new Set(candidates)]) { + const relative = inside(this.root, path.resolve(this.root, candidate)); + const text = read(relative); + dependency.set(relative, text === null ? null : hash(text)); + if (text !== null) return { filePath: relative, source: text }; + } + return null; + } }); + if (result.source.length !== source.length) throw new Error('HDL conditional selection changed source offsets'); + this.sources.set(file, { original: source, selected: result.source, stats: snapshots.get(file)! }); + const diagnostics = result.diagnostics.slice(0, 32); + if (result.diagnostics.length > diagnostics.length) diagnostics.push({ filePath: file, line: 1, + code: 'diagnostic-limit', message: `${result.diagnostics.length - diagnostics.length} additional preprocessing diagnostics omitted for this unit.` }); + this.unitDiagnostics.set(file, diagnostics); + this.diagnostics.push(...diagnostics.slice(0, Math.max(0, 200 - this.diagnostics.length))); + } + this.dependencies = [...dependency].sort(([a], [b]) => a.localeCompare(b)).map(([path, sha256]) => ({ path, sha256 })); + this.fingerprint = hash(JSON.stringify([EXTRACTION_VERSION, this.profile.fingerprint, this.dependencies])); + } + + isHdl(file: string): boolean { return detectLanguage(file, undefined, this.overrides) === 'verilog'; } + accepts(file: string): boolean { return !this.profile || !this.isHdl(file) || this.sources.has(file); } + files(scanned: string[]): string[] { + return this.profile ? [...scanned.filter(file => !this.isHdl(file)), ...this.profile.files] : scanned; + } + source(file: string, original: string): string { + if (!this.profile || !this.isHdl(file)) return original; + const cached = this.sources.get(file); + if (!cached) throw new Error(`HDL source is outside active profile: ${file}`); + if (cached.original !== original) throw new Error(`HDL source changed during profile preparation; retry indexing: ${file}`); + return cached.selected; + } + context(): string { + return JSON.stringify(this.profile ? { mode: 'profile', configurationFingerprint: this.profile.fingerprint, + files: this.profile.files, includeDirs: this.profile.includeDirs, defines: this.profile.defines, + topModules: this.profile.topModules, languageMode: this.profile.languageMode, + dependencies: this.dependencies, unitDiagnostics: Object.fromEntries(this.unitDiagnostics), diagnostics: this.diagnostics.slice(0, 200), + diagnosticCount: this.diagnostics.length, incomplete: this.diagnostics.length > 0, + } : { mode: 'raw' }); + } +} + +/** Include changes can invalidate a profile even when codegraph.json is unchanged. */ +export function hdlDependenciesChanged(root: string, context: string | null): boolean { + let value: unknown; + try { value = context ? JSON.parse(context) : null; } catch { return true; } + if (!value || typeof value !== 'object' || !('dependencies' in value) || !Array.isArray(value.dependencies)) return false; + const canonicalRoot = fs.realpathSync(root); + return value.dependencies.some((entry: HdlDependency) => { + if (!entry || typeof entry.path !== 'string') return true; + try { + const full = path.resolve(canonicalRoot, entry.path); + inside(canonicalRoot, full); inside(canonicalRoot, fs.realpathSync(full)); + const stat = fs.statSync(full); + return !stat.isFile() || stat.size > 1024 * 1024 || hash(fs.readFileSync(full, 'utf8')) !== entry.sha256; + } catch { return entry.sha256 !== null; } + }); +} diff --git a/src/hdl/preprocess.ts b/src/hdl/preprocess.ts new file mode 100644 index 000000000..538854a3c --- /dev/null +++ b/src/hdl/preprocess.ts @@ -0,0 +1,237 @@ +/** Bounded, source-preserving HDL conditional selection. No macro expansion. */ +export interface HdlPreprocessDiagnostic { + filePath: string; + line: number; + code: string; + message: string; +} +export interface HdlIncludeSource { filePath: string; source: string } +export interface HdlPreprocessOptions { + filePath: string; + defines?: Record; + includeDirs?: string[]; + /** The caller owns filesystem access and must enforce its allowed roots. */ + readInclude?: (request: string, fromFile: string, includeDirs: readonly string[]) => HdlIncludeSource | null; + maxIncludeDepth?: number; +} +export interface HdlPreprocessResult { + source: string; + dependencies: string[]; + diagnostics: HdlPreprocessDiagnostic[]; + /** All encountered constructs were handled within this bounded mode. */ + complete: boolean; +} +interface Conditional { + parent: boolean; + active: boolean; + taken: boolean; + uncertain: boolean; + elseSeen: boolean; +} +const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*/; +const OTHER_DIRECTIVES = new Set(['timescale', 'default_nettype', 'celldefine', 'endcelldefine', + 'resetall', 'unconnected_drive', 'nounconnected_drive', 'line', 'begin_keywords', 'end_keywords', 'pragma']); + +/** Each call is a separate translation unit. Includes affect macro definedness, + * but are never pasted into the returned text. All offsets are original UTF-16 + * offsets; inactive text is replaced with spaces while CR/LF bytes stay put. */ +export function preprocessVerilog(source: string, options: HdlPreprocessOptions): HdlPreprocessResult { + const diagnostics: HdlPreprocessDiagnostic[] = []; + const record = (diagnostic: HdlPreprocessDiagnostic): void => { + if (diagnostics.length < 512) diagnostics.push(diagnostic); + else if (diagnostics.length === 512) diagnostics.push({ ...diagnostic, code: 'diagnostic-limit', + message: 'Additional preprocessing diagnostics were omitted after 512 entries.' }); + }; + const dependencies = new Set(); + const macros = new Map(); + let unknownMacros = false; + const trace = new Set(); + const requestedDepth = options.maxIncludeDepth; + const maxDepth = Number.isSafeInteger(requestedDepth) && requestedDepth! >= 0 ? Math.min(requestedDepth!, 256) : 64; + const invalidateMacros = (): void => { macros.clear(); unknownMacros = true; }; + const mask = (text: string): string => text.replace(/[^\r\n]/g, ' '); + for (const name of Object.keys(options.defines ?? {})) { + if (IDENTIFIER.exec(name)?.[0] === name) macros.set(name, true); + else record({ filePath: options.filePath, line: 1, code: 'invalid-profile-define', message: `Invalid macro name: ${name}` }); + } + + const process = (text: string, filePath: string, depth: number): string => { + const output = text.split(''); + const stack: Conditional[] = []; + let fatal = false; + const lineStarts = [0]; + for (let at = text.indexOf('\n'); at >= 0; at = text.indexOf('\n', at + 1)) lineStarts.push(at + 1); + const active = (): boolean => stack.length ? stack[stack.length - 1]!.active : true; + const report = (offset: number, code: string, message: string): void => { + let lo = 0, hi = lineStarts.length; + while (lo + 1 < hi) { const mid = (lo + hi) >>> 1; if (lineStarts[mid]! <= offset) lo = mid; else hi = mid; } + record({ filePath, line: lo + 1, code, message }); + }; + const blank = (start: number, end: number): void => { + for (let n = start; n < end; n++) if (text[n] !== '\n' && text[n] !== '\r') output[n] = ' '; + }; + const lineEnd = (start: number): number => { + let end = text.indexOf('\n', start); + if (end < 0) return text.length; + while (end > 0 && text[end - (text[end - 1] === '\r' ? 2 : 1)] === '\\') { + const next = text.indexOf('\n', end + 1); + if (next < 0) return text.length; + end = next; + } + return end; + }; + const nameAfter = (offset: number): { name?: string; end: number } => { + let start = offset; + while (text[start] === ' ' || text[start] === '\t') start++; + const name = IDENTIFIER.exec(text.slice(start))?.[0]; + return { name, end: name ? start + name.length : lineEnd(start) }; + }; + const defined = (name: string): boolean | undefined => macros.get(name) ?? (unknownMacros ? undefined : false); + const condition = (name: string | undefined, invert: boolean, offset: number): boolean | undefined => { + const value = name ? defined(name) : undefined; + if (value === undefined) { + report(offset, name ? 'unknown-conditional' : 'unsupported-conditional', name + ? `Cannot determine whether ${name} is defined; no branch was selected.` + : 'Only a simple macro identifier is supported in a conditional directive.'); + // Either unknown arm could define or undefine names used later. + invalidateMacros(); + return undefined; + } + return invert ? !value : value; + }; + + let i = 0; + while (i < text.length) { + const start = i; + // Directives inside comments, strings and escaped identifiers are text. + if (text.startsWith('//', i)) { + const end = text.indexOf('\n', i + 2); + i = end < 0 ? text.length : end; + } else if (text.startsWith('/*', i)) { + const end = text.indexOf('*/', i + 2); + i = end < 0 ? text.length : end + 2; + } else if (text[i] === '"') { + i++; + while (i < text.length) { + if (text[i] === '\\') { i += Math.min(2, text.length - i); continue; } + if (text[i] === '"') { i++; break; } + if (text[i] === '\n' || text[i] === '\r') break; + i++; + } + } else if (text[i] === '\\') { + i++; + while (i < text.length && !/\s/.test(text[i]!)) i++; + } else if (text[i] === '`') { + const directive = IDENTIFIER.exec(text.slice(i + 1))?.[0]; + if (!directive) { + if (active()) report(i, 'unsupported-macro-form', 'Unrecognized backtick form was preserved.'); + if (!active()) blank(i, i + 1); + i++; + continue; + } + const tokenEnd = i + 1 + directive.length; + if (['ifdef', 'ifndef', 'elsif'].includes(directive)) { + const argument = nameAfter(tokenEnd); + blank(i, argument.end); + if (directive !== 'elsif') { + const parent = active(); + const value = parent ? condition(argument.name, directive === 'ifndef', i) : false; + stack.push({ parent, active: parent && value === true, taken: value === true, + uncertain: value === undefined, elseSeen: false }); + } else { + const frame = stack[stack.length - 1]; + if (!frame || frame.elseSeen) { + report(i, 'malformed-conditional', '`elsif without a matching open conditional, or after `else.'); fatal = true; + } else if (!frame.parent || frame.taken || frame.uncertain) frame.active = false; + else { + const value = condition(argument.name, false, i); + frame.active = value === true; + frame.taken = value === true; + frame.uncertain = value === undefined; + } + } + i = argument.end; + continue; + } + if (directive === 'else' || directive === 'endif') { + blank(i, tokenEnd); + const frame = stack[stack.length - 1]; + if (!frame || (directive === 'else' && frame.elseSeen)) { + report(i, 'malformed-conditional', `Unexpected or duplicate \`${directive}.`); fatal = true; + } else if (directive === 'endif') stack.pop(); + else { frame.active = frame.parent && !frame.taken && !frame.uncertain; frame.taken ||= frame.active; frame.elseSeen = true; } + i = tokenEnd; + continue; + } + if (directive === 'define') { + const end = lineEnd(tokenEnd); + const argument = nameAfter(tokenEnd); + if (active()) { + if (argument.name) macros.set(argument.name, true); + else { report(i, 'unsupported-definition', 'Macro definition without a supported simple name was preserved.'); invalidateMacros(); } + } else blank(i, end); + i = end; + continue; + } + if (directive === 'undef') { + const argument = nameAfter(tokenEnd); + if (active()) { + if (argument.name) macros.set(argument.name, false); + else { report(i, 'unsupported-definition', 'Macro undefinition without a supported simple name was preserved.'); invalidateMacros(); } + } else blank(i, argument.end); + i = argument.end; + continue; + } + if (directive === 'undefineall') { + if (active()) { macros.clear(); unknownMacros = false; } else blank(i, tokenEnd); + i = tokenEnd; + continue; + } + if (directive === 'include') { + let cursor = tokenEnd; + while (text[cursor] === ' ' || text[cursor] === '\t') cursor++; + const opening = text[cursor]; + const closing = opening === '"' ? '"' : opening === '<' ? '>' : ''; + const end = closing ? text.indexOf(closing, cursor + 1) : -1; + const supported = end >= 0 && !/[\r\n]/.test(text.slice(cursor, end)); + const includeEnd = supported ? end + 1 : lineEnd(cursor); + if (active()) { + if (!supported) { + report(i, 'unsupported-include', 'Only literal quoted or bracketed include paths are supported.'); invalidateMacros(); + } else { + const request = text.slice(cursor + 1, end); + let included: HdlIncludeSource | null = null; + try { included = options.readInclude?.(request, filePath, options.includeDirs ?? []) ?? null; } + catch { report(i, 'include-read-failed', `Cannot read include ${request}.`); } + if (!included) { report(i, 'unresolved-include', `Include ${request} was not resolved; subsequent macro state is uncertain.`); invalidateMacros(); } + else { + dependencies.add(included.filePath); + const key = JSON.stringify([included.filePath, unknownMacros, [...macros].sort(([a], [b]) => a.localeCompare(b))]); + if (depth >= maxDepth || trace.has(key)) { + report(i, 'include-cycle-or-depth', `Include recursion cannot be resolved at ${included.filePath}.`); invalidateMacros(); + } else { trace.add(key); process(included.source, included.filePath, depth + 1); trace.delete(key); } + } + } + } else blank(i, includeEnd); + i = includeEnd; + continue; + } + if (active()) { + report(i, OTHER_DIRECTIVES.has(directive) ? 'unsupported-directive' : 'unsupported-macro-expansion', + `\`${directive} was preserved; ${OTHER_DIRECTIVES.has(directive) ? 'this compiler directive is not interpreted' : 'macro expansion is not supported'}.`); + // A replacement can itself contain `define/`undef/`include. Without + // expanding it, later macro definedness cannot be assumed unchanged. + if (!OTHER_DIRECTIVES.has(directive)) invalidateMacros(); + } else blank(i, tokenEnd); + i = tokenEnd; + continue; + } else i++; + if (!active()) blank(start, i); + } + if (stack.length) { report(text.length, 'malformed-conditional', 'Conditional directive has no matching `endif.'); fatal = true; } + if (fatal) { invalidateMacros(); return mask(text); } + return output.join(''); + }; + const selected = process(source, options.filePath, 0); + return { source: selected, dependencies: [...dependencies], diagnostics, complete: diagnostics.length === 0 }; +} diff --git a/src/hdl/profile.ts b/src/hdl/profile.ts new file mode 100644 index 000000000..4d3babb92 --- /dev/null +++ b/src/hdl/profile.ts @@ -0,0 +1,213 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { createHash } from 'node:crypto'; + +export interface ActiveHdlProfile { + name: string; + files: string[]; + includeDirs: string[]; + defines: Record; + topModules: string[]; + languageMode: 'verilog' | 'systemverilog'; + /** Filelist contents participate in the fingerprint; include contents belong to preprocessing dependencies. */ + dependencies: Array<{ path: string; sha256: string }>; + fingerprint: string; +} +export interface HdlProfileLoadResult { + status: 'none' | 'active' | 'invalid-config' | 'invalid-profile'; + profile: ActiveHdlProfile | null; + diagnostics: string[]; + configInvalid: boolean; + configPresent: boolean; +} +interface ProfileSpec { + files: string[]; + filelists: string[]; + includeDirs: string[]; + defines: Record; + topModules: string[]; + languageMode: 'verilog' | 'systemverilog'; +} +export function isReservedHdlPath(relativePath: string): boolean { + return relativePath.split(/[\\/]/).some(part => part === '.git' || part === '.jj' || part === '.codegraph' || part.startsWith('.codegraph-')); +} +const MAX_BYTES = 1024 * 1024, MAX_LISTS = 256, MAX_DEPTH = 32, MAX_TOKENS = 100000; +const hash = (text: string) => createHash('sha256').update(text).digest('hex'); +const object = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); +function fail(message: string): never { throw new Error(message); } +function strings(value: unknown, label: string): string[] { + if (value === undefined) return []; + if (!Array.isArray(value) || !value.every(v => typeof v === 'string' && v.trim().length > 0)) fail(`${label}: expected an array of non-empty strings`); + return value; +} +function strictKeys(value: Record, keys: string[], label: string): void { + for (const key of Object.keys(value)) if (!keys.includes(key)) fail(`${label}: unsupported field ${key}`); +} +function spec(value: unknown, label: string): ProfileSpec { + if (!object(value)) fail(`${label}: expected an object`); + strictKeys(value, ['files', 'filelists', 'includeDirs', 'defines', 'topModules', 'languageMode'], label); + const defines: Record = Object.create(null); + if (value.defines !== undefined) { + if (!object(value.defines)) fail(`${label}.defines: expected an object with string values`); + for (const [name, val] of Object.entries(value.defines)) { + if (!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(name) || typeof val !== 'string') fail(`${label}.defines: invalid macro name or non-string value`); + defines[name] = val; + } + } + const languageMode = value.languageMode ?? 'systemverilog'; + if (languageMode !== 'verilog' && languageMode !== 'systemverilog') fail(`${label}.languageMode: expected verilog or systemverilog`); + return { files: strings(value.files, `${label}.files`), filelists: strings(value.filelists, `${label}.filelists`), + includeDirs: strings(value.includeDirs, `${label}.includeDirs`), defines, + topModules: strings(value.topModules, `${label}.topModules`), languageMode }; +} + +/** Preserve quoting so '+' inside a quoted define value or directory is not a separator. */ +function tokens(text: string): string[] { + const out: string[] = []; + let token = '', quote = ''; + const flush = () => { if (token) { out.push(token); token = ''; } }; + for (let i = 0; i < text.length; i++) { + const c = text[i]!, next = text[i + 1]; + if (c === '\\' && next !== undefined) { + if (next === '\n') { i++; continue; } + if (next === '\r' && text[i + 2] === '\n') { i += 2; continue; } + token += c + next; i++; continue; + } + if (quote) { token += c; if (c === quote) quote = ''; continue; } + // An apostrophe in 1'b1 / '0 is a Verilog literal, not a path quote. + if (c === '"' || (c === "'" && (!token || token.endsWith('+')))) { quote = c; token += c; continue; } + if ((c === '/' && next === '/') || (c === '#' && !token)) { + flush(); while (i < text.length && text[i] !== '\n') i++; continue; + } + if (c === '/' && next === '*') { + flush(); const end = text.indexOf('*/', i + 2); if (end < 0) fail('Unterminated filelist block comment'); i = end + 1; continue; + } + if (/\s/.test(c)) { flush(); continue; } + token += c; + } + if (quote) fail('Unterminated filelist quote'); + flush(); + return out; +} +function splitPlus(token: string): string[] { + const parts: string[] = []; + let part = '', quote = ''; + for (let i = 0; i < token.length; i++) { + const c = token[i]!; + if (c === '\\' && i + 1 < token.length) { part += c + token[++i]; continue; } + if (quote) { part += c; if (c === quote) quote = ''; continue; } + if (c === '"' || (c === "'" && !part)) { quote = c; part += c; continue; } + if (c === '+') { parts.push(part); part = ''; } else part += c; + } + parts.push(part); return parts; +} +function unquote(token: string): string { + let out = '', quote = ''; + for (let i = 0; i < token.length; i++) { + const c = token[i]!; + if (c === '\\' && i + 1 < token.length && /[\\"'\s]/.test(token[i + 1]!)) { out += token[++i]; continue; } + if (quote) { if (c === quote) quote = ''; else out += c; continue; } + if (c === '"' || (c === "'" && i === 0)) { quote = c; continue; } + out += c; + } + if (quote) fail('Unterminated filelist quote'); + return out; +} + +/** Load only a selected, explicit source profile. Invalid HDL never means "scan everything". + * No HDL section preserves legacy behavior. Malformed overall JSON is reported separately + * so the existing config loader can retain its policy and an indexed-profile guard can fail safely. + */ +export function loadHdlProfile(projectRoot: string): HdlProfileLoadResult { + const result = (status: HdlProfileLoadResult['status'], diagnostics: string[] = [], profile: ActiveHdlProfile | null = null): HdlProfileLoadResult => + ({ status, profile, diagnostics, configInvalid: status === 'invalid-config', configPresent: present }); + let present = false; + let root: string; + try { root = fs.realpathSync(projectRoot); } catch { return result('invalid-config', ['HDL project root is unavailable']); } + const contained = (candidate: string): string => { + if (isReservedHdlPath(path.relative(root, candidate))) fail('Reserved VCS/CodeGraph paths are unsupported in HDL profiles'); + const real = fs.realpathSync(candidate); + const rel = path.relative(root, real); + if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) fail('Paths outside the project are unsupported in HDL profiles'); + if (isReservedHdlPath(rel)) fail('Reserved VCS/CodeGraph paths are unsupported in HDL profiles'); + return real; + }; + const relative = (absolute: string) => path.relative(root, absolute).split(path.sep).join('/') || '.'; + const read = (absolute: string) => { + const stat = fs.statSync(absolute); + if (!stat.isFile() || stat.size > MAX_BYTES) fail('Config/filelist must be a regular file of at most 1 MiB'); + return fs.readFileSync(absolute, 'utf8'); + }; + let config: unknown; + try { + const file = path.join(root, 'codegraph.json'); + if (!fs.existsSync(file)) return result('none'); + present = true; + config = JSON.parse(read(contained(file))); + if (!object(config)) return result('invalid-config', ['codegraph.json: expected a JSON object']); + } catch (error) { return result('invalid-config', [`codegraph.json could not be read: ${(error as Error).message}`]); } + if (!Object.prototype.hasOwnProperty.call(config, 'hdl')) return result('none'); + try { + const hdl = config.hdl; + if (!object(hdl)) fail('codegraph.json hdl: expected an object'); + strictKeys(hdl, ['activeProfile', 'profiles'], 'hdl'); + if (typeof hdl.activeProfile !== 'string' || !hdl.activeProfile.trim()) fail('hdl.activeProfile: expected a non-empty profile name'); + if (!object(hdl.profiles) || !Object.prototype.hasOwnProperty.call(hdl.profiles, hdl.activeProfile)) fail('hdl.activeProfile does not select an existing profile'); + const selected = spec(hdl.profiles[hdl.activeProfile], `hdl.profiles.${hdl.activeProfile}`); + const files = new Set(), includeDirs = new Set(), dependencies = new Map(); + const defines: Record = Object.assign(Object.create(null), selected.defines); + const resolve = (name: string, base: string, directory: boolean) => { + const real = contained(path.resolve(base, name)); + const stat = fs.statSync(real); + if (directory ? !stat.isDirectory() : !stat.isFile()) fail(`HDL path has the wrong file type: ${relative(real)}`); + return real; + }; + const addFile = (name: string, base: string) => files.add(relative(resolve(name, base, false))); + const addInclude = (name: string, base: string) => includeDirs.add(relative(resolve(name, base, true))); + for (const file of selected.files) addFile(file, root); + for (const directory of selected.includeDirs) addInclude(directory, root); + let tokenCount = 0, listCount = 0; + const stack = new Set(); + const visited = new Set(); + const visit = (name: string, lookupBase: string, localPaths: boolean, depth: number): void => { + if (depth > MAX_DEPTH) fail('HDL filelist nesting exceeds 32 levels'); + const file = resolve(name, lookupBase, false), key = `${file}\0${localPaths}`; + if (stack.has(file)) fail(`HDL filelist cycle: ${relative(file)}`); + if (visited.has(key)) return; + if (++listCount > MAX_LISTS) fail('HDL profile exceeds 256 filelists'); + stack.add(file); visited.add(key); + const text = read(file), entries = tokens(text); + dependencies.set(relative(file), hash(text)); + tokenCount += entries.length; + if (tokenCount > MAX_TOKENS) fail('HDL profile exceeds 100000 filelist tokens'); + const base = localPaths ? path.dirname(file) : root; + for (let i = 0; i < entries.length; i++) { + const raw = entries[i]!, token = unquote(raw); + if (token === '-f' || token === '-F') { + const next = entries[++i]; if (!next) fail(`Missing argument for ${token}`); + visit(unquote(next), base, token === '-F', depth + 1); + } else if (raw.startsWith('+incdir+')) { + const dirs = splitPlus(raw.slice('+incdir+'.length)); + if (dirs.some(d => !d)) fail('Empty +incdir+ entry'); + for (const directory of dirs) addInclude(unquote(directory), base); + } else if (raw.startsWith('+define+')) { + for (const definition of splitPlus(raw.slice('+define+'.length))) { + const decoded = unquote(definition), eq = decoded.indexOf('='); + const macro = eq < 0 ? decoded : decoded.slice(0, eq), value = eq < 0 ? '1' : decoded.slice(eq + 1); + if (!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(macro)) fail('Invalid +define+ macro name'); + if (Object.prototype.hasOwnProperty.call(defines, macro) && defines[macro] !== value) fail(`Conflicting values for HDL macro ${macro}`); + defines[macro] = value; + } + } else if (/^[+-]/.test(token)) fail(`Unsupported HDL filelist option: ${token.split('+').slice(0, 2).join('+')}`); + else addFile(token, base); + } + stack.delete(file); + }; + for (const filelist of selected.filelists) visit(filelist, root, path.extname(filelist) === '.F', 1); + const resolved = { name: hdl.activeProfile, files: [...files], includeDirs: [...includeDirs], + defines: Object.fromEntries(Object.entries(defines).sort(([a], [b]) => a.localeCompare(b))), + topModules: [...new Set(selected.topModules)], languageMode: selected.languageMode, + dependencies: [...dependencies].map(([file, sha256]) => ({ path: file, sha256 })).sort((a,b) => a.path.localeCompare(b.path)) }; + return result('active', [], { ...resolved, fingerprint: hash(JSON.stringify(resolved)) }); + } catch (error) { return result('invalid-profile', [(error as Error).message]); } +} diff --git a/src/hdl/semantic-import.ts b/src/hdl/semantic-import.ts new file mode 100644 index 000000000..bab8abcb8 --- /dev/null +++ b/src/hdl/semantic-import.ts @@ -0,0 +1,234 @@ +import * as path from 'node:path'; + +export interface HdlMacroPoint { file: string; line: number; column: number; byteOffset: number } +export interface HdlMacroFrame { name: string | null; argument: boolean; spelling?: HdlMacroPoint; invocation?: { start: HdlMacroPoint; end: HdlMacroPoint } } + +export type HdlExpressionRole = 'initializer' | 'declared-initializer' | 'type'; +export interface HdlExpressionOrigin { + role: HdlExpressionRole; + sourceOrigin: 'macro'; + source?: { file: string; line: number; column: number | null }; + macroExpansion: HdlMacroFrame[]; + macroExpansionComplete: boolean; +} +export interface HdlExpressionCoverage { + initializer: 'checked' | 'not-applicable' | 'unavailable' | 'command-line'; + declaredInitializer: 'checked' | 'not-applicable' | 'unavailable'; + type: 'checked' | 'not-applicable' | 'unavailable'; + truncated: boolean; +} + +export interface HdlSemanticFact { + kind: 'parameter' | 'port' | 'type'; + name: string; + instancePath: string; + value?: string; + type?: string; + width?: number; + direction?: string; + source?: { file: string; line: number; column: number | null }; + sourceOrigin?: 'direct' | 'macro'; + macroExpansion?: HdlMacroFrame[]; + macroExpansionComplete?: boolean; + expressionOrigins?: HdlExpressionOrigin[]; + expressionOriginCoverage?: HdlExpressionCoverage; +} + +type AstObject = Record; +const object = (value: unknown): value is AstObject => typeof value === 'object' && value !== null && !Array.isArray(value); +function fail(message: string): never { throw new Error(`Invalid or unsupported slang AST: ${message}`); } +const MAX_DEPTH = 128; +const MAX_NODES = 1_000_000; +const MAX_FACTS = 100_000; +const MAX_TEXT = 64 * 1024 * 1024; +const INTEGER_WIDTHS = new Map(Object.entries({ bit: 1, logic: 1, reg: 1, byte: 8, shortint: 16, int: 32, integer: 32, longint: 64, time: 64 })); + +/** Import facts from successful frontend output; the caller must check compiler + * exit/diagnostics first (slang can emit JSON even when compilation failed). + * Addresses are temporary reference keys only, never public identities. */ +export function importSlangSemantics(ast: unknown, sourceRoot: string): HdlSemanticFact[] { + if (!object(ast) || !object(ast.design) || ast.design.kind !== 'Root' || !Array.isArray(ast.design.members)) fail('expected design Root with members'); + const design = ast.design as AstObject; + const root = path.resolve(sourceRoot); + const catalog = new Map(); + const active = new Set(); + const seen = new Set(); + const pending: Array<{ value: unknown; depth: number; exit?: boolean }> = [{ value: ast, depth: 0 }]; + let nodeCount = 0, textSize = 0; + while (pending.length) { + const { value, depth, exit } = pending.pop()!; + if (exit) { active.delete(value as object); continue; } + if (++nodeCount > MAX_NODES || depth > MAX_DEPTH) fail('input size/depth limit exceeded'); + if (typeof value === 'string') { textSize += value.length; if (textSize > MAX_TEXT) fail('input text limit exceeded'); } + if (!object(value) && !Array.isArray(value)) continue; + if (active.has(value)) fail('cyclic input object'); + if (seen.has(value)) continue; + seen.add(value); active.add(value); + pending.push({ value, depth, exit: true }); + if (object(value) && value.addr !== undefined) { + const addr = typeof value.addr === 'number' && Number.isSafeInteger(value.addr) && value.addr >= 0 + ? String(value.addr) : typeof value.addr === 'string' && /^\d+$/.test(value.addr) ? value.addr : fail('malformed address'); + const existing = catalog.get(addr); + if (existing && (existing.kind !== value.kind || existing.name !== value.name)) fail('conflicting address definitions'); + if (!existing || Object.keys(value).length > Object.keys(existing).length) catalog.set(addr, value); + } + const values = Array.isArray(value) ? value : Object.values(value); + if (values.length > MAX_NODES - nodeCount - pending.length) fail('input node limit exceeded'); + for (let i = values.length - 1; i >= 0; i--) pending.push({ value: values[i], depth: depth + 1 }); + } + const resolve = (value: unknown): AstObject => { + if (object(value)) return value; + if (typeof value === 'string') { + const match = /^(\d+)\s/.exec(value); + if (match && catalog.has(match[1]!)) return catalog.get(match[1]!)!; + } + return fail('unresolved symbol/body reference'); + }; + const members = (scope: AstObject): unknown[] => { + if (scope.members === undefined) return []; + if (!Array.isArray(scope.members)) return fail('members must be an array'); + return scope.members; + }; + const nameOf = (node: AstObject): string => { + if (typeof node.name !== 'string' || !node.name || node.name.length > 4096) return fail('missing or oversized symbol name'); + return node.name; + }; + const segment = (name: string): string => /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name) ? name : `\\${name} `; + const join = (parent: string, name: string): string => parent ? `${parent}.${segment(name)}` : segment(name); + const range = (value: unknown): [number, number] => { + const match = typeof value === 'string' && /^\[\s*(-?\d+)\s*:\s*(-?\d+)\s*\]$/.exec(value); + if (!match) return fail('expected a constant numeric range'); + const left = Number(match[1]), right = Number(match[2]); + if (!Number.isSafeInteger(left) || !Number.isSafeInteger(right) || !Number.isSafeInteger(Math.abs(left - right) + 1)) return fail('range exceeds exact numeric bounds'); + return [left, right]; + }; + const stripAddresses = (text: string): string | undefined => { + let unknown = false; + const cleaned = text.replace(/\b(\d+)\s+(?=[A-Za-z_$\\])/g, (match, addr: string) => { + if (catalog.has(addr)) return ''; + unknown = true; return match; + }); + return unknown ? undefined : cleaned; + }; + const typeInfo = (value: unknown, ancestors = new Set()): { type?: string; width?: number } => { + if (typeof value === 'string') { + const ref = /^(\d+)\s/.exec(value); + if (ref) { + const target = catalog.get(ref[1]!); + if (!target) return {}; + return { ...typeInfo(target, ancestors), type: stripAddresses(value) }; + } + // Composite pretty-printed strings do not prove packed vs unpacked shape. + const base = value.replace(/\s+(signed|unsigned)$/, ''); + return { type: stripAddresses(value), ...(INTEGER_WIDTHS.has(base) ? { width: INTEGER_WIDTHS.get(base)! } : {}) }; + } + if (!object(value)) return {}; + if (ancestors.size >= MAX_DEPTH || ancestors.has(value)) return fail('cyclic or overly deep type'); + const next = new Set(ancestors); next.add(value); + const name = typeof value.name === 'string' && value.name ? stripAddresses(value.name) : undefined; + if (value.kind === 'TypeAlias') return { ...typeInfo(value.target, next), ...(name ? { type: name } : {}) }; + if (value.kind === 'ScalarType' || value.kind === 'PredefinedIntegerType') { + const base = name?.replace(/\s+(signed|unsigned)$/, '') ?? ''; + return { type: name, ...(INTEGER_WIDTHS.has(base) ? { width: INTEGER_WIDTHS.get(base)! } : {}) }; + } + if (value.kind === 'PackedArrayType') { + const [left, right] = range(value.range); + const element = typeInfo(value.elementType, next); + const width = element.width === undefined ? undefined : element.width * (Math.abs(left - right) + 1); + if (width !== undefined && !Number.isSafeInteger(width)) fail('packed width exceeds exact numeric bounds'); + return { ...(element.type ? { type: `${element.type}[${left}:${right}]` } : {}), ...(width !== undefined ? { width } : {}) }; + } + return name ? { type: name } : {}; + }; + const sourceOf = (node: AstObject): HdlSemanticFact['source'] => { + const file = node.source_file; + if (file === undefined) return undefined; + if (typeof file !== 'string') return fail('invalid source filename'); + if (!file || file.startsWith('<') || (!path.isAbsolute(file) && path.win32.isAbsolute(file))) return undefined; + const full = path.resolve(root, file.replace(/\\/g, '/')); + const relative = path.relative(root, full); + if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return undefined; + const line = node.source_line, column = node.source_column; + if (!Number.isSafeInteger(line) || (line as number) < 0 || (column !== undefined && (!Number.isSafeInteger(column) || (column as number) < 0))) return fail('invalid source coordinates'); + if (line === 0) return undefined; + return { file: relative.split(path.sep).join('/'), line: line as number, column: column === undefined || column === 0 ? null : column as number }; + }; + const indexValue = (value: unknown): string => { + if (typeof value !== 'string' || !/^-?\d+$/.test(value) || !Number.isSafeInteger(Number(value))) return fail('generate index is not a known decimal integer'); + return String(Number(value)); + }; + + const facts: HdlSemanticFact[] = []; + const keys = new Set(); + let visits = 0, factText = 0; + const visit = (node: AstObject, scopePath: string, ancestry: Set, depth: number): void => { + if (++visits > MAX_FACTS || depth > MAX_DEPTH || scopePath.length > 16384) fail('reachable hierarchy size/depth limit exceeded'); + if (ancestry.has(node)) fail('cyclic instance hierarchy'); + const chain = new Set(ancestry); chain.add(node); + if (node.kind === 'Parameter' || node.kind === 'Port') { + const name = nameOf(node); + const fact: HdlSemanticFact = { kind: node.kind === 'Parameter' ? 'parameter' : 'port', name, instancePath: scopePath, ...typeInfo(node.type) }; + if (node.kind === 'Parameter' && ['string', 'number', 'boolean'].includes(typeof node.value)) fact.value = String(node.value); + if (node.kind === 'Port' && typeof node.direction === 'string') fact.direction = node.direction; + const source = sourceOf(node); if (source) fact.source = source; + const key = JSON.stringify([fact.kind, scopePath, name]); + if (keys.has(key)) fail('duplicate fact identity'); + keys.add(key); factText += JSON.stringify(fact).length; + if (facts.length >= MAX_FACTS || factText > MAX_TEXT) fail('fact output size limit exceeded'); + facts.push(fact); return; + } + if (node.kind === 'Instance') { + const body = resolve(node.body); + if (body.kind !== 'InstanceBody') fail('instance body has wrong kind'); + visit(body, scopePath, chain, depth + 1); return; + } + if (node.kind === 'GenerateBlock' && node.isUninstantiated === true) return; + if (node.kind === 'GenerateBlockArray') { + // Slang's loopVariable can refer to an evaluator-only symbol absent + // from the serialized catalog. Its explicit name still identifies the + // matching implicit local Parameter in every materialized iteration. + const variableName = object(node.loopVariable) ? nameOf(node.loopVariable) + : typeof node.loopVariable === 'string' ? /^\d+ (.+)$/.exec(node.loopVariable)?.[1] : undefined; + if (!variableName) fail('missing generate loop variable name'); + const indices = new Set(); + for (const entry of members(node)) { + const block = resolve(entry); + if (block.kind === 'Genvar') continue; + if (block.kind !== 'GenerateBlock') fail('invalid generate array member'); + if (block.isUninstantiated === true) continue; + const parameters = members(block).map(resolve).filter(n => n.kind === 'Parameter' && n.name === variableName); + if (parameters.length !== 1) fail('missing or ambiguous generate index parameter'); + const index = indexValue(parameters[0]!.value); + if (indices.has(index)) fail('duplicate generate index'); + indices.add(index); visit(block, `${scopePath}[${index}]`, chain, depth + 1); + } + return; + } + if (node.kind === 'InstanceArray') { + const [left, right] = range(node.range); + const entries = members(node); + if (entries.length !== Math.abs(left - right) + 1) fail('instance array length disagrees with range'); + // Slang serializes elements in ascending numeric index order, including + // descending declarations; verified with distinct per-index defparams. + for (let i = 0; i < entries.length; i++) { + const child = resolve(entries[i]); + if (!['Instance', 'InstanceArray'].includes(String(child.kind))) fail('invalid instance array member'); + visit(child, `${scopePath}[${Math.min(left, right) + i}]`, chain, depth + 1); + } + return; + } + if (!['InstanceBody', 'GenerateBlock', 'Root'].includes(String(node.kind))) return; + for (const entry of members(node)) { + const child = resolve(entry); + if (['Instance', 'InstanceArray', 'GenerateBlockArray', 'GenerateBlock'].includes(String(child.kind))) { + if (child.kind === 'GenerateBlock' && child.isUninstantiated === true) continue; + visit(child, join(scopePath, nameOf(child)), chain, depth + 1); + } else if (child.kind === 'Parameter' || child.kind === 'Port') visit(child, scopePath, chain, depth + 1); + } + }; + for (const entry of members(design)) { + const node = resolve(entry); + if (node.kind === 'Instance' || node.kind === 'InstanceArray') visit(node, segment(nameOf(node)), new Set(), 0); + } + return facts; +} diff --git a/src/hdl/semantic-runner.ts b/src/hdl/semantic-runner.ts new file mode 100644 index 000000000..3e08227e8 --- /dev/null +++ b/src/hdl/semantic-runner.ts @@ -0,0 +1,263 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as os from 'node:os'; +import { createHash } from 'node:crypto'; +import { spawn } from 'node:child_process'; +import { loadHdlProfile, isReservedHdlPath, type ActiveHdlProfile } from './profile'; + +export interface RunSlangOptions { + executable?: string; + pythonExecutable?: string; + top?: string; + parameters?: Record; + allowUseBeforeDeclare?: boolean; + signal?: AbortSignal; + timeoutMs?: number; +} +export interface RunSlangResult { + ast: unknown; + /** The deleted immutable snapshot root; use only to remap AST paths to sources keys. */ + sourceRoot: string; + profileName: string; + configurationFingerprint: string; + fingerprint: string; + version: string; + executableSha256: string; + frontend: 'slang' | 'pyslang'; + exporterSha256?: string; + librarySha256?: string; + diagnostics: string[]; + sources: Record; + top: string; + parameters: Record; + allowUseBeforeDeclare: boolean; + languageStandard: '1364-2005' | '1800-2023'; + compilationUnitMode: 'separate'; + runnerVersion: string; + compilerLimits: readonly string[]; +} +const RUNNER_VERSION = 'slang-snapshot-v2'; +const COMPILER_LIMITS = Object.freeze(['--max-hierarchy-depth=128','--max-generate-steps=10000','--max-instance-array=10000','--max-constexpr-steps=1000000','--max-constant-size=1048576']); +const FILE_LIMIT = 1024 * 1024, TOTAL_LIMIT = 64 * 1024 * 1024, AST_LIMIT = 64 * 1024 * 1024; +const OUTPUT_LIMIT = 1024 * 1024, FILE_COUNT_LIMIT = 20000; +const digest = (content: Buffer | string) => createHash('sha256').update(content).digest('hex'); +function abort(signal?: AbortSignal): void { if (signal?.aborted) throw new Error('slang semantic run cancelled'); } +function resolveExecutable(name: string, preserveLauncherPath = false): string { + if (!name || name.includes('\0')) throw new Error('A slang executable must be specified'); + const candidates = name.includes('/') ? [path.resolve(name)] : (process.env.PATH ?? '').split(path.delimiter).map(dir => path.join(dir, name)); + for (const candidate of candidates) { + try { fs.accessSync(candidate, fs.constants.X_OK); const real = fs.realpathSync(candidate); if (fs.statSync(real).isFile()) return preserveLauncherPath ? path.resolve(candidate) : real; } catch { /* try next PATH entry */ } + } + throw new Error(`slang executable is unavailable: ${name}`); +} +async function executableHash(file: string): Promise { + const hash = createHash('sha256'); + for await (const chunk of fs.createReadStream(file)) hash.update(chunk); + return hash.digest('hex'); +} + +/** Literal includes only. Macro-computed includes cannot be snapshotted without a compiler filesystem API. */ +function includes(source: string): Array<{ name: string; local: boolean }> { + const result: Array<{name: string; local: boolean}> = []; + for (let i = 0; i < source.length;) { + if (source.startsWith('//', i)) { const end = source.indexOf('\n', i); i = end < 0 ? source.length : end; continue; } + if (source.startsWith('/*', i)) { const end = source.indexOf('*/', i + 2); i = end < 0 ? source.length : end + 2; continue; } + if (source[i] === '"') { i++; while (i < source.length) { if (source[i] === '\\') i += 2; else if (source[i++] === '"') break; } continue; } + if (source[i] === '\\') { i++; while (i < source.length && !/\s/.test(source[i]!)) i++; continue; } + const match = source.slice(i).match(/^`include\b[ \t]*/); + if (!match) { i++; continue; } + i += match[0].length; + const opening = source[i], closing = opening === '"' ? '"' : opening === '<' ? '>' : ''; + if (!closing) throw new Error('Macro-computed include paths are unsupported by the slang snapshot runner'); + const end = source.indexOf(closing, i + 1); + if (end < 0 || /[\r\n\\`]/.test(source.slice(i + 1, end))) throw new Error('Unsupported literal include path in slang snapshot'); + result.push({name: source.slice(i + 1, end), local: opening === '"'}); i = end + 1; + } + return result; +} +interface Snapshot { + sources: Record; + hashes: Record; + missing: string[]; +} +function collect(root: string, profile: ActiveHdlProfile, signal?: AbortSignal, deadline?: number): Snapshot { + const check = () => { abort(signal); if(deadline && Date.now() >= deadline)throw new Error('slang semantic run timed out'); }; + const macroIncludes = Object.values(profile.defines).flatMap(includes); + const sources: Record = Object.create(null), hashes: Record = Object.create(null); + const missing = new Set(), visitedDirs = new Set(); + const queue: string[] = []; + let bytes = 0, includeCandidates = 0; + const relative = (file: string) => path.relative(root, file).split(path.sep).join('/'); + const confined = (file: string): string => { + const rel = relative(file); + if (!rel || rel === '..' || rel.startsWith('../') || path.isAbsolute(rel) || isReservedHdlPath(rel)) throw new Error('slang snapshot input escapes the project or uses a reserved path'); + let current = root; + for (const part of rel.split('/')) { + current = path.join(current, part); + try { if (fs.lstatSync(current).isSymbolicLink()) throw new Error('Symbolic links are unsupported in slang snapshot inputs'); } + catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') break; throw error; } + } + return rel; + }; + const add = (file: string, required: boolean): void => { + check(); + if(!required && ++includeCandidates > 100000)throw new Error('slang snapshot exceeds the include candidate limit'); + const rel = confined(file); + if (Object.prototype.hasOwnProperty.call(hashes, rel)) return; + let stat: fs.Stats; + try { stat = fs.statSync(file); } catch (error) { + if (!required && (error as NodeJS.ErrnoException).code === 'ENOENT') { missing.add(rel); return; } + throw error; + } + if (!stat.isFile()) throw new Error('slang snapshot input is not a regular file'); + if (stat.size > FILE_LIMIT || bytes + stat.size > TOTAL_LIMIT || queue.length >= FILE_COUNT_LIMIT) throw new Error('slang snapshot exceeds the input size/file count limit'); + const content = fs.readFileSync(file); + if (content.length > FILE_LIMIT || bytes + content.length > TOTAL_LIMIT) throw new Error('slang snapshot exceeds the input size limit'); + let text: string; + try { text = new TextDecoder('utf-8', {fatal:true,ignoreBOM:true}).decode(content); } catch { throw new Error(`Non-UTF-8 file in slang snapshot: ${rel}`); } + sources[rel] = text; hashes[rel] = digest(content); bytes += content.length; queue.push(rel); + }; + const tree = (dir: string, depth = 0): void => { + check(); + if(depth > 128 || visitedDirs.size >= FILE_COUNT_LIMIT)throw new Error('slang snapshot exceeds the directory traversal limit'); + if (visitedDirs.has(dir)) return; visitedDirs.add(dir); + for (const entry of fs.readdirSync(dir, {withFileTypes:true})) { + const file = path.join(dir, entry.name), rel = relative(file); + if (isReservedHdlPath(rel)) continue; + confined(file); + if (entry.isDirectory()) tree(file,depth+1); else if (entry.isFile()) add(file, true); else throw new Error('Special files are unsupported in slang snapshot inputs'); + } + }; + for (const file of profile.files) add(path.resolve(root,file), true); + for (const dir of profile.includeDirs) { + const absolute = path.resolve(root,dir); + if (absolute !== root) confined(absolute); + tree(absolute); + } + // Read every literal candidate, including inactive branches and lower-priority + // shadow candidates. Missing candidates are fingerprinted and checked again. + for (let index = 0; index < queue.length; index++) { + const file = queue[index]!; + for (const include of [...includes(sources[file]!), ...macroIncludes]) { + if (!include.name || path.isAbsolute(include.name) || /^[A-Za-z]:/.test(include.name)) throw new Error('Absolute include paths are unsupported by the slang snapshot runner'); + const bases = [...(include.local ? [path.dirname(path.join(root,file))] : []), ...profile.includeDirs.map(dir => path.join(root,dir))]; + for (const base of bases) add(path.resolve(base,include.name), false); + } + } + return {sources,hashes:Object.fromEntries(Object.entries(hashes).sort(([a],[b])=>a.localeCompare(b))),missing:[...missing].sort()}; +} + +function run(executable: string, args: string[], cwd: string, timeout: number, signal?: AbortSignal, astFile?: string): Promise<{stdout:string;stderr:string}> { + abort(signal); + return new Promise((resolve,reject) => { + const child = spawn(executable,args,{cwd,detached:true,shell:false,stdio:['ignore','pipe','pipe']}); + let stdout = '', stderr = '', size = 0, failure: Error | undefined, finished = false; + const stop = (message: string): void => { + failure ??= new Error(message); + if (child.pid) { try { process.kill(-child.pid,'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* already exited */ } } } + }; + const timer = setTimeout(()=>stop('slang semantic run timed out'),timeout); + const onAbort = () => stop('slang semantic run cancelled'); + signal?.addEventListener('abort',onAbort,{once:true}); + const monitor = astFile ? setInterval(()=>{ + try { if (fs.statSync(astFile).size > AST_LIMIT) stop('slang AST output exceeds 64 MiB'); } catch { /* not created yet */ } + },25) : undefined; + const finish = (error?: Error): void => { + if (finished) return; finished=true; clearTimeout(timer); if(monitor)clearInterval(monitor); signal?.removeEventListener('abort',onAbort); + if(error)reject(error); else resolve({stdout,stderr}); + }; + child.stdout.on('data',(chunk:Buffer)=>{size+=chunk.length;if(size>OUTPUT_LIMIT)stop('slang output exceeds 1 MiB');else stdout+=chunk.toString('utf8');}); + child.stderr.on('data',(chunk:Buffer)=>{size+=chunk.length;if(size>OUTPUT_LIMIT)stop('slang output exceeds 1 MiB');else stderr+=chunk.toString('utf8');}); + child.on('error',error=>finish(error)); + child.on('close',(code)=>{ + // Do not leave detached descendants alive after the owned frontend exits. + if(child.pid)try{process.kill(-child.pid,'SIGKILL');}catch{/* group already gone */} + finish(failure ?? (code===0 ? undefined : new Error(`slang failed (${code}): ${stderr.slice(0,4000) || stdout.slice(0,4000)}`))); + }); + if(signal?.aborted)onAbort(); + }); +} + +/** Optional on-demand frontend. Snapshot confinement is not an adversarial OS compiler sandbox. */ +export async function runSlangSemantics(projectRoot: string, options: RunSlangOptions): Promise { + if(process.platform==='win32')throw new Error('slang semantic runner is not supported on Windows until process-tree cancellation is verified'); + const requestedSlang=options.executable, requestedPython=options.pythonExecutable; + if((requestedSlang!==undefined)===(requestedPython!==undefined))throw new Error('Specify exactly one of executable or pythonExecutable'); + const frontend:RunSlangResult['frontend']=requestedPython!==undefined?'pyslang':'slang'; + const requestedExecutable=requestedPython??requestedSlang; + if(typeof requestedExecutable!=='string'||!requestedExecutable.trim())throw new Error('A frontend executable must be specified'); + const signal=options.signal, allowUseBeforeDeclare=!!options.allowUseBeforeDeclare; + abort(signal); + const timeout=options.timeoutMs??30000; + if(!Number.isSafeInteger(timeout)||timeout<1||timeout>300000)throw new Error('slang timeoutMs must be between 1 and 300000'); + const deadline=Date.now()+timeout, remaining=()=>Math.max(1,deadline-Date.now()); + const root=fs.realpathSync(projectRoot), loaded=loadHdlProfile(root); + if(loaded.status!=='active'||!loaded.profile)throw new Error(`slang semantics requires a valid active HDL profile: ${loaded.diagnostics.join('; ')||loaded.status}`); + const profile=loaded.profile; + const top=options.top??(profile.topModules.length===1?profile.topModules[0]:undefined); + if(!top||!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(top))throw new Error('slang semantics requires one explicit simple top module'); + if(!profile.files.length)throw new Error('slang semantics requires at least one source unit'); + const parameters:Record=Object.create(null); + for(const [name,value] of Object.entries(options.parameters??{}).sort(([a],[b])=>a.localeCompare(b))) { + if(!/^[A-Za-z_][A-Za-z0-9_$]*$/.test(name)||typeof value!=='string'||value.includes('\0'))throw new Error('Invalid slang parameter override'); + parameters[name]=value; + } + // Preserve the venv launch path: realpath'ing bin/python before exec would + // select the base interpreter's site-packages instead of the configured venv. + const executable=resolveExecutable(requestedExecutable,frontend==='pyslang'); + const exporter=frontend==='pyslang'?path.join(__dirname,'source-map-export.py'):undefined; + if(exporter) { + try { const stat=fs.statSync(exporter); if(!stat.isFile()||stat.size>FILE_LIMIT)throw new Error('invalid exporter'); } + catch { throw new Error('Shipped pyslang exporter is unavailable or exceeds 1 MiB'); } + } + const commandPrefix=exporter?['-I',exporter]:[]; + const executableSha256=await executableHash(executable); + const exporterSha256=exporter?await executableHash(exporter):undefined; + const before=collect(root,profile,signal,deadline); + const base=process.env.CODEGRAPH_SEMANTIC_TMPDIR||path.join(os.homedir(),'storage','codegraph-semantic-runs'); + fs.mkdirSync(base,{recursive:true,mode:0o700}); + const baseRelative=path.relative(root,fs.realpathSync(base)); + if(!baseRelative||(!baseRelative.startsWith(`..${path.sep}`)&&baseRelative!=='..'&&!path.isAbsolute(baseRelative)))throw new Error('Semantic snapshot directory must be outside the source project'); + const temp=fs.mkdtempSync(path.join(base,'run-')),sourceRoot=path.join(temp,'sources'),astFile=path.join(temp,'ast.json'); + try { + fs.mkdirSync(sourceRoot,{mode:0o700}); + for(const dir of profile.includeDirs) fs.mkdirSync(path.join(sourceRoot,dir),{recursive:true,mode:0o700}); + for(const [file,source] of Object.entries(before.sources)) {const dest=path.join(sourceRoot,file);fs.mkdirSync(path.dirname(dest),{recursive:true,mode:0o700});fs.writeFileSync(dest,source,{mode:0o600});} + const version=(await run(executable,[...commandPrefix,'--version'],sourceRoot,Math.min(remaining(),5000),signal)).stdout.trim(); + const pythonVersion=version.match(/^codegraph pyslang 11\.0\.0 exporter 1 native ([a-f0-9]{64})$/); + const supportedVersion=frontend==='pyslang'?!!pythonVersion:/^slang version 11\.0(?:\.\d+)?(?:\+[A-Za-z0-9]+)?(?:\s.*)?$/.test(version); + if(!supportedVersion)throw new Error(`Unsupported ${frontend} schema family: ${version.slice(0,200)}`); + const librarySha256=pythonVersion?.[1]; + const languageStandard=profile.languageMode==='verilog'?'1364-2005':'1800-2023'; + const args=['--ast-json',astFile,'--ast-json-source-info','--ast-json-detailed-types','--threads=1','--error-limit=20',`--std=${languageStandard}`,'--top',top]; + args.push(...COMPILER_LIMITS); + if(allowUseBeforeDeclare)args.push('--allow-use-before-declare'); + for(const dir of profile.includeDirs)args.push('-I',path.join(sourceRoot,dir)); + for(const [name,value] of Object.entries(profile.defines))args.push(`-D${name}=${value}`); + for(const [name,value] of Object.entries(parameters).sort(([a],[b])=>a.localeCompare(b)))args.push('-G',`${name}=${value}`); + args.push('--',...profile.files); + const output=await run(executable,[...commandPrefix,...args],sourceRoot,remaining(),signal,astFile); + const stat=fs.lstatSync(astFile); + if(!stat.isFile()||stat.isSymbolicLink()||stat.size>AST_LIMIT)throw new Error('Invalid or oversized slang AST output'); + const astText=fs.readFileSync(astFile,'utf8');if(Buffer.byteLength(astText)>AST_LIMIT)throw new Error('slang AST output exceeds 64 MiB'); + const ast:unknown=JSON.parse(astText); + if(!ast||typeof ast!=='object'||Array.isArray(ast))throw new Error('slang AST root must be an object'); + if(frontend==='pyslang'&&((ast as Record).codegraphSemanticVersion!==1||!Array.isArray((ast as Record).facts)))throw new Error('Invalid pyslang semantic exporter envelope'); + if(await executableHash(executable)!==executableSha256)throw new Error('slang executable changed during the run; result discarded'); + if(exporter&&await executableHash(exporter)!==exporterSha256)throw new Error('pyslang exporter changed during the run; result discarded'); + if(frontend==='pyslang') { + const verifiedVersion=(await run(executable,[...commandPrefix,'--version'],sourceRoot,Math.min(remaining(),5000),signal)).stdout.trim(); + if(verifiedVersion!==version)throw new Error('pyslang library changed during the run; result discarded'); + } + const snapshotAfter=collect(sourceRoot,profile,signal,deadline); + if(JSON.stringify(before.hashes)!==JSON.stringify(snapshotAfter.hashes)||JSON.stringify(before.missing)!==JSON.stringify(snapshotAfter.missing))throw new Error('slang modified its source snapshot; result discarded'); + const afterConfig=loadHdlProfile(root); + const after=collect(root,profile,signal,deadline); + if(afterConfig.status!=='active'||afterConfig.profile?.fingerprint!==profile.fingerprint||JSON.stringify(before.hashes)!==JSON.stringify(after.hashes)||JSON.stringify(before.missing)!==JSON.stringify(after.missing))throw new Error('HDL sources or configuration changed during the slang run; result discarded'); + abort(signal); + if(Date.now()>=deadline)throw new Error('slang semantic run timed out'); + const provenance={configurationFingerprint:profile.fingerprint,hashes:before.hashes,missing:before.missing,version,executableSha256,frontend,exporterSha256,librarySha256,top,parameters,allowUseBeforeDeclare,languageStandard,compilationUnitMode:'separate' as const,runnerVersion:RUNNER_VERSION,compilerLimits:COMPILER_LIMITS}; + return {ast,sourceRoot,profileName:profile.name,configurationFingerprint:profile.fingerprint,fingerprint:digest(JSON.stringify(provenance)),version,executableSha256,frontend,...(exporterSha256?{exporterSha256}:{}),...(librarySha256?{librarySha256}:{}), + diagnostics:[output.stdout,output.stderr].map(s=>s.trim()).filter(Boolean),sources:before.sources,top,parameters,allowUseBeforeDeclare,languageStandard,compilationUnitMode:'separate' as const,runnerVersion:RUNNER_VERSION,compilerLimits:COMPILER_LIMITS}; + } finally {fs.rmSync(temp,{recursive:true,force:true});} +} diff --git a/src/hdl/semantics.ts b/src/hdl/semantics.ts new file mode 100644 index 000000000..ee7e27c5b --- /dev/null +++ b/src/hdl/semantics.ts @@ -0,0 +1,76 @@ +import { createHash } from 'node:crypto'; +import { runSlangSemantics, type RunSlangOptions } from './semantic-runner'; +import { importSlangSemantics, type HdlSemanticFact } from './semantic-import'; +import { importMappedSemantics } from './source-map-import'; +import type { HdlProfileStatus } from './status'; +import type { Node, FileRecord } from '../types'; + +export interface HdlSemanticOptions extends RunSlangOptions { + /** Exact fact name, instance path, or instance-qualified fact name. */ + query?: string; + limit?: number; +} +export interface HdlSemanticResult { + schemaVersion: 1; + provenance: { frontend: 'slang' | 'pyslang'; exporterSha256?: string; librarySha256?: string; version: string; executableSha256: string; + profile: string; configurationFingerprint: string; fingerprint: string; top: string; + parameters: Record; allowUseBeforeDeclare: boolean; + languageStandard: string; compilationUnitMode: 'separate'; runnerVersion: string; compilerLimits: readonly string[] }; + sourceGraphProfileMatches: boolean; + facts: Array; + totalMatches: number; + truncated: boolean; + diagnostics: string[]; + limitations: string[]; +} +export interface HdlSemanticSourceGraph { + profile(): HdlProfileStatus | null; + stale(): boolean; + file(file: string): FileRecord | null | undefined; + nodes(file: string): Node[]; +} + +/** On-demand semantic view; no graph writes or persistent compiler cache. */ +export async function analyzeHdlSemantics(root: string, options: HdlSemanticOptions, + graph: HdlSemanticSourceGraph): Promise { + const limit = options.limit ?? 100; + const query = options.query; + if (!Number.isInteger(limit) || limit < 1 || limit > 1000) throw new Error('HDL semantic limit must be 1..1000'); + const result = await runSlangSemantics(root, options); + const facts = result.frontend === 'pyslang' ? importMappedSemantics(result.ast, result.sources) : importSlangSemantics(result.ast, result.sourceRoot); + const profile = graph.profile(); + const linked = !graph.stale() && profile?.state === 'matches' && + profile.indexed.configurationFingerprint === result.configurationFingerprint; + const byFile = new Map(); + const matches = facts.filter(fact => !query || fact.name === query || + fact.instancePath === query || `${fact.instancePath}.${fact.name}` === query); + const selected = matches.slice(0, limit).map(fact => { + const source = fact.source; + if (!source) return fact; + const text = result.sources[source.file]; + const line = text?.split(/\r?\n/)[source.line - 1]; + // Column 0 / macro invocation remains line-only; never fabricate a source node link. + if (text === undefined || line === undefined) { const { source: _source, ...rest } = fact; return rest; } + if (fact.sourceOrigin === 'macro' || !linked || source.column === null || source.column < 1 || source.column > Buffer.byteLength(line) + 1 || !line.includes(fact.name)) return fact; + if (!byFile.has(source.file)) { + const hash = createHash('sha256').update(text).digest('hex'); + byFile.set(source.file, graph.file(source.file)?.contentHash === hash ? graph.nodes(source.file) : []); + } + const nodes = byFile.get(source.file)!.filter(node => node.name === fact.name && node.startLine === source.line && + (fact.kind === 'parameter' ? node.kind === 'constant' : fact.kind === 'type' ? node.kind === 'type_alias' : node.kind === 'field' && !!node.decorators?.includes('hdl:port'))); + return nodes.length === 1 ? { ...fact, sourceNodeId: nodes[0]!.id } : fact; + }); + return { schemaVersion: 1, provenance: { frontend: result.frontend, exporterSha256: result.exporterSha256, librarySha256: result.librarySha256, version: result.version, + executableSha256: result.executableSha256, profile: result.profileName, + configurationFingerprint: result.configurationFingerprint, fingerprint: result.fingerprint, + top: result.top, parameters: { ...result.parameters }, allowUseBeforeDeclare: result.allowUseBeforeDeclare, + languageStandard: result.languageStandard, compilationUnitMode: result.compilationUnitMode, + runnerVersion: result.runnerVersion, compilerLimits: result.compilerLimits }, + sourceGraphProfileMatches: !!linked, facts: selected, totalMatches: matches.length, + truncated: matches.length > selected.length, diagnostics: result.diagnostics, + limitations: ['Computed on demand from a source snapshot; no persistent semantic cache.', + 'Source columns retain frontend coordinates; null means unavailable, including macro-generated declarations.', + 'Expression origins cover directly visited compiler syntax tokens; coverage/truncation is explicit, not a transitive constant-dependency proof.', + 'Only supported parameter/port facts are returned; absent or unknown widths are not zero.', + 'Compiler semantics are not simulation, synthesis, timing or hardware validation.'] }; +} diff --git a/src/hdl/source-map-export.py b/src/hdl/source-map-export.py new file mode 100644 index 000000000..a0d448cd4 --- /dev/null +++ b/src/hdl/source-map-export.py @@ -0,0 +1,407 @@ +#!/usr/bin/env python3 +"""Compiler-backed, bounded pyslang export; no textual macro-position recovery. + +macroExpansion contains a set of provenance frames reached through BOTH original +and expansion parent relations. It is not an ordered invocation stack. Points +are physical UTF-8 byte positions in root-confined source files. A pasted token's +spelling point can identify its prefix argument; it is never a name range. +Expression origins describe only macro tokens in the selected direct syntax, +not transitive value/type dependencies. 'checked' plus truncated=true is a +bounded partial scan; command-line initializers remain separate from defaults. +""" +import hashlib +import json +from pathlib import Path +import shlex +import sys + +MAX_FACTS = 100_000 +MAX_DEPTH = 128 +MAX_BYTES = 64 * 1024 * 1024 +MAX_DIAGNOSTICS = 256 * 1024 +MAX_EXPRESSION_ORIGINS = 32 +MAX_PROVENANCE_FRAMES = 100_000 +MAX_SYNTAX_VISITS = 1_000_000 + + +def native_version(pyslang): + if pyslang.__version__ != '11.0.0': + raise RuntimeError('This exporter requires explicitly installed pyslang 11.0.0') + native = Path(pyslang.pyslang.__file__).resolve(strict=True) + with native.open('rb') as stream: + digest = hashlib.file_digest(stream, 'sha256').hexdigest() + return f'codegraph pyslang 11.0.0 exporter 1 native {digest}' + + +class Exporter: + def __init__(self, manager, root): + self.manager = manager + self.root = root.resolve(strict=True) + self.sources = {} + self.source_bytes = 0 + self.origins = {} + self.facts = [] + self.identities = set() + self.visits = 0 + self.output_bytes = 0 + self.syntax_cache = {} + self.syntax_visits = 0 + self.provenance_frames = 0 + + def point(self, location): + try: + if not location.buffer.id or not self.manager.isFileLoc(location): + return None + file = Path(self.manager.getFullPath(location.buffer)).resolve(strict=True) + relative = file.relative_to(self.root).as_posix() + if file not in self.sources: + if not file.is_file() or file.stat().st_size > 8 * 1024 * 1024: + return None + data = file.read_bytes() + self.source_bytes += len(data) + if self.source_bytes > MAX_BYTES: + raise RuntimeError('Source mapping byte limit exceeded') + self.sources[file] = data + data = self.sources[file] + offset = location.offset + if offset < 0 or offset > len(data): + return None + # Do not use logical `line remappings or display/Unicode columns. + line = data.count(b'\n', 0, offset) + 1 + column = offset - data.rfind(b'\n', 0, offset) + return {'file': relative, 'line': line, 'column': column, 'byteOffset': offset} + except (OSError, ValueError): + return None + + def provenance(self, location): + key = (location.buffer.id, location.offset) + if key in self.origins: + return self.origins[key] + sm = self.manager + macro = bool(location.buffer.id and sm.isMacroLoc(location)) + result = {'sourceOrigin': 'macro' if macro else 'direct'} + source = self.point(sm.getFullyOriginalLoc(location)) if location.buffer.id else None + if source is None and location.buffer.id: + source = self.point(sm.getFullyExpandedLoc(location)) + if source: + result['source'] = {k: source[k] for k in ['file', 'line', 'column']} + if macro: + pending, visited, frames = [location], set(), [] + complete = True + while pending: + current = pending.pop() + current_key = (current.buffer.id, current.offset) + if current_key in visited or not sm.isMacroLoc(current): + continue + if len(visited) >= MAX_DEPTH: + complete = False + break + visited.add(current_key) + original = sm.getOriginalLoc(current) + expanded = sm.getExpansionLoc(current) + expansion = sm.getExpansionRange(current) + frame = {'name': sm.getMacroName(current) or None, 'argument': sm.isMacroArgLoc(current)} + spelling = self.point(sm.getFullyOriginalLoc(original)) + if spelling: + frame['spelling'] = spelling + start = self.point(sm.getFullyExpandedLoc(expansion.start)) + end = self.point(sm.getFullyExpandedLoc(expansion.end)) + if start and end and start['file'] == end['file'] and end['byteOffset'] >= start['byteOffset']: + frame['invocation'] = {'start': start, 'end': end} + if not spelling or 'invocation' not in frame or start['byteOffset'] == end['byteOffset']: + complete = False + frames.append(frame) + # Original links preserve argument provenance; expansion links + # preserve nested replacement provenance. Neither alone is full. + for parent in [original, expanded]: + if parent.buffer.id and sm.isMacroLoc(parent): + pending.append(parent) + result['macroExpansion'] = frames + result['macroExpansionComplete'] = complete + self.origins[key] = result + return result + + def bounded_origin(self, origin): + result = dict(origin) + frames = origin.get('macroExpansion', []) + remaining = max(0, MAX_PROVENANCE_FRAMES - self.provenance_frames) + truncated = len(frames) > remaining + if 'macroExpansion' in origin: + result['macroExpansion'] = frames[:remaining] + self.provenance_frames += len(result['macroExpansion']) + if truncated: + result['macroExpansionComplete'] = False + return result, truncated + + @staticmethod + def syntax_key(syntax): + if syntax is None: + return None + span = syntax.sourceRange + return (syntax.kind.name, span.start.buffer.id, span.start.offset, + span.end.buffer.id, span.end.offset) + + def command_line_syntax(self, syntax): + if syntax is None: + return False + location = self.manager.getFullyExpandedLoc(syntax.getFirstToken().location) + if not location.buffer.id: + return False + # Driver's override buffer has an explicit command-line line mapping + # and an unnamed synthetic path, unlike a physical `line spoof. + return (self.manager.getFileName(location) == '' + and self.manager.getRawFileName(location.buffer).startswith(' 20_000 or self.syntax_visits > MAX_SYNTAX_VISITS: + truncated = True + return VisitAction.Interrupt + if isinstance(item, Token) and item.location.buffer.id and self.manager.isMacroLoc(item.location): + token_key = (item.location.buffer.id, item.location.offset) + if token_key not in seen: + if len(locations) >= MAX_EXPRESSION_ORIGINS: + truncated = True + return VisitAction.Interrupt + seen.add(token_key) + locations.append(item.location) + return VisitAction.Advance + + syntax.visit(visit) + # Retain the syntax wrapper: a bare Python id could otherwise be reused + # after collection, yielding origins for an unrelated syntax node. + self.syntax_cache[key] = (syntax, locations, truncated) + return locations, truncated + + def expression_origins(self, symbol): + coverage = {'initializer': 'not-applicable', 'declaredInitializer': 'not-applicable', + 'type': 'unavailable', 'truncated': False} + plans = [] + declared_type = getattr(symbol, 'declaredType', None) + declaration = symbol.syntax + type_declarations = [declaration] if declaration is not None else [] + if symbol.kind.name == 'Port': + internal = symbol.internalSymbol + if declared_type is None: + declared_type = getattr(internal, 'declaredType', None) if internal else None + # A non-ANSI PortReference is just the header name. The actual + # declarator (including unpacked dimensions) belongs to its net/var. + internal_syntax = getattr(internal, 'syntax', None) if internal else None + if internal_syntax is not None: + type_declarations.insert(0, internal_syntax) + if symbol.kind.name == 'Parameter': + expression = symbol.initializer + effective = expression.syntax if expression is not None else None + if effective is None and expression is not None and declared_type is not None: + effective = declared_type.initializerSyntax + clause = getattr(declaration, 'initializer', None) if declaration is not None else None + default = getattr(clause, 'expr', None) if clause is not None else None + if effective is None: + coverage['initializer'] = 'unavailable' + else: + coverage['initializer'] = 'command-line' if self.command_line_syntax(effective) else 'checked' + plans.append(('initializer', effective)) + # isOverridden alone is insufficient: Driver -G overrides currently + # leave it false, but their effective syntax is a distinct buffer. + overridden = symbol.isOverridden or (effective is not None and default is not None + and self.syntax_key(effective) != self.syntax_key(default)) + if overridden and default is not None: + coverage['declaredInitializer'] = 'checked' + plans.append(('declared-initializer', default)) + type_syntax = declared_type.typeSyntax if declared_type is not None else None + dimensions = [] + dimensions_known = symbol.kind.name != 'Port' + for candidate in type_declarations: + declared_dimensions = getattr(candidate, 'dimensions', None) + if declared_dimensions is not None: + dimensions_known = True + dimensions.extend(declared_dimensions) + type_inputs = ([type_syntax] if type_syntax is not None else []) + dimensions + if type_inputs and dimensions_known: + coverage['type'] = 'checked' + seen_types = set() + for node in type_inputs: + key = self.syntax_key(node) + if key not in seen_types: + seen_types.add(key) + plans.append(('type', node)) + origins, seen, normalized_seen = [], set(), set() + for role, syntax in plans: + locations, truncated = self.scan_macro_tokens(syntax, role) + coverage['truncated'] |= truncated + for location in locations: + key = (role, location.buffer.id, location.offset) + if key in seen: + continue + seen.add(key) + if len(origins) >= MAX_EXPRESSION_ORIGINS: + coverage['truncated'] = True + break + origin, limited = self.bounded_origin(self.provenance(location)) + coverage['truncated'] |= limited + entry = {'role': role, **origin} + normalized = json.dumps(entry, sort_keys=True, ensure_ascii=False, separators=(',', ':')) + if normalized in normalized_seen: + continue + normalized_seen.add(normalized) + origins.append(entry) + return {'expressionOrigins': origins, 'expressionOriginCoverage': coverage} + + @staticmethod + def segment(name): + import re + if not name or len(name) > 4096: + raise RuntimeError('Unsupported empty/oversized hierarchy name') + return name if re.fullmatch(r'[A-Za-z_][A-Za-z0-9_$]*', name) else '\\' + name + ' ' + + def append(self, symbol, instance_path): + kind = symbol.kind.name + semantic_type = symbol.canonicalType if kind == 'TypeAlias' else symbol.type + origin, _ = self.bounded_origin(self.provenance(symbol.location)) + fact = {'kind': {'Parameter': 'parameter', 'Port': 'port', 'TypeAlias': 'type'}[kind], + 'name': symbol.name, 'instancePath': instance_path, + 'type': str(semantic_type), **origin, **self.expression_origins(symbol)} + if semantic_type.isIntegral: + width = semantic_type.bitWidth + if 0 < width <= 2**53 - 1: + fact['width'] = width + if kind == 'Parameter': + value = symbol.value + if bool(value) and not value.isContainer(): + fact['value'] = str(value) # Preserve SV width/base and X/Z text. + elif kind == 'Port': + fact['direction'] = symbol.direction.name + identity = (fact['kind'], instance_path, symbol.name) + if identity in self.identities: + raise RuntimeError('Duplicate semantic fact identity') + self.identities.add(identity) + self.output_bytes += len(json.dumps(fact, ensure_ascii=False).encode('utf8')) + if len(self.facts) >= MAX_FACTS or self.output_bytes > MAX_BYTES: + raise RuntimeError('Semantic fact output limit exceeded') + self.facts.append(fact) + + def scope(self, scope, instance_path, depth=0, active=None): + if depth > MAX_DEPTH or len(instance_path) > 16384: + raise RuntimeError('Semantic hierarchy depth/path limit exceeded') + active = set() if active is None else active + key = id(scope) + if key in active: + raise RuntimeError('Cyclic instance hierarchy') + active = active | {key} + for symbol in scope: + self.visits += 1 + if self.visits > 1_000_000: + raise RuntimeError('Semantic traversal size limit exceeded') + kind = symbol.kind.name + if kind in ['Parameter', 'Port', 'TypeAlias']: + self.append(symbol, instance_path) + elif kind == 'Instance': + self.scope(symbol.body, instance_path + '.' + self.segment(symbol.name), depth + 1, active) + elif kind == 'GenerateBlock': + if not symbol.isUninstantiated: + self.scope(symbol, instance_path + '.' + self.segment(symbol.externalName), depth + 1, active) + elif kind == 'GenerateBlockArray': + name = instance_path + '.' + self.segment(symbol.externalName) + for block in symbol.entries: + if block.isUninstantiated: + continue + index = block.arrayIndex + if index is None or index.hasUnknown: + raise RuntimeError('Unknown generate iteration index') + self.scope(block, name + '[' + str(int(index)) + ']', depth + 1, active) + elif kind == 'InstanceArray': + self.array(symbol, instance_path + '.' + self.segment(symbol.name), depth + 1, active) + + def array(self, array, array_path, depth, active): + if depth > MAX_DEPTH: + raise RuntimeError('Instance array depth limit exceeded') + for entry in array.elements: + if entry.kind.name == 'InstanceArray': + self.array(entry, array_path, depth + 1, active) + elif entry.kind.name == 'Instance': + indices = ''.join('[' + str(index) + ']' for index in entry.arrayPath) + self.scope(entry.body, array_path + indices, depth + 1, active) + else: + raise RuntimeError('Unsupported instance array member') + + +def main(argv): + import pyslang + from pyslang.driver import Driver, CommandLineOptions + version = native_version(pyslang) + if argv == ['--version']: + print(version) + return 0 + output = None + compiler_argv = [] + index = 0 + while index < len(argv): + arg = argv[index] + if arg == '--': + compiler_argv.extend(argv[index:]) + break + if arg == '--ast-json': + if index + 1 >= len(argv) or output is not None: + raise RuntimeError('Expected one --ast-json output path') + output = Path(argv[index + 1]) + index += 2 + continue + if arg.startswith('--ast-json='): + if output is not None: + raise RuntimeError('Expected one --ast-json output path') + output = Path(arg.split('=', 1)[1]) + elif arg not in ['--ast-json-source-info', '--ast-json-detailed-types']: + compiler_argv.append(arg) + index += 1 + if output is None or str(output) == '-': + raise RuntimeError('A file --ast-json output path is required') + driver = Driver() + driver.addStandardArgs() + options = CommandLineOptions() + options.ignoreProgramName = True + options.expandEnvVars = False + # Binding accepts one command-line string; shlex.join quotes every original + # argv atom. No shell is invoked, no environment interpolation is enabled. + if not driver.parseCommandLine(shlex.join(compiler_argv), options) or not driver.processOptions() or not driver.parseAllSources(): + driver.reportDiagnostics(True) + return 1 + compilation = driver.createCompilation() + driver.reportCompilation(compilation, True) + analysis = driver.runAnalysis(compilation) # Keep native analysis alive through export. + diagnostics = driver.textDiagClient.getString() + if diagnostics: + sys.stderr.write(diagnostics[:MAX_DIAGNOSTICS]) + if len(diagnostics) > MAX_DIAGNOSTICS: + sys.stderr.write('\nAdditional compiler diagnostics truncated.\n') + if driver.diagEngine.numErrors or compilation.hasFatalErrors or compilation.hasIssuedErrors: + return 1 + exporter = Exporter(driver.sourceManager, Path.cwd()) + for instance in compilation.getRoot().topInstances: + exporter.scope(instance.body, exporter.segment(instance.name)) + payload = {'codegraphSemanticVersion': 1, 'facts': exporter.facts} + encoded = json.dumps(payload, ensure_ascii=False, separators=(',', ':')).encode('utf8') + if len(encoded) > MAX_BYTES: + raise RuntimeError('Semantic JSON size limit exceeded') + output.write_bytes(encoded) + return 0 + + +if __name__ == '__main__': + try: + sys.exit(main(sys.argv[1:])) + except (Exception, ImportError) as error: + print('CodeGraph pyslang exporter: ' + str(error), file=sys.stderr) + sys.exit(2) diff --git a/src/hdl/source-map-import.ts b/src/hdl/source-map-import.ts new file mode 100644 index 000000000..67f757118 --- /dev/null +++ b/src/hdl/source-map-import.ts @@ -0,0 +1,99 @@ +import * as path from 'node:path'; +import type { HdlSemanticFact, HdlMacroPoint, HdlMacroFrame, HdlExpressionCoverage, HdlExpressionOrigin } from './semantic-import'; +const object = (v: unknown): v is Record => !!v && typeof v === 'object' && !Array.isArray(v); +function fail(message: string): never { throw new Error('Invalid pyslang semantic export: ' + message); } + +/** Validate the separately versioned exporter protocol against immutable source bytes. */ +export function importMappedSemantics(value: unknown, sources: Record): HdlSemanticFact[] { + if (!object(value) || value.codegraphSemanticVersion !== 1 || !Array.isArray(value.facts) || value.facts.length > 100000) fail('unsupported schema or fact count'); + const cache = new Map(); + const fileData = (file: unknown) => { + if (typeof file !== 'string' || !file || file.includes('\\') || file.includes('\0') || path.posix.isAbsolute(file) || /^[A-Za-z]:/.test(file) || path.posix.normalize(file) !== file || file.split('/').includes('..') || !Object.prototype.hasOwnProperty.call(sources, file)) fail('source outside snapshot'); + if (!cache.has(file)) { + const bytes = Buffer.from(sources[file]!, 'utf8'), starts = [0]; + for (let i = 0; i < bytes.length; i++) if (bytes[i] === 10) starts.push(i + 1); + cache.set(file, { bytes, starts }); + } + return { file, ...cache.get(file)! }; + }; + const source = (v: unknown): NonNullable => { + if (!object(v)) fail('source must be an object'); + const data = fileData(v.file), line = v.line, column = v.column; + if (!Number.isSafeInteger(line) || (line as number) < 1 || (line as number) > data.starts.length || (column !== null && (!Number.isSafeInteger(column) || (column as number) < 1))) fail('invalid source coordinates'); + if (column !== null) { + const offset = data.starts[(line as number) - 1]! + (column as number) - 1; + const end = data.starts[line as number] ?? data.bytes.length; + if (((line as number) < data.starts.length && offset >= end) || offset > end || offset > data.bytes.length || (offset < data.bytes.length && (data.bytes[offset]! & 0xc0) === 0x80)) fail('column outside source line or inside UTF-8 code point'); + } + return { file: data.file, line: line as number, column: column as number | null }; + }; + const point = (v: unknown): HdlMacroPoint => { + const location = source(v); + if (!object(v) || location.column === null || !Number.isSafeInteger(v.byteOffset) || (v.byteOffset as number) < 0) fail('invalid source byte offset'); + const data = fileData(location.file); + if (data.starts[location.line - 1]! + location.column! - 1 !== v.byteOffset) fail('source coordinates disagree with byte offset'); + return { ...location, column: location.column!, byteOffset: v.byteOffset as number }; + }; + const text = (v: unknown, label: string, max = 4096): string => { + if (typeof v !== 'string' || !v || v.length > max || v.includes('\0')) fail('invalid ' + label); + return v; + }; + const result: HdlSemanticFact[] = []; + const identities = new Set(); + let frames = 0; + const macro = (item: Record): Omit => { + if (!Array.isArray(item.macroExpansion) || item.macroExpansion.length > 128 || typeof item.macroExpansionComplete !== 'boolean') fail('invalid macro chain'); + frames += item.macroExpansion.length; + if (frames > 100000) fail('macro frame count limit'); + const macroExpansion = item.macroExpansion.map((frame: unknown): HdlMacroFrame => { + if (!object(frame) || typeof frame.argument !== 'boolean') fail('invalid macro frame'); + const out: HdlMacroFrame = { name: frame.name === null ? null : text(frame.name, 'macro name'), argument: frame.argument }; + if (frame.spelling !== undefined) out.spelling = point(frame.spelling); + if (frame.invocation !== undefined) { + if (!object(frame.invocation)) fail('invalid invocation range'); + out.invocation = { start: point(frame.invocation.start), end: point(frame.invocation.end) }; + if (out.invocation.start.file !== out.invocation.end.file || out.invocation.start.byteOffset > out.invocation.end.byteOffset) fail('invalid invocation order'); + } + return out; + }); + const macroExpansionComplete = item.macroExpansionComplete; + if (macroExpansionComplete && (!macroExpansion.length || macroExpansion.some(f => !f.spelling || !f.invocation || f.invocation.start.byteOffset === f.invocation.end.byteOffset))) fail('incomplete chain marked complete'); + return { macroExpansion, macroExpansionComplete, ...(item.source !== undefined ? { source: source(item.source) } : {}) }; + }; + for (const item of value.facts) { + if (!object(item) || (item.kind !== 'parameter' && item.kind !== 'port' && item.kind !== 'type') || (item.sourceOrigin !== 'direct' && item.sourceOrigin !== 'macro')) fail('invalid fact kind/origin'); + const fact: HdlSemanticFact = { kind: item.kind, name: text(item.name, 'name'), instancePath: text(item.instancePath, 'instance path'), sourceOrigin: item.sourceOrigin }; + const id = JSON.stringify([fact.instancePath, fact.kind, fact.name]); + if (identities.has(id)) fail('duplicate instance fact'); identities.add(id); + if (item.value !== undefined) fact.value = text(item.value, 'value', 65536); + if (item.type !== undefined) fact.type = text(item.type, 'type', 65536); + if (item.width !== undefined) { + if (!Number.isSafeInteger(item.width) || (item.width as number) < 1) fail('invalid width'); + fact.width = item.width as number; + } + if (item.direction !== undefined) fact.direction = text(item.direction, 'direction'); + if (item.source !== undefined) fact.source = source(item.source); + if (item.sourceOrigin === 'macro') Object.assign(fact, macro(item)); + else if (item.macroExpansion !== undefined || item.macroExpansionComplete !== undefined) fail('direct fact carries macro chain'); + if (item.expressionOrigins !== undefined || item.expressionOriginCoverage !== undefined) { + if (!Array.isArray(item.expressionOrigins) || item.expressionOrigins.length > 32 || !object(item.expressionOriginCoverage)) fail('invalid expression origins'); + const coverage = item.expressionOriginCoverage; + const statuses = ['checked', 'not-applicable', 'unavailable']; + if (typeof coverage.initializer !== 'string' || typeof coverage.declaredInitializer !== 'string' || typeof coverage.type !== 'string' || ![...statuses, 'command-line'].includes(coverage.initializer) || !statuses.includes(coverage.declaredInitializer) || !statuses.includes(coverage.type) || typeof coverage.truncated !== 'boolean') fail('invalid expression coverage'); + fact.expressionOriginCoverage = { initializer: coverage.initializer, declaredInitializer: coverage.declaredInitializer, type: coverage.type, truncated: coverage.truncated } as HdlExpressionCoverage; + const keys = { initializer: 'initializer', 'declared-initializer': 'declaredInitializer', type: 'type' } as const; + const seen = new Set(); + fact.expressionOrigins = item.expressionOrigins.map((entry: unknown): HdlExpressionOrigin => { + if (!object(entry) || (entry.role !== 'initializer' && entry.role !== 'declared-initializer' && entry.role !== 'type') || entry.sourceOrigin !== 'macro') fail('invalid expression origin role'); + const status = coverage[keys[entry.role]]; + if (status !== 'checked' && status !== 'command-line') fail('expression origin contradicts coverage'); + const normalized: HdlExpressionOrigin = { role: entry.role, sourceOrigin: 'macro', ...macro(entry) }; + const key = JSON.stringify(normalized); + if (seen.has(key)) fail('duplicate expression origin'); seen.add(key); + return normalized; + }); + } + result.push(fact); + } + return result; +} diff --git a/src/hdl/status.ts b/src/hdl/status.ts new file mode 100644 index 000000000..7383b1200 --- /dev/null +++ b/src/hdl/status.ts @@ -0,0 +1,129 @@ +/** Public profile state is deliberately separate from the currently configured + * profile: queries describe the context stored with the index until rebuilt. */ +export interface HdlProfileDisplayIdentity { + name: string; + fingerprint: string; + files?: readonly string[]; + includeDirs?: readonly string[]; + defines?: Readonly>; + topModules?: readonly string[]; + languageMode?: 'verilog' | 'systemverilog'; +} +export interface HdlProfileSettingsSummary { + fileCount?: number; + includeDirs?: string[]; + defineNames?: string[]; + topModules?: string[]; + languageMode?: 'verilog' | 'systemverilog'; +} +export interface HdlProfileConfiguration { + status: 'none' | 'active' | 'invalid-config' | 'invalid-profile'; + profile: HdlProfileDisplayIdentity | null; + diagnostics: string[]; +} +export interface HdlIndexedProfileMetadata { + name: string | null; + fingerprint: string | null; + context: string | null; +} +export interface HdlProfileStatus { + configured: { mode: 'raw' | 'profile' | 'invalid'; name: string | null; fingerprint: string | null } & HdlProfileSettingsSummary; + indexed: { mode: 'raw' | 'profile' | 'unknown'; name: string | null; fingerprint: string | null; configurationFingerprint: string | null } & HdlProfileSettingsSummary; + state: 'matches' | 'mismatch' | 'unknown' | 'configuration-error'; + mismatch: boolean | null; + reindexRecommended: boolean; + diagnostics: string[]; + indexedDiagnostics: string[]; + incomplete: boolean; + limitations: string[]; +} +const RAW_LIMIT = 'Raw source may contain multiple conditional alternatives; no HDL build profile was applied.'; +const PROFILE_LIMIT = 'HDL preprocessing selects conditional branches and include macro state; macro invocations are not fully expanded. Displayed snippets are original source and may include inactive branches; indexed symbols follow the selected profile.'; +const DIALECT_LIMIT = 'languageMode records the intended dialect; the SystemVerilog grammar does not enforce Verilog-only conformance. topModules are recorded roots, not an elaborated hierarchy.'; +const SOURCE_LIMIT = 'Source relationships are not elaboration, synthesis, simulation or timing validation.'; + +function settingsSummary(value: object | null): HdlProfileSettingsSummary { + const record = value as Record | null; + if (!record) return {}; + const strings = (candidate: unknown): candidate is string[] => Array.isArray(candidate) && candidate.every(n => typeof n === 'string'); + return { + ...(strings(record.files) ? {fileCount:record.files.length} : {}), + ...(strings(record.includeDirs) ? {includeDirs:[...record.includeDirs]} : {}), + ...(record.defines && typeof record.defines === 'object' && !Array.isArray(record.defines) + ? {defineNames:Object.keys(record.defines).sort()} : {}), + ...(strings(record.topModules) ? {topModules:[...record.topModules]} : {}), + ...(record.languageMode === 'verilog' || record.languageMode === 'systemverilog' ? {languageMode:record.languageMode} : {}), + }; +} + +export function buildHdlProfileStatus( + configuration: HdlProfileConfiguration, + metadata: HdlIndexedProfileMetadata, + hasHdlFiles: boolean, +): HdlProfileStatus | null { + if (!hasHdlFiles && configuration.status === 'none' && !metadata.context && !metadata.fingerprint) return null; + let context: unknown; + try { context = metadata.context ? JSON.parse(metadata.context) : null; } catch { context = null; } + const record = context && typeof context === 'object' && !Array.isArray(context) + ? context as Record : null; + const raw = record?.mode === 'raw'; + if (!hasHdlFiles && configuration.status === 'none' && (raw || !metadata.name)) return null; + const indexed: HdlProfileStatus['indexed'] = raw + ? {mode:'raw',name:null,fingerprint:metadata.fingerprint,configurationFingerprint:null} + : metadata.name && metadata.fingerprint && record?.mode === 'profile' && typeof record.configurationFingerprint === 'string' + ? {mode:'profile',name:metadata.name,fingerprint:metadata.fingerprint,configurationFingerprint:record.configurationFingerprint,...settingsSummary(record)} + : {mode:'unknown',name:metadata.name,fingerprint:metadata.fingerprint,configurationFingerprint:null}; + const indexedDiagnostics = Array.isArray(record?.diagnostics) ? record.diagnostics.flatMap(value => { + if (typeof value === 'string') return [value]; + if (value && typeof value === 'object' && typeof value.message === 'string') { + const location = typeof value.filePath === 'string' ? `${value.filePath}${typeof value.line === 'number' ? ':' + value.line : ''}: ` : ''; + return [location + value.message]; + } + return []; + }) : []; + const invalid = configuration.status === 'invalid-config' || configuration.status === 'invalid-profile' + || (configuration.status === 'active' && !configuration.profile); + const configured: HdlProfileStatus['configured'] = invalid + ? {mode:'invalid',name:configuration.profile?.name ?? null,fingerprint:null} + : configuration.status === 'none' + ? {mode:'raw',name:null,fingerprint:null} + : {mode:'profile',name:configuration.profile!.name,fingerprint:configuration.profile!.fingerprint,...settingsSummary(configuration.profile)}; + const unknown = indexed.mode === 'unknown'; + const mismatch = invalid || unknown ? null : configured.mode !== indexed.mode + || (configured.mode === 'profile' && (configured.fingerprint !== indexed.configurationFingerprint || configured.name !== indexed.name)); + return {configured,indexed,state:invalid ? 'configuration-error' : unknown ? 'unknown' : mismatch ? 'mismatch' : 'matches', + mismatch,reindexRecommended:!invalid && (unknown || mismatch === true),diagnostics:[...configuration.diagnostics], + indexedDiagnostics,incomplete:record?.incomplete === true || indexedDiagnostics.length > 0, + limitations:[...(indexed.mode === 'raw' ? [RAW_LIMIT] : indexed.mode === 'profile' ? [PROFILE_LIMIT,DIALECT_LIMIT] : []), SOURCE_LIMIT]}; +} + +/** Shared compact note for CLI status and MCP explore responses. */ +export function formatHdlProfileStatus(status: HdlProfileStatus | null): string { + if (!status) return ''; + const label = (identity: HdlProfileStatus['indexed'] | HdlProfileStatus['configured']): string => { + if (identity.mode === 'raw') return 'raw source (no profile)'; + if (identity.mode === 'unknown') return 'unknown'; + if (identity.mode === 'invalid') return 'invalid configuration'; + return `${JSON.stringify(identity.name)} (${identity.fingerprint?.slice(0,12)})`; + }; + const lines = [`HDL context: indexed ${label(status.indexed)}; configured ${label(status.configured)}.`]; + if (status.indexed.mode === 'profile') { + const summary = status.indexed; + const parts = [summary.fileCount == null ? undefined : `${summary.fileCount} file(s)`, + summary.includeDirs ? `${summary.includeDirs.length} include path(s)` : undefined, + summary.defineNames ? `${summary.defineNames.length} define(s)` : undefined, + summary.topModules ? `${summary.topModules.length} recorded top module(s)` : undefined, + summary.languageMode ? `intended dialect ${summary.languageMode}` : undefined].filter(Boolean); + if (parts.length) lines.push(`Indexed HDL settings: ${parts.join('; ')}.`); + } + if (status.state === 'mismatch') lines.push('Profile mismatch: rebuild the index before interpreting results as the configured profile.'); + if (status.state === 'unknown') lines.push('Indexed profile provenance is unknown; rebuild the index to establish its HDL context.'); + if (status.state === 'configuration-error') lines.push('Fix the HDL profile configuration before rebuilding; results retain the indexed context.'); + if (status.incomplete) lines.push('Indexed HDL preprocessing is incomplete; unresolved regions are not evidence of an inactive branch.'); + for (const [label, diagnostics] of [['HDL configuration', status.diagnostics], ['Indexed HDL', status.indexedDiagnostics]] as const) { + lines.push(...diagnostics.slice(0,3).map(d=>`${label}: ${d.length > 240 ? d.slice(0,237) + '...' : d}`)); + if (diagnostics.length > 3) lines.push(`${label}: ${diagnostics.length - 3} additional diagnostics; inspect codegraph status --json.`); + } + lines.push(...status.limitations); + return lines.join('\n'); +} diff --git a/src/index.ts b/src/index.ts index 9941b9c16..a665f5c71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,6 +28,11 @@ import { import { DatabaseConnection, getDatabasePath, removeDatabaseFiles } from './db'; import { WalCheckpointValve, resolveWalValveMb } from './db/wal-valve'; import { QueryBuilder } from './db/queries'; +import { loadHdlProfile } from './hdl/profile'; +import { hdlDependenciesChanged } from './hdl/index-context'; +import { buildHdlProfileStatus, type HdlProfileStatus } from './hdl/status'; +import { analyzeHdlSemantics, type HdlSemanticOptions, type HdlSemanticResult } from './hdl/semantics'; +export type { HdlSemanticOptions, HdlSemanticResult } from './hdl/semantics'; import { isInitialized, createDirectory, @@ -40,6 +45,8 @@ import { IndexResult, SyncResult, extractFromSource, + getGitHeadSha, + INDEXED_AT_COMMIT_KEY, initGrammars, } from './extraction'; import { @@ -453,6 +460,12 @@ export class CodeGraph { this.db.close(); } + /** Stop watching and drain in-flight index writes before closing SQLite. */ + async closeAsync(): Promise { + this.unwatch(); + await this.indexMutex.withLock(() => this.close()); + } + /** * Get the project root directory */ @@ -476,6 +489,7 @@ export class CodeGraph { } catch { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } + const headBeforeIndex = getGitHeadSha(this.projectRoot); // Defer WAL auto-checkpointing for the whole bulk run (#1231): the // default 1000-page interval re-writes hot pages into the main DB file // over and over — ~95% of all disk I/O during a bulk index, and a @@ -692,6 +706,12 @@ export class CodeGraph { try { this.queries.setMetadata('indexed_with_version', CodeGraphPackageVersion); this.queries.setMetadata('indexed_with_extraction_version', String(EXTRACTION_VERSION)); + // ...and the commit whose tree it was built from, so change + // detection can ask git what has been COMMITTED since — the half + // `git status` structurally cannot answer. (#1829) + if (headBeforeIndex && result.filesErrored === 0) { + this.queries.setMetadata(INDEXED_AT_COMMIT_KEY, headBeforeIndex); + } } catch { /* metadata is advisory — never fail an index over it */ } } @@ -726,6 +746,7 @@ export class CodeGraph { } } catch { /* metadata is advisory — never fail an index over it */ } + if (result.success && result.filesErrored === 0) this.orchestrator.commitHdlProfile(); return result; } finally { // Restore the auto-checkpoint interval AFTER the fold-up above so the @@ -763,7 +784,9 @@ export class CodeGraph { return { success: false, filesIndexed: 0, filesSkipped: 0, filesErrored: 0, nodesCreated: 0, edgesCreated: 0, errors: [{ message: 'Could not acquire file lock - another process may be indexing', severity: 'error' as const }], durationMs: 0 }; } try { - return this.orchestrator.indexFiles(filePaths); + const result = await this.orchestrator.indexFiles(filePaths); + if (result.success && result.filesErrored === 0) this.orchestrator.commitHdlProfile(); + return result; } finally { this.fileLock.release(); } @@ -780,7 +803,7 @@ export class CodeGraph { try { this.fileLock.acquire(); } catch { - return { filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 }; + return { skippedReason: 'locked', filesChecked: 0, filesAdded: 0, filesModified: 0, filesRemoved: 0, nodesUpdated: 0, durationMs: 0 }; } // Defer WAL auto-checkpointing for the whole incremental run, exactly // as indexAll does for the bulk path (#1231): sync's store loop and its @@ -822,6 +845,12 @@ export class CodeGraph { // timer-driven PASSIVE checkpoints ran, and a query-pool reader could // pin frames while the WAL grew without a bound. const backpressure = walValve ? () => walValve!.backpressure() : undefined; + // Captured BEFORE change detection runs, not after: if a commit lands + // while this sync is working, stamping the NEW head would claim we + // absorbed a diff we never looked at. Stamping the older commit only + // costs the next run a re-check of what it already has. (#1829) + const headBeforeSync = getGitHeadSha(this.projectRoot); + const result = await this.orchestrator.sync(options.onProgress, options.paths, backpressure); // Fold the store phase's WAL BEFORE the post-store reads below @@ -945,6 +974,11 @@ export class CodeGraph { result.definitionDelta, result.changedFilePaths ?? [] ); + // A deleted duplicate module can make an HDL binding unique again. + // Deletion-only syncs skip the changed-file failed-ref retry above. + const hdlRetry = this.queries.getRetryableFailedReferences(result.definitionDelta) + .filter(ref => ref.referenceName.startsWith('hdl:wildcard:') || ref.referenceName.startsWith('hdl:port-position:')); + if (hdlRetry.length > 0) await this.resolver.resolveAndPersistListYielding(hdlRetry); if (process.env.CODEGRAPH_SYNTH_TIMINGS) { console.error( `[phase-timing] sync-rebind: ${Date.now() - tRebind}ms (${result.definitionDelta.length} changed names, ${rebound} edges re-opened)` @@ -952,6 +986,10 @@ export class CodeGraph { } } + // Access direction is signature-dependent even if no node name changed. + // Re-open typed and still-unclassified HDL argument sites in untouched files. + this.orchestrator.resurrectHdlCallArgumentEdges(result.changedFilePaths ?? [], result.filesRemoved > 0); + // Orphan sweep (#1187). A resolution pass that dies mid-run — the #850 // daemon liveness watchdog's SIGKILL (#1122), Ctrl-C, a crash — leaves // the refs it never reached in unresolved_refs, and the git-scoped fast @@ -1028,6 +1066,18 @@ export class CodeGraph { try { this.queries.setMetadata('index_state', 'complete'); } catch { /* advisory */ } } + // Stamp the commit this sync brought the tree up to date at, so the + // NEXT change detection can see what was committed since. Only on a + // whole-tree sync: a `--paths` subset absorbed part of the diff, and + // stamping HEAD would tell the next run the rest was already indexed. + // Unlike the extraction stamp above, a sync DOES advance this one — + // it is about which tree the files came from, not which extractor + // produced their symbols. (#1829) + if (fullReconcile && headBeforeSync) { + try { this.queries.setMetadata(INDEXED_AT_COMMIT_KEY, headBeforeSync); } catch { /* advisory */ } + } + + this.orchestrator.commitHdlProfile(); return result; } finally { // Mirror indexAll's teardown: stop the valve, then restore the @@ -1081,7 +1131,8 @@ export class CodeGraph { const filesChanged = result.filesAdded + result.filesModified + result.filesRemoved; return { filesChanged, durationMs: result.durationMs }; }, - options + options, + () => this.getHdlDependencyPaths() ); return this.watcher.start(); @@ -1221,6 +1272,36 @@ export class CodeGraph { * index built before stamping existed (treated as stale). See * `extraction-version.ts` and `isIndexStale()`. */ + getHdlDependencyPaths(): string[] { + try { + const context = JSON.parse(this.queries.getMetadata('hdl_profile_context') ?? 'null'); + return Array.isArray(context?.dependencies) ? context.dependencies.flatMap((d: unknown) => + d && typeof d === 'object' && 'path' in d && typeof d.path === 'string' ? [d.path] : []) : []; + } catch { return []; } + } + + /** Explicit optional compiler query; source graph remains available if compilation fails. */ + async getHdlSemantics(options: HdlSemanticOptions): Promise { + return analyzeHdlSemantics(this.projectRoot, options, { + profile: () => this.getHdlProfileStatus(), stale: () => this.isIndexStale(), + file: file => this.queries.getFileByPath(file), nodes: file => this.queries.getNodesByFile(file), + }); + } + + getHdlProfileStatus(): HdlProfileStatus | null { + const configuration = loadHdlProfile(this.projectRoot); + const metadata = { name: this.queries.getMetadata('hdl_profile_name') || null, + fingerprint: this.queries.getMetadata('hdl_profile_fingerprint'), context: this.queries.getMetadata('hdl_profile_context') }; + const hasHdl = this.queries.hasLanguage('verilog'); + if (!hasHdl && !metadata.name && configuration.status === 'invalid-config') return null; + const status = buildHdlProfileStatus(configuration, metadata, hasHdl); + if (status?.state === 'matches' && status.indexed.mode === 'profile' && hdlDependenciesChanged(this.projectRoot, metadata.context)) { + status.state = 'mismatch'; status.mismatch = true; status.reindexRecommended = true; + status.diagnostics.push('HDL include dependency changed since indexing.'); + } + return status; + } + getIndexBuildInfo(): { version: string | null; extractionVersion: number | null } { const version = this.queries.getMetadata('indexed_with_version'); const ev = this.queries.getMetadata('indexed_with_extraction_version'); diff --git a/src/mcp/daemon-paths.ts b/src/mcp/daemon-paths.ts index c860ee76c..df42752b4 100644 --- a/src/mcp/daemon-paths.ts +++ b/src/mcp/daemon-paths.ts @@ -169,6 +169,9 @@ export function decodeLockInfo(raw: string): DaemonLockInfo | null { if (!trimmed) return null; try { const parsed = JSON.parse(trimmed); + if (typeof parsed === 'number' && Number.isSafeInteger(parsed) && parsed > 0) { + return { pid: parsed, version: 'unknown', socketPath: '', startedAt: 0 }; + } if ( parsed && typeof parsed.pid === 'number' && diff --git a/src/mcp/daemon.ts b/src/mcp/daemon.ts index a5735d168..0342ee44a 100644 --- a/src/mcp/daemon.ts +++ b/src/mcp/daemon.ts @@ -350,7 +350,7 @@ export class Daemon { await new Promise((resolve) => this.server!.close(() => resolve())); this.server = null; } - this.engine.stop(); + await this.engine.stop(); this.cleanupLockfile(); deregisterDaemon(this.projectRoot); if (process.platform !== 'win32') { @@ -378,6 +378,9 @@ export class Daemon { // timeout, a non-hello first line, an early close — yields null pids and we // fall back to the socket-close lifecycle exactly as before (#692). void readClientHello(socket).then((peers) => { + // The peer may close while hello is being read; close is not replayed + // for the transport listeners we would install afterwards (#1356). + if (socket.destroyed) return; const transport = new SocketTransport(socket); const session = new MCPSession(transport, this.engine, { explicitProjectPath: this.projectRoot, @@ -665,7 +668,7 @@ export function clearStaleDaemonLock( if (expectedDeadPid !== undefined && info.pid !== expectedDeadPid) return false; // PID liveness is normally sufficient. The takeover caller may override // it only after a failed identity handshake proves PID reuse. - if (!opts.allowLivePid && info.pid > 0 && isProcessAlive(info.pid)) return false; + if ((!opts.allowLivePid || !info.socketPath) && info.pid > 0 && isProcessAlive(info.pid)) return false; } fs.unlinkSync(pidPath); return true; diff --git a/src/mcp/engine.ts b/src/mcp/engine.ts index 734fafa4f..b581e522f 100644 --- a/src/mcp/engine.ts +++ b/src/mcp/engine.ts @@ -12,6 +12,7 @@ import * as os from 'os'; import * as path from 'path'; +import { realpathSync } from 'fs'; import type CodeGraph from '../index'; import { resolveServerRoot } from '../directory'; import { watchDisabledReason } from '../sync'; @@ -67,11 +68,12 @@ export class MCPEngine { // Throttle for the retry path's sub-project down-scan (#1606) — the scan is // bounded but shouldn't run on every tool call in the no-default state. private lastRetrySubScanAt = 0; - private watcherStarted = false; + private watchedProjects = new Set(); /** Set when this engine holds writer.pid (#1740). */ - private writerLockRoot: string | null = null; + private writerLockRoots = new Set(); private opts: Required; private closed = false; + private stopPromise: Promise | null = null; // Off-loop read-tool pool (daemon mode only). Created lazily once the default // project is open — workers each hold their own WAL read connection. private queryPool: QueryPool | null = null; @@ -79,6 +81,7 @@ export class MCPEngine { constructor(opts: MCPEngineOptions = {}) { this.opts = { watch: opts.watch ?? true, queryPool: opts.queryPool ?? false }; this.toolHandler = new ToolHandler(null); + this.toolHandler.setProjectLifecycle((cg) => this.activateProject(cg)); } /** @@ -179,7 +182,7 @@ export class MCPEngine { this.lastRetrySubScanAt = Date.now(); if (!res.root) this.toolHandler.setKnownSubprojects(res.candidates, searchFrom); } - const resolvedRoot = res.root; + const resolvedRoot = res.root ? realpathSync(res.root) : null; if (!resolvedRoot) return; if (res.viaSubScan) this.logSubprojectAdoption(searchFrom, resolvedRoot); try { @@ -190,9 +193,8 @@ export class MCPEngine { } this.cg = loadCodeGraph().openSync(resolvedRoot); this.projectPath = resolvedRoot; - this.toolHandler.setDefaultCodeGraph(this.cg); - this.startWatching(); - this.catchUpSync(); + this.cg = this.toolHandler.setDefaultCodeGraph(this.cg); + this.toolHandler.setCatchUpGate(this.activateProject(this.cg)); this.maybeStartPool(resolvedRoot); } catch { // Still failing — caller will try again on the next tool call. @@ -203,25 +205,28 @@ export class MCPEngine { * Close everything. Used on graceful daemon shutdown (SIGTERM/idle timeout) * and on direct-mode stop. Idempotent. */ - stop(): void { - if (this.closed) return; + stop(): Promise { + return this.stopPromise ??= this.doStop(); + } + + private async doStop(): Promise { this.closed = true; - if (this.writerLockRoot) { - releaseWriterLock(this.writerLockRoot); - this.writerLockRoot = null; - } + if (this.initPromise) await this.initPromise; // Detach + terminate the worker pool first so no tool call routes to a // worker mid-teardown; outstanding pool calls resolve with graceful guidance. this.toolHandler.setQueryPool(null); if (this.queryPool) { - void this.queryPool.destroy(); + await this.queryPool.destroy(); this.queryPool = null; } - this.toolHandler.closeAll(); + await this.toolHandler.closeAllAsync(); if (this.cg) { - try { this.cg.close(); } catch { /* ignore */ } + await this.cg.closeAsync(); this.cg = null; } + for (const root of this.writerLockRoots) releaseWriterLock(root); + this.writerLockRoots.clear(); + this.watchedProjects.clear(); } private async doInitialize(searchFrom: string): Promise { @@ -234,7 +239,7 @@ export class MCPEngine { // variant of this state read as "CodeGraph is broken" and was diagnosable // only by knowing to look for a missing ~/.codegraph/daemons/ entry. const res = resolveServerRoot(searchFrom); - const resolvedRoot = res.root; + const resolvedRoot = res.root ? realpathSync(res.root) : null; if (!resolvedRoot) { // Sessions may still discover a project later via roots/list, and the // per-call retry re-resolves — this state is recoverable, hence stderr @@ -257,9 +262,8 @@ export class MCPEngine { this.projectPath = resolvedRoot; try { this.cg = await loadCodeGraph().open(resolvedRoot); - this.toolHandler.setDefaultCodeGraph(this.cg); - this.startWatching(); - this.catchUpSync(); + this.cg = this.toolHandler.setDefaultCodeGraph(this.cg); + this.toolHandler.setCatchUpGate(this.activateProject(this.cg)); this.maybeStartPool(resolvedRoot); } catch (err) { const msg = err instanceof Error ? err.message : String(err); @@ -277,19 +281,29 @@ export class MCPEngine { /** * Start file watching on the active CodeGraph instance. Idempotent — the - * watcher is per-engine, not per-session, which is why the daemon path - * collapses N inotify sets to one. The wording of the disabled-reason log - * exactly matches the prior in-tree implementation so log-driven dashboards - * keep working. + * watcher is per-project in the engine, not per-session. Explicit projectPath + * opens get the same writer ownership and catch-up lifecycle as the default. */ - private startWatching(): void { - if (!this.cg || this.watcherStarted || !this.opts.watch) return; + private activateProject(cg: CodeGraph): Promise { + const root = realpathSync(cg.getProjectRoot()); + if (this.closed || this.watchedProjects.has(root)) return Promise.resolve(); + const disabledReason = !this.opts.watch ? 'watch disabled for this engine' : watchDisabledReason(root); + if (disabledReason) { + this.watchedProjects.add(root); + process.stderr.write(`[CodeGraph MCP] Auto-sync disabled for ${root} — ${disabledReason}. Run \`codegraph sync\` to refresh.\n`); + return Promise.resolve(); + } + if (!this.startWatching(cg)) return Promise.resolve(); + this.watchedProjects.add(root); + return this.catchUpSync(cg); + } + private startWatching(cg: CodeGraph): boolean { // #1740: only one live watcher/writer per project. Daemon and startDirect // usually already hold writer.pid (re-entrant for this pid). Proxy // in-process fallback acquires here; if another writer holds it, skip the // watcher so we never contend on codegraph.lock until auto-sync degrades. - const lockRoot = this.projectPath; + const lockRoot = cg.getProjectRoot(); if (lockRoot) { const writer = tryAcquireWriterLock(lockRoot, 'fallback'); if (writer.kind === 'taken') { @@ -297,20 +311,9 @@ export class MCPEngine { process.stderr.write( `[CodeGraph MCP] File watcher not started — ${msg}\n` ); - this.watcherStarted = true; - return; + return false; } - this.writerLockRoot = lockRoot; - } - - const disabledReason = watchDisabledReason(this.projectPath ?? process.cwd()); - if (disabledReason) { - process.stderr.write( - `[CodeGraph MCP] File watcher disabled — ${disabledReason}. ` + - `The graph will not auto-update; run \`codegraph sync\` (or install the git sync hooks via \`codegraph init\`) to refresh.\n` - ); - this.watcherStarted = true; - return; + this.writerLockRoots.add(lockRoot); } // Optional override for the debounce window via env var (issue #403). @@ -323,7 +326,7 @@ export class MCPEngine { process.stderr.write(`[CodeGraph MCP] File watcher debounce: ${debounceMs}ms (CODEGRAPH_WATCH_DEBOUNCE_MS)\n`); } - const started = this.cg.watch({ + const started = cg.watch({ debounceMs, onSyncComplete: (result) => { if (result.filesChanged > 0) { @@ -345,7 +348,6 @@ export class MCPEngine { }, }); - this.watcherStarted = true; if (started) { process.stderr.write('[CodeGraph MCP] File watcher active — graph will auto-sync on changes\n'); } else { @@ -353,6 +355,7 @@ export class MCPEngine { '[CodeGraph MCP] File watcher unavailable on this platform — run `codegraph sync` to refresh the graph after changes.\n' ); } + return true; } /** @@ -365,10 +368,8 @@ export class MCPEngine { * and the per-file staleness banner can't help because `getPendingFiles()` * is populated by the watcher, not by catch-up). */ - private catchUpSync(): void { - const cg = this.cg; - if (!cg) return; - const p = cg + private catchUpSync(cg: CodeGraph): Promise { + return cg .sync() .then((result) => { const changed = result.filesAdded + result.filesModified + result.filesRemoved; @@ -380,7 +381,6 @@ export class MCPEngine { const msg = err instanceof Error ? err.message : String(err); process.stderr.write(`[CodeGraph MCP] Catch-up sync failed: ${msg}\n`); }); - this.toolHandler.setCatchUpGate(p); } } diff --git a/src/mcp/hdl-access.ts b/src/mcp/hdl-access.ts new file mode 100644 index 000000000..9e1713ed2 --- /dev/null +++ b/src/mcp/hdl-access.ts @@ -0,0 +1,102 @@ +import type CodeGraph from '../index'; +import type { Edge, Node } from '../types'; +import { readFileSync, statSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { validatePathWithinRoot } from '../utils'; + +export const HDL_ACCESS_FILTERS = ['read', 'write', 'readwrite', 'control', 'event', 'all'] as const; +export type HdlAccessFilter = typeof HDL_ACCESS_FILTERS[number]; +const ACCESS_KINDS = new Set(['read', 'write', 'readwrite', 'control', 'event']); + +export function hdlAccessKinds(edge: Edge): string[] { + const value = edge.metadata?.hdlAccess; + return Array.isArray(value) ? [...new Set(value.filter((v): v is string => typeof v === 'string' && ACCESS_KINDS.has(v)))].sort() : []; +} +function matches(kinds: string[], filter: HdlAccessFilter): boolean { + if (filter === 'all') return kinds.length > 0; + if (filter === 'read') return kinds.some(k => ['read', 'readwrite', 'control', 'event'].includes(k)); + if (filter === 'write') return kinds.some(k => k === 'write' || k === 'readwrite'); + if (filter === 'readwrite') return kinds.includes('readwrite') || (kinds.includes('read') && kinds.includes('write')); + return kinds.includes(filter); +} + +/** Exact HDL signal access sites, using raw edges rather than neighbour-deduped callers. */ +export function formatHdlAccess(cg: CodeGraph, query: string, filter: HdlAccessFilter, maxFiles = 12): string { + const name = query.trim(); + const exact = name.includes('::') ? cg.getNodesByQualifiedName(name) : cg.getNodesByName(name); + const matchesByName = exact.length || !name.includes('.') || name.includes('\\') + ? exact : cg.getNodesByQualifiedName(name.replace(/\./g, '::')); + const targets = matchesByName + .filter(n => n.language === 'verilog' && ['field', 'variable', 'constant'].includes(n.kind)) + .sort((a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.qualifiedName.localeCompare(b.qualifiedName)); + if (!targets.length) return `No exact HDL signal matches "${name}". Use one signal name or qualified name (for example top::data).`; + const lines = [`**HDL access: ${name} — ${filter}**`, + 'Source occurrences only: read includes control/event uses; write follows a syntactic assignment/update or a known callable formal\'s argument direction, not an elaborated driver or timing result.']; + const sourceCache = new Map(); + let snippetBudget = 12000, shownSites = 0, totalSites = 0, unclassified = 0; + const sourceLines = (file: string): string[] | null => { + if (sourceCache.has(file)) return sourceCache.get(file)!; + if (sourceCache.size >= maxFiles) return null; + let source: string[] | null = null; + try { + const record = cg.getFile(file); + const absolute = validatePathWithinRoot(cg.getProjectRoot(), file); + if (record && absolute) { + const before = statSync(absolute); + if (before.size === record.size && Math.floor(before.mtimeMs) === Math.floor(record.modifiedAt)) { + const text = readFileSync(absolute, 'utf8'); + const after = statSync(absolute); + // Same SHA-256 over UTF-8 text as extraction's hashContent. Avoid + // importing the heavy extraction module on the MCP tools/list path. + const hash = createHash('sha256').update(text).digest('hex'); + if (before.size === after.size && before.mtimeMs === after.mtimeMs && hash === record.contentHash) source = text.split(/\r?\n/); + } + } + } catch { /* A missing or changed file must never be presented as the indexed occurrence. */ } + sourceCache.set(file, source); + return source; + }; + for (const target of targets) { + const sites: { edge: Edge; owner: Node; kinds: string[] }[] = []; + for (const edge of cg.getIncomingEdges(target.id)) { + if (edge.kind !== 'references') continue; + const owner = cg.getNode(edge.source); + if (!owner || owner.language !== 'verilog') continue; + const kinds = hdlAccessKinds(edge); + if (!kinds.length) { unclassified++; continue; } + if (matches(kinds, filter)) sites.push({ edge, owner, kinds }); + } + sites.sort((a, b) => a.owner.filePath.localeCompare(b.owner.filePath) || (a.edge.line ?? 0) - (b.edge.line ?? 0) || (a.edge.column ?? 0) - (b.edge.column ?? 0) || a.owner.qualifiedName.localeCompare(b.owner.qualifiedName)); + totalSites += sites.length; + const definitionVerified = sourceLines(target.filePath) !== null; + lines.push('', `**${target.qualifiedName}** — ${target.filePath}:${target.startLine}`, `${sites.length} matching access site(s).`); + if (!definitionVerified) lines.push('Definition location is from the index; current source is unverified (changed/unavailable or maxFiles reached).'); + for (const { edge, owner, kinds } of sites) { + if (shownSites >= 60) continue; + shownSites++; + lines.push(`- [${kinds.join(', ')}] ${owner.qualifiedName} — ${owner.filePath}:${edge.line ?? owner.startLine}${typeof edge.column === 'number' ? `:${edge.column + 1}` : ''}`); + const callableId = edge.metadata?.hdlCallTargetId, formalId = edge.metadata?.hdlFormalId; + if (typeof callableId === 'string' && typeof formalId === 'string') { + const callable = cg.getNode(callableId), formal = cg.getNode(formalId); + const direction = formal?.decorators?.find(d => d.startsWith('hdl:direction:'))?.slice('hdl:direction:'.length); + if (callable?.language === 'verilog' && formal?.language === 'verilog' && direction) { + const status = sourceLines(callable.filePath) === null ? ' [indexed definition; source unverified]' : ''; + lines.push(` Formal direction: ${callable.qualifiedName}(${direction} ${formal.name}).${status}`); + } + } + const source = sourceLines(owner.filePath); + if (!source) { lines.push(' Source omitted: changed/unavailable since indexing, or maxFiles reached.'); continue; } + if (!edge.line || edge.line > source.length) { lines.push(' No verified source line for this occurrence.'); continue; } + const start = Math.max(1, edge.line - 1), end = Math.min(source.length, edge.line + 1); + const excerpt = source.slice(start - 1, end).map((s, i) => `${start + i}\t${s}`).join('\n'); + if (excerpt.length > snippetBudget || excerpt.length > 2000) { lines.push(' Source snippet omitted by output budget.'); continue; } + snippetBudget -= excerpt.length; + const fence = '`'.repeat(Math.max(3, ...(excerpt.match(/`+/g) ?? []).map(s => s.length + 1))); + lines.push(`${fence}systemverilog`, excerpt, fence); + } + } + if (shownSites < totalSites) lines.push('', `Showing ${shownSites} of ${totalSites} matching sites; narrow with a qualified signal name.`); + if (unclassified) lines.push('', `${unclassified} unclassified reference(s) excluded. Connections and unknown uses do not imply reads or writes.`); + lines.push('', 'An empty result is not proof of no access outside the supported source patterns.'); + return lines.join('\n'); +} diff --git a/src/mcp/index.ts b/src/mcp/index.ts index e4a14a4fb..0e90a0a2a 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -329,13 +329,9 @@ export class MCPServer { * connected session; in direct mode it mirrors the pre-#411 behavior (close * cg, exit). Proxy mode never routes through here — the proxy exits itself. */ - stop(): void { + async stop(): Promise { if (this.stopped) return; this.stopped = true; - if (this.writerLockRoot) { - releaseWriterLock(this.writerLockRoot); - this.writerLockRoot = null; - } if (this.ppidWatchdog) { clearInterval(this.ppidWatchdog); this.ppidWatchdog = null; @@ -345,7 +341,7 @@ export class MCPServer { this.livenessWatchdog = null; } if (this.daemon) { - void this.daemon.stop('stop()'); + await this.daemon.stop('stop()'); // Daemon.stop calls process.exit; nothing else to do. return; } @@ -354,9 +350,13 @@ export class MCPServer { this.session = null; } if (this.engine) { - this.engine.stop(); + await this.engine.stop(); this.engine = null; } + if (this.writerLockRoot) { + releaseWriterLock(this.writerLockRoot); + this.writerLockRoot = null; + } process.exit(0); } @@ -380,7 +380,7 @@ export class MCPServer { } this.engine = new MCPEngine(); - const transport = new StdioTransport(); + const transport = new StdioTransport({ exitOnClose: false, onClose: () => { void this.stop(); } }); this.session = new MCPSession(transport, this.engine, { explicitProjectPath: this.projectPath, }); diff --git a/src/mcp/proxy.ts b/src/mcp/proxy.ts index 27e684e51..293f9dc43 100644 --- a/src/mcp/proxy.ts +++ b/src/mcp/proxy.ts @@ -247,10 +247,10 @@ export async function runLocalHandshakeProxy(deps: LocalHandshakeDeps): Promise< const writeClient = (obj: JsonRpc | string): void => { try { process.stdout.write((typeof obj === 'string' ? obj : JSON.stringify(obj)) + '\n'); } catch { /* host gone */ } }; - const shutdown = (): void => { + const shutdown = async (): Promise => { if (shuttingDown) return; shuttingDown = true; try { daemonSocket?.destroy(); } catch { /* ignore */ } - try { engine?.stop(); } catch { /* ignore */ } + await engine?.stop(); process.exit(0); }; const ensureEngine = (): Promise => { diff --git a/src/mcp/query-pool.ts b/src/mcp/query-pool.ts index 0575def38..45a8fd14d 100644 --- a/src/mcp/query-pool.ts +++ b/src/mcp/query-pool.ts @@ -220,8 +220,11 @@ export class QueryPool { if (!m) return; if (m.type === 'ready') { this.pendingWorkers.delete(w); - if (m.ok === false) this.totalCrashes++; // hard open failure - else this.everReady = true; + if (m.ok === false) { + this.onWorkerGone(w); + return; + } + this.everReady = true; this.idle.push(w); this.drain(); return; @@ -256,6 +259,11 @@ export class QueryPool { this.settle(job, { isError: true, content: [{ type: 'text', text: 'codegraph worker crashed; please retry the call.' }] }); } } + if (!this.healthy) { + for (const queued of this.queue.splice(0)) { + this.settle(queued, { isError: true, content: [{ type: 'text', text: 'codegraph workers could not start; retry after checking the project index.' }] }); + } + } this.drain(); } @@ -291,6 +299,7 @@ export class QueryPool { /** Run a read tool on the pool. Always resolves (never rejects). */ run(toolName: string, args: Record): Promise { + if (!this.healthy) return Promise.resolve({ isError: true, content: [{ type: 'text', text: 'codegraph worker pool unavailable; retry after checking the project index.' }] }); return new Promise((resolve) => { const job: Job = { id: this.nextId++, toolName, args, resolve, diff --git a/src/mcp/server-instructions.ts b/src/mcp/server-instructions.ts index 4cb22150d..132718e7c 100644 --- a/src/mcp/server-instructions.ts +++ b/src/mcp/server-instructions.ts @@ -58,13 +58,23 @@ calls; a grep/read exploration is dozens. - Qualified symbol names accept dots, \`::\`, or slashes, including containers whose names contain dots (for example, \`AppWeb.Format.group\`). - Named-symbol call paths require exact matches; partial or mistyped names are never silently substituted as flow endpoints. If a graph query reports a missing symbol with did-you-mean suggestions, query the suggested name explicitly. +## Verilog / SystemVerilog + +Computed parameters and port widths are available through the explicit codegraph hdl-semantic CLI with an installed slang executable and an active HDL profile. Standard explore returns source relationships; it does not automatically run the compiler. Compiler failures do not invalidate source navigation. + +An optional codegraph.json HDL profile selects source units and conditional branches. Exploration reports the indexed profile separately from current configuration. On mismatch, sync/rebuild before interpreting results as the new profile; invalid configuration must be corrected. Snippets remain original source, including inactive branches. Includes contribute macro state; full macro expansion and elaboration are not performed. + +For signal readers/writers, pass one exact or qualified signal name as query and set hdlAccess to read, write, readwrite, control, event, or all. Read includes control/event and readwrite; write includes readwrite. Results show source positions and known formal-argument direction evidence. Unknown references remain unclassified; empty results do not prove absence of access. + +Query module and instance names to inspect hierarchy and the exact port-connection source. Ports/signals, lexical block scopes, generate template parameters, modports and always/assign blocks are searchable. Local signal references belong to their block; calls retain the enclosing function/process. Parser-recovered procedural regions may omit uncertain signal references. Query an instance-qualified connection such as top.u_rx1.rx to inspect its local expression and declared target port; an explicit empty connection still names a formal port but has no local endpoint. Bit/part-select references identify the base signal and indexed expressions, not evaluated widths. HDL references record syntactic signal use, not drive direction, timing or elaborated connectivity; positional bindings require authoritative header order, and wildcard matches require known formal ports and unambiguous local signals (explicit connections take priority). Generate iterations are not expanded. Qualified package calls retain package identity. + ## Anti-patterns - **Trust codegraph's results — don't re-verify them with grep.** They come from a full AST parse; re-checking with grep is slower, less accurate, and wastes context. - **Don't grep or Read first** to find or understand indexed code — ONE \`codegraph_explore\` returns the relevant symbols' source together in a single round-trip. Reach for raw \`Read\`/\`Grep\` only to confirm a specific detail codegraph didn't cover, or for what codegraph doesn't index (configs, docs). - **Don't reconstruct a flow by hand** — name the endpoints in one \`codegraph_explore\` and it surfaces the path between them, dynamic-dispatch hops included. - **After editing, check the staleness banner.** When a tool response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph. A different, rarer banner — "⚠️ CodeGraph auto-sync is DISABLED…" — means live watching stopped entirely (the whole index is frozen, not just a few files); until it's resolved, Read files directly to confirm anything that may have changed. -- **A file flagged "⚠ changed on disk after the last index sync" drifted from its index** (most common on projects queried via \`projectPath\`, which have no live watcher). Codegraph never serves a possibly-mis-sliced body from such a file — it either shows the file's full CURRENT source (trust it as a Read) or omits the source with this flag. When the source was omitted, Read that specific file; line numbers referencing it elsewhere in the response may be shifted until that project's next sync. All unflagged files remain trustworthy. +- **A file flagged "⚠ changed on disk after the last index sync" drifted from its index** (for example when watching is disabled or another process owns live sync). Codegraph never serves a possibly-mis-sliced body from such a file — it either shows the file's full CURRENT source (trust it as a Read) or omits the source with this flag. When the source was omitted, Read that specific file; line numbers referencing it elsewhere in the response may be shifted until that project's next sync. All unflagged files remain trustworthy. - **Source is re-served on every call by default**, including for fresh subagents and after context compaction. Cross-call dedup requires \`CODEGRAPH_EXPLORE_DEDUP=1\` and is only suitable for hosts that guarantee one durable context per connection. With that opt-in, **"Already sent earlier in this conversation"** points to exact, unchanged source returned by an earlier \`codegraph_explore\` in that context. Use that copy; don't re-fetch it and don't Read the file. The bytes it freed went into source you have not seen yet, elsewhere in the same response. diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 42fdd2288..21be9ce8d 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -1,3 +1,4 @@ +import { formatHdlProfileStatus } from '../hdl/status'; /** * MCP Tool Definitions * @@ -5,6 +6,7 @@ */ import type CodeGraph from '../index'; +import { formatHdlAccess, HDL_ACCESS_FILTERS, type HdlAccessFilter } from './hdl-access'; import type { QueryPool } from './query-pool'; import { findNearestCodeGraphRoot } from '../directory'; // Lazy-load the heavy CodeGraph chain off the MCP startup path — see the same @@ -36,6 +38,7 @@ import { groupDefinitions, lastQualifierPart, matchesSymbol } from '../graph/sym import { extractQueryPaths, queryMightContainPaths } from '../search/query-paths'; import { existsSync, + realpathSync, readFileSync, statSync, } from 'fs'; @@ -1342,6 +1345,11 @@ export const tools: ToolDefinition[] = [ description: 'Maximum number of files to include source code from (default: 12)', default: 12, }, + hdlAccess: { + type: 'string', + description: 'HDL signal access filter. With this option query must be one exact signal name or qualified name; read includes control/event uses, write includes readwrite. Returns access sites and source, not elaborated drivers.', + enum: [...HDL_ACCESS_FILTERS], + }, projectPath: projectPathProperty, }, required: ['query'], @@ -1496,6 +1504,14 @@ export class ToolHandler { // main loop stays free for the MCP transport under concurrent load. Null in // direct/in-process mode (one client, no concurrency to parallelize). private queryPool: QueryPool | null = null; + private projectLifecycle: ((cg: CodeGraph) => Promise) | null = null; + private projectGates = new Map>(); + private closing = false; + + /** Engine owns watching and writer locks; read-only handlers never activate projects. */ + setProjectLifecycle(activate: (cg: CodeGraph) => Promise): void { + this.projectLifecycle = activate; + } constructor(private cg: CodeGraph | null) {} @@ -1512,8 +1528,18 @@ export class ToolHandler { /** * Update the default CodeGraph instance (e.g. after lazy initialization) */ - setDefaultCodeGraph(cg: CodeGraph): void { + setDefaultCodeGraph(cg: CodeGraph): CodeGraph { + const root = realpathSync(cg.getProjectRoot()); + const cached = this.projectCache.get(root); + if (cached && cached !== cg) { + // An explicit request may have opened this project before default-root + // discovery completed. Preserve its watcher and in-flight catch-up. + cg.close(); + cg = cached; + } + this.projectCache.delete(root); // default ownership moves to the engine this.cg = cg; + return cg; } /** @@ -1524,9 +1550,24 @@ export class ToolHandler { * stale data, which is what would have happened without the gate. */ setCatchUpGate(p: Promise | null): void { + if (this.projectLifecycle && this.cg) { + // Engine defaults and explicit aliases share the same pending gate. + // Do not overwrite a catch-up already running on a promoted cache entry. + if (p && !this.projectGates.has(this.cg)) this.rememberProjectGate(this.cg, p); + if (!p) this.projectGates.delete(this.cg); + return; + } this.catchUpGate = p; } + private rememberProjectGate(cg: CodeGraph, gate: Promise): void { + this.projectGates.set(cg, gate); + const clear = (): void => { + if (this.projectGates.get(cg) === gate) this.projectGates.delete(cg); + }; + void gate.then(clear, clear); + } + /** * Await the catch-up gate, but no longer than the configured timeout (#905). * If the reconcile settles first, we got the fully-reconciled answer. If the @@ -1709,6 +1750,7 @@ export class ToolHandler { * similar to how git finds .git/ directories. */ private getCodeGraph(projectPath?: string): CodeGraph { + if (this.closing) throw new Error('MCP engine is shutting down'); if (!projectPath) { if (!this.cg) { const searched = this.defaultProjectHint ?? process.cwd(); @@ -1753,7 +1795,8 @@ export class ToolHandler { // worktree kept being served the parent checkout's index until restart // (#926). The DB connection itself is still cached (by resolved root, // below), so re-resolving costs only the stat walk, never a reopen. - const resolvedRoot = findNearestCodeGraphRoot(projectPath); + const nearestRoot = findNearestCodeGraphRoot(projectPath); + const resolvedRoot = nearestRoot ? realpathSync(nearestRoot) : null; if (!resolvedRoot) { throw new NotIndexedError( @@ -1771,11 +1814,11 @@ export class ToolHandler { // support) that surfaces as intermittent // "database is locked" on concurrent tool calls. See issue #238. The // default instance is owned/closed by the server, so it's never cached. - if (this.cg && this.cg.getProjectRoot() === resolvedRoot) { + if (this.cg && realpathSync(this.cg.getProjectRoot()) === resolvedRoot) { return this.freshen(this.cg); } - // Cache the open DB connection by RESOLVED ROOT only — never by the input + // Cache the open DB connection by CANONICAL ROOT only — never by the input // path. One key per instance means closeAll() closes each exactly once, and // a changed resolution maps to a different entry instead of a stale hit. const cached = this.projectCache.get(resolvedRoot); @@ -1819,9 +1862,20 @@ export class ToolHandler { cg.close(); } this.projectCache.clear(); + this.projectGates.clear(); this.worktreeMismatchCache.clear(); } + /** Engine shutdown must drain active writers before closing cached projects. */ + async closeAllAsync(): Promise { + this.closing = true; + const projects = [...this.projectCache.values()]; + this.projectCache.clear(); + this.projectGates.clear(); + this.worktreeMismatchCache.clear(); + await Promise.all(projects.map((cg) => cg.closeAsync())); + } + /** * Validate that a value is a non-empty string within length bounds. * @@ -2143,6 +2197,20 @@ export class ToolHandler { if (typeof check === 'object' && check !== undefined) return check; } + // Resolve on the engine thread even when dispatch uses a read worker: + // explicit projects need the same watcher and initial reconcile as default. + if (this.projectLifecycle && (pathCheck || this.cg)) { + const cg = this.getCodeGraph(pathCheck); + let gate = this.projectGates.get(cg); + if (!gate) { + // Retry writer ownership on later calls: another engine may have + // closed since we opened this connection in read-only fallback. + gate = this.projectLifecycle(cg); + this.rememberProjectGate(cg, gate); + } + await this.awaitCatchUpGate(gate); + } + // codegraph_status reports watcher state (pending files, degraded mode, // worktree warning) and embeds its own sections — it must run on the MAIN // thread against the watched default instance, so it is NEVER off-loaded to @@ -2295,7 +2363,17 @@ export class ToolHandler { case 'codegraph_callers': return await this.handleCallers(args); case 'codegraph_callees': return await this.handleCallees(args); case 'codegraph_impact': return await this.handleImpact(args); - case 'codegraph_explore': return await this.handleExplore(args); + case 'codegraph_explore': { + const result = await this.handleExplore(args); + if (result.isError) return result; + const profile = this.getCodeGraph(args.projectPath as string | undefined).getHdlProfileStatus?.() ?? null; + const note = formatHdlProfileStatus(profile); + const first = result.content[0]; + if (!note || first?.type !== 'text') return result; + const emission = result[EXPLORE_EMISSION_KEY]; + return { ...result, content: [{type:'text', text:`${note}\n\n${first.text}`}, ...result.content.slice(1)], + ...(emission ? {[EXPLORE_EMISSION_KEY]: {...emission,responseBytes:emission.responseBytes + note.length + 2}} : {}) }; + } case 'codegraph_node': return await this.handleNode(args); case 'codegraph_files': return await this.handleFiles(args); default: return this.errorResult(`Unknown tool: ${toolName}`); @@ -3288,6 +3366,13 @@ export class ToolHandler { const cg = this.getCodeGraph(args.projectPath as string | undefined); const projectRoot = cg.getProjectRoot(); + if (args.hdlAccess !== undefined) { + if (typeof args.hdlAccess !== 'string' || !HDL_ACCESS_FILTERS.includes(args.hdlAccess as HdlAccessFilter)) { + return this.errorResult(`hdlAccess must be one of: ${HDL_ACCESS_FILTERS.join(', ')}`); + } + return this.textResult(this.truncateOutput(formatHdlAccess(cg, rawQuery, args.hdlAccess as HdlAccessFilter, + clamp((args.maxFiles as number) || 12, 1, 20)))); + } // Resolve adaptive output budget from project size. Falls back to the // largest-tier defaults if stats aren't available, which preserves @@ -6520,8 +6605,8 @@ export class ToolHandler { let cg = this.getCodeGraph(args.projectPath as string | undefined); // Same trick as withStalenessNotice — when an explicit projectPath // resolves to the same project as the default session cg, prefer the - // default so getPendingFiles() (only populated by the default's watcher) - // is non-empty when there are pending edits. + // default so getPendingFiles() reads the same watcher as the default + // session when there are pending edits. if (this.cg && cg !== this.cg) { try { if (resolvePath(this.cg.getProjectRoot()) === resolvePath(cg.getProjectRoot())) { diff --git a/src/resolution/callback-synthesizer.ts b/src/resolution/callback-synthesizer.ts index dd2e735fb..7bf4f23a9 100644 --- a/src/resolution/callback-synthesizer.ts +++ b/src/resolution/callback-synthesizer.ts @@ -38,6 +38,7 @@ import { createYielder, type MaybeYield } from './cooperative-yield'; import { crossTierEdges } from './tier-synthesizer'; import { enclosingFn, makeLineAt } from './synth-utils'; import { resolveImportPath } from './import-resolver'; +import { matchFunctionRef } from './name-matcher'; const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/; const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i; @@ -143,6 +144,50 @@ function* methodAndFunctionNodes(queries: QueryBuilder): IterableIterator yield* queries.iterateNodesByKind('function'); } +function fieldCallbackOwner(queries: QueryBuilder, caller: Node): Node | undefined { + let level = [caller.id]; + const seen = new Set(); + while (level.length) { + const parents: Node[] = []; + for (const id of level) { + if (seen.has(id)) continue; + seen.add(id); + for (const edge of queries.getIncomingEdges(id, ['contains'])) { + const node = queries.getNodeById(edge.source); + if (node) parents.push(node); + } + } + const owners = [...new Map(parents.filter(n => n.kind === 'class' || n.kind === 'component').map(n => [n.id, n])).values()]; + if (owners.length) return owners.length === 1 ? owners[0] : undefined; + level = parents.map(n => n.id); + } + return undefined; +} + +/** Resolve this.handler on the registering class, including inherited methods. */ +function fieldCallbackMethod(queries: QueryBuilder, ctx: ResolutionContext, caller: Node, name: string): Node | null { + const owner = fieldCallbackOwner(queries, caller); + if (!owner) return null; + let level = [owner.id]; + const seen = new Set(); + const candidates = ctx.getNodesByName(name).filter(n => n.kind === 'method' || n.kind === 'function'); + while (level.length) { + const found: Node[] = []; + const next: string[] = []; + for (const id of level) { + if (seen.has(id)) continue; + seen.add(id); + const children = new Set(queries.getOutgoingEdges(id, ['contains']).map(e => e.target)); + found.push(...candidates.filter(n => children.has(n.id))); + next.push(...queries.getOutgoingEdges(id, ['extends']).map(e => e.target)); + } + const unique = [...new Map(found.map(n => [n.id, n])).values()]; + if (unique.length) return unique.length === 1 ? unique[0]! : null; + level = next; + } + return null; +} + /** Phase 1: field-backed observer channels (registrar/dispatcher share a store). */ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext, onYield: MaybeYield): Promise { const registrars: Array<{ node: Node; field: string }> = []; @@ -165,10 +210,11 @@ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext, const seen = new Set(); for (const reg of registrars) { const chDispatchers = dispatchers.filter( - (d) => d.node.filePath === reg.node.filePath && d.field === reg.field + (d) => d.node.filePath === reg.node.filePath && d.field === reg.field && + fieldCallbackOwner(queries, d.node)?.id === fieldCallbackOwner(queries, reg.node)?.id ); if (chDispatchers.length === 0) continue; - const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(?:this\\.)?(\\w+)`); + const argRe = new RegExp(`${reg.node.name}\\s*\\(\\s*(this\\.)?(\\w+)\\s*(?=[,)]|\\.bind\\s*\\()`); let added = 0; for (const e of queries.getIncomingEdges(reg.node.id, ['calls'])) { if (added >= MAX_CALLBACKS_PER_CHANNEL) break; @@ -178,7 +224,14 @@ async function fieldChannelEdges(queries: QueryBuilder, ctx: ResolutionContext, const line = ctx.readFile(caller.filePath)?.split('\n')[e.line - 1]; const am = line?.match(argRe); if (!am) continue; - const fn = ctx.getNodesByName(am[1]!).find((n) => n.kind === 'method' || n.kind === 'function'); + const fn = am[1] + ? fieldCallbackMethod(queries, ctx, caller, am[2]!) + : (() => { + const target = matchFunctionRef({ fromNodeId: caller.id, referenceName: am[2]!, + referenceKind: 'function_ref', filePath: caller.filePath, language: caller.language, + line: e.line!, column: am.index! }, ctx); + return target ? queries.getNodeById(target.targetNodeId) ?? null : null; + })(); if (!fn) continue; for (const disp of chDispatchers) { if (disp.node.id === fn.id) continue; diff --git a/src/resolution/cpp-constructor.ts b/src/resolution/cpp-constructor.ts new file mode 100644 index 000000000..67b55a14b --- /dev/null +++ b/src/resolution/cpp-constructor.ts @@ -0,0 +1,58 @@ +import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; + +/** The constructor-only reference emitted for a local C++ object declaration. */ +export function isCppConstructorRef(ref: UnresolvedRef): boolean { + return ref.language === 'cpp' && ref.referenceKind === 'calls' && /::[^:]+\/\d+$/.test(ref.referenceName); +} + +/** Count top-level parameters; nested template/function/default commas are not separators. */ +function arity(signature: string | undefined): { min: number; max: number } | null { + if (!signature?.startsWith('(') || !signature.endsWith(')')) return null; + const text = signature.slice(1, -1).trim(); + if (!text || text === 'void') return { min: 0, max: 0 }; + const parts: string[] = []; + let start = 0, depth = 0, quote = ''; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (quote) { if (c === '\\') i++; else if (c === quote) quote = ''; continue; } + if (c === '"' || c === "'") { quote = c; continue; } + if ('(<[{'.includes(c)) depth++; + if (')>]}'.includes(c)) depth--; + if (c === ',' && depth === 0) { parts.push(text.slice(start, i)); start = i + 1; } + } + if (depth !== 0 || quote) return null; + parts.push(text.slice(start)); + // Complex declarator/default expressions need a compiler; don't infer their arity. + if (parts.some(p => /[<>]=|==|!=/.test(p))) return null; + const variadic = parts.some(p => p.includes('...')); + return { min: parts.filter(p => !p.includes('=') && !p.includes('...')).length, + max: variadic ? Infinity : parts.length }; +} + +/** Lexical type identity first, then a unique arity-compatible constructor. */ +export function matchCppConstructor(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const match = ref.referenceName.match(/^(.*)::([^:]+)\/(\d+)$/); + if (!match) return null; + const [, rawType, name, count] = match; + const type = rawType!.replace(/^::/, ''); + if (type.split('::').pop() !== name) return null; + const caller = context.getNodeById?.(ref.fromNodeId); + const scopes = rawType!.startsWith('::') ? [] : (caller?.qualifiedName.split('::') ?? []); + const typeNames: string[] = []; + for (let i = scopes.length; i > 0; i--) typeNames.push(`${scopes.slice(0, i).join('::')}::${type}`); + typeNames.push(type); + for (const qualified of typeNames) { + const owners = context.getNodesByQualifiedName(qualified).filter(n => + n.language === 'cpp' && (n.kind === 'class' || n.kind === 'struct')); + if (!owners.length) continue; + const methods = context.getNodesByName(name!).filter(n => n.language === 'cpp' && + n.kind === 'method' && n.qualifiedName === `${qualified}::${name}`); + // Braced initialization prioritizes initializer_list overloads; arity alone + // cannot safely choose among them and ordinary constructors. + if (methods.some(n => /\binitializer_list\b/.test(n.signature ?? ''))) return null; + const matches = methods.filter(n => { const a = arity(n.signature); return a && a.min <= Number(count) && Number(count) <= a.max; }); + if (matches.length !== 1) return null; + return { original: ref, targetNodeId: matches[0]!.id, confidence: 0.9, resolvedBy: 'qualified-name' }; + } + return null; +} diff --git a/src/resolution/cpp-macro-visibility.ts b/src/resolution/cpp-macro-visibility.ts new file mode 100644 index 000000000..413b51e62 --- /dev/null +++ b/src/resolution/cpp-macro-visibility.ts @@ -0,0 +1,123 @@ +import * as path from 'path'; +import type { ResolutionContext, UnresolvedRef } from './types'; +import { resolveImportPath } from './import-resolver'; +import { stripCommentsForRegex } from './strip-comments'; + +type Truth = boolean | undefined; +type Event = { line: number; defined: Truth }; +type Definition = { defined: Truth; value: Truth; assumedDefined: boolean; assumedValue: Truth; scope: string[] }; +type Cache = { roots: Map>; lines: Map; includes: Map }; +const memo = new WeakMap(); +const and = (a: Truth, b: Truth): Truth => a === false || b === false ? false : a === true && b === true ? true : undefined; +const or = (a: Truth, b: Truth): Truth => a === true || b === true ? true : a === false && b === false ? false : undefined; +const not = (a: Truth): Truth => a === undefined ? undefined : !a; + +export function clearCppMacroVisibility(context: ResolutionContext): void { memo.delete(context); } + +/** Textual include order and locally decidable branches, not a full C preprocessor. */ +export function isVisibleCppMacro(ref: UnresolvedRef, context: ResolutionContext): boolean { + if (!['c', 'cpp'].includes(ref.language) || ref.referenceKind !== 'calls' || !/^\w+$/.test(ref.referenceName)) return false; + if (!context.getNodesByName(ref.referenceName).some(n => + n.kind === 'constant' && /^\s*#\s*define\b/.test(n.signature ?? ''))) return false; + let cache = memo.get(context); + if (!cache) { cache = { roots: new Map(), lines: new Map(), includes: new Map() }; memo.set(context, cache); } + const rootKey = `${ref.language}\0${ref.filePath}`; + let timeline = cache.roots.get(rootKey); + if (!timeline) { + timeline = new Map(); + const definitions = new Map(); + const visiting = new Set(); + const once = new Set(); + const lookup = (name: string, scope: string[], field: 'defined' | 'value'): Truth => { + const def = definitions.get(name); + if (!def) return undefined; + // A definition under an unknown guard is certain while we remain on + // that same hypothetical path, but uncertain after leaving it. + if (def[field] === undefined && def.scope.every((part, i) => scope[i] === part)) { + return field === 'defined' ? def.assumedDefined : def.assumedValue; + } + return def[field]; + }; + const condition = (expression: string, scope: string[]): Truth => { + const text = expression.trim(); + if (/^(?:0x[\da-f]+|\d+)[uUlL]*$/i.test(text)) return Number(text.replace(/[uUlL]+$/, '')) !== 0; + const def = text.match(/^(!)?\s*defined\s*(?:\(\s*(\w+)\s*\)|(\w+))$/); + if (def) { const known = lookup(def[2] ?? def[3]!, scope, 'defined'); return def[1] ? not(known) : known; } + return /^\w+$/.test(text) ? lookup(text, scope, 'value') : undefined; + }; + const scan = (file: string, inherited: Truth, inheritedScope: string[], includeLine?: number): void => { + if (visiting.has(file) || once.has(file)) return; + visiting.add(file); + let lines = cache!.lines.get(file); + if (!lines) { lines = stripCommentsForRegex(context.readFile(file) ?? '', 'cpp').split(/\r?\n/); cache!.lines.set(file, lines); } + const frames: Array<{ parent: Truth; taken: Truth; scope: string[] }> = []; + let active = inherited; + let scope = inheritedScope; + for (let i = 0; i < lines.length; i++) { + const text = lines[i]!; + const branch = text.match(/^\s*#\s*(ifdef|ifndef|if|elif|else|endif)\b(.*)$/); + if (branch) { + const op = branch[1]; + if (op === 'if' || op === 'ifdef' || op === 'ifndef') { + const test = op === 'if' ? condition(branch[2]!, scope) : lookup(branch[2]!.trim(), scope, 'defined'); + const selected = op === 'ifndef' ? not(test) : test; + frames.push({ parent: active, taken: selected, scope }); active = and(active, selected); + if (selected === undefined) scope = [...scope, `${file}:${i}`]; + } else if (op === 'endif') { const frame = frames.pop(); active = frame?.parent ?? inherited; scope = frame?.scope ?? inheritedScope; } + else { + const frame = frames[frames.length - 1]; + if (frame) { + const test = op === 'else' ? true : condition(branch[2]!, frame.scope); + const selected = and(not(frame.taken), test); + active = and(frame.parent, selected); frame.taken = or(frame.taken, test); + scope = selected === undefined ? [...frame.scope, `${file}:${i}`] : frame.scope; + } + } + continue; + } + if (active === false) continue; + if (/^\s*#\s*pragma\s+once\b/.test(text) && active === true) once.add(file); + const directive = text.match(/^\s*#\s*(define|undef)\s+(\w+)\b/); + if (directive) { + const name = directive[2]!; + const defining = directive[1] === 'define'; + const previous = definitions.get(name); + // An absent entry is false; an existing unknown must stay unknown. + const prior = previous ? previous.defined : false; + const defined = defining ? or(prior, active) : and(prior, not(active)); + const assumedValue = defining ? condition(text.slice(directive[0].length), scope) : false; + const value = active === true ? assumedValue : undefined; + definitions.set(name, { defined, value, assumedDefined: defining, assumedValue, scope: [...scope] }); + const events = timeline!.get(name) ?? []; + events.push({ line: includeLine ?? i + 1, defined }); timeline!.set(name, events); + } + const include = text.match(/^\s*#\s*include\s*([<"])([^>"]+)[>"]/); + if (include) { + const spec = include[2]!; + const key = `${ref.language}\0${file}\0${include[1]}${spec}`; + let target = cache!.includes.get(key); + if (target === undefined) { + const local = path.posix.normalize(path.posix.join(path.posix.dirname(file), spec.replace(/\\/g, '/'))); + target = include[1] === '"' && !local.startsWith('../') && !path.posix.isAbsolute(local) && context.fileExists(local) + ? local : resolveImportPath(spec, file, ref.language, context); + if (!target) { + const matches = context.getAllFiles().filter(f => f === spec || f.endsWith('/' + spec)); + if (matches.length === 1) target = matches[0]!; + } + cache!.includes.set(key, target); + } + if (target) scan(target, active, scope, includeLine ?? i + 1); + } + } + visiting.delete(file); + }; + scan(ref.filePath, true, []); + // Bound root timelines; cache only COMPLETE translation-unit walks, never + // a child truncated by an include cycle or evaluated with different flags. + if (cache.roots.size >= 128) cache.roots.delete(cache.roots.keys().next().value!); + cache.roots.set(rootKey, timeline); + } + const before = (timeline.get(ref.referenceName) ?? []).filter(e => e.line <= ref.line); + // An unknown build-flag arm remains possible, so it cannot justify an ordinary call. + return before.length > 0 && before[before.length - 1]!.defined !== false; +} diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 011e0e0f2..881c42978 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -20,6 +20,11 @@ import { isImportableKind, } from './types'; import { isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily, dumpNameMatcherProfile, clearNameMatcherMemos } from './name-matcher'; +import { isVisibleCppMacro } from './cpp-macro-visibility'; +import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; +import { isVerilogMemberRef, matchVerilogMember } from './verilog-members'; +import { isVerilogPortRef, matchVerilogPort } from './verilog-ports'; +import { isVerilogWildcardRef, matchVerilogWildcard } from './verilog-wildcard'; import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath } from './import-resolver'; import { ResolverPool, minRefsForPool } from './resolver-pool'; import { resolveAliasBinding } from './alias-binding'; @@ -33,7 +38,7 @@ import { logDebug } from '../errors'; import { lexicalPathWithinRoot } from '../utils'; import type { ReExport } from './types'; import { LRUCache } from './lru-cache'; -import { JS_BUILT_INS } from './js-builtins'; +import { JS_BUILT_INS, isTsJsNestedCall } from './js-builtins'; /** Node kinds that can declare supertypes (extends/implements). */ const SUPERTYPE_BEARING_KINDS = new Set([ @@ -472,7 +477,7 @@ export class ReferenceResolver { matches = []; for (const m of candidates) { if (m.kind !== 'method') continue; - if (m.language !== language) continue; + if (!sameLanguageFamily(m.language, language)) continue; const qn = m.qualifiedName; if (qn === want || qn.endsWith(`::${want}`)) matches.push(m); } @@ -495,7 +500,7 @@ export class ReferenceResolver { ownerIndex = new Map(); for (const m of candidates) { if (m.kind !== 'method') continue; - if (m.language !== language) continue; + if (!sameLanguageFamily(m.language, language)) continue; const qn = m.qualifiedName; const i2 = qn.lastIndexOf('::'); if (i2 < 0) continue; // single-segment qn can never match `T::m` @@ -731,6 +736,7 @@ export class ReferenceResolver { filePath: ref.filePath || this.getFilePathFromNodeId(ref.fromNodeId), language: ref.language || this.getLanguageFromNodeId(ref.fromNodeId), rowId: ref.rowId, + candidates: ref.candidates, })); const total = refs.length; @@ -876,6 +882,7 @@ export class ReferenceResolver { * the alias names (see ./alias-binding), regardless of the strategy. */ resolveOne(ref: UnresolvedRef): ResolvedRef | null { + if (isVisibleCppMacro(ref, this.context)) return null; const resolved = this.gateTargetKind(this.resolveOneInner(ref), ref); if (!resolved || ref.referenceKind !== 'calls') return resolved; @@ -895,6 +902,10 @@ export class ReferenceResolver { } private resolveOneInner(ref: UnresolvedRef): ResolvedRef | null { + if (isCppConstructorRef(ref)) return matchCppConstructor(ref, this.context); + if (isVerilogWildcardRef(ref)) return matchVerilogWildcard(ref, this.context, matchReference); + if (isVerilogPortRef(ref)) return matchVerilogPort(ref, this.context, matchReference); + if (isVerilogMemberRef(ref)) return matchVerilogMember(ref, this.context); // Skip built-in/external references if (this.isBuiltInOrExternal(ref)) { return null; @@ -1021,6 +1032,14 @@ export class ReferenceResolver { if (this.profileStages) this.stageAdd('frameworks', ref, fwEarly !== null, tFw); if (fwEarly) return fwEarly; + // An imported root is not the called nested member. Keep framework + // evidence, but never bind holder.values.get to holder or an unrelated get. + if (isTsJsNestedCall(ref)) { + return candidates.length > 0 + ? candidates.reduce((best, curr) => curr.confidence > best.confidence ? curr : best) + : null; + } + // Strategy 2: Try import-based resolution // A TS/JS/Python call-receiver chain (`useStore.getState().reset`, #1683) // names the ROOT's import, not the method's: letting resolveViaImport see @@ -1192,6 +1211,8 @@ export class ReferenceResolver { // wrong rebind; edges without refName (pre-#1240, synthesized) are // deliberately NOT resurrected for the same reason. refName: ref.original.referenceName, + ...(ref.original.language === 'verilog' && ref.original.candidates?.length + ? { refCandidates: ref.original.candidates } : {}), ...(ref.original.referenceKind !== kind ? { refKind: ref.original.referenceKind } : {}), // Uniform marker for function-as-value edges (#756), regardless of // which strategy resolved them (import vs matchFunctionRef) — lets @@ -1481,6 +1502,7 @@ export class ReferenceResolver { filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId), language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId), rowId: raw.rowId, + candidates: raw.candidates, }; const result = this.resolveOneTimed(ref); if (result) { @@ -1601,6 +1623,7 @@ export class ReferenceResolver { filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId), language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId), rowId: raw.rowId, + candidates: raw.candidates, }; const result = this.resolveOneTimed(ref); if (result) { @@ -2642,6 +2665,11 @@ export class ReferenceResolver { private gateTargetKind(result: ResolvedRef | null, ref: UnresolvedRef): ResolvedRef | null { if (!result) return result; + if (ref.referenceKind === 'calls') { + const target = this.queries.getNodeById(result.targetNodeId); + if (target?.kind === 'constant' && /^\s*#\s*define\b/.test(target.signature ?? '')) return null; + } + // An `imports` reference names something importable — never a member that // only exists inside a type. if (ref.referenceKind === 'imports') { diff --git a/src/resolution/js-builtins.ts b/src/resolution/js-builtins.ts index ad3e33879..77e0b5941 100644 --- a/src/resolution/js-builtins.ts +++ b/src/resolution/js-builtins.ts @@ -1,3 +1,5 @@ +import type { UnresolvedRef } from './types'; + /** Shared JS/TS built-ins for direct references and inferred receiver types. */ export const JS_BUILT_INS = new Set([ 'console', 'window', 'document', 'global', 'process', @@ -6,3 +8,16 @@ export const JS_BUILT_INS = new Set([ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval', 'fetch', 'require', 'module', 'exports', '__dirname', '__filename', ]); + +/** Nested property calls need framework evidence, not a last-name guess. */ +export function isTsJsNestedCall(ref: UnresolvedRef): boolean { + if (ref.referenceKind !== 'calls' || + !['typescript', 'tsx', 'javascript', 'jsx'].includes(ref.language)) return false; + const receiver = ref.referenceName.slice(0, ref.referenceName.lastIndexOf('.')).trim(); + // Call-result chains have their own validated store/factory resolution. + if (receiver.endsWith('()')) return false; + // The extractors preserve compound receiver text, including brackets, + // optional access and comments. A simple receiver is just the root itself. + const root = receiver.split(/[.\[?\s]/, 1)[0]; + return receiver !== root && root !== 'this' && root !== 'window'; +} diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 4db2c1f17..800bd456c 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -8,7 +8,12 @@ import * as path from 'path'; import { Language, Node } from '../types'; import { UnresolvedRef, ResolvedRef, ResolutionContext, SUPERTYPE_TARGET_KINDS, isInheritanceRef, isImportableKind } from './types'; import { blankStringContents, stripCommentsForRegex } from './strip-comments'; -import { JS_BUILT_INS } from './js-builtins'; +import { JS_BUILT_INS, isTsJsNestedCall } from './js-builtins'; +import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; +import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; +import { isVerilogMemberRef, matchVerilogMember } from './verilog-members'; +import { isVerilogPortRef, matchVerilogPort } from './verilog-ports'; +import { isVerilogWildcardRef, matchVerilogWildcard } from './verilog-wildcard'; /** * Ceiling on how many same-named definitions a FUZZY name-match strategy will @@ -190,6 +195,13 @@ export function crossesKnownFamily(a: string, b: string): boolean { * both-known filter so `.vue`/`.svelte` (own tag) importing `.ts` survives. */ function applyLanguageGate(candidates: Node[], ref: UnresolvedRef): Node[] { + if (ref.referenceKind === 'calls') { + // Cross-language calls need a bridge resolver, never a coincidental name. + // C/C++ enum values cannot be invoked (unlike Rust enum constructors). + return candidates.filter(c => sameLanguageFamily(c.language, ref.language) && + !((ref.language === 'c' || ref.language === 'cpp') && + (c.kind === 'enum_member' || (c.kind === 'constant' && /^\s*#\s*define\b/.test(c.signature ?? ''))))); + } if (ref.referenceKind === 'references' || ref.referenceKind === 'function_ref') { return candidates.filter((c) => sameLanguageFamily(c.language, ref.language)); } @@ -685,9 +697,17 @@ function isBareJsCall(ref: UnresolvedRef, context: ResolutionContext): boolean { const LOCAL_BINDING_MEMO = new WeakMap>(); /** - * Whether a JS/TS file binds `name` itself — as a `const`/`let`/`var`/ - * `function`/`class` declaration (destructuring included) or as a parameter - * of a function or arrow. Such a binding shadows every same-named symbol in + * Whether a JS/TS file binds `name` itself — as a plain `const`/`let`/`var`/ + * `function`/`class` declaration or as a parameter of a function or arrow. + * A binding that only re-names a same-named member of something defined + * elsewhere is NOT one: `const { fetchUser } = useStore.getState()` and the + * selector `const setZipUri = useStore((s) => s.setZipUri)` are how a store + * action reaches its caller, and the store-action resolution follows exactly + * those shapes — treating them as local would drop the `loginFlow → fetchUser` + * edge the graph is built to hold. A plain alias with a fallback (`const now = + * opts.now || Date.now`) is still local: on a Kotlin+JS app it otherwise + * landed 24 `now()` calls on a Kotlin test's `private val now`. A definition + * shadows every same-named symbol in * other files, so a bare call to it has no cross-file candidate: the * `resolve` of `new Promise((resolve, reject) => …)`, a spec's * `const transform = await makeTransform()`, a factory's `const now = @@ -711,12 +731,24 @@ function isLocallyBoundJsName(name: string, filePath: string, context: Resolutio // `const { name } = require('./m')` / `= await import('./m')` binds an IMPORT, // not a shadow: the symbol lives in the other file and the call means it. const declRe = new RegExp( - '\\b(?:const|let|var)\\s+(?:' + n + '\\b|[{\\[][^;=]*?\\b' + n + '\\b[^;=]*?[}\\]])\\s*(?:=\\s*([^;\\n]*))?', + '\\b(?:const|let|var)\\s+' + n + '\\b\\s*(?:=\\s*([^;\\n]*))?', 'g' ); let bound = false; + // A selector: an arrow whose body is the same-named member of its own + // argument — `useStore((s) => s.setZipUri)`, `useSelector((st) => st.now)`. + // `() => Date.now()` is not one: the member is not picked off a parameter. + const selector = new RegExp('\\(?\\s*([\\w$]+)\\s*\\)?\\s*=>\\s*[({]?\\s*\\1\\.' + n + '\\b'); for (const m of source.matchAll(declRe)) { - if (!/^\s*(?:await\s+)?(?:require|import)\s*\(/.test(m[1] ?? '')) { bound = true; break; } + const init = m[1] ?? ''; + // `const x = require(…)` is an import; `const setZipUri = useStore((s) => + // s.setZipUri)` picks a same-named member out of something defined + // elsewhere. Neither defines the name — the graph's symbol is what it means. + // A plain alias with a fallback, `const now = opts.now || Date.now`, IS a + // local binding: nothing in the graph is what that call means. + if (/^\s*(?:await\s+)?(?:require|import)\s*\(/.test(init) || selector.test(init)) continue; + bound = true; + break; } if (!bound) { bound = @@ -1067,7 +1099,7 @@ export function resolveMethodOnType( matches = []; for (const m of methodCandidates) { if (m.kind !== 'method') continue; - if (m.language !== ref.language) continue; + if (!sameLanguageFamily(m.language, ref.language)) continue; const qn = m.qualifiedName; if (qn === want || qn.endsWith(`::${want}`)) { matches.push(m); @@ -1666,6 +1698,7 @@ function getInferScanStates(context: ResolutionContext): Map 10_000) return null; + if (ESM_FAMILY.has(ref.language)) { + // Follow an explicit awaited factory return through the normal binding + // resolver; a same-named function elsewhere is not type evidence (#1840). + const awaited = line.match(memoPatterns(`awaited|${escapedReceiver}`, () => [ + new RegExp(String.raw`\b${escapedReceiver}\b\s*=\s*await\s+([A-Za-z_$][\w$]*)\s*\(`), + ])[0]!); + if (awaited?.[1]) { + const target = matchByExactName({ ...ref, referenceName: awaited[1] }, context); + const callee = target && context.getNodeById?.(target.targetNodeId); + const returnType = callee && (callee.returnType ?? callee.signature?.match(/\)\s*:\s*(Promise\s*<[^>]+>)\s*$/)?.[1]); + const promised = returnType?.match(/^Promise\s*<\s*([\w$]+)\s*>$/); + if (promised?.[1]) { + return promised[1]; + } + } + } for (const re of patterns) { const m = line.match(re); if (m && m[1]) { @@ -2187,6 +2236,7 @@ export function matchMethodCall( ? inferCppReceiverType(objectOrClass!, ref, context) : inferLocalReceiverType(objectOrClass!, ref, context)); if (inferredType) { + if (ESM_FAMILY.has(ref.language) && ['string', 'number', 'boolean', 'bigint', 'symbol'].includes(inferredType)) return null; // Java/Kotlin: when two classes share the simple name, the file's import // pins WHICH one (#314). Other languages disambiguate by call-site file. const importedFqn = @@ -2388,7 +2438,7 @@ export function matchMethodCall( // names like permissionEngine → PermissionRuleEngine. if (methodName) { const strat3 = nmTimedT('mc-byname', ref, (): ResolvedRef | null => { - const methodCandidates = context.getNodesByName(methodName!); + const methodCandidates = applyLanguageGate(context.getNodesByName(methodName!), ref); // Ubiquitous-method ceiling (#999): a method name re-declared across a // vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields // K candidates that receiver-word overlap can't reliably disambiguate — @@ -2769,7 +2819,12 @@ function matchStoreAccessorChain(ref: UnresolvedRef, context: ResolutionContext) if (!(inner === 'get' || inner === 'getState' || inner.endsWith('.getState'))) return null; const callables = context .getNodesByName(method) - .filter((n) => (n.kind === 'function' || n.kind === 'method') && sameLanguageFamily(n.language, ref.language) && n.id !== ref.fromNodeId); + .filter((n) => (n.kind === 'function' || n.kind === 'method') && sameLanguageFamily(n.language, ref.language) && n.id !== ref.fromNodeId) + // An interface signature describes the action; it is not a second + // implementation. Keep real class methods in the uniqueness check. + .filter((n) => !(['typescript', 'tsx'].includes(n.language) && n.kind === 'method' && + context.getNodesInFile(n.filePath).some((parent) => + (parent.kind === 'interface' || parent.kind === 'type_alias') && rangeWithin(n, parent)))); if (callables.length !== 1) return null; return { original: ref, targetNodeId: callables[0]!.id, confidence: 0.6, resolvedBy: 'exact-match' }; } @@ -3035,10 +3090,41 @@ export function dumpNameMatcherProfile(label: string): void { } } +function isVerilogSimPath(filePath: string): boolean { + return ( + /(^|\/)(sim|tb|tests?|testbench|dv)\//i.test(filePath) || + /_(stub|tb|sim)\.s?vh?$/i.test(filePath) + ); +} + export function matchReference( ref: UnresolvedRef, context: ResolutionContext ): ResolvedRef | null { + if (isVisibleCppMacro(ref, context)) return null; + if (isCppConstructorRef(ref)) return matchCppConstructor(ref, context); + if (isVerilogWildcardRef(ref)) return matchVerilogWildcard(ref, context, matchReference); + if (isVerilogPortRef(ref)) return matchVerilogPort(ref, context, matchReference); + if (isVerilogMemberRef(ref)) return matchVerilogMember(ref, context); + if (ref.language === 'verilog' && ref.referenceKind === 'instantiates' && !isVerilogSimPath(ref.filePath)) { + const modules = context + .getNodesByName(ref.referenceName) + .filter((n) => n.language === 'verilog' && (n.kind === 'class' || n.kind === 'interface')); + const synth = modules.filter((n) => !isVerilogSimPath(n.filePath)); + if (synth.length > 0 && synth.length < modules.length) { + const best = synth.length === 1 ? synth[0]! : findBestMatch(ref, synth, context); + if (best) { + const proximity = computePathProximity(ref.filePath, best.filePath); + return { + original: ref, + targetNodeId: best.id, + confidence: synth.length === 1 || proximity >= 30 ? 0.7 : 0.4, + resolvedBy: 'exact-match', + }; + } + } + } + // Function-as-value refs (#756) resolve ONLY through the dedicated matcher — // never the fuzzy/qualified fallthrough below (a wrong callback edge is // worse than none). @@ -3149,6 +3235,9 @@ export function matchReference( } } + // No generic fallback can establish an unknown nested receiver's type. + if (isTsJsNestedCall(ref)) return null; + // Try strategies in order of confidence let result: ResolvedRef | null; diff --git a/src/resolution/verilog-call-access.ts b/src/resolution/verilog-call-access.ts new file mode 100644 index 000000000..4faedce82 --- /dev/null +++ b/src/resolution/verilog-call-access.ts @@ -0,0 +1,55 @@ +import { VERILOG_CALL_ARGUMENT_PREFIX, type VerilogCallArgument } from '../extraction/languages/verilog-call-access'; +import type { UnresolvedRef, ResolutionContext, ResolvedRef } from './types'; + +export interface VerilogCallAccess { + access: 'read' | 'write' | 'readwrite'; + callableId: string; + formalId: string; +} + +/** Classify a call actual only after a precise callable and complete formal list are known. */ +export function resolveVerilogCallArgumentAccess( + ref: UnresolvedRef, + context: ResolutionContext, + resolveCallable: (ref: UnresolvedRef, context: ResolutionContext) => ResolvedRef | null, +): VerilogCallAccess | undefined { + const token = ref.candidates?.find(c => c.startsWith(VERILOG_CALL_ARGUMENT_PREFIX)); + if (!token) return undefined; + let info: VerilogCallArgument; + try { info = JSON.parse(token.slice(VERILOG_CALL_ARGUMENT_PREFIX.length)); } catch { return undefined; } + if (!info || typeof info.callee !== 'string' || typeof info.scope !== 'string' || typeof info.callableId !== 'string' + || !Array.isArray(info.slots) || !['value', 'index'].includes(info.role) || typeof info.writeTarget !== 'boolean' + || typeof info.referenceTarget !== 'boolean') return undefined; + const caller = context.getNodeById?.(info.callableId); + if (!caller || caller.filePath !== ref.filePath || caller.language !== 'verilog') return undefined; + const target = resolveCallable({ ...ref, fromNodeId: caller.id, referenceName: info.callee, + referenceKind: 'calls', candidates: [`hdl:scope:${info.scope}`] }, context); + const callable = target && context.getNodeById?.(target.targetNodeId); + if (!callable || callable.language !== 'verilog' || !['function', 'method'].includes(callable.kind)) return undefined; + const formals = context.getNodesInFile(callable.filePath).filter(n => n.decorators?.includes('hdl:formal') + && n.qualifiedName === `${callable.qualifiedName}::${n.name}` && n.startLine >= callable.startLine && n.endLine <= callable.endLine); + const indexed = formals.map(node => ({node, index: Number(node.decorators?.find(d => d.startsWith('hdl:formal-index:'))?.slice('hdl:formal-index:'.length))})).sort((a,b) => a.index-b.index); + if (!indexed.length || indexed.some((f,i) => f.index !== i)) return undefined; + const canonical = (name: string) => name.replace(/^\\/, ''); + if (new Set(indexed.map(f => canonical(f.node.name))).size !== indexed.length) return undefined; + const assigned = new Set(); + let selected: typeof indexed[number] | undefined; + for (const slot of info.slots) { + if (!slot || typeof slot.omitted !== 'boolean') return undefined; + const formal = typeof slot.key === 'number' && Number.isSafeInteger(slot.key) ? indexed[slot.key] + : typeof slot.key === 'string' ? indexed.find(f => canonical(f.node.name) === canonical(slot.key as string)) : undefined; + if (!formal || assigned.has(formal.index)) return undefined; + assigned.add(formal.index); + if (slot.omitted && !formal.node.decorators?.includes('hdl:default')) return undefined; + if (slot.key === info.argument && !slot.omitted) selected = formal; + } + if (!selected || indexed.some(f => !assigned.has(f.index) && !f.node.decorators?.includes('hdl:default'))) return undefined; + const direction = selected.node.decorators?.find(d => d.startsWith('hdl:direction:'))?.slice('hdl:direction:'.length); + let access: VerilogCallAccess['access']; + if (info.role === 'index' || direction === 'input' || direction === 'const-ref') access = 'read'; + else if (!info.writeTarget) return undefined; + else if (direction === 'output') access = 'write'; + else if (direction === 'inout' || (direction === 'ref' && info.referenceTarget)) access = 'readwrite'; + else return undefined; + return {access, callableId: callable.id, formalId: selected.node.id}; +} diff --git a/src/resolution/verilog-members.ts b/src/resolution/verilog-members.ts new file mode 100644 index 000000000..9ba8ae93c --- /dev/null +++ b/src/resolution/verilog-members.ts @@ -0,0 +1,74 @@ +import type { Node } from '../types'; +import { resolveVerilogCallArgumentAccess } from './verilog-call-access'; +import type { ResolvedRef, ResolutionContext, UnresolvedRef } from './types'; + +export function isVerilogMemberRef(ref: UnresolvedRef): boolean { + return ref.language === 'verilog' && (ref.referenceKind === 'calls' || ref.referenceKind === 'type_of' || + (ref.referenceKind === 'references' && (ref.referenceName.includes('::') || ref.referenceName.startsWith('hdl:signal:')))); +} + +/** Resolve only explicit HDL identity or visible lexical/import scope, never a trailing-name guess. */ +export function matchVerilogMember(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + const exact = (name: string, kinds: Node['kind'][]) => context.getNodesByQualifiedName(name) + .filter(n => n.language === 'verilog' && kinds.includes(n.kind)); + const result = (nodes: Node[]): ResolvedRef | null => { + const unique = [...new Map(nodes.map(n => [n.id, n])).values()]; + if (unique.length !== 1) return null; + let metadata: Record | undefined; + if (ref.referenceKind === 'references' && ref.referenceName.startsWith('hdl:signal:')) { + const access = [...new Set((ref.candidates ?? []).filter(c => c.startsWith('hdl:access:')) + .map(c => c.slice('hdl:access:'.length)).filter(c => ['read', 'write', 'readwrite', 'control', 'event'].includes(c)))]; + const argument = access.length ? undefined : resolveVerilogCallArgumentAccess(ref, context, matchVerilogMember); + if (argument) access.push(argument.access); + const events = [...new Set((ref.candidates ?? []).filter(c => c.startsWith('hdl:event:')) + .map(c => c.slice('hdl:event:'.length)).filter(c => ['posedge', 'negedge'].includes(c)))]; + if (access.length) metadata = { hdlAccess: access, ...(events.length ? { hdlEvent: events } : {}), + ...(argument ? { hdlCallTargetId: argument.callableId, hdlFormalId: argument.formalId } : {}) }; + } + return { original: ref, targetNodeId: unique[0]!.id, confidence: 0.95, + resolvedBy: 'qualified-name', ...(metadata ? { metadata } : {}) }; + }; + if (ref.referenceKind === 'references' && ref.referenceName.startsWith('hdl:signal:')) { + const name = ref.referenceName.slice('hdl:signal:'.length); + const caller = context.getNodeById?.(ref.fromNodeId); + const scopes = caller?.qualifiedName.split('::') ?? []; + for (let i = scopes.length; i > 0; i--) { + const matches = exact(`${scopes.slice(0, i).join('::')}::${name}`, ['field', 'variable', 'constant']) + .filter(n => n.filePath === ref.filePath); + if (matches.length) return result(matches); + } + return null; + } + if (ref.referenceKind === 'references') return result(exact(ref.referenceName, ['field', 'variable', 'constant']) + .filter(n => n.filePath === ref.filePath)); + if (ref.referenceKind === 'type_of') return result(exact(ref.referenceName.replace(/\./g, '::'), ['interface', 'class', 'type_alias'])); + if (ref.referenceName.includes('::')) return result(exact(ref.referenceName, ['function', 'method'])); + // Hierarchical instance calls require elaborated receiver identity, which this pass does not infer. + if (ref.referenceName.includes('.')) return null; + const caller = context.getNodeById?.(ref.fromNodeId); + const hint = ref.candidates?.find(c => c.startsWith('hdl:scope:'))?.slice('hdl:scope:'.length); + const lexical = hint && caller && context.getNodesByQualifiedName(hint).some(n => + n.language === 'verilog' && n.filePath === ref.filePath && n.kind === 'namespace' + && n.qualifiedName.startsWith(`${caller.qualifiedName}::`) && n.startLine <= ref.line && n.endLine >= ref.line) + ? hint : caller?.qualifiedName; + const scopes = lexical?.split('::') ?? []; + const imports = context.getNodesInFile(ref.filePath).filter(n => n.kind === 'import' && n.startLine <= ref.line); + // Resolve one lexical level at a time: an inner package import can shadow + // an outer module function, just like a nearer declaration can. + for (let i = scopes.length; i >= 0; i--) { + const scope = scopes.slice(0, i).join('::'); + const matches = exact(scope ? `${scope}::${ref.referenceName}` : ref.referenceName, ['function', 'method']) + .filter(n => n.filePath === ref.filePath); + if (matches.length) return result(matches); + const imported: Node[] = []; + for (const item of imports) { + if (item.qualifiedName.split('::').slice(0, -1).join('::') !== scope) continue; + const match = item.signature?.trim().match(/^([\w$]+)::([\w$]+|\*)$/); + if (match && (match[2] === '*' || match[2] === ref.referenceName)) { + imported.push(...exact(`${match[1]}::${ref.referenceName}`, ['function', 'method'])); + } + } + if (imported.length) return result(imported); + } + return null; +} diff --git a/src/resolution/verilog-ports.ts b/src/resolution/verilog-ports.ts new file mode 100644 index 000000000..d6dbf5fe9 --- /dev/null +++ b/src/resolution/verilog-ports.ts @@ -0,0 +1,93 @@ +import type { Node } from '../types'; +import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types'; + +const PREFIX = 'hdl:port:'; +const POSITION_PREFIX = 'hdl:port-position:'; + +export function isVerilogPortRef(ref: UnresolvedRef): boolean { + return ref.language === 'verilog' && ref.referenceKind === 'references' && (ref.referenceName.startsWith(PREFIX) || ref.referenceName.startsWith(POSITION_PREFIX)); +} + +/** Bind a named source connection to the selected module's own declared port. + * Reuse the instantiation matcher so simulation-file selection cannot diverge. + * This is source correspondence, not an elaborated net or drive-direction edge. + */ +export function matchVerilogPort( + ref: UnresolvedRef, + context: ResolutionContext, + resolveModule: (ref: UnresolvedRef, context: ResolutionContext) => ResolvedRef | null, +): ResolvedRef | null { + const positional = ref.referenceName.startsWith(POSITION_PREFIX); + let parts: unknown; + try { parts = JSON.parse(ref.referenceName.slice(positional ? POSITION_PREFIX.length : PREFIX.length)); } catch { return null; } + if (!Array.isArray(parts)) return null; + let moduleName: string, instanceId: string, portName: string; + let position = -1, count = -1; + if (positional) { + if (parts.length !== 4 || typeof parts[0] !== 'string' || !parts[0] || typeof parts[3] !== 'string' || !parts[3] + || !Number.isSafeInteger(parts[1]) || !Number.isSafeInteger(parts[2]) || parts[1] < 0 || parts[2] <= parts[1]) return null; + [moduleName, position, count, instanceId] = parts as [string, number, number, string]; + portName = ''; + } else { + if (parts.length !== 3 || !parts.every(p => typeof p === 'string' && p.length > 0)) return null; + [moduleName, portName, instanceId] = parts as [string, string, string]; + } + const selected = selectVerilogModule(ref, context, resolveModule, moduleName, instanceId); + if (!selected) return null; + const module = selected.module; + if (positional) { + const tag = module.decorators?.find(d => d.startsWith('hdl:port-order:')); + let order: unknown; + try { order = tag && JSON.parse(tag.slice('hdl:port-order:'.length)); } catch { return null; } + if (!Array.isArray(order) || !order.every(n => typeof n === 'string') || count > order.length + || new Set(order.map(n => n.replace(/^\\/, ''))).size !== order.length) return null; + portName = order[position]!; + } + const canonicalPort = portName.replace(/^\\/, ''); + const names = [canonicalPort, `\\${canonicalPort}`]; + const ports = names.flatMap(name => context.getNodesByQualifiedName(`${module.qualifiedName}::${name}`)) + .filter(n => n.language === 'verilog' && n.filePath === module.filePath && n.kind === 'field' + && n.decorators?.includes('hdl:port') && n.startLine >= module.startLine && n.endLine <= module.endLine); + if (ports.length !== 1) return null; + return { original: ref, targetNodeId: ports[0]!.id, confidence: selected!.confidence, + resolvedBy: 'qualified-name', ...(positional ? { alsoTargets: [{ targetNodeId: module.id, metadata: { binding: 'hdl-positional-dependency', endpoint: 'module' } }] } : {}), + metadata: { binding: positional ? 'hdl-positional-port' : 'hdl-named-port', moduleId: module.id, instanceId, ...(positional ? { position, moduleName } : {}) } }; +} + +/** Shared selection for named, positional and wildcard source bindings. */ +export function selectVerilogModule( + ref: UnresolvedRef, + context: ResolutionContext, + resolveModule: (ref: UnresolvedRef, context: ResolutionContext) => ResolvedRef | null, + moduleName: string, + instanceId: string, +): { module: Node; confidence: number } | null { + const instance = context.getNodeById?.(instanceId); + if (!instance || instance.language !== 'verilog' || instance.kind !== 'variable' || instance.filePath !== ref.filePath) return null; + const selected = resolveModule({ ...ref, fromNodeId: instance.id, referenceName: moduleName, + referenceKind: 'instantiates', line: instance.startLine, column: instance.startColumn }, context); + const module = selected && context.getNodeById?.(selected.targetNodeId); + if (!module || module.name !== moduleName || module.language !== 'verilog' + || !['class', 'interface'].includes(module.kind)) return null; + let candidates = context.getNodesByName(moduleName) + .filter(n => n.language === 'verilog' && ['class', 'interface'].includes(n.kind)); + const simulation = (file: string) => /(^|\/)(sim|tb|tests?|testbench|dv)\//i.test(file) + || /_(stub|tb|sim)\.s?vh?$/i.test(file); + if (!simulation(ref.filePath)) { + const synthesis = candidates.filter(n => !simulation(n.filePath)); + if (synthesis.length) candidates = synthesis; + } + // The module matcher may choose the first equal candidate. Port correspondence + // is stricter: a tie in source-file/directory proximity stays unresolved. + const dirs = ref.filePath.split('/').slice(0, -1); + const score = (file: string): number => { + const other = file.split('/').slice(0, -1); + let shared = 0; + while (shared < Math.min(dirs.length, other.length) && dirs[shared] === other[shared]) shared++; + return (file === ref.filePath ? 100 : 0) + Math.min(shared * 15, 80); + }; + if (candidates.some(n => n.id !== module.id && score(n.filePath) >= score(module.filePath))) return null; + if (context.getNodesByQualifiedName(module.qualifiedName).filter(n => n.filePath === module.filePath + && n.language === 'verilog' && ['class', 'interface'].includes(n.kind)).length !== 1) return null; + return { module, confidence: selected!.confidence }; +} diff --git a/src/resolution/verilog-wildcard.ts b/src/resolution/verilog-wildcard.ts new file mode 100644 index 000000000..fcce42c33 --- /dev/null +++ b/src/resolution/verilog-wildcard.ts @@ -0,0 +1,70 @@ +import type { Node } from '../types'; +import type { ResolutionContext, ResolvedRef, UnresolvedRef } from './types'; +import { selectVerilogModule } from './verilog-ports'; + +const PREFIX = 'hdl:wildcard:'; +export function isVerilogWildcardRef(ref: UnresolvedRef): boolean { + return ref.language === 'verilog' && ref.referenceKind === 'references' && ref.referenceName.startsWith(PREFIX); +} + +/** One source .* clause, with paired endpoints for only unambiguous source names. + * This records source correspondence, not elaborated nets or signal direction. */ +export function matchVerilogWildcard( + ref: UnresolvedRef, + context: ResolutionContext, + resolveModule: (ref: UnresolvedRef, context: ResolutionContext) => ResolvedRef | null, +): ResolvedRef | null { + let parts: unknown; + try { parts = JSON.parse(ref.referenceName.slice(PREFIX.length)); } catch { return null; } + if (!Array.isArray(parts) || parts.length !== 3 || typeof parts[0] !== 'string' || !parts[0] + || typeof parts[1] !== 'string' || !parts[1] || !Array.isArray(parts[2]) || !parts[2].every(n => typeof n === 'string')) return null; + const [moduleName, instanceId, exclusions] = parts as [string, string, string[]]; + const selected = selectVerilogModule(ref, context, resolveModule, moduleName, instanceId); + const instance = context.getNodeById?.(instanceId); + if (!selected || !instance) return null; + const module = selected.module; + const canonical = (name: string) => name.replace(/^\\/, ''); + const excluded = new Set(exclusions.map(canonical)); + const tag = module.decorators?.find(d => d.startsWith('hdl:port-order:')); + let order: unknown; + try { order = tag && JSON.parse(tag.slice('hdl:port-order:'.length)); } catch { order = null; } + // Non-ANSI alias/concatenation headers and unknown macro headers do not prove + // that an internal input declaration is an externally named formal port. + const names: string[] = Array.isArray(order) && order.every(n => typeof n === 'string' && canonical(n)) + && new Set(order.map(canonical)).size === order.length ? order.map(canonical) : []; + const declared = context.getNodesInFile(module.filePath).filter(n => n.language === 'verilog' + && n.kind === 'field' && n.decorators?.includes('hdl:port') + && n.qualifiedName === `${module.qualifiedName}::${n.name}` + && n.startLine >= module.startLine && n.endLine <= module.endLine); + const formals = names.flatMap(name => { + const matches = declared.filter(n => canonical(n.name) === name); + return matches.length === 1 ? matches : []; + }); + const scopes = instance.qualifiedName.split('::').slice(0, -1); + const localSignal = (name: string): Node | null => { + for (let i = scopes.length; i > 0; i--) { + const matches = [name, `\\${name}`].flatMap(n => context.getNodesByQualifiedName(`${scopes.slice(0, i).join('::')}::${n}`)) + .filter(n => n.language === 'verilog' && n.filePath === ref.filePath); + if (!matches.length) continue; + // Any nearer declaration shadows the outer scope; unsupported declarations + // and duplicates must not silently become the outer signal. + return matches.length === 1 && matches[0]!.kind === 'field' + && matches[0]!.decorators?.some(d => d === 'hdl:port' || d === 'hdl:signal') ? matches[0]! : null; + } + return null; + }; + const targets: { targetNodeId: string; metadata: Record }[] = []; + for (const formal of formals) { + const name = canonical(formal.name); + if (excluded.has(name) || formals.filter(n => canonical(n.name) === name).length !== 1) continue; + const local = localSignal(name); + if (!local) continue; + const metadata = { binding: 'hdl-wildcard-port', moduleId: module.id, instanceId, + formalNodeId: formal.id, portName: formal.name }; + targets.push({ targetNodeId: formal.id, metadata: { ...metadata, endpoint: 'formal' } }, + { targetNodeId: local.id, metadata: { ...metadata, endpoint: 'actual' } }); + } + return { original: ref, targetNodeId: module.id, confidence: selected.confidence, + resolvedBy: 'qualified-name', metadata: { binding: 'hdl-wildcard-dependency', endpoint: 'module', moduleId: module.id, instanceId }, + alsoTargets: targets }; +} diff --git a/src/sync/hdl-watch-scope.ts b/src/sync/hdl-watch-scope.ts new file mode 100644 index 000000000..009def098 --- /dev/null +++ b/src/sync/hdl-watch-scope.ts @@ -0,0 +1,47 @@ +import * as path from 'node:path'; +import { loadHdlProfile, isReservedHdlPath, type ActiveHdlProfile } from '../hdl/profile'; + +/** Watch inputs to a selected HDL profile, including files outside the ordinary source filter. + * Retain the last valid scope after a transient deletion/invalid config so a repair is observable. */ +export class HdlWatchScope { + private profile: ActiveHdlProfile | null = null; + private files = new Set(); + private filelists = new Set(); + private includeDirs: string[] = []; + private sourceDirs = new Set(); + private identity = ''; + constructor(private readonly root: string, private readonly getDependencies?: () => string[]) {} + refresh(): boolean { + const loaded = loadHdlProfile(this.root); + if (loaded.status === 'active') this.profile = loaded.profile; + else if (loaded.status === 'none') this.profile = null; + const profile = this.profile; + const files = new Set(profile?.files ?? []); + this.filelists = new Set(profile?.dependencies.map(d => d.path) ?? []); + for (const file of this.filelists) files.add(file); + const dependencies = profile ? this.getDependencies?.() : []; + if (Array.isArray(dependencies)) for (const file of dependencies) { + if (typeof file !== 'string') continue; + const rel = path.relative(this.root, path.resolve(this.root, file)).split(path.sep).join('/'); + if (rel && rel !== '..' && !rel.startsWith('../') && !path.isAbsolute(rel) && !isReservedHdlPath(rel)) files.add(rel); + } + this.files = files; + this.includeDirs = profile?.includeDirs ?? []; + this.sourceDirs = new Set(profile?.files.map(file => path.posix.dirname(file)) ?? []); + const identity = JSON.stringify([[...files].sort(), [...this.filelists].sort(), this.includeDirs, [...this.sourceDirs].sort()]); + const changed = identity !== this.identity; + this.identity = identity; + return changed; + } + isFilelist(rel: string): boolean { return this.filelists.has(rel); } + matchesFile(rel: string): boolean { + if (isReservedHdlPath(rel)) return false; + return this.files.has(rel) || this.sourceDirs.has(path.posix.dirname(rel)) + || this.includeDirs.some(dir => dir === '.' || rel.startsWith(`${dir}/`)); + } + matchesDirectory(rel: string): boolean { + if (!this.profile || isReservedHdlPath(rel)) return false; + return [...this.files].some(file => file.startsWith(`${rel}/`)) + || this.includeDirs.some(dir => dir === '.' || dir === rel || dir.startsWith(`${rel}/`) || rel.startsWith(`${dir}/`)); + } +} diff --git a/src/sync/watcher.ts b/src/sync/watcher.ts index 5ea6436b6..56b2506ce 100644 --- a/src/sync/watcher.ts +++ b/src/sync/watcher.ts @@ -40,6 +40,7 @@ import { logDebug, logWarn } from '../errors'; import { normalizePath } from '../utils'; import { isCodeGraphDataDir } from '../directory'; import { watchDisabledReason } from './watch-policy'; +import { HdlWatchScope } from './hdl-watch-scope'; /** * Number of consecutive lock-contention retries the watcher tolerates before @@ -339,6 +340,7 @@ export class FileWatcher { // scope (#1728). An embedded repo created after start() joins the scope on // the next scope refresh / watcher restart / re-index. private ignoreMatcher: ScopeIgnore | null = null; + private readonly hdlScope: HdlWatchScope; private readonly projectRoot: string; private readonly debounceMs: number; @@ -351,9 +353,11 @@ export class FileWatcher { constructor( projectRoot: string, syncFn: (paths?: string[]) => Promise<{ filesChanged: number; durationMs: number }>, - options: WatchOptions = {} + options: WatchOptions = {}, + getHdlDependencyPaths?: () => string[] ) { this.projectRoot = projectRoot; + this.hdlScope = new HdlWatchScope(projectRoot, getHdlDependencyPaths); this.syncFn = syncFn; this.debounceMs = options.debounceMs ?? 2000; this.onSyncComplete = options.onSyncComplete; @@ -385,6 +389,7 @@ export class FileWatcher { // Reuse the indexer's ignore set so the watcher and indexer agree on scope. this.ignoreMatcher = buildScopeIgnore(this.projectRoot); + this.hdlScope.refresh(); try { if (this.inertForTests) { @@ -474,14 +479,25 @@ export class FileWatcher { * full sync. The initial startup walk passes false (the engine's catch-up * sync owns the baseline). */ - private watchTree(dir: string, markExisting: boolean): void { + private watchTree(dir: string, markExisting: boolean, rescan = false): void { // A degrade() mid-walk (exhaustion on an earlier directory) calls stop(), // which sets `stopped`; bail so the recursion unwinds without adding more // watches to a watcher that is shutting down. `inotifyLimitWarned` does the // same after ENOSPC — the kernel budget is gone, so stop trying the rest of // the tree (every add would fail) while keeping the watches already set. if (this.stopped || this.degradedReason || this.inotifyLimitWarned) return; - if (this.dirWatchers.has(dir)) return; + if (this.dirWatchers.has(dir)) { + // A profile switch can admit a formerly ignored child of an already watched directory. + if (rescan) { + let entries: fs.Dirent[]; + try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; } + for (const entry of entries) if (entry.isDirectory()) { + const child = path.join(dir, entry.name); + if (!this.shouldIgnoreDir(child)) this.watchTree(child, markExisting, true); + } + } + return; + } if (this.dirWatchers.size >= maxDirWatches()) { if (!this.dirCapWarned) { this.dirCapWarned = true; @@ -593,7 +609,12 @@ export class FileWatcher { this.refreshScope(rel); return; } - if (this.ignoreMatcher && this.ignoreMatcher.ignores(rel)) return; + const hdlChange = this.hdlScope.matchesFile(rel) || this.hdlScope.matchesDirectory(rel); + if (this.hdlScope.isFilelist(rel)) { + this.refreshHdlScope(); + this.needsFullScan = true; + } + if (!hdlChange && this.ignoreMatcher && this.ignoreMatcher.ignores(rel)) return; // A nested `.gitignore` (an embedded child repo's own rules, #514, or a // subdirectory rule the git-backed full scan honors) is only a scope // change when it sits INSIDE the current scope — checked after the matcher @@ -604,7 +625,7 @@ export class FileWatcher { this.refreshScope(rel); return; } - if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) { + if (!hdlChange && !isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) { this.maybeScheduleForRemovedDir(rel); return; } @@ -645,10 +666,17 @@ export class FileWatcher { private refreshScope(rel: string): void { logDebug('Scope config changed; rebuilding watcher scope', { file: rel }); this.ignoreMatcher = buildScopeIgnore(this.projectRoot); + this.refreshHdlScope(); this.needsFullScan = true; this.scheduleSync(); } + private refreshHdlScope(): void { + if (this.hdlScope.refresh() && !this.inert && !this.recursiveWatcher && this.dirWatchers.size > 0) { + this.watchTree(this.projectRoot, false, true); + } + } + /** * A deleted DIRECTORY arrives as one event on the directory's own path — * no source extension, so the source-file filter drops it, and the files @@ -713,6 +741,7 @@ export class FileWatcher { const rel = normalizePath(path.relative(this.projectRoot, dirPath)); if (!rel || rel === '.' || rel.startsWith('..')) return false; // root / outside if (this.isAlwaysIgnored(rel)) return true; + if (this.hdlScope.matchesDirectory(rel)) return false; if (!this.ignoreMatcher) return false; return this.ignoreMatcher.ignores(rel + '/'); } @@ -912,6 +941,7 @@ export class FileWatcher { try { const result = await this.syncFn(scoped); + this.refreshHdlScope(); if (!scoped) this.needsFullScan = false; this.lockRetryCount = 0; // a clean sync clears any contention backoff this.syncFailureRetryCount = 0; // ...and any generic-failure backoff diff --git a/src/types.ts b/src/types.ts index 44ffaf4e4..f1365ac48 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,6 +102,7 @@ export const LANGUAGES = [ 'pascal', 'scala', 'lua', + 'verilog', 'luau', 'objc', 'r',