Skip to content

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 NodeRef handles, interned TypeId values, 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; on is 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.cr corpus 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 record have 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 support yield, block.body, and block.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, indexed resolve/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_error calls, four nested parse_type failures, 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, and global?. Recursive control-flow structure exposes case/select conditions, branches and exhaustive flags plus exception bodies, rescue names/types, else, and ensure. Function declaration views cover Def, Macro, FunDef, nested Arg values, signatures, bodies, flags, visibility, and external names. Type declaration views cover ClassDef, ModuleDef, EnumDef, AnnotationDef, LibDef, and CStructOrUnionDef, including bodies, inheritance/base types, generic parameters, splats, and declaration flags. Inline assembly views cover Asm and nested AsmOperand values, including text, operands, constraints, clobbers, and option flags; type-syntax views cover TypeDeclaration, ProcNotation, Metaclass, Generic, Union, and Path, including resolve/resolve? behavior. Expression views cover ProcLiteral, 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 explicit MacroExpansionContext inputs and participate in cache fingerprints; Facet does not execute arbitrary shell commands. The contextual gate also replays 106 structured TypeNode snapshots 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; production QueryDb#expand continues 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

  1. 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.
  2. Feed SemanticSnapshot types/bindings into completion, hover, navigation, and call resolution under shadow comparison, then retire matching Psi heuristics slice by slice.
  3. Extend call-graph differential coverage across overloads, dynamic receivers, and representative workspaces, then retire its legacy Crystal fallback.
  4. Compare Facet-first public LSP results on stdlib and representative workspaces, not only focused declaration contracts.
  5. 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/syntax requires 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.