Skip to content

Commit ab095e0

Browse files
authored
refactor(compiler): split semantic IR source maps and target plans (#755)
* add semantic IR identities * refactor(compiler): move endpoint source fidelity to source map * fix(compiler): address semantic id review feedback
1 parent 58e99ef commit ab095e0

22 files changed

Lines changed: 429 additions & 136 deletions

docs/compiler/pipeline.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ The IR models packages, source files, page routes, backend endpoints from
6161
source-selected assets, asset scope/hash metadata, parsed view nodes, typed
6262
literal records, and imported build-data call metadata.
6363

64+
Standalone Go endpoint comments lower into normalized `Program.Endpoints`
65+
records with stable `EndpointID` values. Exact source spelling and route-param
66+
spans for those comments live in `Program.SourceMap`; validators can use that
67+
source map for diagnostics, but generated-output planners consume the normalized
68+
endpoint record and carry the semantic ID into target-specific plans.
69+
6470
`gwdkir.Blocks` retains source bodies for spans, formatting, inspection, and
6571
compatibility only. Parser/analyzer lowering must populate parsed `view {}`
6672
nodes, ordered literal `paths {}`/`build {}` records, and imported build-data

docs/engineering/architecture.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ request-time lane selected with `server {}` or `go server {}`.
3535
`internal/gwdkir.Program` is the compiler handoff. New generated-output work
3636
should consume that IR or add fields there first.
3737

38+
Semantic records carry stable typed IDs for pages, routes, components, layouts,
39+
and endpoints. Exact source spelling and spans that are only needed for
40+
diagnostics or inspection belong in source-map records instead of duplicate raw
41+
semantic entities. Build output and generated app code use explicit target plans
42+
(`buildgen.BuildPlan`, `appgen.ApplicationPlan`) so target-only decisions do not
43+
expand the shared semantic program.
44+
3845
Current handoff path:
3946

4047
```text

internal/appgen/adapter_ir.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const (
3232
)
3333

3434
type BackendEndpointRegistration struct {
35+
ID gwdkir.EndpointID
3536
Kind BackendEndpointKind
3637
Method string
3738
Path string
@@ -149,6 +150,7 @@ func backendAdapterIR(options Options) BackendAdapterIR {
149150
var ir BackendAdapterIR
150151
for _, action := range sortedActionEndpoints(options.Actions) {
151152
endpoint := BackendEndpointRegistration{
153+
ID: action.EndpointID,
152154
Kind: BackendEndpointAction,
153155
Method: actionMethod(action),
154156
Path: action.Route,
@@ -223,6 +225,7 @@ func backendAdapterIR(options Options) BackendAdapterIR {
223225
}
224226
for _, api := range sortedAPIEndpoints(options.APIs) {
225227
endpoint := BackendEndpointRegistration{
228+
ID: api.EndpointID,
226229
Kind: BackendEndpointAPI,
227230
Method: api.Method,
228231
Path: api.Route,
@@ -268,6 +271,7 @@ func backendAdapterIR(options Options) BackendAdapterIR {
268271
}
269272
for _, fragment := range sortedFragmentEndpoints(options.Fragments) {
270273
endpoint := BackendEndpointRegistration{
274+
ID: fragment.EndpointID,
271275
Kind: BackendEndpointFragment,
272276
Method: fragment.Method,
273277
Path: fragment.Route,

internal/appgen/adapter_ir_test.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
1212
ir := backendAdapterIR(Options{
1313
Actions: []ActionEndpoint{{
14+
EndpointID: gwdkir.EndpointID("action-id"),
1415
PageID: "newsletter",
1516
ActionName: "Subscribe",
1617
Method: "POST",
@@ -32,11 +33,12 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
3233
BackendAlias: "newsletter",
3334
}},
3435
APIs: []APIEndpoint{{
35-
PageID: "status",
36-
APIName: "Health",
37-
Method: "GET",
38-
Route: "/api/health",
39-
Guards: []string{"public"},
36+
EndpointID: gwdkir.EndpointID("api-id"),
37+
PageID: "status",
38+
APIName: "Health",
39+
Method: "GET",
40+
Route: "/api/health",
41+
Guards: []string{"public"},
4042
Binding: source.BackendBinding{
4143
Status: source.BackendBindingBound,
4244
ImportPath: "example.com/app/status",
@@ -46,6 +48,7 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
4648
BackendAlias: "status",
4749
}},
4850
Fragments: []FragmentEndpoint{{
51+
EndpointID: gwdkir.EndpointID("fragment-id"),
4952
PageID: "patients",
5053
FragmentName: "List",
5154
Method: "GET",
@@ -78,6 +81,9 @@ func TestBackendAdapterIRCapturesRouteAndHandlerMetadata(t *testing.T) {
7881
if ir.Registrations[0].Kind != BackendEndpointAction || ir.Registrations[0].Path != "/newsletter" || ir.Registrations[0].Handler != "action" {
7982
t.Fatalf("unexpected action registration: %#v", ir.Registrations[0])
8083
}
84+
if ir.Registrations[0].ID != gwdkir.EndpointID("action-id") || ir.Registrations[1].ID != gwdkir.EndpointID("api-id") || ir.Registrations[2].ID != gwdkir.EndpointID("fragment-id") {
85+
t.Fatalf("expected semantic endpoint IDs in registrations, got %#v", ir.Registrations)
86+
}
8187
if ir.Registrations[1].Kind != BackendEndpointAPI || ir.Registrations[1].Path != "/api/health" || ir.Registrations[1].Handler != "api" {
8288
t.Fatalf("unexpected API registration: %#v", ir.Registrations[1])
8389
}

internal/appgen/ir.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,14 @@ func actionEndpointsFromIR(config gowdk.Config, ir gwdkir.Program) ([]ActionEndp
2828
if route == "" {
2929
route = page.Route
3030
}
31-
bindingRoute := route
3231
fragments, err := actionFragmentsFromIR(action)
3332
if err != nil {
3433
return nil, fmt.Errorf("%s.%s: %w", page.ID, action.Name, err)
3534
}
35+
semanticEndpoint := irEndpointForBlock(ir.Endpoints, gwdkir.EndpointAction, page.ID, action.Name, method, route)
3636
for _, localized := range actionEndpointRoutes(config.I18N, page.Route, route, action.Route == "") {
3737
endpoints = append(endpoints, ActionEndpoint{
38+
EndpointID: semanticEndpoint.SemanticID(),
3839
PageID: page.ID,
3940
ActionName: action.Name,
4041
Method: method,
@@ -51,7 +52,7 @@ func actionEndpointsFromIR(config gowdk.Config, ir gwdkir.Program) ([]ActionEndp
5152
Redirect: action.Redirect,
5253
Fragments: fragments,
5354
ErrorPage: action.ErrorPage,
54-
Binding: bindings[irEndpointKey(gwdkir.EndpointAction, page.ID, action.Name, method, bindingRoute)],
55+
Binding: bindings[semanticEndpoint.SemanticID()],
5556
Source: page.Source,
5657
SourceSpan: action.Span,
5758
})
@@ -62,8 +63,9 @@ func actionEndpointsFromIR(config gowdk.Config, ir gwdkir.Program) ([]ActionEndp
6263
if endpoint.Kind != gwdkir.EndpointAction || endpoint.Source != gwdkir.EndpointSourceGo {
6364
continue
6465
}
65-
binding := bindings[irEndpointKey(endpoint.Kind, endpoint.PageID, endpoint.Symbol, endpoint.Method, endpoint.Path)]
66+
binding := bindings[endpoint.SemanticID()]
6667
endpoints = append(endpoints, ActionEndpoint{
68+
EndpointID: endpoint.SemanticID(),
6769
PageID: endpoint.PageID,
6870
ActionName: endpoint.Symbol,
6971
Method: endpoint.Method,
@@ -116,15 +118,17 @@ func apiEndpointsFromIR(ir gwdkir.Program) ([]APIEndpoint, error) {
116118
if route == "" {
117119
route = page.Route
118120
}
121+
semanticEndpoint := irEndpointForBlock(ir.Endpoints, gwdkir.EndpointAPI, page.ID, api.Name, method, route)
119122
endpoints = append(endpoints, APIEndpoint{
123+
EndpointID: semanticEndpoint.SemanticID(),
120124
PageID: page.ID,
121125
APIName: api.Name,
122126
Method: method,
123127
Route: route,
124128
Guards: append([]string(nil), page.Guards...),
125129
ErrorPage: api.ErrorPage,
126130
CORS: cloneEndpointCORS(api.CORS),
127-
Binding: bindings[irEndpointKey(gwdkir.EndpointAPI, page.ID, api.Name, method, route)],
131+
Binding: bindings[semanticEndpoint.SemanticID()],
128132
Source: page.Source,
129133
SourceSpan: api.Span,
130134
})
@@ -135,13 +139,14 @@ func apiEndpointsFromIR(ir gwdkir.Program) ([]APIEndpoint, error) {
135139
continue
136140
}
137141
endpoints = append(endpoints, APIEndpoint{
142+
EndpointID: endpoint.SemanticID(),
138143
PageID: endpoint.PageID,
139144
APIName: endpoint.Symbol,
140145
Method: endpoint.Method,
141146
Route: endpoint.Path,
142147
ErrorPage: endpoint.ErrorPage,
143148
CORS: cloneEndpointCORS(endpoint.CORS),
144-
Binding: bindings[irEndpointKey(endpoint.Kind, endpoint.PageID, endpoint.Symbol, endpoint.Method, endpoint.Path)],
149+
Binding: bindings[endpoint.SemanticID()],
145150
Source: endpoint.SourceFile,
146151
SourceSpan: endpoint.Span,
147152
})
@@ -175,18 +180,21 @@ func fragmentEndpointsFromIR(ir gwdkir.Program) ([]FragmentEndpoint, error) {
175180
if method == "" {
176181
method = "GET"
177182
}
183+
route := strings.TrimSpace(fragment.Route)
184+
semanticEndpoint := irEndpointForBlock(ir.Endpoints, gwdkir.EndpointFragment, page.ID, fragment.Name, method, route)
178185
endpoints = append(endpoints, FragmentEndpoint{
186+
EndpointID: semanticEndpoint.SemanticID(),
179187
PageID: page.ID,
180188
FragmentName: fragment.Name,
181189
Method: method,
182-
Route: strings.TrimSpace(fragment.Route),
183-
RouteParams: gwdkir.RouteParamsFromPath(strings.TrimSpace(fragment.Route)),
190+
Route: route,
191+
RouteParams: gwdkir.RouteParamsFromPath(route),
184192
Target: fragment.Target,
185193
HTML: html,
186194
Package: page.Package,
187195
Uses: uses,
188196
Guards: append([]string(nil), page.Guards...),
189-
Binding: bindings[irEndpointKey(gwdkir.EndpointFragment, page.ID, fragment.Name, method, strings.TrimSpace(fragment.Route))],
197+
Binding: bindings[semanticEndpoint.SemanticID()],
190198
Source: page.Source,
191199
SourceSpan: fragment.Span,
192200
})
@@ -316,8 +324,8 @@ func irExportTypes(exports []gwdkir.Export) map[string]clientlang.ValueType {
316324
return out
317325
}
318326

319-
func irBindingsByEndpoint(endpoints []gwdkir.Endpoint) map[string]source.BackendBinding {
320-
out := map[string]source.BackendBinding{}
327+
func irBindingsByEndpoint(endpoints []gwdkir.Endpoint) map[gwdkir.EndpointID]source.BackendBinding {
328+
out := map[gwdkir.EndpointID]source.BackendBinding{}
321329
for _, endpoint := range endpoints {
322330
if endpoint.Binding.Status == "" && endpoint.Binding.ImportPath == "" && endpoint.Binding.FunctionName == "" {
323331
continue
@@ -326,7 +334,7 @@ func irBindingsByEndpoint(endpoints []gwdkir.Endpoint) map[string]source.Backend
326334
if endpoint.Kind == gwdkir.EndpointAPI {
327335
kind = "api"
328336
}
329-
out[irEndpointKey(endpoint.Kind, endpoint.PageID, endpoint.Symbol, endpoint.Method, endpoint.Path)] = source.BackendBinding{
337+
out[endpoint.SemanticID()] = source.BackendBinding{
330338
Kind: kind,
331339
PageID: endpoint.PageID,
332340
Source: endpoint.SourceFile,
@@ -350,8 +358,23 @@ func irBindingsByEndpoint(endpoints []gwdkir.Endpoint) map[string]source.Backend
350358
return out
351359
}
352360

353-
func irEndpointKey(kind gwdkir.EndpointKind, pageID, symbol, method, route string) string {
354-
return strings.Join([]string{string(kind), pageID, symbol, method, route}, "\x00")
361+
func irEndpointForBlock(endpoints []gwdkir.Endpoint, kind gwdkir.EndpointKind, pageID, symbol, method, route string) gwdkir.Endpoint {
362+
for _, endpoint := range endpoints {
363+
if endpoint.Kind == kind &&
364+
endpoint.PageID == pageID &&
365+
endpoint.Symbol == symbol &&
366+
endpoint.Method == method &&
367+
endpoint.Path == route {
368+
return endpoint
369+
}
370+
}
371+
return gwdkir.Endpoint{
372+
Kind: kind,
373+
PageID: pageID,
374+
Symbol: symbol,
375+
Method: method,
376+
Path: route,
377+
}
355378
}
356379

357380
func actionFragmentsFromIR(action gwdkir.Action) ([]ActionFragment, error) {

internal/appgen/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func OptionsFromIR(config gowdk.Config, ir *gwdkir.Program) Options {
7171

7272
// ActionEndpoint describes a generated action handler.
7373
type ActionEndpoint struct {
74+
EndpointID gwdkir.EndpointID
7475
PageID string
7576
ActionName string
7677
Method string
@@ -114,6 +115,7 @@ type ActionValidationRule struct {
114115

115116
// APIEndpoint describes a generated API handler.
116117
type APIEndpoint struct {
118+
EndpointID gwdkir.EndpointID
117119
PageID string
118120
APIName string
119121
Method string
@@ -129,6 +131,7 @@ type APIEndpoint struct {
129131

130132
// FragmentEndpoint describes a generated server fragment handler.
131133
type FragmentEndpoint struct {
134+
EndpointID gwdkir.EndpointID
132135
PageID string
133136
FragmentName string
134137
Method string

internal/compiler/backend_bindings.go

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,20 @@ func computeBackendBindings(ir gwdkir.Program) []source.BackendBinding {
7474
}
7575
}
7676
}
77-
for _, endpoint := range ir.GoEndpoints {
78-
dir := sourceDir(endpoint.Source)
77+
for _, endpoint := range ir.Endpoints {
78+
if endpoint.Source != gwdkir.EndpointSourceGo {
79+
continue
80+
}
81+
dir := sourceDir(endpoint.SourceFile)
7982
pkg, ok := cache[dir]
8083
if !ok {
8184
pkg = inspectFeaturePackage(dir)
8285
cache[dir] = pkg
8386
}
8487
switch endpoint.Kind {
85-
case "act", "action":
88+
case gwdkir.EndpointAction:
8689
bindings = append(bindings, bindStandaloneAction(endpoint, pkg))
87-
case "api":
90+
case gwdkir.EndpointAPI:
8891
bindings = append(bindings, bindStandaloneAPI(endpoint, pkg))
8992
}
9093
}
@@ -156,12 +159,8 @@ func bindLoadFromPackage(page gwdkir.Page, functionName string, pkg featurePacka
156159
return binding
157160
}
158161

159-
func bindStandaloneAction(endpoint gwdkir.GoEndpoint, pkg featurePackage) source.BackendBinding {
160-
method := endpoint.Method
161-
if method == "" {
162-
method = "POST"
163-
}
164-
return bindActionEndpoint(baseStandaloneBackendBinding(endpoint, actionHandlerKind, method, pkg), pkg)
162+
func bindStandaloneAction(endpoint gwdkir.Endpoint, pkg featurePackage) source.BackendBinding {
163+
return bindActionEndpoint(baseStandaloneBackendBinding(endpoint, actionHandlerKind, endpoint.Method, pkg), pkg)
165164
}
166165

167166
func bindActionEndpoint(binding source.BackendBinding, pkg featurePackage) source.BackendBinding {
@@ -193,12 +192,8 @@ func bindActionEndpoint(binding source.BackendBinding, pkg featurePackage) sourc
193192
return binding
194193
}
195194

196-
func bindStandaloneAPI(endpoint gwdkir.GoEndpoint, pkg featurePackage) source.BackendBinding {
197-
method := endpoint.Method
198-
if method == "" {
199-
method = "GET"
200-
}
201-
return bindAPIEndpoint(baseStandaloneBackendBinding(endpoint, apiHandlerKind, method, pkg), pkg)
195+
func bindStandaloneAPI(endpoint gwdkir.Endpoint, pkg featurePackage) source.BackendBinding {
196+
return bindAPIEndpoint(baseStandaloneBackendBinding(endpoint, apiHandlerKind, endpoint.Method, pkg), pkg)
202197
}
203198

204199
func bindAPIEndpoint(binding source.BackendBinding, pkg featurePackage) source.BackendBinding {
@@ -315,18 +310,18 @@ func baseBackendBinding(page gwdkir.Page, kind, blockName, method, route string,
315310
}
316311
}
317312

318-
func baseStandaloneBackendBinding(endpoint gwdkir.GoEndpoint, kind, method string, pkg featurePackage) source.BackendBinding {
313+
func baseStandaloneBackendBinding(endpoint gwdkir.Endpoint, kind, method string, pkg featurePackage) source.BackendBinding {
319314
return source.BackendBinding{
320315
Kind: kind,
321-
PageID: standaloneEndpointPageID(endpoint.Package, endpoint.Name),
322-
Source: endpoint.Source,
316+
PageID: endpoint.PageID,
317+
Source: endpoint.SourceFile,
323318
Span: endpoint.Span,
324-
BlockName: endpoint.Name,
319+
BlockName: endpoint.Symbol,
325320
Method: method,
326-
Route: endpoint.Route,
321+
Route: endpoint.Path,
327322
ImportPath: pkg.ImportPath,
328323
PackageName: bindingPackageName(pkg.Name, endpoint.Package),
329-
FunctionName: endpoint.Name,
324+
FunctionName: endpoint.Symbol,
330325
Status: source.BackendBindingMissing,
331326
}
332327
}

0 commit comments

Comments
 (0)