Skip to content
Spice Framework on GitHub

ADR 0004: Typed Application Markers and One Immutable IR

frameworkMaturity: previewSource: spice@c0641b3Exact reviewed source

Ownership update: the compiler paths described below live in the standalone github.com/spice-framework/toolchain module; the application and lifecycle contracts remain in core.

Status: Accepted

Decision

The preferred @Application marks the ordinary process entrypoint:

package main
import (
"os"
spiceapp "example.com/shop/internal/spicegen/shop"
)
// @Application
func main() {
os.Exit(spiceapp.Main(os.Args[1:]))
}

The preferred marker is package main’s parameterless, result-free, non-generic func main(). Its selected local Go package scope supplies compile-time discovery of package-documentation modules and supported annotated application features. The handwritten command explicitly imports the generated target package and calls its exported Main. Every annotation-derived unit is emitted under that target’s nested sources tree; generated code is never written beside handwritten source. There is no runtime scan, registration hook, reflection, or dummy module import.

During the pre-1.0 compatibility period, a non-main package-level marker may retain exact parameter roots:

// @Application
func Commerce(server *httpapi.Server, worker jobs.Worker) {}

Each legacy parameter must be semantically identical under go/types.Identical to one validated @Bean output. Aliases are accepted because they preserve Go type identity; assignability, implicit interface implementation, pointer/value conversion, and underlying-type equality do not select roots.

Verification may analyze a package set with zero or multiple application markers. Each marker has a stable symbol identity and becomes one generation target. A later generation command must select an unambiguous target before writing files.

Application model

compiler/application is the single immutable-by-convention compiler boundary for generation. It consumes the existing loaded program and resolved annotations, then assembles:

  1. the validated provider catalog and cleanup flags;
  2. the exact provider graph and dependency-first construction order;
  3. lifecycle components ordered by provider construction order;
  4. validated package-main discovery targets or compatible exact provider-backed legacy roots.

The model never reloads or reparses packages, executes declaration bodies, reflects on runtime values, or writes files. Any stage diagnostic makes the model invalid for generation. Accessors return defensive slice and nested metadata copies.

Consequences

  • Application source remains ordinary valid Go.
  • The ordinary main.go expresses process ownership without framework assembly or manual module enumeration.
  • Legacy marker functions remain compile-time root metadata only.
  • Provider and application bodies cannot cause analysis side effects.
  • Generator packages consume one authoritative model rather than rebuilding provider, graph, or lifecycle metadata.
  • Missing or implicitly assignable roots fail before generation.