Facet Integration Status¶
Decision¶
cr-analyzer is moving to Facet as its only syntax frontend. The migration is
incremental so editor features stay available while semantic consumers are
ported from Crystal::ASTNode to Facet's native arena-backed AST.
Facet's AST is not expected to match Crystal's AST. Consumers use
SyntaxTree / SyntaxNode, a stable named-query facade over Facet's own node
roles, spans, and symbols.
Current boundary¶
| Concern | Current implementation |
|---|---|
| Source versions and query invalidation | Workspace-owned Facet SourceManager + QueryDb |
| Syntax diagnostics | Cached Facet parse; Crystal diagnostic fallback remains available |
| UTF-8 byte / LSP UTF-16 conversion | Facet LineIndex |
| Cursor lookup and selection ranges | Facet SyntaxTree#node_at / FacetNodeFinder |
| Document/workspace symbols | Facet collector is authoritative; Crystal collector is empty-result fallback |
| Declaration semantic index | Facet primary index for types, methods, aliases, includes, inheritance, enum members, docs, and locations; Crystal retains macro fallback data |
| Completion prefixes, enclosing syntax, keywords | Facet LineIndex + FacetNodeFinder + named condition roles |
| Receiver/member and named-argument completion | Facet-first named receiver/call/parameter roles; Crystal fallback for unsupported inference shapes |
| Locals and scoped variables | Facet local-name collection and typed/constructor assignment inference, including incomplete buffers |
| Navigation, hover, signature help, type hierarchy | Facet-first semantic resolution; Crystal fallback remains |
| References, rename, document highlights | Facet-first scope-aware occurrence collection; Crystal fallback remains |
| Inline values | Facet syntax plus semantic local classification |
| Call graph | Facet per-file call-site cache plus lazy revision-cached semantic resolution; Crystal fallback for legacy items |
| Macro support | Facet QueryDb#expand plus generated-declaration delta is primary for standard declaration macros and supported user macros, including indexed type/member introspection; Crystal interpreter is fallback for remaining APIs |
| Compiler semantics | Facet 0.2.0 SemanticDb: require-aware reachability, canonical types, revision-safe facts, basic inference/method lookup, and strict/tolerant snapshots |
| Semantic diagnostics | Confidence-graded facet.undefined_method; shadow-computed by default, with only conclusive findings published by CRA_FACET_SEMANTICS=on |
| Facet-only validation | CRA_FACET_ONLY=1 disables Crystal AST construction; all workspace LSP specs run against Facet alone in CI |
Each URI has a stable Facet FileId. A document version is parsed once and its
AstFile, SyntaxTree, diagnostics, parent map, and line index are reused by
all syntax consumers. Unchanged source bytes retain the cached query result.
Macro expansion invalidation is footprint-based, so edits to unrelated files
do not force ordinary macro re-expansion. Materialized type-aware consumers use
a conservative workspace-declaration dependency so changed indexed members
cannot leave generated declarations stale.
The incremental databases are process-local today; restarting the language
server rereads and reindexes the workspace. A reusable disk cache should be a
separate content-addressed layer keyed by cache schema, Facet/Crystal versions,
target options, and compiler-context fingerprints. Process-local FileId,
NodeId, and TypeId values must be rebound when loading cached facts.
The server advertises incremental LSP text synchronization and applies UTF-16 range edits before advancing the Facet source revision. Frontend invalidation is currently file-grained: an edited file is reparsed, but unchanged files and unrelated macro expansions stay cached.
Project-owned macro consumers are materialized during the initial semantic pass. Dependency and stdlib consumers remain lazy/legacy until needed; dirty queues contain only expansions that were actually materialized, preventing an editor startup from eagerly expanding the entire toolchain.
Current performance snapshot¶
On 2026-08-31, a release build initialized against this repository with a
median of 5.797 seconds on the legacy frontend and 3.626 seconds in
CRA_FACET_ONLY=1 mode (3 measured runs after 1 warmup), a 1.60x
speedup. Reproduce the machine-local comparison with
python3 scripts/bench_lsp_initialize.py; the result is evidence for the
current architecture, not a fixed CI threshold.
Completed gates¶
- A committed first compiler-semantic corpus from eight official Crystal 1.21 suites: 397 examples execute 529 type/error contracts. Facet matches the current 243-contract baseline exactly; all 286 remaining contracts are listed with explicit deferred reasons, so no semantic input is silently skipped.
- Require-aware project/dependency/stdlib reachability plus strict/tolerant
snapshots, revision-safe
NodeRefhandles, internedTypeIdvalues, declaration/method indexing, inheritance/includes, constructors, generic return substitution, unions, macro-generated methods, bare zero-argument calls, untyped/defaulted call-site specialization, target-aware overload selection, and conservative undefined-method diagnostics. -
cr-analyzer shadow/on integration with coded LSP diagnostics. Shadow is the default;
onis covered for ordinary, unknown-receiver, and macro-generated method cases. -
Exact Crystal 1.21 parser decision parity on all 4,378 captured upstream cases, including exact diagnostics for 941 rejected inputs.
- Exact common semantic AST projection on all 3,437 accepted upstream inputs.
- Clean parse and native AST integrity across all 1,625 Crystal stdlib files.
- Clean parse and native AST integrity across all Facet and cr-analyzer source/spec files in the corpus.
- Stable declaration roles, parent/ancestor traversal, cursor lookup, name spans, doc comments, and UTF-16 conversion in Facet syntax queries.
- Automatic revision-based parse/syntax/index/expand cache invalidation.
- Tests proving stable file IDs, cache reuse, unrelated-edit expansion reuse, macro-provider invalidation, and UTF-16 incremental edits.
- Facet/Crystal document-symbol contract parity for representative nested declarations, methods, enum members, and fields.
- A Facet-native two-pass declaration semantic producer plus representative parity for types, methods, aliases, includes, inheritance, enum members, docs, locations, nested names, and reopen-file invalidation.
- The reusable
scripts/check_facet_semantic_parity.crcorpus gate; the current cr-analyzer source/spec corpus is exact on 68/68 Crystal-accepted files, with one additional Facet recovery from a Crystal-rejected file. - Tested document-symbol and selection-range behavior on incomplete buffers rejected by Crystal::Parser.
- Facet-backed completion prefixes, enclosing type context, and keyword-context classification, including incomplete conditions and astral UTF-16 positions.
- Facet-first receiver/member and named-argument completion, including typed and constructor-assigned locals, chained generic returns, local names, and instance/class variables in buffers rejected by Crystal::Parser.
- Facet block-call roles and collection yield inference for common Array, Hash, indexed collection, and fluent-call patterns.
- Facet-first declaration/type/implementation navigation, hover, signature help, references, rename, highlights, document/workspace symbols, and type hierarchy, with regression tests that explicitly remove the Crystal AST.
- Facet-native inline-value collection for parameters, locals, and scoped vars.
- Facet-native incoming/outgoing call hierarchy with per-file extraction, lazy
edge caching, edit invalidation, cross-file typed receivers, constructors,
class methods,
super, and authoritative empty results in buffers rejected by Crystal::Parser. - Ordinary and bare user macro calls resolve by lexical scope and arity in
Facet; standard accessor families and
recordhave Facet-native lowering. Macro arguments outside the evaluator subset remain source-backed AST values, so generic types and nested expressions survive generated declarations. Source-backed macro blocks supportyield,block.body, andblock.args, with block content included in incremental expansion cache keys. Evaluated collections support lexical map/select/predicate/iteration blocks used by declaration-generating macro control flow. - Macro strings, symbols, identifiers, and opaque AST arguments retain distinct
source rendering and scalar values; direct interpolation, delegated string
methods,
id,stringify,symbolize,chars, cross-kind macro-id equality, and basic AST predicates preserve Crystal syntax roles. - Facet-native type-aware macro values expose lexical
@type, indexedresolve/resolve?, methods, instance variables, constants, method/argument metadata, explicit superclasses/ancestors, kind predicates, and subtype checks. Types, methods, instance variables, and arguments also expose annotations with positional and named values. Declaration and annotation edits invalidate and requeue materialized type-aware expansion consumers. - Facet's committed Crystal 1.21 runtime macro corpus captures all 1,042
contracts executed by the official evaluator specs and matches exact output,
expected diagnostic text, and output effects for all 1,042/1,042: 900
portable and 142 program-context cases, including all 25 official
assert_macro_errorcalls, four nestedparse_typefailures, and all 371 self-contained contracts. Captured AST values retain start/end locations, documentation, and structural root names with generic/non-generic variants. Recursive call structure exposes arguments, receiver/block data, nested named-argument names/values, andglobal?. Recursive control-flow structure exposescase/selectconditions, branches and exhaustive flags plus exception bodies, rescue names/types,else, andensure. Function declaration views coverDef,Macro,FunDef, nestedArgvalues, signatures, bodies, flags, visibility, and external names. Type declaration views coverClassDef,ModuleDef,EnumDef,AnnotationDef,LibDef, andCStructOrUnionDef, including bodies, inheritance/base types, generic parameters, splats, and declaration flags. Inline assembly views coverAsmand nestedAsmOperandvalues, including text, operands, constraints, clobbers, and option flags; type-syntax views coverTypeDeclaration,ProcNotation,Metaclass,Generic,Union, andPath, includingresolve/resolve?behavior. Expression views coverProcLiteral,ProcPointer,Cast,NilableCast,If,Assign,MultiAssign,RangeLiteral,And,Or, unary expressions, predicates, uninitialized variables, macro-control nodes, aliases, visibility modifiers, offsets, requires, blocks, expression containers, loops, control expressions, yields, annotations, typedefs, external variables, read-instance-variable nodes, and string interpolations, including immutable returned collections, range iteration, and boolean operands. Array and hash literal views additionally preserve elements, declared key/value types, and custom literal types. Facet-native macro arguments provide the same views. All portable direct AST-field, returned-AST-collection, environment/flag,parse_type, and backtick contracts are exact. Environment values, compiler flags, and captured command output are explicitMacroExpansionContextinputs and participate in cache fingerprints; Facet does not execute arbitrary shell commands. The contextual gate also replays 106 structuredTypeNodesnapshots and compares six print-family output effects byte-for-byte. A second committed gate replays all 147 expansion events emitted by the 133 official semantic macro examples: 69 user-macro calls and 78 inline expansions, with 131 successful outputs and 16 errors. Facet matches 147/147 by exact text or equivalent Facet semantic AST with no skipped events. Generic/free-variable bindings, named-tuple key locations, type-member snapshots, compile-time constants, resolved paths, and exact path errors are explicit expansion-context inputs and participate in cache fingerprints. Structured@caller, yielded arguments,skip_file, tuple splat binding, semantic type-argument resolution, and exact macro failure diagnostics are Facet-native. - A broader gate captures all 3,288 official compiler-semantic examples (nine
upstream pending), reducing 150,926 raw stdlib/bootstrap macro events to
2,736 unique invocation/definition/scope/target-flag/semantic contexts. Facet
matches 2,736/2,736 (100%): all 1,077 user-macro call events and all 1,659
inline expansions. Successful output compares literal payloads as well as
semantic AST shape, and failures compare exact diagnostics. The harness
compares one upstream expansion with
MacroExpander#expand_once; productionQueryDb#expandcontinues iterating to a fixed point. - Live expansion passes the cr-analyzer build target flags through an explicit
MacroExpansionContext; Facet includes its fingerprint in the incremental expansion cache key. Environment values and shell outputs remain absent unless a trusted caller supplies them explicitly. - Expanded Facet ASTs feed generated-only semantic slices, including completion, navigation, and call hierarchy in Crystal-rejected buffers. Macro-provider edits reindex only the footprint-invalidated consumer files.
- The complete workspace LSP contract runs with no Crystal AST: 103/103 examples cover completion, navigation, diagnostics/lints, symbols, references, rename, inline values, call/type hierarchy, macro-generated declarations, and dependent reindexing.
- Facet infers generic return substitution for user-defined index operators and owns unused method/block argument diagnostics.
- Facet include/superclass dependencies drive incremental invalidation and are verified through public declaration results before and after provider edits.
Remaining cutover work¶
- Grow the 243/529 semantic baseline across constants, typed/named overload restrictions, free variables, control-flow narrowing, blocks, and remaining diagnostics; keep every non-matching case explicitly deferred.
- Feed
SemanticSnapshottypes/bindings into completion, hover, navigation, and call resolution under shadow comparison, then retire matching Psi heuristics slice by slice. - Extend call-graph differential coverage across overloads, dynamic receivers, and representative workspaces, then retire its legacy Crystal fallback.
- Compare Facet-first public LSP results on stdlib and representative workspaces, not only focused declaration contracts.
- Supply the fully covered macro gate's remaining type/annotation context from
the live require-aware index; then remove the cr-analyzer interpreter and all
compiler/crystal/syntaxrequires after shadow validation.
Run the local declaration gate after semantic changes:
crystal run scripts/check_facet_semantic_parity.cr
Run the LSP cutover gate without constructing any Crystal AST:
CRA_FACET_ONLY=1 crystal spec spec/cra/workspace
After a release build, compare end-to-end initialization with and without the temporary Crystal frontend:
python3 scripts/bench_lsp_initialize.py --repeat 3
Local release-build baseline on 2026-08-30, scanning this repository and the installed stdlib after one warmup per mode (three alternating samples): legacy initialization was 6.180-7.021 s, median 6.400 s; Facet-only was 3.516-4.758 s, median 3.637 s. The median improvement was 1.76x. Treat these as local orientation rather than portable throughput; the script reports every sample so changes can be compared under the same environment.
Ownership¶
Facet owns source text, revisions, lexing, parsing, syntax diagnostics, native AST queries, macro expansion, and expansion provenance. cr-analyzer owns LSP transport, workspace policy, the editor semantic model, and protocol results.