Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Multi-dialect support design

Design for issue #16: selectable SPICE dialects (default HSPICE), retained Ngspice support, a maintainable system for growing syntax/reference knowledge, and reuse of that data for hover (and later completion).

Status: Phase A (dialect switch) and Phase B (reference corpus + hover) implemented.
Related: Dialect reference and net semantics, LSP features, Architecture.


1. Goals and non-goals

Goals

GoalDetail
User-selectable dialectVS Code setting + Command Palette command
Default = HSPICEMatches issue #16; Ngspice remains fully supported
Shared knowledge systemOne corpus drives diagnostics policy, hover, and (later) completion docs
Low-friction authoringAdding a directive/element/rule is mostly data + a test, not scattered Rust strings
Keep current Ngspice behaviorExisting fixtures and diagnostics stay green under ngspice

Non-goals (this design / first implementation slices)

  • Full HSPICE / LTspice grammar parity on day one
  • Scraping simulator manuals at runtime (bash-lsp man / explainshell style)
  • Per-file dialect auto-detection from content (may come later as a hint)
  • Formatter dialect profiles (still v0.4+)
  • Connectivity analysis (still v0.5; dialect-agnostic graph with dialect-specific ground aliases later)

2. Lessons from reference systems

2.1 Ruff (astral-sh/ruff, rules docs)

What they do well

  • Single source of truth next to behavior: rule docs live as structured /// sections on the violation type; cargo dev generate-docs projects them to Markdown for the public site.
  • Registration table + codegen: codes.rs + proc macros produce the Rule enum and metadata accessors so “forgot to register” fails loudly.
  • CI gates: generate-all --mode check and check_docs_formatted.py reject missing sections / stale generated output.
  • Stable IDs + human names: codes and kebab-case names with redirects.

What we should not copy wholesale

  • Embedding long simulator-manual prose in Rust doc comments (wrong medium for SPICE).
  • A giant proc-macro registry for hundreds of rules before we need it.
  • MkDocs Material as a second doc site unless we later publish a public “reference catalog.”

Takeaway for spice-lsp: treat checked-in structured data as the SSOT (like Ruff treats rule metadata), generate indexes / book pages / Rust embeds from it, and CI-check that generated artifacts match.

2.2 bash-language-server (bash-lsp/bash-language-server)

What they do well

  • Layered hover: optional rich external docs (explainshell) → shell help/man → file-local symbol comments.
  • Markdown LSP contract: hover is always MarkupContent markdown.
  • Memoization of expensive doc lookups.
  • Opt-in external services (explainshell off by default).

What we should not copy

  • Runtime man / network scrape as the primary SPICE reference (manuals are not on man, dialects diverge).
  • Detecting a “dialect” (shebang) without switching documentation corpora.
  • Letting external docs replace file-local hover instead of stacking with it.

Takeaway for spice-lsp: keep a priority chain for hover (reference corpus → file-local CST → nothing), cache the corpus at startup, never require an external service for basic tips.


3. Product behavior

3.1 Dialects

IdLabelInitial role
hspiceHSPICEDefault
ngspiceNgspiceCurrent parser/diagnostics baseline; keep working
ltspiceLTspiceStub corpus + setting value; grammar/rules grow later

Unknown dialect values → error diagnostic on initialize / config change, fall back to hspice with a logged warning.

3.2 How the user chooses

  1. Setting: spiceLsp.dialect — enum hspice | ngspice | ltspice, default hspice.
  2. Command: SPICE LSP: Set Dialect… — QuickPick; writes the setting (workspace if a folder is open, else user) and restarts / notifies the server.
  3. Status bar (recommended in the same slice): show current dialect; click opens the QuickPick.

Optional later (not required for #16):

  • # spice-lsp dialect=hspice file header / .spice-lsp.toml
  • Infer from path heuristics (*.sp in an HSPICE tree) as a suggestion only

3.3 Client ↔ server contract

initialize.initializationOptions.dialect  →  "hspice" | "ngspice" | "ltspice"
workspace/didChangeConfiguration          →  spiceLsp.dialect

Extension always sends the resolved dialect on start and on change. Server stores it per-session (workspace-wide for v1; per-document overrides later).

Changing dialect:

  1. Re-analyze all open documents with the new dialect profile.
  2. Republish diagnostics.
  3. Clear hover/completion caches keyed by dialect.

4. Architecture: one corpus, many consumers

                    ┌─────────────────────────────────────┐
                    │  reference/  (SSOT, authored data)  │
                    │  schema + per-dialect entries       │
                    └──────────────┬──────────────────────┘
                                   │
              pixi run reference-codegen / validate
                                   │
           ┌───────────────────────┼───────────────────────┐
           ▼                       ▼                       ▼
   spice-reference crate    docs book pages         CI snapshots
   (embedded index)         (optional catalog)      (hover / schema)
           │
           ▼
   spice-parser  ←── DialectProfile (syntax flags, comment rules, …)
           │
           ▼
   spice-lsp  (diagnostics, hover, later completion)
           │
           ▼
   VS Code extension (setting, command, status bar)

Principle: Rust implements mechanisms (parse, index, lookup, render). Humans author knowledge as data under reference/. Parser dialect quirks that cannot be expressed as data yet live in a small DialectProfile table in Rust, keyed by the same dialect ids.


5. Reference corpus (Ruff-inspired authoring)

5.1 Layout

Evolve the plan in § Dialect reference with an explicit shared + override model:

reference/
├── schema.json                 # JSON Schema for entries
├── _shared/                    # constructs common across dialects
│   ├── directives/
│   │   └── subckt.json
│   └── elements/
│       └── R.json
├── hspice/
│   ├── dialect.toml            # metadata: display name, aliases, comment styles
│   ├── directives/
│   │   └── option.json         # HSPICE-specific or override
│   └── elements/
├── ngspice/
│   ├── dialect.toml
│   └── …
└── ltspice/
    ├── dialect.toml
    └── …

5.2 Entry shape (v1)

{
  "id": "hspice.directive.tran",
  "kind": "directive",
  "name": ".tran",
  "summary": "Transient analysis",
  "syntax": ".TRAN tstep tstop [tstart [tmax]] [UIC]",
  "parameters": [
    { "name": "tstep", "description": "Printing / sampling step.", "units": "s" }
  ],
  "examples": [".TRAN 1p 10n"],
  "seeAlso": ["hspice.directive.option"],
  "diagnostics": ["spice/unknown-directive"],
  "since": "0.3.0",
  "dialectNotes": "HSPICE accepts …"
}

Required sections (CI-enforced, Ruff-style): id, kind, name, summary, syntax.
Optional: parameters, examples, seeAlso, diagnostics, deprecated, dialectNotes.

5.3 Merge rules

  1. Load _shared/ as base for the active dialect.
  2. Overlay reference/<dialect>/ by id / (kind, name) — dialect file wins.
  3. Missing entry → no hover / no completion doc (not an error). Gaps are filled by adding JSON.

5.4 Codegen / validation tasks

TaskPurpose
pixi run reference-validateLoad embedded corpus; unit tests for merge/lookup
pixi run reference-docsWrite mdBook pages under docs/reference/ from the corpus
pixi run reference-docs-checkCI: fail if catalog markdown drifts from JSON

Authoring workflow (add a new directive):

  1. Add/edit reference/<dialect>/directives/foo.json (or _shared/ if universal).
  2. pixi run reference-validate.
  3. pixi run reference-docs (regenerate catalog chapters).
  4. Add hover snapshot fixture under test-data/hover/<dialect>/ when needed.
  5. pixi run test.
  6. No Rust change unless a new kind or lookup path is needed.

This is the spice-lsp analogue of Ruff’s “add rule → docs fall out of metadata,” with JSON as the authoring surface instead of /// comments.


6. DialectProfile (syntax / semantics knobs)

Until grammars fully diverge, keep a Rust profile beside the corpus:

#![allow(unused)]
fn main() {
struct DialectProfile {
    id: DialectId,
    // Comments recognized for toggle / highlighting hints
    line_comment_prefixes: &'static [&'static str], // e.g. hspice: ["*"], ngspice: ["*", ";", "$"]
    // Directives treated as unknown → warning vs ignore
    unknown_directive_severity: Severity,
    // Element letter sets, continuation rules, case sensitivity, …
}
}

v1 behavior

Concernhspicengspiceltspice
Parse grammarCurrent line-oriented grammar (shared)SameSame
Comment styles (docs / future toggle)* primary*, ;, $$ / * (document; refine later)
Semantic diagnosticsSame engines; corpus may gate “unknown directive” listsCurrent fixturesMinimal
Hoverhspice corpus (+ _shared)ngspice corpusltspice stub

Later: dialect-specific Tree-sitter grammars or grammar injections only when shared tokens are insufficient (do not fork three full grammars prematurely).


7. Hover design (bash-lsp layering + corpus)

7.1 Resolution order

cursor token
  1. dialect reference lookup (kind + name + active dialect)
  2. file-local hover (subckt pins, in-file .model / .param)     // v0.3 slice
  3. null

Never call out to the network. Render markdown:

### `.tran` — Transient analysis
**Dialect:** HSPICE

.TRAN tstep tstop [tstart [tmax]] [UIC]


| Parameter | Description | Units |
|-----------|-------------|-------|
| tstep | … | s |

**Examples**
- `.TRAN 1p 10n`

7.2 Mapping cursor → entry

  1. Classify line / token: directive name, element type letter, .option keyword, etc. (reuse / extend CST + symbol index).
  2. Build key (dialect, kind, normalized_name).
  3. Lookup in embedded index; try dialect overlay then _shared.

7.3 Same data for completion (follow-on)

Completion items attach documentation from the same entry. No parallel doc strings in Rust.


8. VS Code extension changes

ItemChange
package.json settingsspiceLsp.dialect enum, default hspice
CommandspiceLsp.setDialect → QuickPick
Status barHSPICE / Ngspice / LTspice
LanguageClient initinitializationOptions: { dialect }
Middleware / config listenerOn dialect change → DidChangeConfiguration + optional restart if needed
Marketplace READMEDocument default HSPICE; how to switch to Ngspice

TextMate grammar stays shared initially; dialect-specific highlighting can wait.


9. Phased delivery

Phase A — Dialect switch plumbing (unblocks #16 UX)

  • Setting + command + status bar; default hspice.
  • Server accepts dialect; re-analyzes on change.
  • DialectProfile stub; behavior still matches today’s Ngspice parser for both hspice and ngspice except profile metadata / empty HSPICE corpus.
  • Docs: default dialect, how to switch.

Exit criteria: user can switch dialects; Ngspice fixtures pass with spiceLsp.dialect=ngspice; HSPICE default does not break open/diagnostics smoke tests.

Phase B — Reference crate + hover

  • Stand up reference/ schema + _shared + starter hspice / ngspice entries (small set: .subckt, .ends, .model, .param, .tran, R, C, X).
  • Grow HSPICE overlays for analysis/control directives requested in follow-ups (.data, multi-mode .dc, .op, plus .ac / .measure / .probe / .lib / …).
  • spice-reference crate + validate/codegen pixi tasks.
  • Implement textDocument/hover with layered resolution.
  • Snapshot tests per dialect.

Phase C — Dialect-sensitive diagnostics / grammar

  • Unknown-directive / option lists from corpus.
  • Comment / continuation profile differences.
  • Split grammar only where needed; grow LTspice.

Phase D — Catalog docs

  • Generate mdBook pages under docs/reference/ from the embedded corpus (spice-reference-catalog).
  • Pixi: reference-docs (write) and reference-docs-check (CI drift guard).
  • Still one SSOT: edit JSON under reference/, then regenerate.

Issue #16 is satisfied by Phase A + a clear path through B; B can ship in the same epic as follow-up PRs.


10. Testing strategy

LayerTests
SchemaEvery JSON entry validates; required sections present
MergeOverlay wins; shared fallback works
LSPinitialize with dialect; didChangeConfiguration republishes
HoverFixtures per dialect; missing entry → null
RegressionExisting Ngspice stdio tests run with explicit ngspice
ExtensionSetting default is hspice; command updates config (smoke / manual)

11. Risks and decisions

TopicDecision
Default dialectHSPICE per #16 (overrides earlier docs that said Ngspice default)
One grammar vs manyOne shared grammar in Phase A–B; profile flags first
Doc authoring mediumJSON under reference/, not Rust comments
External doc servicesOut of scope; optional later, opt-in only
LTspiceEnum + stub corpus early; deep support later
Breaking changeDefault dialect change may surprise Ngspice users — document prominently; one-click switch

12. Open questions (resolve during Phase A implementation)

  1. Should dialect be workspace-only or allow per-file override in v1?
  2. Do we embed the full corpus in the binary, or load from an extension-relative path for faster iteration?
  3. Minimum HSPICE starter set for Phase B (which directives matter first for the author’s flows)?
  4. Status bar vs Command Palette only for v1 UX?

13. Implementation checklist (when coding starts)

  • spiceLsp.dialect + spiceLsp.setDialect + status bar
  • Server session dialect + config update path
  • DialectProfile + Ngspice parity tests under ngspice
  • Update LSP features, limitations, Marketplace README for default HSPICE
  • Scaffold reference/schema.json, _shared/, hspice/, ngspice/
  • Expand HSPICE corpus: .data, .dc (sweep modes), .op, plus common controls (.ac, .measure, .probe, .lib, …)
  • spice-reference + validate/codegen tasks
  • Hover provider + snapshots
  • Close #16 when Phase A is shipped and Phase B is scheduled/linked
  • Phase C: dialect-sensitive diagnostics / grammar splits
  • Phase D: catalog docs from JSON (docs/reference/, reference-docs / reference-docs-check)

14. References

  • Issue: https://github.com/amirhosseindavoody/spice-lsp/issues/16
  • Ruff rules: https://docs.astral.sh/ruff/rules/
  • Ruff repo (docs codegen): crates/ruff_dev/src/generate_docs.rs, scripts/generate_mkdocs.py, CONTRIBUTING.md (“Adding a new rule”)
  • bash-language-server hover: server/src/server.ts, server/src/util/sh.ts, server/src/analyser.ts (explainshell)
  • Existing spice-lsp plan: Dialect reference and net semantics