// Package emit is plumb's final phase: it turns the resolved plans into the // gofmt-canonical generated file and the -v report. It chooses import aliases, // allocates local names, renders each injector via the plan's instances, and // aggregates cleanup and error handling. It produces text; it makes no wiring // decisions and reports no diagnostics (a malformed render is an invariant // violation and panics).
package emit import ( ) // Result is the outcome of a successful generation. It is emit's output, handed // back through gen to the caller. type Result struct { // Source is the gofmt-canonical generated Go source. Source string // Report is the human-readable discovery-and-inference report (the body of // the -v output). It is informational and not contractual. Report string } // File turns the resolved plans into the gofmt-canonical generated file and the // -v report. func (, string, []*discover.Package, []*solve.Plan, *solve.DestInfo) *Result { := slices.ContainsFunc(, planNeedsErrors) := liftedNamesOf() // Pass 1: collect the packages every function references by rendering each // plan under a recording qualifier and discarding the text (see // recordPlanPackages). := newRecording() for , := range { recordPlanPackages(, , , ) } := .recordedPackages() // Assign import aliases and (if needed) the errors qualifier. Imports live in // file scope and must avoid the generated function (set) names, which are // package-scope declarations in the same file: an import qualifier equal to a // set name would be a redeclaration. := map[string]bool{} for , := range { [.Name] = true } , , := assignAliases(, , , , , ) // Pass 2: render each function. := newFinal(, ) var []string for , := range { = append(, renderPlan(, , , , )) } := assembleFile(, , ) , := format.Source([]byte()) if != nil { // A formatting failure means plumb produced syntactically invalid Go, // an internal invariant violation, never reachable from valid input. panic(fmt.Sprintf("plumb: generated source did not format: %v\n---\n%s", , )) } := buildReport(, , , , ) return &Result{Source: string(), Report: } } // recordPlanPackages records, into the recording qualifier q, every package the // generated function will reference. It does so by rendering the plan under q and // discarding the text: the render is the single authoritative enumeration of // every qualified reference (signature, lifted constraints, and each instance's // producing expression), so recording through it means the import block cannot // drift from the emitted body: there is no second enumeration to keep in step. // The errors qualifier is irrelevant while recording (whether errors is imported // is decided separately by planNeedsErrors, and it is written as a literal alias, // never through q), so it is left empty. func ( *solve.Plan, *qualifier, *solve.DestInfo, map[string]bool) { _ = renderPlan(, , "", , ) } func ( []*solve.Plan) map[string]bool { := map[string]bool{} for , := range { for , := range .Lifted { [.Obj().Name()] = true } } return } // assignAliases assigns a deterministic qualifier to each imported package, // aliasing on collision with destination identifiers, lifted parameter names, // or another import. func ( []*types.Package, bool, string, , map[string]bool, *solve.DestInfo) (map[string]string, string, []string) { := reservedIdents(, ) for := range { [] = true } type struct { string string string } var [] := map[string]bool{} := func(, string) { if == || [] { return } [] = true = append(, {: , : }) } for , := range { (.Path(), .Name()) } if { ("errors", "errors") } slices.SortFunc(, func(, ) int { return strings.Compare(., .) }) := map[string]string{} for := range { := []. if == "" { = "pkg" } := for := 2; [] || token.IsKeyword(); ++ { = + strconv.Itoa() } [] = true []. = [[].] = } var []string for , := range { if . == . { = append(, fmt.Sprintf("%q", .)) } else { = append(, fmt.Sprintf("%s %q", ., .)) } } := "" if { = ["errors"] } return , , } // renderPlan renders one set's injector function. func ( *solve.Plan, *qualifier, string, *solve.DestInfo, map[string]bool) string { := newAllocator(, , ) // localOf maps a value type (by identity) to the local/param/result name // holding it. local() reads it; the name is always present by the time it is // read, so a missing entry is an internal-invariant violation. var gotypes.Map[types.Type, string] := func( types.Type, string) { .Set(, ) } := func( types.Type) string { , := .At() if ! { panic(fmt.Sprintf("plumb: emit: no local bound for %s", gotypes.TypeName())) } return } // Named results: outputs, then cleanup, then error. var []string := make([]string, len(.Outputs)) for , := range .Outputs { := .alloc(baseName()) [] = = append(, +" "+.typeString()) } := "" if .AnyCleanup { = .alloc("cleanup") := "func()" if .CleanupFailable { = "func() error" } = append(, +" "+) } := "" if .Fallible { = .alloc("err") = append(, +" error") } // Parameters. Prefer the source parameter/field name as a readable base, // falling back to a type-derived name. var []string for , := range .Inputs { := baseName(.Type) if := .Name; != "" && != "_" { = lowerCamel() } := .alloc() (.Type, ) = append(, +" "+.typeString(.Type)) } // Body. var []string var []cleanupRef := "" := false for , := range .Order { := make([]string, len(.Inputs)) for , := range .Args[] { [] = coerceExpr((.SrcType), .Coerce) } := renderInstance(, , ) var []string := false := false var []cleanupRef for , := range .Results { switch .Kind { case solve.ResultKindValue: := .alloc(baseName(.Typ)) (.Typ, ) = append(, ) = true case solve.ResultKindCleanup: := .alloc(cleanupBaseName(.Prov.Fn)) = append(, ) = true = append(, cleanupRef{name: , failable: .Failable}) case solve.ResultKindError: if == "" { = .alloc("e") } = append(, ) = true default: panic(fmt.Sprintf("plumb: unhandled result kind %d", .Kind)) } } switch { case len(.Results) == 0: = append(, ) default: := ":=" if ! && { = "=" } = append(, strings.Join(, ", ")+" "++" "+) } if { = true , := renderUnwind(, , , , ) := []string{"if " + + " != nil {"} = append(, ...) = append(, +" = "+, "return", "}") = append(, ...) } = append(, ...) } // Success path: assign outputs, build the aggregated cleanup, return. for , := range .Outputs { = append(, []+" = "+()) } if .AnyCleanup { = append(, +" = "+renderAggregate(, , , )) } // A naked return is required to surface the named results; a result-less, // side-effect-only injector (an init/main-style set) has none, so the trailing // return would be dead syntax. if len() > 0 { = append(, "return") } var strings.Builder .WriteString("func ") .WriteString(.Name) .WriteString(renderLiftedHeader(.Lifted, .typeString)) .WriteByte('(') .WriteString(strings.Join(, ", ")) .WriteByte(')') if len() > 0 { .WriteString(" (") .WriteString(strings.Join(, ", ")) .WriteByte(')') } .WriteString(" {\n") for , := range { .WriteString() .WriteByte('\n') } .WriteString("}") return .String() } type cleanupRef struct { name string failable bool } // renderUnwind renders the teardown of already-acquired cleanups on a mid-build // error: newest first, with the build error joined in when cleanups are failable. func ( []cleanupRef, string, *solve.Plan, string, *allocator) ( []string, string) { := reverseCleanups() // Without any failable cleanup to unwind, the error flows through directly: // non-failable cleanups run as plain statements and err = e. if !.CleanupFailable || !anyFailable() { for , := range { = append(, .name+"()") } return , } , := cleanupStmts(, ) return , joinErrs(, append([]string{}, ...)) } func ( []cleanupRef) bool { for , := range { if .failable { return true } } return false } // renderAggregate renders the single aggregated cleanup returned on success. func ( []cleanupRef, *solve.Plan, string, *allocator) string { := reverseCleanups() if !.CleanupFailable { var strings.Builder .WriteString("func() {\n") for , := range { .WriteString(.name) .WriteString("()\n") } .WriteString("}") return .String() } , := cleanupStmts(, ) var strings.Builder .WriteString("func() error {\n") for , := range { .WriteString() .WriteByte('\n') } .WriteString("return ") .WriteString(joinErrs(, )) .WriteString("\n}") return .String() } // cleanupStmts renders each cleanup call in run order: a failable cleanup // captures its error in a "cleanupErr" local, a non-failable one is called bare. // It returns the statements and the captured error-local names, in run order. // The error locals live in this branch's scope alone, so they are allocated from // a fresh per-branch allocator and reset across branches. func ( []cleanupRef, *allocator) (, []string) { := .branch() for , := range { if .failable { := .alloc("cleanupErr") = append(, +" := "+.name+"()") = append(, ) } else { = append(, .name+"()") } } return , } // joinErrs returns the lone error expression when there is one, or an // errors.Join over all of them when there are several. func ( string, []string) string { if len() == 1 { return [0] } return + ".Join(" + strings.Join(, ", ") + ")" } // planNeedsErrors reports whether the plan's generated code emits errors.Join, // and so must import the errors package. It mirrors the two join sites exactly, // since claiming the import without an emitted Join (or vice versa) breaks the // generated build: // // - renderAggregate joins on the success path when two or more cleanups are // failable (a single failable cleanup is returned directly); // - renderUnwind joins the build error with the failable cleanups acquired so // far when a fallible provider fails after at least one was acquired. func ( *solve.Plan) bool { if !.CleanupFailable { return false } := 0 for , := range .Order { for , := range .Results { if .Kind == solve.ResultKindCleanup && .Failable { ++ } } } if >= 2 { return true } // A single failable cleanup still forces a Join if a later provider can fail // after it was acquired, because the unwind joins the build error with it. := 0 for , := range .Order { if >= 1 && instanceFallible() { return true } for , := range .Results { if .Kind == solve.ResultKindCleanup && .Failable { ++ } } } return false } func ( *solve.Instance) bool { for , := range .Results { if .Kind == solve.ResultKindError { return true } } return false } func ( []cleanupRef) []cleanupRef { := slices.Clone() slices.Reverse() return } // coerceExpr renders the value/pointer bridge for an argument: &x passes a value // local where a pointer is wanted (every consumer takes the address of the same // local, so they share one instance), *x derefs a pointer where a value is // wanted. As a method receiver these prefixed forms are parenthesized by // asReceiver. func ( string, solve.Coerce) string { switch { case solve.CoerceNone: return case solve.CoerceToPtr: return "&" + case solve.CoerceToVal: return "*" + default: panic(fmt.Sprintf("plumb: unhandled coercion %d", )) } } // renderLiftedHeader renders a generic injector's type-parameter list // [T constraint, ...] from its lifted free parameters, collapsing an // empty-interface constraint to the bare "any"; qual renders a non-collapsing // constraint. An empty list renders nothing. Both the generated header (via // q.typeString) and the -v report (via its fallback qualifier) route through // here so the two spellings cannot drift. func ( []*types.TypeParam, func(types.Type) string) string { if len() == 0 { return "" } var []string for , := range { := "any" if !gotypes.ConstraintCollapsesToAny(.Constraint()) { = (.Constraint()) } = append(, .Obj().Name()+" "+) } return "[" + strings.Join(, ", ") + "]" } func ( string, , []string) string { var strings.Builder .WriteString("// Code generated by plumb. DO NOT EDIT.\n\n") .WriteString("package ") .WriteString() .WriteString("\n\n") if len() > 0 { .WriteString("import (\n") for , := range { .WriteString() .WriteByte('\n') } .WriteString(")\n\n") } .WriteString(strings.Join(, "\n\n")) .WriteString("\n") return .String() }