Reference

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 lines
psy inspect --ast spec/framework.psy

IR

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.json

Provenance

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  unresolved

Framework

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

PackageContents
@psy/corelexer.ts, parser.ts, ast.ts, binder.ts, resolver.ts, checker.ts, ir.ts, provenance.ts, format.ts, lint.ts
@psy/frameworkthe types a framework or adapter consumes
@psy/configpsy.config.ts loading and typing
@psy/morphicthe skill / agent / command vocabulary
@psy/adapter-claudeClaude Code output
@psy/adapter-dataJSON, YAML and TOML output
@psy/clithe 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.

On this page