Declaration Specs and Language Lowering
This chapter defines the implemented ownership model for structured
declarations in spec/* and the built-in lang/* adapters. Frozen pre-0.6.8
compatibility paths for external adapters and legacy direct facades are
described in 0.6.8 Legacy Compatibility and
Migration.
Decision
Declaration specs describe what to declare. A language adapter decides whether that intent is representable and owns how to spell it. Specs do not assemble a target-language grammar by interpreting a shared collection of keywords, separators, placement enums, or ordering flags.
The complete pipeline is:
builder
|
v
declaration spec target-independent intent
|
+-- intrinsic validation invariants of the intent itself
|
+-- capability validation target-specific representability
|
v
language-local lowering exact grammar, spelling, and token order
|
v
CodeBlock / CodeNode::TypeRef target-associated structured source
|
+-- source rewrite, then TypeName lowering
+-- import collection and alias resolution
+-- layout and indentation
|
v
source text
This is a compiler pipeline, not a general declaration-formatting engine.
Ownership
| Concern | Owner | Examples |
|---|---|---|
| Declaration intent | spec/* | Name, parameters, result type, type parameters, bounds, members, visibility intent, modifiers, body |
| Intrinsic coherence | spec/* | Non-empty names, internally consistent parameter lists, valid builder state |
| Target representability | language capabilities and validation | Whether a context supports type parameters, requires typed parameters, permits a body, or accepts a constructor |
| Target grammar | language adapter | Keywords, ordering, placement, punctuation, modifier spelling, constructor syntax |
| Structured output | CodeBlock | Target literals plus semantic TypeRef, nesting, statement, and layout nodes |
| Final text mechanics | renderer | Imports, aliases, indentation, width decisions, and string emission |
The ownership test is deliberately simple:
- A fact about the requested declaration belongs to the spec.
- A statement that the target supports, requires, or forbids a semantic fact belongs to capability validation.
- A decision about which token appears, where it appears, or in what order it appears belongs to language-local lowering.
- A decision about import names, indentation, width, or document layout belongs to the renderer.
Declaration Specs
A declaration spec is a target-independent declaration model, not the syntax tree of a hypothetical universal language. It may be richer than any one target. A target adapter either lowers the requested semantics or returns a validation error; it must not silently discard unsupported intent.
For example, one function declaration may contain:
name: id
type parameters: T
parameters: x of type T
result: T
body: ...
That intent can become:
Kotlin: fun <T> id(x: T): T
Rust: fn id<T>(x: T) -> T
Java: <T> T id(T x)
There is no semantic type parameter placement property in the declaration.
Placement exists only after selecting a target grammar.
Specs can contain target-specific CodeBlock payloads for bodies, raw
annotations, suffixes, or other escape hatches. These payloads are explicitly
opaque to generic specs and shared lowerers: their presence does not make the
declaration shell or its grammar a shared syntax model. Lowering composes them
structurally and preserves their TypeRef nodes, but does not reinterpret their
literal syntax. A private Python validator recognizes the documented 0.6.8
is_static plus decorator pattern as a frozen adapter-local compatibility
exception; new behavior must use semantic intent instead of extending it.
Capabilities Are Semantic
The shared capability vocabulary describes representability. For example,
ParametricPolymorphism says that a declaration context can express type
parameters; TypedParameters says that parameter types are supported or
required; and FunctionBodyPolicy says whether a body is legal. None of these
concepts describes the position or spelling of a token.
Capabilities may be contextual. A target can support a bodyful top-level function while forbidding a body on an interface member, or support ordinary methods while rejecting constructors. Such differences remain semantic validation rules even though the rules are language-specific.
If a proposed capability cannot be defined without mentioning a keyword, delimiter, token order, or formatting example, it is probably target grammar rather than a semantic capability.
Language-Local Lowering
The external declaration seams first validate classified intent and then lower a complete validated declaration into a structured block:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{TypeIntent, ValidatedType};
trait Example {
fn validate_type(
&self,
type_: TypeIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_type_validation_errors(
&self,
type_: TypeIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_type(
&self,
type_: ValidatedType<'_>,
) -> Result<Vec<CodeBlock>, SigilStitchError>;
}
}
The vector return is target grammar: an adapter may produce one declaration or
several related blocks, such as a Rust definition and impl.
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{FunctionIntent, ValidatedFunction};
trait Example {
fn validate_function(
&self,
function: FunctionIntent<'_>,
) -> Result<(), SigilStitchError>;
fn lower_function(
&self,
function: ValidatedFunction<'_>,
) -> Result<CodeBlock, SigilStitchError>;
}
}
Enum variants use the same shape at sequence granularity:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{ValidatedVariants, VariantIntent};
trait Example {
fn validate_variants(&self, variants: VariantIntent<'_>)
-> Result<(), SigilStitchError>;
fn collect_variant_validation_errors(
&self,
variants: VariantIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_variants(&self, variants: ValidatedVariants<'_>)
-> Result<CodeBlock, SigilStitchError>;
}
}
Fields also cross the adapter boundary as one complete sequence:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{FieldSequenceIntent, ValidatedFields};
trait Example {
fn validate_fields(&self, fields: FieldSequenceIntent<'_>)
-> Result<(), SigilStitchError>;
fn collect_field_validation_errors(
&self,
fields: FieldSequenceIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_fields(&self, fields: ValidatedFields<'_>)
-> Result<CodeBlock, SigilStitchError>;
}
}
Computed properties cross as one complete semantic declaration:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::{PropertyIntent, ValidatedProperty};
trait Example {
fn validate_property(
&self,
property: PropertyIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_property_validation_errors(
&self,
property: PropertyIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
fn lower_property(
&self,
property: ValidatedProperty<'_>,
) -> Result<Vec<CodeBlock>, SigilStitchError>;
}
}
Relationships among different member families use a validation-only owner view rather than another lowering abstraction:
#![allow(unused)]
fn main() {
extern crate sigil_stitch;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::lang::TypeMembersIntent;
trait Example {
fn validate_type_members(
&self,
members: TypeMembersIntent<'_>,
) -> Result<(), SigilStitchError>;
fn collect_type_members_validation_errors(
&self,
members: TypeMembersIntent<'_>,
errors: &mut Vec<SigilStitchError>,
);
}
}
FunctionIntent provides read-only access after context and form
classification and crate-owned semantic validation against the selected
adapter. ValidatedFunction can only be constructed by the crate after the
adapter’s additional validation succeeds. FunSpec::emit() remains the
convenience facade: it delegates validation and lowering without interpreting
target grammar switches itself.
TypeIntent provides one complete declaration before target-local validation:
kind, semantic modifiers, preamble data, type parameters and constraints,
nominal relationships, primary-constructor parameters, variants, and member
families. ValidatedType is constructed only after type-level and child
validation succeeds. It exposes fields, properties, methods, and variants
through their validated wrappers so lower_type() can own member order and
declaration shape while reusing each child’s complete lowerer. The type adapter
also owns alias/newtype forms, empty-body behavior, and whether output is inline
or split; it must return one or more non-empty blocks. None of those choices
lives in TypeSpec.
VariantIntent provides the owner name and kind, every variant in declaration
order, a has_non_variant_members() fact covering fields, properties, methods,
embedded types, and opaque members, structured-constructor arity evidence, and
separate evidence that opaque members may provide target-specific constructor
syntax. The variant adapter derives first/last position and owns preambles,
payload grammar, separators, and section termination for the complete
sequence. The type adapter chooses the sequence’s position. Variant
capabilities name semantic forms—discriminant, constructor arguments,
positional payload, record payload, and attributes—not their spelling. The
additive collector reports independent sibling failures; ValidatedVariants
is constructed only after intrinsic, profile, and every adapter-local
validation phase succeeds.
FieldSequenceIntent provides every field in declaration order and a semantic
FieldContext: direct emission, ordinary type members, or a variant record
payload. Owner and variant names are included when they exist. Field profiles
declare supported and required semantic capabilities for each context, while
the adapter-local validator handles identifier rules, escaped-name collisions,
modifier combinations, and target-specific restrictions. The additive
collector retains independent sibling failures during FileSpec validation;
ValidatedFields is created only after intrinsic, profile, and adapter-local
validation all succeed. lower_fields() owns the sequence’s complete grammar,
including documentation, annotations, access sections, tags, separators, and
declarator restrictions.
Optional presence and optional values are separate semantics. A field marked
with FieldSpec::is_optional() may be absent from its containing value and
requests FieldCapability::OptionalPresence. A TypeName::Optional(T) field
is still present but may hold the target language’s option or null
representation. An adapter must not substitute one meaning for the other.
PropertyIntent provides one property and its semantic PropertyContext:
direct emission with the legacy declaration context, or a member of an owning
TypeKind. Property profiles distinguish explicit type information, read and
write behavior, attributes, and static behavior. A getter or setter body is
semantic implementation input; whether the target expresses it as accessor
declarations, a field-style computed property, or ordinary target-local methods
belongs entirely to lower_property(). ValidatedProperty is constructed only
after intrinsic, profile, and adapter-local validation succeeds.
TypeMembersIntent provides the owning type’s name and kind together with its
semantic fields, computed properties, and explicit methods. It is constructed
once after the per-family validation passes. The crate rejects exact duplicate
property names; the adapter owns collisions created by target lowering, such
as PHP’s case-insensitive generated accessor names colliding with another
property accessor or an explicit method. The intent contains no target grammar,
has no validated wrapper, and has no lowering method: every accepted property
still follows ValidatedProperty -> lower_property() independently.
Each adapter owns the complete ordering and spelling of a declaration. Private leaf helpers may render structured fragments such as a parameter list or body, but do not choose their relative order. Related adapters may additionally share a genuinely family-specific lowering helper. An adapter can bypass either without adding a new variant to a shared grammar interface.
Built-in type and function lowerers spell declaration generics locally,
including bounds, lifetimes, kinds, context bounds, and explicit constraint
clauses. Only the frozen permissive compatibility path interprets the
deprecated shared generic configuration. A strict adapter that advertises a
function profile but omits lower_function() fails with
MissingFunctionLowerer instead of silently selecting compatibility grammar.
A useful locality test is to add a language with a previously unseen syntax. The change should be confined to that adapter, its private helpers, and its tests. If the change requires a new shared placement enum and new branches in a generic spec emitter, target grammar has crossed the seam.
Closed Sum Declarations
A closed sum is a type declaration with a complete ordered set of named cases.
The set itself is semantic intent; sealed, enum, data, nesting, and
sibling placement are possible target representations rather than shared
configuration.
The public construction entry point is ClosedSumSpec::builder(...) with
ClosedSumCaseSpec cases. It is a sibling of TypeSpec, not an enum mode and
not a modifier on TypeSpecBuilder. ClosedSumCapabilityProfile is the
opt-in representability profile; ordinary enum profiles and lowering remain
unchanged.
Both declaration families use TypeDeclarationCapability for polymorphism
and root attributes. Ordinary TypeKindCapabilityProfiles keep those features
separate from ordinary TypeCapability values. Closed-sum case forms and
scoped record fields live only in ClosedSumCapabilityProfile. Case metadata
does not request root attributes; each target validates and places it locally.
Closed sums describe these case shapes independently of ordinary enum storage:
| Case shape | Meaning |
|---|---|
| No cases | Empty sum; a named uninhabited type |
| Unit case | One named alternative with no carried data |
| Positional payload | One named alternative carrying types in order |
| Record payload | One named alternative carrying named typed fields |
Discriminants, legacy variant values, and enum-entry constructor arguments are invalid on a closed sum. They describe a value representation or an expression evaluated at an enum declaration, not data carried by a sum case. Structured and opaque case annotations remain metadata, while wire discriminators and serialization tagging stay in the caller or its annotations.
An empty closed sum declares a named uninhabited type. It is not the unit type:
the empty sum has no values, while the empty product or unit type has exactly
one. A Never reference or bottom type can likewise have no values, but it is a
type-expression or subtype concept rather than a declaration of this named
case set. This feature does not add TypeName::Never or treat the two concepts
as shared semantic identity. A target may reuse its canonical empty type only
if the result preserves the requested name and every valid use position; it
otherwise rejects the empty form even if it supports non-empty closed sums.
Case(T) always means a generated case that carries T. It does not claim
that an existing declaration for T is a subtype of the root. Nominal
membership of pre-existing types has different declaration ownership and is
outside this interface.
Complete ClosedSum lowering owns the output topology. Rust, Swift, Haskell, OCaml,
and Scala can use native algebraic declarations. Java uses one public sealed
root with nested case declarations so one generated file does not contain
several public top-level types. Kotlin uses a private-constructor sealed class
with nested data object and data class cases so no additional direct case
can be declared elsewhere in the same module. Dart uses a sealed root and
generated final cases. Every other built-in returns a structured
unsupported-intent error until it has an accepted exact representation; no
adapter widens a closed sum to Object, Any, an open hierarchy, or an
ordinary enum.
Support for the empty shape is validated separately from general closed-sum
support because several targets require additional language features or lack
an exact named uninhabited declaration. Each accepted case shape still follows the existing
intrinsic validation, target capability checks, complete target-local
validation, non-empty CodeBlock output checks, type-name lowering, import
resolution, and rendering pipeline.
Compatibility and Migration
Public declaration grammar that was already part of the 0.6.8 adapter surface may remain behind a deprecated, frozen compatibility lowerer. Compatibility is not permission to extend that design:
- Do not add new shared declaration-placement enums, flags, or keyword fields.
- Do not add new branches in specs to interpret target grammar.
- New built-in behavior should enter through a complete language-owned lowering seam.
- Concepts introduced after 0.6.8 may be changed or removed instead of being preserved as another compatibility layer.
- External adapters can migrate one declaration family at a time, with rendered-output tests at the adapter seam and parity coverage across direct and pretty paths. Built-in adapters already use complete lowerers.
Compatibility is bounded by validity: a built-in adapter may restrict an old entry point rather than emit malformed or unverifiable target code. The full version boundary, deprecated-surface matrix, builder recipes, and external-adapter sequence are centralized in 0.6.8 Legacy Compatibility and Migration.
Scope of This Decision
This decision governs declaration grammar interpreted by spec/*. It does not
prohibit shared semantic data, structured rendering nodes, or private reusable
helpers. It also does not by itself redesign lower-level seams such as
TypeName presentation or block layout; those mechanisms have their own design
documents and must be evaluated against their own callers and invariants.