From 81001602fc1f9c62e4145f3eee418cd3e7eae80a Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 23 Jul 2026 16:44:23 +0200 Subject: [PATCH] Generate the TNumber value collections from the type relations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getValues and the other value-domain collection accessors return a base-typed collection — a tfloat's value range is a floatspanset, an intspanset for a tint. The concrete wrapper is base-dependent, so these returns were held back. Read the base-to-collection type-relation registry the catalog now carries, and resolve each concrete class's base to its value set, span and span set type, then to the layer's wrapper by the value-collection naming convention. valuespans (getValues) and toSpan are base-generic on the abstract TNumber, so they are specialised onto the concrete int and float surfaces where the base resolves the wrapper; valueBins folds the value span array into that wrapper. A base whose wrapper the layer does not provide (bigint) defers rather than dangling, so the resolution stays honest. GeneratedTInt and GeneratedTFloat now return FloatSpanSet/IntSpanSet, their value spans and value span bins. A parity test checks valuespans, toSpan and valueBins against direct GeneratedFunctions calls. The mapping is derived entirely from the catalog, with no per-class hand-coding. --- .../src/main/java/ObjectLayerGenerator.java | 102 +++++++++++++++++- .../GeneratedConcreteNumberParityTest.java | 28 +++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/codegen/src/main/java/ObjectLayerGenerator.java b/codegen/src/main/java/ObjectLayerGenerator.java index 62788badf..e953cc5d7 100644 --- a/codegen/src/main/java/ObjectLayerGenerator.java +++ b/codegen/src/main/java/ObjectLayerGenerator.java @@ -53,6 +53,15 @@ private record ClassSpec(String objectKey, String interfaceName, String superInt /** The interface this run emits. */ private ClassSpec spec; + /** Base type name to its {@code {role -> collection type name}} map, from the catalog typeRelations. */ + private final Map> typeRelByBase = new HashMap<>(); + + /** Temporal type name to its base type name, inverted from the typeRelations registry. */ + private final Map tempToBase = new HashMap<>(); + + /** The base type name of the class this run emits, or {@code null} for an abstract or time surface. */ + private String valueBase; + /** The object-model roles this surface generates. */ private static final Set ROLES = Set.of("accessor", "conversion", "predicate", "restriction", "output", "constructor"); @@ -101,7 +110,7 @@ public static void main(String[] args) throws IOException { } } - /** Index the catalog once: enum names, functions, and the struct strides the folds read. */ + /** Index the catalog once: enum names, functions, the struct strides and the type-relation registry. */ private void init(JsonNode root) { collectEnumNames(root); indexFunctions(root); @@ -109,10 +118,23 @@ private void init(JsonNode root) { matchFields = structFields(root, "Match"); matchStride = structSize(root, "Match"); tboxStride = structSize(root, "TBox"); + JsonNode byBase = root.path("typeRelations").path("byBase"); + byBase.fields().forEachRemaining(e -> { + Map roles = new HashMap<>(); + e.getValue().fields().forEachRemaining(r -> roles.put(r.getKey(), r.getValue().asText())); + typeRelByBase.put(e.getKey(), roles); + String temporal = roles.get("temporal"); + if (temporal != null) { + tempToBase.put(temporal, e.getKey()); + } + }); } private void run(ClassSpec spec, JsonNode root, Path out, String inputPath) throws IOException { this.spec = spec; + // The base type this surface is over, when it is a concrete value type. A value-domain collection + // return then resolves to its concrete wrapper through the type-relation registry. + this.valueBase = tempToBase.get(spec.objectKey().toLowerCase()); deferred.clear(); System.out.println("--- " + spec.interfaceName() + " (objectModel.classes." + spec.objectKey() + ") ---"); @@ -138,6 +160,29 @@ private void run(ClassSpec spec, JsonNode root, Path out, String inputPath) thro methods.add(method); } } + // A value-domain collection return (a value span or span set) is base-generic on the abstract + // superclass — its concrete wrapper is unknowable there — so it is emitted on this concrete + // surface, where the base resolves the wrapper. Pull those superclass methods down. + if (valueBase != null && spec.superInterface() != null) { + String superKey = spec.superInterface().replace("Generated", ""); + for (JsonNode m : root.path("objectModel").path("classes").path(superKey).path("methods")) { + if (m.path("ooExclude").asBoolean(false) || !ROLES.contains(m.path("role").asText(""))) { + continue; + } + JsonNode fn = functions.get(m.path("function").asText()); + if (fn == null) { + continue; + } + String retC = cleanType(fn.path("returnType").path("c").asText()); + if (!retC.equals("Span *") && !retC.equals("SpanSet *") && !retC.equals("Set *")) { + continue; // only the base-generic value-collection returns are specialised here + } + Method method = classify(m); + if (method != null && seen.add(method.ooName)) { + methods.add(method); + } + } + } methods.sort(Comparator.comparing(x -> x.ooName)); String content = generateInterface(methods); @@ -403,6 +448,7 @@ && cleanType(fn.path("returnType").path("c").asText()).equals("Temporal *")) { String arrayKind = arrayElement != null ? "objectArray" : retC.equals("TimestampTz *") ? "scalarArray" : retC.equals("Span *") && spec.spanReturnsAreTime() ? "spanArray" + : retC.equals("Span *") && valueWrapper("span") != null ? "valueSpanArray" : retC.equals("TBox *") ? "tboxArray" : retC.equals("Match *") ? "matchArray" : null; @@ -422,7 +468,9 @@ && cleanType(fn.path("returnType").path("c").asText()).equals("Temporal *")) { foldArgs.add(a); } if (marshalled) { - String rt = switch (arrayKind) { + String rt = arrayKind.equals("valueSpanArray") + ? "java.util.List<" + valueWrapper("span") + ">" + : switch (arrayKind) { case "objectArray" -> "java.util.List"; case "scalarArray" -> "java.util.List"; case "spanArray" -> "java.util.List"; @@ -541,6 +589,18 @@ && cleanType(fn.path("returnType").path("c").asText()).equals("Temporal *")) { // A Temporal's spanset is its time domain, a tstzspanset; a value spanset waits for the subclass. returnType = "types.collections.time.tstzspanset"; returnKind = "tstzspanset"; + } else if (retC.equals("Span *") && valueWrapper("span") != null) { + // A value temporal's span is its value extent, a base-typed span (an intspan, a floatspan). + returnType = valueWrapper("span"); + returnKind = "valueCollection"; + } else if (retC.equals("SpanSet *") && valueWrapper("spanset") != null) { + // getValues: a value temporal's range is its value span set (an intspanset, a floatspanset). + returnType = valueWrapper("spanset"); + returnKind = "valueCollection"; + } else if (retC.equals("Set *") && valueWrapper("set") != null) { + // A value temporal's distinct-value set (an intset, a floatset). + returnType = valueWrapper("set"); + returnKind = "valueCollection"; } else if (retC.equals("TBox *")) { // A number temporal's bounding box over its value and time extents. returnType = "types.boxes.TBox"; @@ -702,6 +762,40 @@ private static String cleanType(String c) { return c.replace("const ", "").trim(); } + /** + * The concrete value-collection wrapper class for a role ({@code span}, {@code spanset} or + * {@code set}) of the base type this surface is over, or {@code null} when the base has no such + * collection or the layer has no wrapper for it. The collection type name comes from the catalog + * type-relation registry; the wrapper is derived by the value-collection naming convention and used + * only when the hand layer actually provides it, so a base without a wrapper defers rather than + * dangling on a class that does not exist. + */ + private String valueWrapper(String role) { + if (valueBase == null) { + return null; + } + Map roles = typeRelByBase.get(valueBase); + return roles == null ? null : wrapperClass(roles.get(role)); + } + + /** The Java wrapper for a value-collection type name (floatspanset -> FloatSpanSet), if it exists. */ + private static String wrapperClass(String typeName) { + if (typeName == null) { + return null; + } + String suffix = typeName.endsWith("spanset") ? "SpanSet" + : typeName.endsWith("span") ? "Span" + : typeName.endsWith("set") ? "Set" + : null; + if (suffix == null) { + return null; + } + String cls = capitalize(typeName.substring(0, typeName.length() - suffix.length())) + suffix; + Path file = Paths.get(System.getProperty("user.dir"), + "jmeos-core/src/main/java/types/collections/number", cls + ".java"); + return Files.exists(file) ? "types.collections.number." + cls : null; + } + /** * The record component for one split bin-start out-parameter, or {@code null} if this slice does not * model that dimension. A bin out-parameter is a pointer to an array of the bin-start values @@ -822,6 +916,10 @@ private String generateMethod(Method m) { case "tstzspan" -> "\t\treturn new types.collections.time.tstzspan(" + invocation + ");\n"; case "tstzspanset" -> "\t\treturn new types.collections.time.tstzspanset(" + invocation + ");\n"; case "tbox" -> "\t\treturn new types.boxes.TBox(" + invocation + ");\n"; + case "valueCollection" -> "\t\treturn new " + m.returnType + "(" + invocation + ");\n"; + case "valueSpanArray" -> arrayFoldBody(m, "new " + m.returnType.substring( + m.returnType.indexOf('<') + 1, m.returnType.lastIndexOf('>')) + + "(GeneratedFunctions.span_copy(_array.slice((long) _i * " + spanSize + ")))"); case "objectArray" -> arrayFoldBody(m, "Factory.create_temporal(_array.getPointer(" + "(long) _i * Long.BYTES), getCustomType(), " + m.returnSubtype + ")"); case "scalarArray" -> arrayFoldBody(m, "utils.TimestampTzConverter.toOffsetDateTime(" diff --git a/jmeos-core/src/test/java/types/temporal/generated/GeneratedConcreteNumberParityTest.java b/jmeos-core/src/test/java/types/temporal/generated/GeneratedConcreteNumberParityTest.java index b2ca37d3a..4823dcb87 100644 --- a/jmeos-core/src/test/java/types/temporal/generated/GeneratedConcreteNumberParityTest.java +++ b/jmeos-core/src/test/java/types/temporal/generated/GeneratedConcreteNumberParityTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import types.basic.tfloat.TFloatSeq; import types.basic.tint.TIntSeq; +import types.collections.number.FloatSpan; import types.temporal.Temporal; import types.temporal.TemporalType; import utils.ConversionUtils; @@ -111,6 +112,33 @@ void intValueAccessorsMatchTheLibrary() { assertEquals(r2 == null ? null : Integer.valueOf(r2.getInt(0)), g.valueAtTimestamptz(t, true)); } + @Test + void floatValueCollectionsMatchTheLibrary() { + TFloatSeq a = new TFloatSeq("[1@2019-09-01, 3@2019-09-02, 2@2019-09-03]"); + Pointer p = a.getInner(); + GeneratedTFloat g = genFloat(a); + Runtime rt = Runtime.getSystemRuntime(); + int spanBytes = 24; // sizeof(Span) on the 64-bit targets + + // getValues: the value range as a concrete float span set, derived from the type-relation registry. + assertEquals(GeneratedFunctions.spanset_out(GeneratedFunctions.tnumber_valuespans(p), 15), + GeneratedFunctions.spanset_out(g.valuespans().get_inner(), 15)); + + // toSpan: the value extent as a concrete float span. + assertEquals(GeneratedFunctions.span_out(GeneratedFunctions.tnumber_to_span(p), 15), + GeneratedFunctions.span_out(g.toSpan().get_inner(), 15)); + + // valueBins: the value span array folded into a List. + Pointer c = Memory.allocate(rt, Integer.BYTES); + Pointer arr = GeneratedFunctions.tfloat_value_bins(p, 2.0, 0.0, c); + List bins = g.valueBins(2.0, 0.0); + assertEquals(c.getInt(0), bins.size()); + for (int i = 0; i < bins.size(); i++) { + assertEquals(GeneratedFunctions.span_out(arr.slice((long) i * spanBytes), 15), + GeneratedFunctions.span_out(bins.get(i).get_inner(), 15)); + } + } + @Test void intValueSplitMatchesTheLibrary() { TIntSeq a = new TIntSeq("[1@2019-09-01, 3@2019-09-02, 5@2019-09-03]");