Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions client/ts/src/schematic/symbol/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,6 @@ export const handleZ = z.object({
});
export interface Handle extends z.infer<typeof handleZ> {}

/** Viewport is the camera state for viewing or previewing a symbol. */
export const viewportZ = z.object({
/** zoom is the zoom level where 1.0 equals 100%. */
zoom: z.number().default(1),
/** position is the (x, y) pan offset. */
position: spatial.xyZ,
});
export interface Viewport extends z.infer<typeof viewportZ> {}

export const keyZ = z.uuid();
export type Key = z.infer<typeof keyZ>;

Expand Down Expand Up @@ -84,7 +75,7 @@ export const specZ = z.object({
/** scaleStroke indicates whether stroke width scales with the symbol size. */
scaleStroke: z.boolean().default(false),
/** previewViewport is an optional viewport configuration for symbol preview rendering. */
previewViewport: viewportZ.optional(),
previewViewport: spatial.viewportZ.optional(),
});
export interface Spec extends z.infer<typeof specZ> {}

Expand Down
2 changes: 1 addition & 1 deletion console/src/lineplot/types/v0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const axisStateZ = z.object({
key: axisKeyZ,
label: z.string(),
labelDirection: direction.directionZ,
bounds: bounds.boundsZ,
bounds: bounds.boundsZ(),
autoBounds: z.object({ lower: z.boolean(), upper: z.boolean() }),
tickSpacing: z.number(),
labelLevel: Text.levelZ,
Expand Down
2 changes: 1 addition & 1 deletion console/src/lineplot/types/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as v0 from "@/lineplot/types/v0";

export const VERSION = "1.0.0";

export const legendStateZ = v0.legendStateZ.extend({ position: sticky.xy });
export const legendStateZ = v0.legendStateZ.extend({ position: sticky.xyZ });
export interface LegendState extends z.infer<typeof legendStateZ> {}
export const ZERO_LEGEND_STATE: LegendState = {
...v0.ZERO_LEGEND_STATE,
Expand Down
2 changes: 1 addition & 1 deletion console/src/schematic/types/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const VERSION = "1.0.0";

export const legendStateZ = z.object({
visible: z.boolean(),
position: sticky.xy,
position: sticky.xyZ,
colors: z.record(z.string(), z.string()).default({}),
});
export interface LegendState extends z.infer<typeof legendStateZ> {}
Expand Down
39 changes: 39 additions & 0 deletions oracle/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ func analyze(c *analysisCtx) {

for _, typ := range c.table.TypesInNamespace(c.namespace) {
validateExtends(c, typ)
validateTypeParams(c, typ)
}
}

Expand Down Expand Up @@ -887,6 +888,44 @@ func validateExtends(c *analysisCtx, typ resolution.Type) {
}
}

func typeParamsOf(form resolution.TypeForm) []resolution.TypeParam {
switch f := form.(type) {
case resolution.StructForm:
return f.TypeParams
case resolution.AliasForm:
return f.TypeParams
case resolution.DistinctForm:
return f.TypeParams
}
return nil
}

func validateTypeParams(c *analysisCtx, typ resolution.Type) {
for _, tp := range typeParamsOf(typ.Form) {
if tp.Constraint == nil {
continue
}
if tp.Constraint.Name != "numeric" {
continue
}
if tp.Default == nil {
d := diagnostics.Errorf(nil,
"type parameter %s of %s constrained by 'numeric' requires a default (e.g. = float64); the constraint cannot be expressed concretely in Go, C++, Python, or Proto",
tp.Name, typ.Name)
d.File = c.filePath
c.diag.Add(d)
continue
}
if !resolution.IsNumberPrimitive(tp.Default.Name) {
d := diagnostics.Errorf(nil,
"type parameter %s of %s constrained by 'numeric' has non-numeric default %q; default must be a number primitive (int*, uint*, float32, float64)",
tp.Name, typ.Name, tp.Default.Name)
d.File = c.filePath
c.diag.Add(d)
}
}
}

func hasCircularInheritance(typ resolution.Type, table *resolution.Table, visited set.Set[string]) bool {
if visited.Contains(typ.QualifiedName) {
return true
Expand Down
42 changes: 42 additions & 0 deletions oracle/analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,48 @@ var _ = Describe("Analyzer", func() {
Expect(form.TypeParams[0].Constraint.Name).To(Equal("comparable"))
})

It("Should resolve numeric constraint with numeric default without warnings", func(ctx SpecContext) {
source := `
Bounds struct<T extends numeric = float64> {
lower T
upper T
}
`
table, diag := analyzer.AnalyzeSource(ctx, source, "test", loader)
Expect(diag.Ok()).To(BeTrue())

boundsType := table.MustGet("test.Bounds")
form := boundsType.Form.(resolution.StructForm)
Expect(form.TypeParams[0].Constraint).NotTo(BeNil())
Expect(form.TypeParams[0].Constraint.Name).To(Equal("numeric"))
Expect(form.TypeParams[0].Default).NotTo(BeNil())
Expect(form.TypeParams[0].Default.Name).To(Equal("float64"))
})

It("Should reject numeric constraint without a default", func(ctx SpecContext) {
source := `
Bounds struct<T extends numeric> {
lower T
upper T
}
`
_, diag := analyzer.AnalyzeSource(ctx, source, "test", loader)
Expect(diag.Ok()).To(BeFalse())
Expect(diag.String()).To(ContainSubstring("requires a default"))
})

It("Should reject numeric constraint with non-numeric default", func(ctx SpecContext) {
source := `
Bounds struct<T extends numeric = string> {
lower T
upper T
}
`
_, diag := analyzer.AnalyzeSource(ctx, source, "test", loader)
Expect(diag.Ok()).To(BeFalse())
Expect(diag.String()).To(ContainSubstring("non-numeric default"))
})

It("Should parse generic struct with default type parameter", func(ctx SpecContext) {
source := `
Container struct<T = string> {
Expand Down
11 changes: 8 additions & 3 deletions oracle/plugin/go/pb/pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,15 +410,20 @@ func (p *Plugin) processGenericStructForTranslation(
pbName = s.Name
}

data.imports.AddExternal("google.golang.org/protobuf/types/known/anypb")

typeParams := make([]typeParamData, 0, len(form.TypeParams))
typeParamNames := make([]string, 0, len(form.TypeParams))
for _, tp := range resolution.NonDefaultedTypeParams(form.TypeParams) {
typeParams = append(typeParams, typeParamData{Name: tp.Name, Constraint: typeParamConstraint(tp)})
typeParamNames = append(typeParamNames, tp.Name)
}

// anypb is only referenced from translator signatures of structs whose
// generics survive default-substitution; for fully-defaulted generics the
// emitted translator is concrete and the import would be unused.
if len(typeParamNames) > 0 {
data.imports.AddExternal("google.golang.org/protobuf/types/known/anypb")
}

goTypeBase := fmt.Sprintf("%s.%s", data.parentAlias, goName)
goTypeWithParams := goTypeBase
if len(typeParamNames) > 0 {
Expand Down Expand Up @@ -523,7 +528,7 @@ func (p *Plugin) processGenericFieldForTranslation(
BackwardExpr: backwardExpr,
BackwardCast: backwardCast,
IsOptional: isOptional,
IsOptionalStruct: isOptional && isStructType(typeRef, data.table),
IsOptionalStruct: isHardOptional && isStructType(typeRef, data.table),
HasError: hasError,
HasBackwardError: hasBackwardError,
}, false
Expand Down
42 changes: 42 additions & 0 deletions oracle/plugin/go/pb/pb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,48 @@ var _ = Describe("Go PB Plugin", func() {
ExpectContent(resp, "translator.gen.go").
ToContain("Name: r.Name")
})

It("Should round-trip a soft optional struct as a non-nullable wire field", func(ctx SpecContext) {
// A struct field with a single "?" keeps its Go type as a
// value, and the proto field is plain (no `optional` keyword).
// The translator converts unconditionally in both directions:
// no zero-value guard on the Go side, no nil-check carve-out
// on the proto side. AnchorFromPB's own pb == nil guard makes
// the unconditional FromPB call safe even when the proto
// pointer is unset. Enum translators tolerate the Go zero, so
// converting a zero-valued Anchor does not error.
source := `
@go output "core/test"
@pb

Side enum {
left = "left"
right = "right"
}

Anchor struct {
side Side
}

Test struct {
key uuid
anchor Anchor?
}
`
resp := MustGenerate(ctx, source, "test", loader, pbPlugin)

ExpectContent(resp, "translator.gen.go").
ToContain(
"anchorVal, err := AnchorToPB(r.Anchor)",
"Anchor: anchorVal",
"r.Anchor, err = AnchorFromPB(pb.Anchor)",
).
ToNotContain(
"if r.Anchor != (test.Anchor{}) {",
"if pb.Anchor != nil {",
)
})

})

Context("cross-namespace struct reference", func() {
Expand Down
16 changes: 12 additions & 4 deletions oracle/plugin/go/types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1274,9 +1274,12 @@ var _ = Describe("Go Types Plugin", func() {
})

Context("regression tests", func() {
It("Should use snake_case for JSON struct tags regardless of field name casing", func(ctx SpecContext) {
// Regression test: JSON tags should always be snake_case, regardless of
// whether the field name is PascalCase, camelCase, or SCREAMING_CASE.
It("Should emit snake_case JSON struct tags regardless of schema field casing", func(ctx SpecContext) {
// The wire format is canonically snake_case across every
// language. Schema field names are routed through SnakeCase
// regardless of how they're spelled in the schema. TypeScript
// converts at the boundary via x/ts caseconv, so it can use
// camelCase locally without changing the wire format.
source := `
@go output "arc/go/compiler"

Expand All @@ -1286,6 +1289,9 @@ var _ = Describe("Go Types Plugin", func() {
camelCaseField string
PascalCaseField int32
already_snake_case bool
clientX float64
signedWidth float64
targetKey string
}
`
table, diag := analyzer.AnalyzeSource(ctx, source, "compiler", loader)
Expand All @@ -1299,12 +1305,14 @@ var _ = Describe("Go Types Plugin", func() {
Expect(err).To(BeNil())

content := string(resp.Files[0].Content)
// All JSON tags should be snake_case
Expect(content).To(ContainSubstring(`json:"wasm"`))
Expect(content).To(ContainSubstring(`json:"output_memory_bases"`))
Expect(content).To(ContainSubstring(`json:"camel_case_field"`))
Expect(content).To(ContainSubstring(`json:"pascal_case_field"`))
Expect(content).To(ContainSubstring(`json:"already_snake_case"`))
Expect(content).To(ContainSubstring(`json:"client_x"`))
Expect(content).To(ContainSubstring(`json:"signed_width"`))
Expect(content).To(ContainSubstring(`json:"target_key"`))
})

It("Should use alias type name in struct fields instead of expanded target", func(ctx SpecContext) {
Expand Down
Loading
Loading