Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
77cb7d4
chore(drive): symbol-table-resolve project artifacts (spec, plan, sli…
SevInf Sep 18, 2026
03e020c
feat(psl-parser): cache red child wrappers so tree positions have sta…
SevInf Sep 18, 2026
1890f0f
refactor(psl-parser): drop the childAt doc block
SevInf Sep 18, 2026
000f4f2
feat(psl-parser): add the eager binder's declaration and type-referen…
SevInf Sep 18, 2026
6236861
refactor(psl-parser): pin the unresolved-reference code to the contri…
SevInf Sep 18, 2026
c2947fb
feat(psl-parser): resolve attribute names and reference arguments in …
SevInf Sep 18, 2026
4cce74b
fix(psl-parser): stay silent for a referenced-field slot with no decl…
SevInf Sep 18, 2026
e0f9272
feat(psl-parser): export the binder and build attribute contexts from it
SevInf Sep 18, 2026
40c35a0
chore(drive): binder-core slice artifacts through D4
SevInf Sep 18, 2026
50d793f
chore(drive): record base-merge rebase and gate updates
SevInf Sep 18, 2026
49c0339
feat(psl-parser): let the reference combinators speak with the binder…
SevInf Sep 18, 2026
42ca7dd
docs(psl-parser): state the binder-on-context snapshot precondition
SevInf Sep 18, 2026
1cdf68a
chore(drive): binder-core D5 amendments and trace
SevInf Sep 18, 2026
75761fc
feat(psl-parser): make the binder the only resolution path for refere…
SevInf Sep 18, 2026
f01bc72
feat(sql,mongo): build a binder per interpretation and let it voice r…
SevInf Sep 18, 2026
93178eb
fix(mongo,psl-parser): leave one voice per resolution failure in Mongo
SevInf Sep 18, 2026
955d1b0
chore(drive): binder-core D6 artifacts through slice close
SevInf Sep 18, 2026
461e511
refactor(psl-parser): name the reason oneOf can withhold its own diag…
SevInf Sep 21, 2026
13392bb
refactor(mongo): look attribute-spec factories up without casting
SevInf Sep 21, 2026
a9ca365
refactor(psl-parser): give the attribute-context inputs a shared base
SevInf Sep 21, 2026
f516a4e
perf(psl-parser): fill red child slots one index at a time
SevInf Sep 21, 2026
99fd8a7
fix(sql): let the uncomposed-namespace diagnostic speak alone
SevInf Sep 21, 2026
e1eafbd
refactor(psl-parser): say why a parse can fail without complaining, once
SevInf Sep 21, 2026
7b790f8
fix(psl-parser): make a binder from the wrong snapshot fail loudly
SevInf Sep 21, 2026
296aee7
refactor(utils,psl-parser): accumulate parse failures by conjunction
SevInf Sep 21, 2026
a7e66d3
refactor(utils,psl-parser): fold oneOf's alternatives by disjunction
SevInf Sep 21, 2026
e726dca
docs(psl-parser): say where the universe scope gets its name
SevInf Sep 21, 2026
f8fe065
chore(drive): review-round records — 10(B) correction, confinement no…
SevInf Sep 21, 2026
55ba8b2
refactor(psl-parser): name the third scope for what it holds
SevInf Sep 22, 2026
5485c0f
refactor(utils): drop the doc blocks added with and/or
SevInf Sep 22, 2026
88f359c
refactor(psl-parser): seed oneOf's fold with its first alternative
SevInf Sep 22, 2026
2d23e68
test(utils): pin the conjunction and disjunction laws
SevInf Sep 22, 2026
042cbd1
chore(drive): rename contributed-type scope across project records
SevInf Sep 22, 2026
20f5aa8
refactor(psl-parser): let the binder take the real attribute-spec nam…
SevInf Sep 22, 2026
b259dda
refactor(sql,mongo): pass the attribute specs straight to the binder
SevInf Sep 22, 2026
2b53c31
feat(psl): let the binder voice unsupported attribute names through a…
SevInf Sep 22, 2026
256e79b
refactor(sql,mongo): voice unsupported attributes from the binder cal…
SevInf Sep 22, 2026
cf1fb19
chore(drive): record callback-inversion decision and round learnings
SevInf Sep 22, 2026
997791e
refactor(psl): name the binder walk set entities, not owners
SevInf Sep 22, 2026
239a22f
chore(drive): align spec pseudo-code with entity vocabulary
SevInf Sep 22, 2026
f5ae8b7
fix(psl): build every symbol-table record without a prototype
SevInf Sep 22, 2026
04c1ec7
chore(drive): record kind-blind scoping decree (decision 12)
SevInf Sep 22, 2026
d9b992a
fix(psl): assemble a spec named-parameter record without a prototype
SevInf Sep 22, 2026
038a967
refactor(psl): resolve every reference through one kind-blind scope c…
SevInf Sep 22, 2026
9708de6
fix(sql): give the binder the contributed model-attribute specs
SevInf Sep 24, 2026
4920479
Merge origin/main into binder-core
SevInf Sep 24, 2026
b8332ec
refactor(psl): one entity resolver behind main selector surface
SevInf Sep 24, 2026
7ce84ab
test(mongo): bring the interpreter fixtures onto the merged surfaces
SevInf Sep 24, 2026
ea2b46b
chore(drive): record convergence reconciliation rulings (decision 13)
SevInf Sep 24, 2026
eeee7b6
refactor(psl): name the pre-binder entity lookup after its symbols
SevInf Sep 24, 2026
275aa41
Merge origin/main into binder-core (round 2)
SevInf Sep 24, 2026
333a729
test(mongo): hand the single-voice harness a document list
SevInf Sep 24, 2026
a59e59e
refactor(psl): read an entity reference the way a field reference reads
SevInf Sep 24, 2026
1d29973
refactor(psl): scopes hold their parent and answer a lookup themselves
SevInf Sep 24, 2026
f687eed
chore(drive): record scope-class blueprint supersession (decision 12)
SevInf Sep 24, 2026
d7fc587
refactor(psl): an entity reference is resolved only by the binder
SevInf Sep 24, 2026
3f4b371
chore(drive): record pre-binder path amputation (decision 13)
SevInf Sep 24, 2026
d616df5
refactor(psl): the binder pushes and pops the scope it stands in
SevInf Sep 24, 2026
1c32ec8
chore(drive): record the explicit-stack correction (decision 12)
SevInf Sep 24, 2026
f568428
refactor(psl): drop the attribute-ctx builders and unexport the type-…
SevInf Sep 24, 2026
d91af71
fix(psl): the binder is required again, and the compiler says so
SevInf Sep 24, 2026
64438f0
refactor(psl): oneOf tries its alternatives in a plain loop
SevInf Sep 24, 2026
765bac2
chore(drive): record 10(B) dissolution and DoD amendment
SevInf Sep 24, 2026
26d59e2
docs(psl-parser): say why the binder walks entities twice
SevInf Sep 25, 2026
55b05b8
docs(psl-parser): tighten the two-walk comment
SevInf Sep 25, 2026
f28a06d
docs(psl-parser): rephrase the two-walk comment
SevInf Sep 25, 2026
c3ec80c
docs(psl-parser): plain wording for the two-walk comment
SevInf Sep 25, 2026
178a92a
chore(drive): decree completion-on-binder for the lsp slice
SevInf Sep 25, 2026
adfa0d3
Merge origin/main into binder-core (round 3)
SevInf Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type { NotOk, Ok, Result } from '../result';
export { notOk, ok, okVoid } from '../result';
export { and, notOk, ok, okVoid, or } from '../result';
19 changes: 19 additions & 0 deletions packages/1-framework/0-foundation/utils/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,22 @@ const OK_VOID: Ok<void> = ResultImpl.ok<void>(undefined);
export function okVoid(): Ok<void> {
return OK_VOID;
}

export function or<T, U, E>(
left: Result<T, readonly E[]>,
right: Result<U, readonly E[]>,
): Result<T | U, readonly E[]> {
if (left.ok) return left;
if (right.ok) return right;
if (left.failure.length === 0 || right.failure.length === 0) return notOk([]);
return notOk([...left.failure, ...right.failure]);
}

export function and<T, U, E>(
left: Result<T, readonly E[]>,
right: Result<U, readonly E[]>,
): Result<void, readonly E[]> {
if (left.ok) return right.ok ? okVoid() : notOk(right.failure);
if (right.ok) return notOk(left.failure);
return notOk([...left.failure, ...right.failure]);
}
60 changes: 59 additions & 1 deletion packages/1-framework/0-foundation/utils/test/result.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { type NotOk, notOk, type Ok, ok, okVoid } from '../src/result';
import { and, type NotOk, notOk, type Ok, ok, okVoid, or } from '../src/result';

describe('result', () => {
describe('ok()', () => {
Expand Down Expand Up @@ -92,4 +92,62 @@ describe('result', () => {
);
});
});

describe('and()', () => {
it('is ok when both sides are ok', () => {
expect(and(ok(1), ok('two'))).toMatchObject({ ok: true });
});

it('takes the failure when the right side fails', () => {
expect(and(ok(1), notOk(['right']))).toMatchObject({ ok: false, failure: ['right'] });
});

it('takes the failure when the left side fails', () => {
expect(and(notOk(['left']), ok(1))).toMatchObject({ ok: false, failure: ['left'] });
});

it('keeps both sides details in order when both fail', () => {
expect(and(notOk(['left']), notOk(['right']))).toMatchObject({
ok: false,
failure: ['left', 'right'],
});
});

it('keeps a detail-less failure a failure', () => {
expect(and(ok(1), notOk([]))).toMatchObject({ ok: false, failure: [] });
});
});

describe('or()', () => {
it('takes the left value when both sides are ok', () => {
expect(or(ok('left'), ok('right'))).toMatchObject({ ok: true, value: 'left' });
});

it('takes the right value when only the right side is ok', () => {
expect(or(notOk(['left']), ok('right'))).toMatchObject({ ok: true, value: 'right' });
});

it('takes the left value when only the left side is ok', () => {
expect(or(ok('left'), notOk(['right']))).toMatchObject({ ok: true, value: 'left' });
});

it('pools both sides details in order when both fail loudly', () => {
expect(or(notOk(['left']), notOk(['right']))).toMatchObject({
ok: false,
failure: ['left', 'right'],
});
});

it('lets a detail-less failure absorb the left side', () => {
expect(or(notOk([]), notOk(['right']))).toMatchObject({ ok: false, failure: [] });
});

it('lets a detail-less failure absorb the right side', () => {
expect(or(notOk(['left']), notOk([]))).toMatchObject({ ok: false, failure: [] });
});

it('stays detail-less when both sides are detail-less', () => {
expect(or(notOk([]), notOk([]))).toMatchObject({ ok: false, failure: [] });
});
});
});
95 changes: 95 additions & 0 deletions packages/1-framework/2-authoring/psl-parser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ In the provider-based authoring model, PSL providers call `parse` to obtain the
- Parse attributes generically (namespaced or not), including optional argument lists; target semantics live downstream.
- Emit attribute nodes with explicit target (`field` / `model` / `namedType`), attribute name, and parsed argument list with spans.
- Build a scope-aware symbol table from the CST, including duplicate-declaration diagnostics, named-type binding resolution, and descriptor-driven generic-block reconstruction.
- Answer "which declaration does this name denote" for every consumer, once, through the binder — the sole voice of resolution failures.

## Attributes (generic parsing boundary)

Expand All @@ -40,6 +41,8 @@ Interpretation/validation (for example `@internal/sql-contract-psl`) is responsi

- `parse(source, filename, options?)` in `src/parse.ts` (also at `@internal/psl-parser/syntax`) — the CST parser: returns the `DocumentAst`, a `PslSources` registry for resolving nodes to their named `SourceFile`, and syntactic diagnostics. The recursive-descent / lossless-CST path supersedes the legacy `parsePslDocument`.
- `buildSymbolTable({ documents, sources, pslBlockDescriptors })` in `src/symbol-table.ts` — a pure, fault-tolerant pass over an ordered `readonly DocumentAst[]` that returns `{ symbolTable, diagnostics }`, with a scope-aware `SymbolTable` (top-level namespaces / named types / blocks / models / composite-types as keyed records discriminated by `kind`, namespace members and block fields nested under their owner, declaration symbols carrying their CST AST `node` plus declaration `span`, and namespace symbols retaining every authored node and span in `declarations`) plus its own source-associated diagnostics (the same `ParseDiagnostic` shape as parser errors: `filename`, `code`, `message`, and a file-local `range`). Duplicate names are first-wins across documents and kinds within one scope (`PSL_DUPLICATE_DECLARATION`); repeated namespaces reopen the same scope, retaining distinct members and diagnosing duplicate member names across declarations and documents. Every supplied document root must be registered in the shared `sources`, even for empty documents. An empty collection returns an empty scope. Single-file callers pass `documents: [document]`; no file discovery is performed. `pslBlockDescriptors` is supplied from authoring contributions so generic/extension blocks can be reconstructed once into `BlockSymbol.block`. The pass also **resolves** the field/named-type read set once: each `FieldSymbol` carries the split type (`typeName`/`typeNamespaceId`/`typeContractSpaceId`), `optional`/`list`, `typeConstructor?`, rendered `attributes`, and `malformedType?` (set, with a `PSL_INVALID_QUALIFIED_TYPE` diagnostic, when the type is over-qualified); `NamedTypeSymbol` carries the resolved binding (`baseType`/`typeConstructor`/`isConstructor`). Interpreters consume this resolved shape directly — there is no per-package field/attribute view layer.
- `createBinder({ sources, symbolTable, typeConstructors, attributeSpecs })` in `src/binder.ts` — the name resolver. It returns `{ binder, diagnostics }`, mirroring `buildSymbolTable`: resolution runs eagerly over the symbol table at creation, and the returned diagnostics are complete when the factory returns. Queries are map reads and say nothing about when resolution ran. See [the binder section below](#binder).
- `referencedModel` / `modelAttributeContext` / `fieldAttributeContext` in `src/binder-context.ts` — build the ADR 249 parse-time attribute contexts from a binder. `resolveReferencedModel` becomes one map read (`symbolForNode(typeReferenceNode(field))`, narrowed to a model) instead of a resolver each consumer supplies for itself, and the context carries the binder itself so the reference combinators stop raising their own existence diagnostics. See [attribute contexts and the single voice](#attribute-contexts-and-the-single-voice).
- `readResolvedAttribute(s)` / `readResolvedConstructorCall` + the span maps (`nodePslSpan`, `keywordPslSpan`) in `src/resolve.ts` — the shared CST read helpers `buildSymbolTable` uses and that consumers (e.g. enum-block reconstruction) reuse, with `PslSpan` spans derived from `PslSources`. Pure coordinate conversion lives on `SourceFile`: resolve the file with `sources.sourceFileFor(node.syntax)` and call `sourceFile.rangeToPslSpan(range)`, `sourceFile.offsetToPslPosition(offset)`, or `sourceFile.pslSpanToRange(span)`.
- `reconstructExtensionBlock` / `findBlockDescriptor` /
`validateExtensionBlockFromSymbol` in `src/extension-block.ts` — reconstruct a
Expand All @@ -52,6 +55,91 @@ Interpretation/validation (for example `@internal/sql-contract-psl`) is responsi
- `@internal/psl-parser/syntax`
- `@internal/psl-parser/tokenizer`

## Binder

The binder is the single authority on which declaration a name denotes. Every consumer asks it rather than scanning the symbol table itself, so one scoping rule and one diagnostic voice serve the SQL and Mongo interpreters, the attribute-spec contexts, and the language server alike.

```ts
const { binder, diagnostics } = createBinder({
sources,
symbolTable,
typeConstructors,
attributeSpecs,
});

binder.declaredSymbol(modelDeclarationNode); // declaration node -> the symbol it declares
binder.symbolForNode(typeReferenceNode(field)); // reference node -> what it denotes
```

The two questions are kept apart deliberately, as Roslyn separates `GetDeclaredSymbol` from `GetSymbolInfo`: `declaredSymbol` answers for the node that *introduces* a name, `symbolForNode` for a node that *mentions* one.

### Scope chain

An unqualified reference resolves in exactly this order:

1. the **declaring namespace** — the namespace the referring declaration itself sits in;
2. the **top level**;
3. the **contributed types** — the type-position names the configured target and its extensions contribute, built from the injected `typeConstructors` registry.

That third scope holds names nobody declared in a schema: the scalars, type constructors and field presets a target and its composed extension packs bring, in contrast with the models, composite types and named types the documents themselves declare.

**Sibling namespaces are never consulted.** A schema declaration shadowing a contributed type (a `model Uuid` over a contributed `Uuid`) wins **silently** — shadowing is not a diagnostic. A qualified `ns.Name` is looked up in that PSL namespace, then in the type-constructor namespace of the same name (`pgvector.Vector`), and nowhere else.

Qualified references resolve at whole-`QualifiedName` granularity: in `app.Item`, the segments `app` and `Item` do not resolve separately — the one `QualifiedName` node carries the one resolution.

### Resolution kinds

`symbolForNode` returns `undefined` for a node that is not a reference the binder tracks, and otherwise one of:

| Kind | Denotes |
| --- | --- |
| `model` / `compositeType` / `namedType` / `block` | a user declaration; `block` covers `enum` and every other descriptor-driven block, which may be a field's type but never an `@@base` target |
| `contributedType` | a scalar, type constructor or field preset from the injected registry |
| `field` | a field named by an attribute argument (`@@index([a])`, `@relation(fields:, references:)`) |
| `attributeSpec` | the spec an attribute's name denotes |
| `crossSpace` | a reference into another contract space, resolvable only where that space is known — an explicit kind, and deliberately **not** a diagnostic |
| `unresolved` | nothing of that name is in scope; the binder has emitted a diagnostic for it |

A field whose type is malformed (`malformedType`) is skipped entirely: no resolution, no diagnostic, no cascade.

### Diagnostics

The binder owns resolution failures and nothing else. Failures come back under `PSL_UNRESOLVED_REFERENCE` (an unknown type, field, or entity name) and `PSL_UNRESOLVED_ATTRIBUTE` (an unknown attribute name), located by filename and range through `PslSources`. Converted consumers adopt these codes and **never re-emit their own** — the same rule the symbol table set for `PSL_DUPLICATE_DECLARATION`. Shape failures (arity, argument type, malformed literals) remain the spec combinators' voice; they are not resolution. References bind to first-wins symbols, and the binder never restates a duplicate-declaration diagnostic the symbol table already made.

### Attribute contexts and the single voice

`modelAttributeContext` / `fieldAttributeContext` put the whole `Binder` on the parse-time context. `ModelAttributeCtx` **requires** it, so every context that can reach a reference combinator carries one by construction — there is no binder-less path to fall back to and no dual behavior to reason about.

`fieldRef` and `referencedFieldRef` resolve solely through it: they read the argument's resolution out of the binder (`symbolForNode(argumentNode)` — a map read of results already computed at creation, never a second resolution) and

- return the bound field's name when the binder resolved a field;
- return the written name for a `crossSpace` reference, which is deferred by design;
- **fail the argument, carrying no diagnostics of their own**, when the binder bound nothing or bound something that is not a field. The binder has already reported that name as `PSL_UNRESOLVED_REFERENCE`, so a second complaint would be a duplicate. A failed argument fails its attribute rather than quietly yielding a short list or a missing key.

`entityRef` is unchanged: it never checked existence, so it still returns the written name and leaves the verdict to the binder's diagnostics and to downstream lowering.

Shape and arity stay the combinator's voice — "Expected a field name", "Expected a list of field name", wrong argument counts. Only *existence* belongs to the binder. The split is the point: resolution is the binder's, shape is the spec's, and no schema error is ever reported twice.

`AttributeCtx` itself stays binder-free: block attributes are interpreted during `buildSymbolTable`, before a binder can exist, and no block attribute takes a reference argument.

This lookup rests on red-node identity (below): the combinator receives the very `SyntaxNode` the binder keyed its result under.

**Precondition, enforced.** The binder on the context must be built over the *same snapshot* — the same symbol table and `PslSources` — and the same `typeConstructors` / `attributeSpecs` registries as the interpretation consuming it.

The binder records what it examined, including its failures: a reference it could not resolve gets an explicit `unresolved` entry rather than no entry at all. So for a node in reference position, an absent entry cannot mean "the author made a mistake" — it can only mean this binder never saw this tree. The reference combinators therefore **throw an `InternalError`** on a missing entry instead of quietly skipping the check. A mismatched binder fails loudly at the first reference argument rather than silently forgoing existence checking across the whole document.

Fields whose type is malformed are the one deliberate absence: the binder does not examine them, and no combinator reads a type node.

### Snapshot lifetime

The binder is snapshot-scoped: an edit produces a new document, symbol table, and binder, and the old set is dropped whole. There is no invalidation protocol.

The contributed-type scope is the exception — it is configuration-derived, not document-derived, and is shared across snapshots. That sharing is keyed by the **object identity of the `typeConstructors` registry** the caller passes: pass the same registry object and two binders share one scope; rebuild the registry on every parse and sharing silently degrades to a per-snapshot scope. Resolution stays correct either way, but the guarantee is gone, so hold the registry alongside the configuration it came from.

### Node identity

Binder side tables are keyed by red `SyntaxNode` identity, which the red layer guarantees within a snapshot: `SyntaxNode.childAt(index)` caches each child wrapper in its parent's slot on first access, so every traversal reaching the same position — `children()`, `firstChild`, `nextSibling`, `ancestors()`, `tokenAtOffset`, `coveringElement` — returns the identical object (Roslyn's `GetRed` design, single-threaded). Red nodes are therefore sound `WeakMap` keys. Green nodes are not: they are position-free and shareable, so a green-keyed cache would go stale silently. Never key a cache on a green node, and never use a span as a cross-snapshot key.

## Architecture

```mermaid
Expand All @@ -64,9 +152,16 @@ flowchart LR
Descriptors[pslBlockDescriptors] --> Symbols
Symbols --> SymbolTable[SymbolTable]
Symbols --> SymbolDiagnostics[Symbol-table diagnostics]
SymbolTable --> Binder[createBinder]
TypeConstructors[typeConstructors] --> Binder
AttributeSpecs[attributeSpecs] --> Binder
Binder --> BinderQueries[declaredSymbol / symbolForNode]
Binder --> BinderDiagnostics[Resolution diagnostics]
SymbolTable --> Interpreter[Target PSL interpreter]
BinderQueries --> Interpreter
ParseDiagnostics --> Provider[Provider diagnostic seeding]
SymbolDiagnostics --> Provider
BinderDiagnostics --> Provider
```

## Package Boundaries
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { InternalError } from '@internal/utils/internal-error';
import { notOk, ok, type Result } from '@internal/utils/result';
import type { PslDiagnostic } from '../../diagnostic';
import type {
DeclarationFor,
EntitySelector,
ResolvedEntityReference,
} from '../../entity-reference';
import { resolveEntityReference } from '../../entity-reference';
import { describeResolution, entityReference, matchesSelector } from '../../entity-reference';
import { IdentifierAst } from '../../syntax/ast/identifier';
import type { AttributeCtx, EntityRefArgType } from '../types';
import type { EntityRefArgType, ModelAttributeCtx } from '../types';
import { leafDiagnostic } from './diagnostic';

function unbound(name: string): never {
throw new InternalError(
`The binder on this attribute context bound nothing for "${name}". A reference argument is always examined, so the binder must be built over the same snapshot - the same symbol table and sources - as the interpretation consuming it.`,
);
}

export function entityRef<const S extends EntitySelector>(
expected: S,
): EntityRefArgType<DeclarationFor<S>, AttributeCtx> {
): EntityRefArgType<DeclarationFor<S>, ModelAttributeCtx> {
const label = `${expected.kind === 'block' ? expected.keyword : expected.kind} reference`;
return {
kind: 'entityRef',
Expand All @@ -26,28 +33,19 @@ export function entityRef<const S extends EntitySelector>(
if (name === undefined) {
return notOk([leafDiagnostic(ctx, arg, `Expected ${label}`)]);
}
const reference = resolveEntityReference(arg, name, ctx.symbols);
if (reference === undefined) {
return notOk([leafDiagnostic(ctx, arg, `Unknown ${label} "${name}"`)]);
}
if (!matchesSelector(reference, expected)) {
const actual = reference.declaration;
const kind = actual.kind === 'block' ? actual.keyword : actual.kind;
return notOk([leafDiagnostic(ctx, arg, `Expected ${label} "${name}", found ${kind}`)]);
const resolution = ctx.binder.symbolForNode(arg.syntax) ?? unbound(name);
if (resolution.kind === 'unresolved') return notOk([]);
const reference = entityReference(resolution);
if (reference === undefined || !matchesSelector(reference, expected)) {
return notOk([
leafDiagnostic(
ctx,
arg,
`Expected ${label} "${name}", found ${describeResolution(resolution)}`,
),
]);
}
return ok(reference);
},
};
}

function matchesSelector<S extends EntitySelector>(
reference: ResolvedEntityReference,
expected: S,
): reference is ResolvedEntityReference<DeclarationFor<S>> {
const declaration = reference.declaration;
return (
declaration.kind === expected.kind &&
(expected.kind !== 'block' ||
(declaration.kind === 'block' && declaration.keyword === expected.keyword))
);
}
Loading
Loading