compile

package
v0.0.0-...-8d9931c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package compile holds the state every spec compiler needs and the invariants that state carries, so each compiler does not reimplement them.

It owns what every compiler must agree on, and nothing else. It imports only ir.

  • The type registry with the source-coordinate map that keeps invariant 3 true (stable IDs; one node per source coordinate), and the namespace rule that comes with it: a node a lowering mints takes a namespace no source coordinate addresses.
  • Diagnostic accumulation with identity dedup.
  • The canonical naming grammar, which invariant 4 makes a property of the IR rather than of a compiler, and with it the name minted for an entity the source left unnamed: no compiler may emit a node nothing can name.
  • The identifier grammar: the kind prefix that opens an ID and the namespace that follows it.

The package boundary is the point. Architecture tests assert that nothing but this package and ir writes an ir.TypeRegistry or derives a canonical name, and that no compiler builds an ID out of a string — that last one is asked of the compilers alone, because re-typing an ID that already exists is legitimate above them. Rules like those are inexpressible without an outside, which is why a package this small is worth its own directory.

What deliberately stays with the compiler: ir.Document assembly (seventeen of its eighteen fields carry no framework invariant), recursion depth bounds (the right cap for a JSON Schema walk is not the right one for an SDL walk), and the derivation an ID's path comes from — a JSON Pointer, a GraphQL structural path and a protobuf fully-qualified name are different things, and the format that computes one is the only place that can. Promoting a borderline item here later is additive; demoting one is a breaking change across every compiler, so borderline items start outside.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AuthID

func AuthID(space Space, path string) ir.AuthID

AuthID returns the ID of the security scheme at path within space.

func NamingFor

func NamingFor(source string) ir.Naming

NamingFor builds the neutral Naming of a name the source declares: the spelling it used, plus the canonical word sequence derived from it.

It is the constructor to reach for wherever a source name becomes an IR name, because the pairing is the invariant — a Source with no Canonical leaves an emitter to segment the spelling itself, which is the casing decision invariant 4 moves out of the compilers.

A source name that is the empty string declares no spelling to pair with, so it yields a minted hint instead: the entity exists and has to be nameable. A spelling made only of non-word characters ("***") does not come through here — it keeps its Source and canonicalizes to no words at all, which is a name an emitter can still escape from rather than the absence of one.

func NamingHint

func NamingHint(hint string) ir.Naming

NamingHint builds the Naming of an entity nothing declared a name for, from the context-derived hint an emitter should synthesize one from.

It is the second half of the same invariant NamingFor holds, and holds it the same way. A hint is the only name an anonymous entity carries, so it is what an emitter renders that entity's identifier from — the job Canonical does for a declared name — and it is therefore neutral words here too (invariant 4). That matters because a hint is nearly always derived from something a source *did* spell: a component key, an operationId, a header name, a $ref target. Passing those through carried their casing and their punctuation into the one channel no rule was holding (GitHub #54).

A position that carries no name of its own derives an empty hint, and a spelling with no word rune in it ("***") derives no words; both leave the node with no name in any channel, so both are minted one here rather than at every caller.

func OpID

func OpID(space Space, path string) ir.OpID

OpID returns the ID of the operation at path within space.

func PropID

func PropID(space Space, path string) ir.PropID

PropID returns the ID of the property at path within space.

func ServiceID

func ServiceID(space Space, path string) ir.ServiceID

ServiceID returns the ID of the service at path within space.

func SubHint

func SubHint(parent, suffix string) string

SubHint composes the hint of a node named after its position inside another — a list's element, a map's value, a composed variant, an enum branch — out of the enclosing node's hint and the role or index that distinguishes it.

Composing by hand is what NamingHint cannot protect: "" + "_item" is "_item", which is non-empty, so the presence rule passes it, and which is a leading separator no grammar produces. Neutralizing each half first makes the child agree with the node it hangs off, "empty_item" under "empty", rather than leaking the emptiness one level down.

Both halves go through the same minting because either can arrive from a source spelling: the enclosing position's name, and the $ref target a composition branch takes its role from. Two neutral words joined by a single "_" are a neutral word sequence again, which is what lets a composed hint be fed back in as the parent of the next one.

func TypeID

func TypeID(space Space, path string) ir.TypeID

TypeID returns the ID of the type at path within space.

The path is the compiler's own derivation and stays there: a JSON Pointer, a GraphQL structural path and a protobuf fully-qualified name are different things, and nothing here can compute them. What the framework fixes is the grammar around the path — the kind prefix, the space, and the separator between them.

Types

type Diags

type Diags struct {
	// contains filtered or unexported fields
}

Diags accumulates a compile's diagnostics, dropping any whose full identity — severity, code, message and provenance — repeats one already recorded.

Dedup is what makes lowering a referenced component once at its declaration safe to report on: every use site then produces an identical diagnostic, and the second copy tells a reader nothing the first did not. Because identity includes provenance, two positions that genuinely differ still both surface; this collapses repeats, never distinct findings. The key is the whole value, as it is in the engine's merge, so a field Diagnostic gains joins the identity without an edit here.

Suppression that is broader than identity — silencing a whole pointer once any diagnostic lands there — is compiler policy rather than a framework guarantee, and stays with the compiler that wants it.

The zero value is ready to use. It is single-compile state and is not safe for concurrent use.

func (*Diags) Append

func (d *Diags) Append(x ir.Diagnostic)

Append records d unless one identical to it was already recorded.

func (*Diags) AppendAll

func (d *Diags) AppendAll(xs []ir.Diagnostic)

AppendAll records each of xs in order, deduping as Append does. It is the entry point for a pure reader that returns its findings as a slice.

func (*Diags) Len

func (d *Diags) Len() int

Len reports how many distinct diagnostics were recorded.

func (*Diags) List

func (d *Diags) List() []ir.Diagnostic

List returns the recorded diagnostics in the order they were first appended.

The slice is the live backing array, not a copy: it is handed to the compiler that owns this Diags, on its way out of Compile. Callers must not retain or mutate it across further Append calls.

type Space

type Space string

A Space is the namespace an ID's path is addressed in: the format's own name for a node some source coordinate denotes ("openapi"), or a space of its own for a node a lowering mints ("composed").

It is a named type rather than a string so a call cannot transpose the space and the path — two same-typed string parameters being exactly the argument order this package cannot check for a caller.

type Types

type Types struct {
	// contains filtered or unexported fields
}

Types owns the type registry and the source-coordinate to ir.TypeID map that together keep invariant 3 true: one node per source coordinate, addressed by a stable synthetic ID.

The zero value is not usable; call NewTypes. A Types is the mutable state of a single compile and is not safe for concurrent use — compilers are pure and reentrant because each call builds its own, not because this is synchronized.

func NewTypes

func NewTypes() *Types

NewTypes returns an empty registry.

func (*Types) Intern

func (t *Types) Intern(pointer string, id ir.TypeID, build func() ir.TypeDef) ir.TypeID

Intern returns the ID for pointer, calling build on first visit only.

The ID is recorded before build runs, which is what terminates recursive and diamond schemas: a self-reference reached while building hits the map and returns the ID rather than re-entering build.

A second call for the same pointer returns the first ID and does not rebuild, so a caller must not rely on build running — it is the interning table, not a constructor.

func (*Types) InternProvisional

func (t *Types) InternProvisional(pointer string, id ir.TypeID, build func() ir.TypeDef) ir.TypeID

InternProvisional is Intern for a lowering that reached pointer through a reference naming it rather than through the declaration that owns it, and records that the name the node is being given is a placeholder.

A reference can name a coordinate inside another declaration's body, and both lowerings reach it: the declaration through its own structure, the reference through the pointer it spells. Intern calls build for whichever arrives first, so the node's name used to be decided by declaration order — silently, since either spelling is a valid name and nothing compared them.

Only the *name* is a question the declaration answers better; the node itself is the same one either way. So the reference still builds it, and NameFromDeclaration replaces the name when the declaration arrives — in whichever order the two happen.

A coordinate already interned is not marked: the declaration may have been there first, and a name it settled is not a placeholder.

func (*Types) Len

func (t *Types) Len() int

Len reports how many types are interned.

func (*Types) Lookup

func (t *Types) Lookup(pointer string) (ir.TypeID, bool)

Lookup returns the ID interned at pointer, if any.

func (*Types) NameFromDeclaration

func (t *Types) NameFromDeclaration(pointer, hint string)

NameFromDeclaration gives the node at pointer the hint its declaration derives, replacing a placeholder a reference left there first.

It is a no-op for a coordinate that is not carrying a placeholder, which is every coordinate the declaration reached first — there the name is already the one this would write. That is also what makes a second declaration at one coordinate silent here rather than last-write-wins: two declarations claiming one coordinate is what claimID refuses, and re-reporting it as a naming problem would name the symptom instead of the cause.

func (*Types) Node

func (t *Types) Node(id ir.TypeID) (ir.TypeDef, bool)

Node returns the type definition registered under id, if any.

func (*Types) NodeAt

func (t *Types) NodeAt(pointer string) (ir.TypeDef, bool)

NodeAt returns the node interned at pointer.

It exists so the two-step "resolve the coordinate, then fetch the node" cannot be written with a gap between the steps. Intern records a coordinate and its node together, so a coordinate that resolves always has a node — a caller doing Lookup then Node has to write a branch for a state this type does not produce, and an unreachable branch is worse than no branch: it cannot be tested, and it suggests the state is possible.

func (*Types) PrimID

func (t *Types) PrimID(k ir.PrimKind) ir.TypeID

PrimID interns the primitive of kind k and returns its ID.

func (*Types) PrimRef

func (t *Types) PrimRef(k ir.PrimKind) ir.TypeRef

PrimRef interns the primitive of kind k on first use and returns a reference to it. Primitives are leaves reached by kind rather than by position, so they never enter the pointer-keyed table.

The primitive's Provenance.Source is ir.NoSource, the IR's value for a node that addresses no input file. One primitive is shared by every position of its kind in every source, and an index beside the empty pointer would say that file's whole document declared it (GitHub #528). irverify does not yet hold other producers to this (GitHub #590).

It writes the registry directly, claiming neither the ID nor the space: both claims are about a coordinate owning an ID, and a primitive has no coordinate. What that leaves unguarded here — another node landing in the prim space — is caught at the document boundary by irverify's ir/prim-space-reserved, which holds every producer rather than only a compile that went through this type.

func (*Types) Register

func (t *Types) Register(id ir.TypeID, td ir.TypeDef)

Register records td under id without associating it with any source coordinate.

It exists for nodes a lowering mints rather than finds. A composed union variant is the case: the coordinate it would occupy already denotes the branch schema it was built from, so interning it there makes the two race for one pointer — whichever lowered first wins it, and the result depends on declaration order. Such nodes take a synthetic ID and no coordinate entry.

Prefer Intern wherever the node does correspond to a source coordinate: that is the path enforcing one node per coordinate, and the one that terminates recursion. Register overwrites a colliding ID rather than deduplicating, because a synthetic ID that collides is a bug in the minting scheme, not a revisit.

The namespace a minted node lands in is checked rather than assumed: see claimSpace.

func (*Types) Registry

func (t *Types) Registry() ir.TypeRegistry

Registry returns the built registry for assembly into an ir.Document.

It hands over the live map rather than a copy: the caller is the compiler that owns this Types, the value goes straight into a Document it is assembling, and copying a registry per compile to guard against a caller that has no reason to mutate it would cost the whole walk's allocation for nothing. Callers outside the owning compile must treat the result as read-only.

func (*Types) String

func (t *Types) String() string

String renders the registry size, for use in error and debug output.

func (*Types) Violations

func (t *Types) Violations() []string

Violations returns the invariant breaches the registry refused to record, in the order they were attempted.

A non-empty result is a compiler bug, not a spec problem: every entry names an empty ID, an empty coordinate, or a nil type definition, none of which any source can produce. The owning compiler surfaces them as internal-invariant diagnostics rather than dropping them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL