本文へ移動
cccskills
無料GitHub で公開

LSP Server

Comprehensive deep-dive into the Chemical language server (`ChemicalLsp`) — the `server/` subsystem, how it is built on `lib/lsp-framework`, its document/state model (`WorkspaceManager`, `ModuleData`), how it drives `SymbolResolver` for incremental analysis, every implemented LSP capability and where it lives, `chemical.mod` handling, diagnostics mapping, CBI analyzer hooks, and how to build and test the server. Load when working on the language server, editor features, semantic tokens, folding ranges, hover, completion, go-to-definition, signature help, inlay hints, formatting, `server/` internals, `LSPHooks`, or the `chemical/*` IDE libraries.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemicalスター 31(GitHub 31 + cccskills 0)

実行スコア 未検証

インストール方法を見る

含まれるファイル(1)

  • SKILL.md37.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

LSP Server (ChemicalLsp)

The Chemical language server is a standalone C++ executable, ChemicalLsp, that speaks the Language Server Protocol over stdio or a TCP socket. It is editor-driven and incremental: it keeps a long-lived workspace of parsed and symbol-resolved modules and answers IDE requests (diagnostics, completion, hover, navigation, semantic tokens, …) against the in-editor buffer, not the on-disk file.

It is not the batch compiler. Compiler and TCCCompiler are intentionally short-lived, do full end-to-end codegen, and share compiler_main(). ChemicalLsp has its own main() (core/targets/LSPMain.cpp:359), never emits object code, and only re-runs the front-end of the compiler (lex → parse → symbol resolution). It still links TinyCC and lib/lsp-framework, and it shells out to compiler_main for the cc subcommand and to a child ChemicalLsp process to evaluate build.lab/chemical.mod.

Related: CLI Entrypoint and Targets (.agents/skills/cli_entrypoint/SKILL.md), Symbol Resolution (.agents/skills/symres/SKILL.md), Diagnostics (.agents/skills/diagnostics/SKILL.md), Compiler Bindings (.agents/skills/compiler_bindings/SKILL.md), Build System (.agents/skills/build_system/SKILL.md).

1. Key Files and Directories

PathPurpose
core/targets/LSPMain.cppChemicalLsp main(), capability advertisement, request/notification handler registration, stdio & socket transports, --build-lab child mode, DEBUG --run-tests
server/WorkspaceManager.h/.cppPer-client operations manager: caches, document state, request dispatch, build-context bootstrap
server/LspSemanticTokens.cppThe bulk of the state machine: parsing modules, symbol resolution, caching/dirty tracking, semantic tokens, diagnostics publishing, file index (new/deleted)
server/Importer.cppget_lexed() — lex from overridden in-memory source or disk
server/mod_file/Importer.cppprocess_dot_mod_file() — parse and diagnose chemical.mod
server/analyzers/All IDE feature analyzers (completion, hover, goto-def, symbols, folding, formatting, signature help, inlay hints, semantic tokens, caret utils)
server/build/Out-of-process build evaluation: ChildProcessBuild, ContextSerialization, ipc_process, Build.cpp (compile_lab)
server/diagnostics/add_diagnostics() — compiler Diag → lsp::Diagnostic
server/model/Data model: ModuleData, LexResult, ASTResult, AnonymousFileData, ClientKind, SemanticTokenScopes
server/cbi/hooks.hTypedefs for embedded semantic-token / folding-range hooks
server/utils/LRUCache, AnalyzerUtils (get_token_at_position), lspfwd.h, dead PrintUtils
server/tests/LspTests.cppDEBUG-only formatter test harness (run_lsp_tests)
compiler/cbi/bindings/lsp/LSPHooks.h/.cppC ABI shims exposing analyzers to CBI plugins
compiler/cbi/bindings/CBI.cppRegisters the LSPAnalyzers compiler interface (LSP build only)
lib/lsp-framework/Vendored C++20 LSP implementation (types/messages generated from the official meta model)

2. Architecture

2.1 Foundation: lib/lsp-framework

The server is built on the vendored lib/lsp-framework submodule (CMakeLists.txt:718-720). That library generates lsp/types.h and lsp/messages.h from the official LSP meta model and provides lsp::Connection, lsp::MessageHandler, lsp::io::standardIO(), and lsp::io::SocketListener (see lib/lsp-framework/README.md). All request/notification params and results are typed C++ structs; handlers are registered with handler.add<lsp::requests::X>(...).

The server target adds ${CMAKE_SOURCE_DIR}/lib/lsp-framework and the generated-files dir to its include path (CMakeLists.txt:852-856) and links lsp + ${LIBTCC_LIB} (CMakeLists.txt:1016). It is compiled with LSP_BUILD (CMakeLists.txt:768), which enables LSP-only code paths throughout the compiler (e.g. compiler/lab/TargetData.h:81 sets target.lsp = true, surfaced to build scripts as def.lsp).

2.2 Entry point and transports

main() (core/targets/LSPMain.cpp:359) registers these CLI options (LSPMain.cpp:367-381): --resources/--res, --port, --version/-v, --build-lab, cc, --shmName, --evtChildDone, --evtParentAck, --stdio, --client, and (DEBUG) --run-tests.

Three roles:

  1. stdio server (--stdio → run_stdio_session, LSPMain.cpp:325). The default when the option is supplied.
  2. socket server (default if neither --stdio nor --build-lab): listens on --port (default 5007) via lsp::io::SocketListener; each accepted connection is served by a detached thread running run_session (LSPMain.cpp:271, acceptor loop at LSPMain.cpp:485-498). One process can serve multiple clients, each with its own WorkspaceManager.
  3. build-lab child (--build-lab <file> + IPC names, LSPMain.cpp:429-459): calls compile_lab() to evaluate a build.lab/chemical.mod and report the resulting module/job graph back to the parent over shared memory.

Both session functions create an lsp::Connection, a lsp::MessageHandler, and a WorkspaceManager(exePath, handler), then call registerDefaultHandlers() and loop on handler.processIncomingMessages().

--client <zed|vscode|intellij> overrides client detection (LSPMain.cpp:397-408); otherwise WorkspaceManager::initialize sniffs params.clientInfo.name (WorkspaceManager.cpp:213-222). ClientKind (server/model/ClientKind.h) currently only affects the advertised token-type legend.

2.3 registerDefaultHandlers — the LSP surface

registerDefaultHandlers (LSPMain.cpp:49-268) is the single place where every capability is wired to a WorkspaceManager method:

LSP messageHandler locationManager call
initializeLSPMain.cpp:54manager.initialize(params) + capability result
initializedLSPMain.cpp:124manager.register_watched_files_capability()
textDocument/semanticTokens/fullLSPMain.cpp:131get_semantic_tokens_full
textDocument/foldingRangeLSPMain.cpp:146get_folding_range
textDocument/documentSymbolLSPMain.cpp:154get_symbols
textDocument/hoverLSPMain.cpp:162get_hover
textDocument/definitionLSPMain.cpp:171get_definition
textDocument/completionLSPMain.cpp:180get_completion
textDocument/signatureHelpLSPMain.cpp:189get_signature_help
textDocument/inlayHintLSPMain.cpp:198get_hints
textDocument/formattingLSPMain.cpp:209get_formatting
textDocument/didOpenLSPMain.cpp:217OnOpenedFile
textDocument/didChangeLSPMain.cpp:225onChangedContents
textDocument/didSaveLSPMain.cpp:233onSave
workspace/didChangeWatchedFilesLSPMain.cpp:241index_new_file / de_index_deleted_file
shutdownLSPMain.cpp:261sets the session shutdown flag

Advertised capabilities (LSPMain.cpp:103-115): incremental text sync, completion (trigger chars . ::), hover, signature help (trigger ,), definition, document symbol, document formatting, folding range, full semantic tokens, and inlay hints. positionEncoding is not set (the framework default applies).

2.4 WorkspaceManager — the state model

WorkspaceManager (server/WorkspaceManager.h:67) is created once per client session and owns all caches and state. It is documented as outliving an individual session but dying on client disconnect. Key members:

MemberTypePurpose
overriddenSourcesunordered_map<string,string>In-memory editor buffers not yet saved to disk
incremental_change_mutexmutexSerializes didChange edits
process_file_mutexmutexOnly one file is processed (parsed + symresolved) at a time
tokenCacheLRUCache<string, shared_ptr<LexResult>> (cap 10)Lexed tokens per file
filesIndexunordered_map<string_view, ModuleData*>Fast file → module lookup
moduleDataunordered_map<LabModule*, unique_ptr<ModuleData>>Per-module AST/state cache
dirtyModulesunordered_set<ModuleData*>Modules needing re-symbol-resolution
modFileDataLRUCache<string, shared_ptr<ModuleFileDataUnit>> (cap 10)Parsed chemical.mod units
anonFilesDataLRUCache<string, shared_ptr<AnonymousFileData>> (cap 10)Files not belonging to any module
loc_manLocationManagerSingle location table; locations must be disposed when cached files change
controllerAnnotationControllerAnnotation handling
global_allocatorASTAllocatorSession-lifetime arena
modStorage / typeBuilder / pathHandler / coreNodes / implsIndex / instContainer / bindercompiler servicesShared resolver state
context_informationBuildContextInformationSerialized build context from the child build
poolctpl::thread_poolParsing/symres tasks (hardware concurrency threads)
main_job, global_container—Main LabJob*; lazily created GlobalContainer*

ModuleData (server/model/ModuleData.h:47) holds a module-level ASTAllocator, a ModuleScope, a map of CachedASTUnit* (ModuleData.h:15) per file, a dirtyFiles set, and dependencies. completely_symbol_resolved() is symbol_resolved_once && dirtyFiles.empty() (ModuleData.h:123).

LexResult (server/model/LexResult.h:12) holds the token vector plus lexer diags and either a FileInputSource (disk) or overridden_source (editor). Tokens point into that source string, so it must not change after lexing. ASTResult (server/model/ASTResult.h:13) wraps an ASTUnit plus its allocator and diagnostics.

2.5 Initialization sequence

initialize() (WorkspaceManager.cpp:210) records the project path from rootUri/rootPath, then calls build_context_from_build_lab() off the thread pool. That function (WorkspaceManager.cpp:176) looks for chemical.mod then build.lab, launches a child build (launch_child_build), and on success calls post_build_lab() (WorkspaceManager.cpp:97):

  1. Set main_job to the first job in the serialized context.
  2. index_module_files() (WorkspaceManager.cpp:79) — for every module in modStorage, call ASTProcessor::determine_module_files(...) and index each file path in filesIndex to its ModuleData.
  3. compile_cbi(job) (WorkspaceManager.cpp:118) for every LabJobType::CBI job — compiles the plugin in-process via LabBuildCompiler::do_job, leaving the TCC state in the binder so macro/analyzer hooks are callable.

Out-of-process evaluation of build.lab

Evaluating a build script requires running arbitrary Chemical code and leaving a TCCState behind, which the parent cannot reuse directly. So the server evaluates it in a child process and serializes the resulting data:

  • launch_child_build (server/build/ChildProcessBuild.cpp:389) either compiles in-process under DEBUG_CHILD_BUILD (DEBUG only, for debugger friendliness) or performs IPC: get_child_build_payload (ChildProcessBuild.cpp:208 POSIX, :29 Windows) spawns <lsp> --build-lab <file> --shmName … --evtChildDone … --evtParentAck …, waits up to 10 s on a semaphore/event, and reads a shared-memory payload.
  • The child runs compile_lab (server/build/Build.cpp:13), which builds the lab to a TCC state via compiler.built_lab_file(...), invokes chemical_lab_build, and serializes the module graph + jobs with labBuildContext_toJsonStr (server/build/ContextSerialization.cpp). child_create_and_write_shm (server/build/ipc_process.cpp:365) writes it to shared memory.
  • The parent deserializes with labBuildContext_fromJson into context_information (server/build/ContextSerialization.h:18), reconstructing ModuleStorage modules, dependencies, symbol-info pools, and LabJobs.

2.6 Document lifecycle

  • open — OnOpenedFile (WorkspaceManager.cpp:411) → process_any_file_on_open (LspSemanticTokens.cpp:1012): .mod → process_dot_mod_file; .lab → parse; otherwise skip if already cached (tokens present), else process_file.
  • change — onChangedContents (WorkspaceManager.cpp:471) under incremental_change_mutex. It loads the current source (overridden or disk), applies each TextDocumentContentChangeEvent. A whole-document change is fast-pathed (:507-516); range changes are applied by positionToOffset/applyChange (WorkspaceManager.cpp:416,451). The new text is stored in overriddenSources and process_any_file(path, true, false) re-lexes, re-parses, re-symbol-resolves, and republishes diagnostics.
  • save — onSave (WorkspaceManager.cpp:565): if the saved file is chemical.mod/*.lab, the entire module storage, module data, token cache, file index, instantiation container, core nodes, and impls index are cleared and the build context is rebuilt from scratch.
  • close — onClosedFile (WorkspaceManager.cpp:588) drops the overridden source. (textDocument/didClose is not currently registered; the method exists for future use.)

2.7 How this differs from the batch compiler

AspectBatch (Compiler/TCCCompiler)LSP (ChemicalLsp)
Lifetimeone invocationlong-lived, editor session
Inputfiles on diskeditor buffers (overriddenSources)
Pipelinelex → symres → typecheck → 2c → codegen → linklex → parse → symbol resolution only
Type verificationyes (TypeVerifier)not run
Generic instantiationfull finalizationregistration/signature only, per module
Statefresh per runcached ModuleData/LexResult, dirty tracking
Concurrencyper-file passes, allocators cleared per passthread pool for parsing, one file processed at a time

3. How Analysis Reuses Compiler Passes

3.1 process_file — the heart of the server

WorkspaceManager::process_file (server/LspSemanticTokens.cpp:629) is the single path that makes a file "ready" for analysis. Steps:

  1. Lex the file with comments (get_lexed(path, /*keep_comments=*/true)). On lex errors it publishes lexer diagnostics, stores tokens in tokenCache, and returns early — no parsing (LspSemanticTokens.cpp:651-667). This is the primary handling of a broken/erroneous buffer.
  2. Copy tokens (excluding comment tokens) into a copied_tokens vector while recording original indices. Parsing mutates token linked pointers; after symbol resolution the links are copied back to the original tokens by index (LspSemanticTokens.cpp:973-985). This keep-links-out-of-comments arrangement is why semantic tokens can later map tokens to AST nodes.
  3. Build a GlobalInterpretScope and a SymbolResolver with a 10 KB resolver allocator, then bind_or_create_container (LspSemanticTokens.cpp:515) creates or rebinds the session-wide GlobalContainer.
  4. Look up ModuleData by file path (getModuleData). If none, the file is anonymous and handled separately (see §3.4).
  5. If it belongs to a module: parseModuleWithDepsWait parses the module and all dependencies once (:735), then sym_res_mod_deps_seq symbol-resolves dependencies (:743).
  6. Decide whether the current module needs re-resolution (sym_res_curr_mod): true if a dependency was re-resolved, or a different file in the module is dirty (:750-772).
  7. Re-declare/link the module's other files if needed, always declaring dependencies first. Then resolver.declare_and_link_file(...) on the requested file (:914).
  8. Copy linked AST nodes back onto the original tokens, cache tokens in tokenCache, and publish diagnostics built from lexer + parser + resolver diagnostics (:990-997).

3.2 The six front-end passes

Full module symbol resolution is performed in sym_res_mod_sig (LspSemanticTokens.cpp:322) using SymbolResolver directly (not ASTProcessor):

module_scope_start()
  → removeInstantiationsFor(file) + clear module allocator
  → setASTAllocator(module allocator)
  → declareDependencies()                     // recursive
  → resolver.tld_declare_file(...)            // Pass 1: top-level declarations
  → resolver.link_signature_file(...)         // Pass 2: signatures
  → resolver.generic_instantiation_file(...)  // Pass 3: generic registration
  → resolver.after_link_signature_file(...)   // Pass 4: after-link-signature
  → module_scope_end()
  → unmake_module_dirty(modData)

This mirrors the batch compiler's serial ASTProcessor passes (see .agents/skills/build_system/SKILL.md), but is scoped to the modules that changed and does not run body linking for files other than the requested one in the "not dirty" case — that path only declares other module files (SymbolResolverDeclarer, LspSemanticTokens.cpp:850-858). The requested file always gets full declare_and_link_file (:914), which runs declaration + signature + body linking for that file.

3.3 Incomplete / erroneous buffers

The server phases the pipeline so a broken buffer still yields useful output:

  • Lex error → publish lexer diagnostics, cache tokens, stop (:651-667). Semantic tokens and folding ranges still work because they only need tokens.
  • Parse/symres errors → parse_file returns a bool and moves parser.diagnostics into parse_diagnostics (:37-78); resolver collects its own diagnostics. All are appended and published (:990-997).
  • Anonymous / no module → process_file still parses and links the file into an AnonymousFileData with a throwaway module scope (:928-966), so a single open file outside a project still gets highlighting/diagnostics.
  • Dirty tracking prevents stale results: make_module_dirty / unmake_module_dirty (WorkspaceManager.h:350,358) use a single-file dirty model. should_process_file (LspSemanticTokens.cpp:550) short-circuits resolution when nothing relevant changed, and checks exists_in_deps (:538) to decide whether a dirty dependency forces reprocessing.

3.4 Caching and instantiation cleanup

  • tokenCache is an LRU (capacity 10) of LexResult per absolute path (WorkspaceManager.h:116).
  • parseFile (LspSemanticTokens.cpp:106) allocates a 10 KB CachedASTUnit per file and stores it in modData->cachedUnits/fileUnits.
  • Before clearing a module's allocator, all generic instantiations keyed by file id and all declaration-keyed instantiations are removed (instContainer.removeInstantiationsFor, removeDeclInstantiations, LspSemanticTokens.cpp:890-900), preventing dangling instantiation records after the arena is reset.
  • modData->allocator.clear() happens only when the module is re-resolved (:788), not on every request.

4. Feature Reference

Every analyzer takes decoded SourceLocations via LocationManager and either the cached ASTUnit (symbol-resolved features) or the token list (lexical features). get_token_at_position lives in server/utils/AnalyzerUtils.cpp:10.

4.1 Diagnostics (textDocument/publishDiagnostics)

  • Compiler Diag → lsp::Diagnostic in add_diagnostics (server/diagnostics/DiagnosticUtils.cpp:5): maps diag.range to lsp::Range, copies message, and casts diag.severity directly to lsp::DiagnosticSeverity (compiler severities are Error/Warning/Information/Hint; see the Diagnostics skill).
  • Published synchronously or asynchronously via publish_diagnostics (LspSemanticTokens.cpp:1266), which cancels any in-flight async publish using publish_diagnostics_cancel_flag (WorkspaceManager.h:222) and an std::async task.
  • Build failures are not published as document diagnostics — report_build_failure (WorkspaceManager.cpp:596) sends a custom chemical/buildStatus notification with { success, uri, message } so it does not clobber fine-grained parse/import diagnostics on the same chemical.mod/build.lab URI.
  • relatedInformation, tags, and fix-its are not currently propagated — add_diagnostics only copies range/message/severity.

4.2 Semantic tokens (textDocument/semanticTokens/full)

get_semantic_tokens_full (LspSemanticTokens.cpp:1037) ensures the file is processed, then get_semantic_tokens (:1288) runs SemanticTokensAnalyzer (server/analyzers/SemanticTokensAnalyzer.cpp). It walks tokens and emits the delta-encoded 5-int tuples LSP expects. put_auto (:115) maps keyword token types to SemanticTokenScopes, and put_node_token (:56) maps a token's linked AST node kind to a scope (functions, structs, enums, variables, params, namespaces, …). Strings, chars, comments (including multiline splitting via putMultilineToken, :386), and numbers are handled explicitly. #macro tokens dispatch to a CBI hook (§7). Token type ordering is fixed by getTokenTypes in LSPMain.cpp:37 and mirrored by SemanticTokenScopes (server/model/SemanticTokenScopes.h).

4.3 Completion (textDocument/completion)

get_completion (WorkspaceManager.cpp:287) → CompletionItemAnalyzer (server/analyzers/CompletionItemAnalyzer.cpp). analyze (:414) reverses the current file's top-level nodes, descends into the scope containing the caret, and emits completions for top-level symbols of the current file, the module, and its direct dependencies. Completion items carry markdown docs and small details built by markdown_documentation / small_detail_of (server/analyzers/Documentation.h, implemented in HoverAnalyzer.cpp:49,124). Note many member-level completions are still //TODO stubs (access-chain completion before the caret is detected via chain_before_caret but not yet resolved, CompletionItemAnalyzer.cpp:419-427).

4.4 Hover (textDocument/hover)

get_hover (WorkspaceManager.cpp:399) → HoverAnalyzer::markdown_hover (server/analyzers/HoverAnalyzer.cpp:261). It finds the token at the caret, follows token->linked (either directly an ASTNode, or via get_ref_linked_node()), and renders markdown with markdown_documentation (HoverAnalyzer.cpp:124): "Defined in" relative path plus a fenced code signature for functions, structs, interfaces, enums, variables, params, and typealiases.

4.5 Go-to-definition (textDocument/definition)

get_definition (WorkspaceManager.cpp:373) → GotoDefAnalyzer::analyze (server/analyzers/GotoDefAnalyzer.cpp:22). It reads the token at the caret, follows its linked node, decodes the definition's SourceLocation, and returns one DefinitionLink with the defining file URI and range. No multi-file related bindings.

4.6 Document symbols (textDocument/documentSymbol)

get_symbols (WorkspaceManager.cpp:385) → DocumentSymbolsAnalyzer (server/analyzers/DocumentSymbolsAnalyzer.cpp). Visitors are VisitFunctionDecl (:69), VisitStructDecl, VisitUnionDecl, VisitVariantDecl, VisitInterfaceDecl, VisitTypealiasStmt, VisitEnumDecl, VisitVarInitStmt (:75-101). Nested symbols and detailed signatures are TODO (:14-16).

4.7 Folding ranges (textDocument/foldingRange)

get_folding_range (WorkspaceManager.cpp:264) → FoldingRangeAnalyzer (server/analyzers/FoldingRangeAnalyzer.cpp). analyze (:58) scans tokens tracking a brace stack (LBrace/RBrace, :31-40) and emits region ranges; #macro tokens dispatch to a CBI hook (:41-50). folding_range (:17) tags comment vs region.

4.8 Formatting (textDocument/formatting)

get_formatting (WorkspaceManager.cpp:332) lexes with comments and reads the source (overridden or disk), then FormatterAnalyzer::format (server/analyzers/FormatterAnalyzer.cpp:6) produces a whole-file TextEdit. It regenerates indentation, spacing, and newlines from tokens, preserving comments, handling else/else if, arrays, annotations, and collapsing runs of blank lines to at most one. This is the only feature tested by server/tests/LspTests.cpp.

4.9 Signature help (textDocument/signatureHelp)

get_signature_help (WorkspaceManager.cpp:353) → SignatureHelpAnalyzer::analyze (server/analyzers/SignatureHelpAnalyzer.cpp:50). It scans left from the caret to find the unmatched ('s preceding identifier (:74-95), resolves the identifier's linked node → known_type() → FunctionType, and builds one SignatureInformation with parameter labels and an active parameter based on the caret line (:127-154).

4.10 Inlay hints (textDocument/inlayHint)

get_hints (WorkspaceManager.cpp:317) → inlay_hint_analyze (server/analyzers/InlayHintAnalyzer.cpp:17, API in InlayHintAnalyzerApi.h). The RecursiveVisitor skips nodes outside the requested Range (should_compute, :27) and emits:

  • function-call argument-name hints (VisitFunctionCall, :36, using FunctionType::func_param_for_arg_at), and
  • inferred-type hints for var declarations without an explicit type (VisitVarInitStmt, :78).

4.11 Watched files (create/delete)

register_watched_files_capability (LspSemanticTokens.cpp:1214) registers a watcher for **/*.{ch,mod} with create+delete watch kinds. index_new_file (:1094) finds the owning module via find_module_parent_of (:1082), appends the file to mod->direct_files, parses it, and adds it to filesIndex. de_index_deleted_file (:1147) removes it from cachedUnits, fileUnits, filesIndex, and direct_files. Uses weakly_canonical since deleted paths throw in canonical.

4.12 Not implemented

There are no handlers for textDocument/references, rename/prepareRename, codeAction, documentHighlight, workspace/symbol, completionItem/resolve, documentLink (the analyzer exists but returns an empty vector — server/analyzers/DocumentLinksAnalyzer.cpp:7), or executeCommand. Do not document or call these as if they exist.

5. chemical.mod Handling in the Editor

.mod files do not go through the module SymbolResolver pipeline as Chemical source; they are parsed for structure and diagnosed for missing paths/imports.

WorkspaceManager::process_any_file (LspSemanticTokens.cpp:1001) routes any path ending in chemical.mod to process_dot_mod_file (server/mod_file/Importer.cpp:81):

  1. Lex with comments; store tokens in tokenCache.
  2. Copy non-comment tokens while recording original indices.
  3. Get or create a ModuleFileDataUnit (LRU), clearing it for reuse.
  4. BasicParser::parseModuleFile(unit->allocator, unit->modFileData) parses the .mod into ModuleFileData (compiler/processor/ModuleFileData.h:79) — sources list, link libs, ship files, include dirs, C files, compiler interfaces, options.
  5. Copy AST links back to the original tokens (same token/index mapping as Chemical files).
  6. diagnoseModuleFileDataUnit (server/mod_file/Importer.cpp:31):
    • every source "<path>" is resolved relative to the .mod and must exist;
    • every leading import statement is checked:
      • native imports (std, cstd, …) via pathHandler.resolve_native_lib and containsModOrLab (:14), which requires a chemical.mod or build.lab in the resolved library directory;
      • local imports via resolve_sibling + containsModOrLab.
  7. Diagnostics are converted and published for the .mod URI (server/mod_file/Importer.cpp:150-153).

Because the project's build context is loaded from chemical.mod, saving it triggers a full context rebuild (onSave, §2.6). .lab files are, for now, handled by the normal process_file path (LspSemanticTokens.cpp:1004-1006).

6. Diagnostics Mapping Detail

  • Source: parser (parser.diagnostics), lexer (LexResult::diags), and SymbolResolver diagnostics (resolver.diagnostics), accumulated in process_file (LspSemanticTokens.cpp:990-997).
  • Conversion: add_diagnostics (server/diagnostics/DiagnosticUtils.cpp:5) assumes diag.severity.has_value() — Diag::severity is an std::optional<DiagSeverity> and is cast straight to lsp::DiagnosticSeverity. Ranges come pre-computed on Diag from LocationManager-backed positions.
  • Publishing: publish_diagnostics (LspSemanticTokens.cpp:1266) uses lsp::notifications::TextDocument_PublishDiagnostics (which replaces all diagnostics for a URI), so concurrent publishes are serialized by publish_diagnostics_mutex and the previous async task is cancelled.
  • Multi-file/related info: not emitted today. Definition links and hover do carry a cross-file URI via LocationManager::getPathForFileId, but Diag's relatedInformation/tags/fixits_ are dropped by the converter.

7. CBI Integration in the Server

Compiler plugins can highlight and fold their embedded languages (#html, #css, #js, #md, #universal) inside the editor.

7.1 Analyzer hooks

server/cbi/hooks.h defines two function-pointer typedefs: EmbeddedSemanticTokensPut and EmbeddedFoldingRangesPut. When SemanticTokensAnalyzer::put_auto sees a TokenType::HashMacro (SemanticTokensAnalyzer.cpp:318-331) it strips the leading #, looks up the hook by name + CBIFunctionType::SemanticTokensPut in the binder, and calls it (((EmbeddedSemanticTokensPut) hook)(this, &t, end_token)). FoldingRangeAnalyzer does the same for CBIFunctionType::FoldingRangesPut (FoldingRangeAnalyzer.cpp:41-50). The hook returns the next token to continue from.

7.2 C ABI shims

compiler/cbi/bindings/lsp/LSPHooks.h/.cpp implement the extern "C" symbols the TCC-compiled plugin calls: SemanticTokensAnalyzerputAuto, …put, …putToken, FoldingRangeAnalyzerput, …stackPush, …stackEmpty, …stackPop. They are thin wrappers over the C++ analyzer methods (e.g. LSPHooks.cpp:8-57).

7.3 Interface registration

compiler/cbi/bindings/CBI.cpp:468-478 defines LSPAnalyzersMap (prefix ide_) and registers it as the LSPAnalyzers compiler interface only under LSP_BUILD (CBI.cpp:493-495). On the Chemical side, lang/libs/ide/src/SemanticTokensAnalyzer.ch and FoldingRangeAnalyzer.ch declare the matching @compiler.interfaces.

7.4 Plugin wiring

A plugin's build.lab conditionally indexes its hook functions when building for the LSP, using the def.lsp target flag:

comptime if(def.lsp) {
    ctx.index_def_cbi_fn(cbi, std::string_view("md_semanticTokensPut"), CBIFunctionType.SemanticTokensPut);
    ctx.index_def_cbi_fn(cbi, std::string_view("md_foldingRangesPut"), CBIFunctionType.FoldingRangesPut);
}

The implementations live in lang/libs/<x>_ide/src/semantic_tokens.ch and folding_ranges.ch as @no_mangle functions (e.g. md_semanticTokensPut in lang/libs/md_ide/src/semantic_tokens.ch:68). They call analyzer.putToken(...)/analyzer.put(...)/analyzer.stackPush/Pop. Hook lookup and storage are in CompilerBinder::findHook / registerHook (compiler/cbi/model/CompilerBinder.h:102,110); the enum values live in compiler/cbi/model/CBIFunctionType.h:33,35. Note the enum-sync rule: adding a CBI enum value requires updating lang/libs/compiler/src/... mirrors.

8. Build and Test

8.1 Build

./scripts/build.sh --lsp              # builds ChemicalLsp
./scripts/build.sh --all              # builds TCCCompiler, Compiler, ChemicalLsp

scripts/build.sh:34 maps --lsp/--ChemicalLsp to the ChemicalLsp CMake target. Any change under server/ or compiler/cbi/bindings/lsp/ requires rebuilding this target. ChemicalLsp is intentionally compiled with RTTI/exceptions enabled (it links against lib/lsp-framework, CMakeLists.txt:845-848), unlike Compiler/TCCCompiler.

8.2 Run

# stdio transport (typical editor integration)
cmake-build-debug/ChemicalLsp --stdio

# socket transport (default port 5007)
cmake-build-debug/ChemicalLsp --port 5007

# constrain to a client
cmake-build-debug/ChemicalLsp --stdio --client zed

Build scripts are evaluated via the --build-lab child mode launched automatically by launch_child_build; you normally never invoke that manually.

8.3 Tests

The LSP test harness is server/tests/LspTests.cpp, compiled into ChemicalLsp and exposed only in DEBUG builds via --run-tests (LSPMain.cpp:390-395):

./scripts/build.sh --lsp
cmake-build-debug/ChemicalLsp --run-tests

run_lsp_tests (server/tests/LspTests.cpp:201) is currently a formatter-only suite: it lexes small snippets and asserts exact formatted output across spacing, indentation, comments, annotations, arrays, and vertical spacing. There is no automated end-to-end test for the LSP protocol itself; IDE features are typically verified with a live client.

9. Gotchas, Performance, and Extending

9.1 Gotchas

  • Single LocationManager, lifetime coupling. loc_man is session-wide (WorkspaceManager.h:153). Locations encoded for a cached file become stale when the file is re-lexed; tokens keep linked pointers into AST nodes, so never mutate a buffer between lexing and link-back.
  • Link-back by index. Parsing runs on comment-stripped copied_tokens; results are copied to the original token vector via originalIndexes (LspSemanticTokens.cpp:977-985). Changing the token-copy scheme breaks semantic tokens, hover, and goto-def simultaneously.
  • Editor buffers override disk. overriddenSources wins in get_lexed (server/Importer.cpp:27-39). Features that read the file directly (formatting, get_formatting) must explicitly fall back to disk, as it does at WorkspaceManager.cpp:338-344.
  • chemical.mod diagnostics are not symbols. The .mod parser populates ModuleFileData; it does not create Chemical symbols, so hover/completion inside .mod are limited to what the generic path offers.
  • Whole-document changes. If didChange delivers an unhandled whole-document event in a release build it silently mishandles it; DEBUG throws (WorkspaceManager.cpp:546-550).
  • Async publish races. Diagnostics publishing is async; the cancel flag and mutex must be respected to avoid publishing stale diagnostics out of order.
  • get_target_triple() returns "LSP" (WorkspaceManager.cpp:48-53) — target conditions are a virtual LSP target, not the host triple.
  • DocumentLinksAnalyzer is a no-op — the commented-out body returns an empty vector; the capability is not advertised.

9.2 Performance

  • Incremental dirty tracking. dirtyModules + should_process_file (LspSemanticTokens.cpp:550) avoid re-resolving untouched dependencies. Single-file dirty tracking means one edited file at a time.
  • One file at a time. process_file_mutex serializes symbol resolution, while parsing uses the CTPl thread pool (parseModuleWithDeps, :201). The resolver allocators are batched arenas cleared only on re-resolution, so routine requests are cheap.
  • LRU caches bound token (tokenCache, cap 10), .mod (modFileData, cap 10), and anonymous (anonFilesData, cap 10) memory. Evicted token results are re-lexed on demand.
  • Batched AST units cost ~10 KB each (parseFile, :115); large projects should rely on the LRU eviction and dirty pruning rather than holding everything hot.
  • Cancellation. publish_diagnostics_cancel_flag allows a pending diagnostics task to be aborted when a newer request arrives.

9.3 Adding a new LSP feature

  1. Advertise the capability in the Initialize handler result (LSPMain.cpp:103-115), and (if it has triggers) add CompletionOptions/ SignatureHelpOptions entries.
  2. Register the handler in registerDefaultHandlers (LSPMain.cpp), converting the protocol Position/Range to the manager's Position and calling a new WorkspaceManager method. Guard against exceptions with the existing pattern (semantic tokens wraps in try/catch and throws "UNCAUGHT_EXCEPTION", LSPMain.cpp:136-143).
  3. Add the manager method in WorkspaceManager.h/.cpp. Follow the established shape: canonical(path), getModuleData, process_file_on_request(...), then use get_cached_unit (WorkspaceManager.cpp:248), the cached LexResult from tokenCache for lexical features, or anonFilesData for anonymous files.
  4. Implement an analyzer under server/analyzers/ and add it to the ChemicalLsp source list in CMakeLists.txt:664-716.
  5. If the feature needs compiler-internal information not on the AST, prefer an existing intrinsic or extend the symbol-resolution pass — do not add a second symbol table.
  6. Test via server/tests/LspTests.cpp where a pure-function equivalent exists (formatters, tokenizers), and manually via a live client otherwise.

10. Related Skills

  • CLI Entrypoint and Targets (.agents/skills/cli_entrypoint/SKILL.md) — ChemicalLsp target, main(), TargetData
  • Build System (LabBuildCompiler) (.agents/skills/build_system/SKILL.md) — jobs, modules, compile_cbi
  • Symbol Resolution (symres) (.agents/skills/symres/SKILL.md) — SymbolResolver, tld_declare_file, link_signature_file, declare_and_link_file
  • Diagnostics and Error Reporting (.agents/skills/diagnostics/SKILL.md) — Diag, ASTDiagnoser, severities
  • Compiler Bindings (.agents/skills/compiler_bindings/SKILL.md) — CBI, CompilerBinder, enum-sync rule
  • Compiler Plugin API (CBI) (.agents/skills/cbi_plugin_api/SKILL.md) — writing the *_ide hooks
  • Performance Optimization (.agents/skills/performance/SKILL.md) — arenas, caching, parallelization

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Building

無料

All documentation related to building and running Project or Tests

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

日本語の概要は準備中です。原文の説明を表示しています。

chemicallang/chemical312026年10月26日 更新

chemicallang のスキルをすべて見る

このスキルの問題を報告する