

# Silk Language Server (LSP)

This document specifies the current Language Server Protocol (LSP)
implementation for Silk.

The goal of the language server is to provide editor and IDE integrations
(diagnostics, hover, go-to-definition, references, rename, completion,
signature help, semantic tokens, inlay hints, document symbols, and future
tooling extensions) while remaining a thin, spec-driven wrapper around the
existing compiler front-end.

## Overview

The Silk language server:

- is implemented in Zig and shipped as a separate executable (`silk-lsp`),
- speaks the Language Server Protocol over stdin/stdout using JSON-RPC 2.0 and `Content-Length` framing,
- reuses the existing lexer, parser, and type checker for semantics,
- builds a workspace cache from open documents, nearest-package
 `silk.toml` roots, dependency package graphs, manifest definition files, and
 standard library modules when enabled,
- maintains a lightweight native C symbol index for manifest-owned `.c` / `.h` / `.m`
 sources so `ext` declarations can resolve to local native definitions when
 those sources are available,
- does not change the language surface or ABI; it is a tooling layer on top of the existing compiler.

No language features or CLI options are introduced by the LSP itself. Any future extensions that affect language semantics or user-facing flags must still be documented in the appropriate `docs/language/` or [cli silk](/silk/docs/compiler/cli-silk/) files first.

## Running the Server

The `silk-lsp` binary is built and installed alongside the `silk` CLI:

- `zig build install` (or the project’s preferred build wrapper) will install both `silk` and `silk-lsp` into the configured prefix.
- Editor and IDE integrations should launch `silk-lsp` as a stdio-based LSP server, without extra arguments, and then speak JSON-RPC 2.0 over its stdin/stdout.
- The server writes protocol messages to stdout and may emit diagnostic logs to stderr; LSP clients must not treat stderr as protocol traffic.
- Optional flags:
 - `--std-root <path>` overrides the stdlib root used for resolving `import std::...;`.
 - `--std <path>` is an accepted alias of `--std-root <path>`.
 - `--nostd` disables stdlib auto-loading entirely.
 - `-h` / `--help` prints the current usage text.

Typical client configurations (e.g., Vim/Neovim LSP, VS Code, or other LSP frontends) should:

- set the command to `["silk-lsp"]`,
- enable standard LSP text document synchronization,
- rely on the capabilities advertised in `initialize`; the current request/notification surface includes diagnostics, hover, definition, references, rename, completion, formatting, signature help, semantic tokens, inlay hints, document symbols, shutdown, and exit.

## Transport and Protocol

The language server:

- reads requests from standard input using the LSP message framing (`Content-Length: <n>\r\n\r\n<json>`),
- writes responses and server-initiated notifications to standard output using the same framing,
- implements JSON-RPC 2.0 semantics (`jsonrpc: "2.0"`, `id`, `method`, `params`, `result` / `error`).

The server does not depend on any external networking libraries; it uses Zig standard library I/O and JSON support.

Position handling note:

- The lexer tracks byte offsets and byte-based columns.
- The LSP layer maps between byte columns and LSP `utf-16` character positions.
- Clients should treat returned `line`/`character` values as LSP positions in UTF-16.
- If a request uses a character position beyond the end of a line, the server clamps it to the line end when locating tokens.

## Initialization

The server supports the standard LSP initialization sequence:

- `initialize` (request):
 - Advertised capabilities:
 - `positionEncoding`: `"utf-16"` (the server currently operates in UTF-16 positions for maximum client compatibility).
 - `textDocumentSync`:
 - `openClose: true`,
 - `change: 1` (Full document sync),
 - `save: { includeText: false }`.
 - `hoverProvider: true` (semantic hover on literals, declarations, imports, and native `ext` targets as described below).
 - `definitionProvider: true` (definition lookups backed by the module-set symbol index; see below).
 - `referencesProvider: true` (reference queries across the current cached module set; see below).
 - `renameProvider: true` (identifier renames across the current cached module set; see below).
 - `documentFormattingProvider: true` (whole-document formatting via the canonical `silk format` formatter; see below).
 - `documentSymbolProvider: true` (hierarchical document symbols for declarations and nested bodies as described below).
 - `semanticTokensProvider`:
 - legend-driven `full` semantic token responses,
 - token types include namespaces, types, enums, interfaces, structs, parameters, variables, properties, enum members, functions, methods, keywords, comments, strings, numbers, and operators.
 - `inlayHintProvider: true` (type inlay hints for currently supported local bindings; see below).
 - `completionProvider`:
 - does not support resolve,
 - advertises trigger characters `.`, `:`, `{`, `,`, `"`, `` ` ``, and `/`,
 - offers keyword, identifier, import-specifier, and symbol-aware suggestions as described below.
 - `signatureHelpProvider`:
 - trigger characters `(` and `,`,
 - provides function and method signatures for the current call.
- The server uses `rootUri` (or `rootPath`) to help locate a stdlib root when no explicit `--std-root` or `SILK_STD_ROOT` is set.
- `initialized` (notification):
 - Accepted but does not currently trigger additional behavior.
- `shutdown` (request) and `exit` (notification) are honored as in the LSP spec.
- Requests received after `shutdown` (other than `exit`) are treated as invalid and answered with an error response.
- `$\/cancelRequest` notifications are accepted and ignored; the initial server does not track per-request cancellation state.

Any additional capabilities beyond this documented set must be documented here before being implemented.

## Hover

The server provides `textDocument/hover` for open documents using the cached
workspace module set.

- Hover requests are handled for the current contents of an open document (as tracked in the server’s in-memory document table).
- The server computes hover information lexically, based on the token at the given position:
 - integer literals are reported as “int literal”,
 - floating-point literals as “float literal”,
 - boolean literals (`true`/`false`) as “bool literal”,
 - string and character literals as “string literal” and “char literal”,
 - the context-sensitive keyword forms `panic`, `await`, `await *`, `yield`,
 `yield *`, `sizeof`, and `as raw` as Markdown usage help, including
 hovering either token in the two-token `*` and `as raw` forms,
 - identifiers are reported as `identifier 'name'`.
- Hover now includes lightweight semantic hints:
 - function identifiers show their `fn name (...) -> result` signature when available,
 - `let` bindings show the declared (or literal-inferred) type when available,
 - struct / enum / interface / error identifiers report `struct Name` / `enum Name` / `interface Name` / `error Name`,
 - `ext` declarations report `ext name: <type>` when available,
 - field and method accesses (`value.field`, `value.method`) report the field type for known struct or error receivers, and report method signatures for known struct receivers,
 - chained field receivers (`box.value.field`) are resolved by walking the known struct/error field path, including applied generic structs where direct field type parameters can be substituted before rendering the hover type,
 - imported names are resolved across the module set:
 - package imports (`import ns::pkg;`, `import ns::pkg as alias;`),
 - qualified symbol imports (`import ns::pkg::name;`, `import ns::pkg::name as alias;`, `import ::malloc;`),
 - JS-style imports (`import { name } from "dep/path";`, `import { name as alias } from "dep/path";`, `import alias from ns::pkg;`),
 - and module-scope `using` aliases for imported or local names,
 - when an `ext` declaration resolves to a locally indexed native C symbol, hover includes the native C declaration/prototype in an additional `c` code block,
 - native C lookup is filtered by the `ext` shape (`fn` / `c_fn` externs prefer C functions; non-function externs prefer C variables), so common C tag/function collisions like `struct stat` vs `stat(...)` do not override the callable symbol.
- When the resolved declaration has a doc comment, hover renders it as Markdown:
 - the first block is a `silk` code block containing the signature/header,
 - followed by the rendered doc comment body.
- The hover `range` returned to the client corresponds to the token span (same token line/column/length used for diagnostics); when no suitable token is found at the requested position, the server returns `null` as the hover result.

The native C index is intentionally lexical and top-level only; it is not a
full C parser. It is used to surface nearby declarations/definitions in
manifest-owned C sources, not to typecheck arbitrary C. It tolerates leading
indentation before preprocessor directives so common headers with indented
`#include` / `#define` lines still index the following declarations correctly.

## Go To Definition

The server provides `textDocument/definition` for open documents.

- Definition requests are handled for the current contents of an open document.
- The server first consults the module-set symbol index (open docs + manifest
 package graphs + definition files + std modules when enabled) to resolve:
 - exported or package-local `fn`, `let`, `ext`, `struct`, `enum`, `interface`, and `error` declarations,
 - methods declared in `impl` blocks when invoked as `value.method(...)`,
 - qualified names such as `std::pkg::name` and namespace-qualified names like `alias::name` or `alias::child::name` when `alias` is introduced by:
 - a package import,
 - a default/namespace import,
 - or a module-scope `using` alias.
- Package and import resolution covers:
 - package imports (`import ns::pkg;`, `import ns::pkg as alias;`),
 - qualified symbol imports (`import ns::pkg::name;`, `import ns::pkg::name as alias;`, `import ::name;`),
 - JS-style imports from package specifiers and file specifiers,
 - and `using` aliases for both value names and namespace aliases.
- Local scopes are then consulted to resolve:
 - function parameters,
 - block-scoped `let` bindings,
 - destructuring `let` pattern binders,
 - `let ... else` binders after the statement succeeds,
 - `if let` and `while let` pattern binders, including chained `&& let`
 clauses within the active body,
 - C-style `for` initializer bindings within the loop,
 - [`match`](/silk/wiki/language/flow-match/)-statement binders within the selected arm body.
- Member access (`value.field` / `value.method`) uses the heuristic receiver-type resolver:
 - receiver expressions may be direct locals/types or chained field paths such as `box.value.field`,
 - applied generic struct receivers substitute direct type-parameter fields before resolving the next field in the path,
 - when a struct method is found, the definition points at the `impl` method declaration,
 - when a struct or error field is found, the definition points at that field declaration,
 - otherwise the definition falls back to the receiver struct or error declaration.
- Constructor calls (`new Type(...)`, including namespace/import-qualified `new pkg::Type(...)`) resolve to the public/exported `fn constructor` overload selected by the callsite argument count, using the same arity ranking as the compiler for exact matches, defaulted trailing parameters, and trailing varargs. Empty allocation literals (`new Type{}`) similarly resolve through the zero-argument constructor ranking. If the call shape is incomplete, unmatched, hidden by visibility, or still ambiguous, the server falls back to the `struct Type` declaration.
- When a resolved `ext` declaration has a matching native C symbol in a
 manifest-owned source/header file, go-to-definition prefers the C definition
 (or declaration if no definition is present locally) over the Silk `ext`
 declaration, using the same function-vs-variable matching rule as hover.
- If symbol resolution fails, the server falls back to a lexical scan of the current file for the first matching `let`/`fn`/`ext`/`struct`/`enum`/`interface`/`error` declaration.

Known limitations in this initial support:

- local block scopes and shadowing are modeled for statement-level binders, but
 match-expression binders and full control-flow refinement are not yet a
 complete semantic scope tree,
- ambiguous names across multiple imports are not disambiguated; the first match wins,
- cross-file results are limited to declarations present in the current module
 set (open docs, manifest-discovered package files, and std modules when
 enabled).

Manifest-aware cache rebuild note:

- When a nearest-package graph cannot be loaded (for example because a
 dependency is missing or a manifest hash/version check fails), `silk-lsp`
 emits a concise stderr warning describing the failed package root and the
 manifest diagnostic it received, rather than silently dropping package
 indexing.

## Completion

The server provides `textDocument/completion` using the same cached workspace
module set.

- Completion items are offered for:
 - all language keywords defined in [`src/token.zig`](https://github.com/oro-computer/silk/blob/master/src/token.zig) (via `keywordTable()`),
 tagged with LSP keyword kind,
 - guided keyword-help completions for `panic`, `await`, `await *`, `yield`,
 `yield *`, `sizeof`, `#embed`, and `as raw`, with compact detail strings
 and Markdown documentation for the typed-error, Promise, Task, byte-size,
 compile-time file embedding, and raw-cast usage contracts,
 - all distinct identifiers lexed from the current document (names that are not recognized as keywords),
 - symbol-aware suggestions from the current package and imported packages (functions, lets, ext, structs, enums, interfaces, errors),
 - imported names from:
 - package imports and qualified symbol imports,
 - JS-style named/default imports from file or package specifiers,
 - and module-scope `using` aliases,
 - unqualified local-package and named imported functions in
 statement-position buffers that are temporarily incomplete while typing
 (for example before `(` or `;` has been inserted),
 - import specifier path completion inside `from "..."` strings:
 - relative file specifiers (`"./..."` and `"../..."`) suggest `.slk` files and subdirectories,
 - std package specifiers (`"std/..."`) suggest stdlib package paths (omitting the `.slk` extension),
 - unquoted package-path completion inside import specifiers, including
 package imports such as `import oro::logger;` and JS-style imports such
 as `import logger from oro::logger;`,
 - named-import member completion from unquoted package paths, such as
 `import { format_ } from std::fmt;`,
 - `#embed("path", "...")` encoding completion in the optional second
 argument, offering exactly `"utf8"`, `"utf16"`, `"u8"`, `"u16"`, and
 `"u32"`; omitting the argument is equivalent to `"utf8"`,
 - namespace completions after `::` for known packages, package-import aliases, default/namespace imports, and `using`-introduced namespace aliases, including nested alias chains such as `rt::mem::`,
 - member completions for struct and error fields after `.` when the receiver type is known (including locals with type annotations, struct/error literals, casts, or `new Type(...)` initializers); struct receivers also include instance methods,
 - bare member-trigger completions such as `manager.` by parsing a transient completion probe in memory and returning member results instead of falling back to global lexical keywords,
 - static impl method completions after a type receiver such as `User.`, limited to `impl User` functions without an explicit `self` receiver while value receivers keep instance fields and methods,
 - receiver completions after local bindings initialized from static impl calls, such as `let user = User.create(options); user.`, by using the static impl function result type,
 - array element receiver completions such as `inputs[0].` when `inputs` has an array or slice type annotation,
 - `for item in items { item. }` completions when `items` has an array or slice type annotation,
 - `for item in manager.iter() { item. }` completions when the iterator-returning call result, local type aliases, package/file imports, and `next() -> T?` element type can be resolved through the cached symbol index or transient parse overlay,
 - local completion and receiver inference for statement-level language
 binders, including destructuring `let` patterns, `let ... else`, `if let`
 / `while let` and chained `&& let` clauses, [`match`](/silk/wiki/language/flow-match/) statement arms, and
 C-style `for` initializers,
 - field/method completions for safe pattern payloads when the scrutinee type
 is known, including optional `Some(value)` payloads, generic
 `Result(T, E)`-style `Ok(value)` / `Err(error)` payloads, struct
 destructuring fields, and array destructuring elements,
 - chained field completions such as `box.value.` by walking known
 struct/error receiver fields without invoking the full type checker,
 - applied generic receiver completions such as `b.value.` where
 `b: Box(Inner)` by substituting direct struct type parameters (`T`) into
 field types before resolving the next field,
 - struct- and error-literal field suggestions in `Type { ... }` expressions,
 including `panic Error { ... }` payloads, when the cursor is in a
 field-name position (before the `:`); this contextual completion remains
 limited to record fields while the entry is incomplete, returns field-kind
 items with `field: Type` detail/documentation, inserts the canonical
 `field: ` initializer prefix through `textEdit`, suppresses fields already
 initialized in the current literal, and supplies `filterText` so editors
 can keep showing field choices even when the partial token does not
 prefix-match a field label.
- Currently:
 - returns completion items with `label`, `kind`, and `detail` populated when symbol data is available,
 - attaches Markdown documentation for functions, methods, structs, and documented `let` bindings when doc comments are available, falling back to a compact signature/type preview,
 - filters results by the identifier prefix immediately preceding the cursor position,
 - uses a heuristic symbol index built from the module set (open docs + imports + std modules).
- When the current buffer is syntactically incomplete, ordinary identifier
 completion may parse a transient in-memory probe before falling back to
 lexical identifiers. If a lexical identifier is later resolved to a symbol,
 the server promotes that item with the resolved kind, detail, and
 documentation. If the symbol overlay still cannot represent the current
 file, lexical `fn name` declarations are still promoted to function
 completion items so local functions remain visible.
- Scope precision is still limited:
 - receiver type inference is heuristic (it is not a full typechecker),
 - match-expression binders and full control-flow refinements are not yet
 represented in completion,
 - cross-file results are limited to declarations present in the current module set (open docs + manifest/package import closure + std modules when enabled).

As richer front-end support becomes available, completion may be extended to:

- filter suggestions by lexical/semantic scope,
- distinguish between functions, types, variables, and other symbol kinds,
- surface standard library symbols by consulting the resolver.

## Signature Help

The server provides a minimal implementation of `textDocument/signatureHelp`:

- Signature help is computed for the innermost call expression at the cursor.
- The server supports:
 - direct calls to named functions (`foo(...)`),
 - qualified calls (`std::pkg::foo(...)` and `alias::foo(...)` for namespace imports),
 - method calls (`value.method(...)`) when the receiver resolves to a known struct type.
- constructor calls via heap allocation syntax (`new Type(...)`), which resolves to the `constructor` overload set defined in `impl Type { ... }`.
- calls via named imports and default-imported default exports (`import { f as g } from "..."; g(...)`, `import g from "./mod.slk"; g(...)`) when resolvable.
- When the current buffer is temporarily incomplete after typing `(` or `,`,
 signature help parses a transient in-memory call probe and overlays the
 current document's symbols on the workspace cache, so local, named-imported,
 default-imported, and namespace-qualified function signatures remain
 available while the user is still typing the argument list.
- Active parameter selection is based on delimiter-aware comma counting in the current call. Explicit generic-call arguments before `;` are excluded from the value-argument count, and commas inside nested calls, array literals, and aggregate literals are ignored for the outer call.
- Signature labels follow the Silk surface syntax (e.g. `fn foo (a: int, b: int) -> int`).
- When doc comments are available:
 - `SignatureInformation.documentation` is populated with rendered Markdown from the doc comment,
 - `SignatureParameter.documentation` is populated from `@param` entries when present.

For constructor overload sets, the server returns public/exported signatures and selects an active signature using delimiter-aware value-argument heuristics, with exact arity preferred over defaulted trailing parameters and varargs. The implicit receiver parameter (`mut self: &Type`) is not shown in the signature parameters for `new Type(...)`.

Signature help is heuristic and will become richer as the front-end’s symbol tables evolve. Some clients may request signature help even before `(` is typed; the server will attempt to resolve the identifier under the cursor as a callee in that case.

## Document Symbols

The server provides `textDocument/documentSymbol` for open documents.

- Document symbols are produced hierarchically from the parsed AST:
 - top-level `fn`, `let`, `struct`, `enum`, `error`, `interface`, `ext`, `impl`, `test`, `using`, and inline-module declarations are reported,
 - struct / enum / error / interface children include fields, variants, and methods where applicable,
 - `impl` declarations nest their methods,
 - functions, tests, loops, [`match`](/silk/wiki/language/flow-match/) arms, and similar nested bodies surface supported local declarations and binders beneath their containing symbol.
- Implementation details:
 - ranges are token- or body-based spans derived from the parsed declaration/block structure,
 - `selectionRange` tracks the declaration identifier token,
 - nested coverage is intentionally selective: supported locals and binders are surfaced, but this is not yet a perfect semantic scope tree for every binder form.
- Symbol kinds:
 - functions are reported using the LSP `Function` kind (numeric value `12`),
 - `let` bindings are reported using the `Variable` kind (numeric value `13`),
 - `struct` declarations use `Struct` (numeric value `23`),
 - `enum` declarations use `Enum` (numeric value `10`),
 - `error` declarations use `Struct` (numeric value `23`),
 - `interface` declarations use `Interface` (numeric value `11`),
 - `ext` declarations use `Function` (numeric value `12`),
 - `impl` declarations use `Namespace` (numeric value `3`).

## References and Rename

The server provides `textDocument/references` and `textDocument/rename` for
identifiers that resolve within the current cached module set.

- Reference lookups:
 - resolve the identifier under the cursor to its declaration using the same module-set-aware navigation used by go-to-definition,
 - scan the cached module set for matching identifier occurrences that resolve back to that same declaration,
 - optionally include the declaration itself when the client sets `context.includeDeclaration`.
- Rename:
 - uses the same declaration-resolution and reference collection path,
 - returns a `WorkspaceEdit.documentChanges` payload with per-document text edits,
 - rejects invalid Silk identifier spellings (`newName` must be a valid identifier).

Current boundaries:

- results are limited to the cached module set (open docs, manifest/package/file-import closure, and std modules when enabled),
- rename is identifier-based and does not yet attempt broader semantic refactors outside that cached symbol graph.

## Semantic Tokens

The server provides `textDocument/semanticTokens/full`.

- The implementation combines:
 - lexical token classification for keywords, comments, strings, numbers, and operators,
 - AST-backed semantic marking for declaration and binder tokens such as namespaces, types, structs, enums, interfaces, functions, methods, parameters, variables, properties, and enum members.
- This gives editors a stronger semantic surface than plain TextMate tokenization while still reusing the existing parser/module cache.

Limitations:

- token classification is still declaration/binder-oriented rather than a full resolver/type-driven classification for every identifier use,
- token modifiers are not yet emitted.

## Inlay Hints

The server provides `textDocument/inlayHint`.

- The implementation emits type hints for:
 - unannotated `let` bindings when the initializer’s type is inferable from the current tooling subset,
 - `for` binders over inferable arrays, slices, and ranges,
 - unannotated C-style `for` initializer bindings with the same inferable-type rule.
- Hints are range-aware: the server only returns hints whose insertion positions fall inside the requested LSP range.

Limitations:

- only a narrow type-hint subset is emitted today,
- parameter-name hints and broader expression/result hints are not yet implemented.

## Formatting

The server provides `textDocument/formatting` for open documents.

- Formatting uses the same canonical source formatter as `silk format`.
- The response is whole-document oriented:
 - if the open document is already formatted, the server returns an empty edit list,
 - otherwise it returns one full-range `TextEdit` that replaces the document text with the formatted text.
- Newline-based `if` / `else if` headers use the same canonical layout as the CLI formatter: chained condition lines are indented one level deeper than the control keyword, and the following standalone `{` aligns back to the control keyword.

Limitations:

- LSP formatting currently uses the default formatter configuration; editor-specific `FormattingOptions` and `.silk/format.toml` discovery are not yet applied inside the server.
- Range formatting is not advertised.

## Text Document Lifetime and Diagnostics

The server maintains an in-memory table of open documents, keyed by URI:

- `textDocument/didOpen`:
 - stores the full text of the document,
 - rebuilds a lightweight workspace cache (module set + symbol index + export table) used for hover/definition/completion/signature help,
 - publishes diagnostics via `textDocument/publishDiagnostics` for the opened URI by parsing the opened document and type-checking it against the cached module set (imports + std modules).
- `textDocument/didChange` (full sync):
 - replaces the stored text with the new full contents,
 - increments only the changed document revision for already-open documents so hover/definition/completion/signature-help can continue using the existing workspace/import/std cache while typing,
 - parses the changed document and type-checks it against the cached module set (imports + std modules),
 - publishes updated diagnostics for the changed URI.
- `textDocument/didSave`:
 - rebuilds the module set from all open documents,
 - resolves imports across the module set (packages + file imports) and loads standard library modules when configured,
 - type-checks the module set,
 - publishes diagnostics via `textDocument/publishDiagnostics` for any affected module URI (including imports).
- `textDocument/didClose`:
 - removes the document entry,
 - publishes an empty diagnostics list for the closed URI,
 - rebuilds the workspace cache for the remaining open documents.

For responsiveness, the server caches parsed modules (AST + lightweight module info) per open document revision and reuses them across hover/definition/completion/signatureHelp requests until the document changes.

### Workspace Cache and Package Discovery

The workspace cache is manifest-aware:

- for each open document with a filesystem path, the server walks upward to the
 nearest owning `silk.toml`,
- it loads the full package graph rooted there, including dependency packages,
 using the same contextual package search roots as the CLI for nested
 pathless dependencies,
- it adds manifest-declared definition files to the indexed module set,
- it applies the manifest package name as the default package for source or
 definition files that omit an explicit `package ...;` / `module ...;`
 declaration,
- and it collects manifest-owned `.c` / `.h` / `.m` sources from target inputs
 and shipped C headers for the native symbol index.

This keeps position-time queries on precomputed module/package/native symbol
data rather than rescanning manifests or package graphs for each request.

File-backed URI handling note:

- open-document URIs and file-backed cache entries are normalized to the same
 `file://` form before lookup,
- normalization canonicalizes percent-encoding, `file://localhost/...` versus
 `file:///...`, Windows drive-letter URIs, and UNC host casing so path
 spelling differences do not fragment the cache.

### Standard Library Integration

By default, the language server will load standard library packages referenced by `import std::...` or quoted std path imports such as `import { f } from "std/fmt";` when a stdlib root is available. The stdlib root is selected using the same rules as the compiler, with an additional workspace-root fallback:

- `--std-root <path>` passed to `silk-lsp` (highest priority),
- `SILK_STD_ROOT` when set,
- `./std` when present (development default),
- an executable-relative fallback (`../share/silk/std`) when installed,
- walk upward from the `silk-lsp` executable’s directory to find [`std/`](https://github.com/oro-computer/silk/tree/master/std/) (developer build fallback),
- if none of the above are available and the LSP client provides `rootUri`/`rootPath`, walk upward from that workspace root to find a [`std/`](https://github.com/oro-computer/silk/tree/master/std/) directory.

You can disable stdlib integration entirely with `--nostd`, which is useful for sandboxed editor setups or custom stdlib forks.

### Diagnostics Source and Current Limits

Diagnostics are derived from the existing compiler front-end:

- Parsing uses `parser.Parser` and the existing grammar in [grammar](/silk/docs/language/grammar/).
- Type checking uses `checker.checkModule` and the rules from [types](/silk/docs/language/types/) and related concept docs.

For responsiveness while typing:

- `didChange` diagnostics are computed for the changed document by parsing it and type-checking it against the cached module set (imports + std modules). The cache is not rebuilt on every change.
- A full module-set parse + resolve + type-check (including imports) is performed on `didSave`, and diagnostics are published for all affected modules.

The current LSP diagnostic surface follows these rules:

- Parse errors:
 - reported at the location of the unexpected token using the token’s line/column and length,
 - message text describes the unexpected token and that parsing failed,
 - publish `E0001` and attach structured `data.helps` pointing at
 `silk error E0001` for clients that surface custom diagnostic data,
 - `#embed("path")` expressions use the document URI’s filesystem path for
 source-relative lookup, and unreadable or invalid embedded files are
 reported as parse diagnostics on the path literal.
- Resolve/type-check diagnostics:
 - use the compiler’s existing structured failure data (stable code, message, source span, and any available detail),
 - map that span/code directly onto the LSP diagnostic when the compiler reports one,
 - include exact expected/found type detail for supported incorrect `await`, `await *`, `yield`, and `yield *` task/async operand errors,
 - include a `silk error <code>` lookup hint for stable compiler codes so
 editor users can open the same diagnostic reference available from the CLI,
 - flatten available note/help guidance inline into the LSP `message` field and also attach structured `data.detail`, `data.notes`, and `data.helps` when the compiler provides them.
- Conditional compilation (`if attr(...)`):
 - the server evaluates `attr(...)` query conditions using the host target (`arch`, `os`, `target`) and the enabled feature set (empty in Silk currently),
 - when an `if` / `else if` condition is an attribute-query boolean expression that resolves to a constant `true`/`false`, the inactive branch body is published as a `Hint` diagnostic tagged `Unnecessary` so editors may render it faded (similar to inactive `#if` blocks in C/C++).

Current limits:

- parse and attribute-pass diagnostics are still built locally from the parser / attr pass because those subsystems do not yet surface the same reusable diagnostic struct as the later resolve/type-check stages,
- when an internal error path still lacks an exact span, the server falls back to the owning module with a coarse range rather than dropping the diagnostic entirely.

## Non-Goals

`silk-lsp` still intentionally does **not** provide:

- full semantic completions with perfect scope/type filtering across every binder/control-flow form,
- project-wide navigation or rename outside the current cached module set,
- fully semantic token classification for every identifier use,
- richer inlay-hint categories beyond the current local type hints,
- code actions.

These features are intended as future extensions and must be:

- designed and documented here (and in any relevant `docs/language/` or `docs/std/` docs),
- backed by the underlying compiler front-end and/or standard library,
- covered by tests (Zig and, where appropriate, C) before being advertised as supported capabilities.

## Relationship to Other Tooling

The language server is part of the broader tooling story described in:

- [architecture](/silk/docs/compiler/architecture/) (compiler and tool layout),
- [cli silk](/silk/docs/compiler/cli-silk/) (CLI behavior for `silk`),
- `docs/usage/` (editor integrations, including Vim and LSP-based workflows).

The `tmp/zls/` directory in the Silk compiler repository contains a built-in copy of the Zig Language Server (ZLS) for inspiration and experimentation only:

- it is **not** part of the supported Silk toolchain,
- it must not be treated as authoritative for Silk semantics,
- ideas from it may inform the design and implementation of `silk-lsp`, but Silk remains spec-driven from the `docs/` tree.
