Compiler And Framework Types
The AST, the IR, provenance, and the types a framework or target consumes.
The type definitions, for building on top. Under The Hood explains what they are for.
AST
The AST preserves source syntax and source ranges. It represents imports, exports,
re-exports, use, constants, specs, abstract specs, as, abstract properties,
inheritance, properties, lists, objects, references, super, +, ++,
interpolation, and comments and source positions.
interface Trivia {
leadingComments: Comment[];
trailingComment: Comment | null;
blankLinesBefore: number;
blankLinesAfterComments: number;
}interface SpecDeclarationNode extends Trivia {
kind: "SpecDeclaration";
exported: boolean;
abstract: boolean;
declarationKeyword: IdentifierNode | null; // `skill Foo:`
name: IdentifierNode;
bases: IdentifierNode[];
exposedKeyword: IdentifierNode | null; // `as skill`
members: SpecMember[];
range: Range;
}type ValueNode =
| StringValueNode // style: "bare" | "quoted" | "block"
| NumberValueNode
| BooleanValueNode
| NullValueNode
| IdentifierValueNode // ambiguous: string or reference
| ReferenceValueNode
| SuperValueNode
| ListValueNode // inline
| ObjectValueNode // inline
| BlockValueNode;interface BlockValueNode {
kind: "BlockValue";
blockKind: "object" | "list" | "string" | "compose";
operations: BlockOperation[];
range: Range;
}
type BlockOperation =
| AssignOperation // key: value
| ItemOperation // - value
| ComposeOperation // + value / ++ value
| TextOperation; // raw text linespsy inspect --ast spec/framework.psyIR
Fully resolved. No unresolved references. The only thing frameworks and adapters consume.
interface SpecIR {
id: string; // "<module path>#<name>"
name: string;
module: string;
keyword?: string; // declared with a framework keyword
exposedKeyword?: string; // exposes a keyword via `as`
abstract: boolean;
exported: boolean;
bases: string[];
baseIds: string[];
linearization: string[]; // most specific first, including this spec
properties: Record<string, PropertyIR>;
abstractProperties: Record<string, AbstractPropertyIR>;
source: SourceLocation;
}interface PropertyIR {
name: string;
value: ValueIR;
declaredIn: string; // the spec that supplied the definition
source: SourceLocation;
provenance: Provenance;
}
type ValueIR =
| { kind: "string"; value: string }
| { kind: "number"; value: number }
| { kind: "boolean"; value: boolean }
| { kind: "null" }
| { kind: "list"; items: ValueIR[] }
| { kind: "object"; entries: Record<string, ValueIR> }
| { kind: "specRef"; name: string; specId: string };Every ValueIR also carries provenance.
interface ModuleIR {
path: string;
uses: string[];
imports: ImportIR[];
exports: string[];
keywords: Record<string, string>; // keyword -> spec id
specs: SpecIR[];
consts: ConstIR[];
}
interface ProgramIR {
modules: ModuleIR[];
specs: SpecIR[];
consts: ConstIR[];
}psy inspect --ir
psy inspect --ir --json > ir.jsonProvenance
interface Provenance {
kind: ProvenanceKind;
file: string;
range: Range;
spec?: string;
property?: string;
detail?: string;
contributors?: Provenance[]; // in precedence order
overridden?: Provenance[]; // values this one replaced
}Kinds:
literal interpolation reference self super inherit
assign item compose concat block unresolvedFramework
interface Framework {
name: string;
keywords?: readonly string[];
validate?(context: FrameworkContext): void;
}
interface FrameworkContext {
program: ProgramIR;
report(diagnostic: FrameworkDiagnostic): void;
specsByKeyword(keyword: string): SpecIR[];
inherits(spec: SpecIR, baseName: string): boolean;
}Target
interface Target {
name: string;
output: string; // relative to the root
generate(context: TargetContext): GeneratedFile[];
}
interface GeneratedFile {
path: string; // relative to the target's output directory
contents: string;
}Every generated file should carry PSY_GENERATED_MARKER ("Generated by Psy").
psy build uses it to recognise — and prune — artifacts it previously wrote.
Value helpers
getProperty(spec, name): ValueIR | undefined
getString(spec, name): string | undefined
getNumber(spec, name): number | undefined
getBoolean(spec, name): boolean | undefined
getStringList(spec, name): string[] | undefined
getSpecRefs(spec, name): { name: string; specId: string }[]getSpecRefs is the one that matters for framework vocabularies: it turns
use: [ReadPsySpec, RunTests] into the referenced spec names and ids, rather than
a list of strings you have to hope are names.
The boundary
A framework may validate domain semantics, inspect exported specs, generate output, and produce diagnostics.
A framework may not alter Psy parsing, alter core resolution semantics, introduce declaration keywords, or inject symbols.
Enforced by construction: @psy/framework exposes no route to the lexer, the
parser or the resolver. See Your Own Keywords.
Packages
| Package | Contents |
|---|---|
@psy/core | lexer.ts, parser.ts, ast.ts, binder.ts, resolver.ts, checker.ts, ir.ts, provenance.ts, format.ts, lint.ts |
@psy/framework | the types a framework or adapter consumes |
@psy/config | psy.config.ts loading and typing |
@psy/morphic | the skill / agent / command vocabulary |
@psy/adapter-claude | Claude Code output |
@psy/adapter-data | JSON, YAML and TOML output |
@psy/cli | the psy command line |
Host abstraction
All filesystem access goes through a CompilerHost, which lets the test suite
drive the compiler from in-memory files.
Paths handed to the rest of the compiler are always display paths: POSIX style, relative to the project root. That is what makes diagnostics and generated artifacts byte-for-byte deterministic across machines.