From e6be96c2807b4ef05baa53c1de455843325c10c7 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Wed, 2 Sep 2026 23:35:06 +0200 Subject: [PATCH] Take the lifted stream operations from the catalog catalog_gen.go carries a second table, mathOps: the 22 MEOS functions that take a temporal number and answer one, each with the SQL name it carries on every engine, the MEOS C symbol behind it, and the C type of its one further parameter. The generator selects them from the function's own metadata: the Doxygen group places it among the value transformations, the category transformation separates it from a conversion and from an accessor, it takes a Temporal and answers a Temporal, its leading name token names a class that admits a tfloat, and it takes at most one further parameter that is not itself a Temporal. mathops_meos_gen.go is the cgo call for each of them, behind the meos build tag, and liftInstant dispatches through it. The MEOS C symbol behind an operation is written by hand nowhere: the catalog names it, the generator emits the call, and a rename upstream is a compile error rather than a call to a function that means something else. The one float64 a continuous query carries fills a further parameter through one conversion per C type: a double directly, an int truncated, a bool as non-zero, which is what gives the degrees normalisation flag the false it takes. An operation whose parameter cannot be filled from that single value, as scaleTime and setInterp cannot, gets no case and answers the caller as unserved. 19 of the 22 are callable. Each entry of liftedOps names the SQL function it lifts, and catalog_test.go holds the two together: the name must be one the catalog states over a temporal number, it must name exactly one function, and the query supplies the further parameter exactly when the catalog gives it as a double. Both rules are exercised against a control: pointing an operation at a name MEOS does not carry, and claiming an arity the catalog contradicts, each fail with the sentence naming the disagreement. What stays declared in the tier is WHICH operations are pointwise, and that is the one thing about them MEOS does not state. The catalog places derivative, trend and angularDifference in the same group and the same category as sin and abs, because all five transform a temporal number's values; only the first three do it by walking segments, and a stream record is one instant with no neighbour to read. A test names those three and requires them to stay out of the lifted set, and to stay in the catalog, so it measures something. TestMeosTransform measures all fifteen lifted operations against expected values, so the dispatch is equivalent by measurement rather than by inspection, and it requires that every lifted operation appear among its cases: an operation nobody measures could call the wrong symbol and still pass. Removing one from the cases fails with the sentence naming it. The build job regenerates both files and refuses a difference in either. --- .github/workflows/go.yml | 15 +-- GENERATION.md | 26 ++++- catalog_gen.go | 47 +++++++++ catalog_test.go | 58 +++++++++++ mathops_meos_gen.go | 67 +++++++++++++ stream.go | 52 ++++++---- stream_engine_meos.go | 39 ++----- stream_meos_test.go | 14 +++ tools/codegen/main.go | 212 ++++++++++++++++++++++++++++++++++++++- 9 files changed, 465 insertions(+), 65 deletions(-) create mode 100644 mathops_meos_gen.go diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 19a9787..fb156a9 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -60,13 +60,14 @@ jobs: # generated: a hand-kept copy goes stale in silence instead. - name: The generated table is what the catalog declares run: | - go run ./tools/codegen -catalog "${{ steps.provision.outputs.catalog-path }}" -o catalog_gen.go - if ! git diff --quiet -- catalog_gen.go; then - echo "::error::catalog_gen.go is not what the catalog declares. Regenerate it: go run ./tools/codegen -catalog -o catalog_gen.go" - git diff -- catalog_gen.go + go run ./tools/codegen -catalog "${{ steps.provision.outputs.catalog-path }}" \ + -o catalog_gen.go -o-meos mathops_meos_gen.go + if ! git diff --quiet -- catalog_gen.go mathops_meos_gen.go; then + echo "::error::the generated files are not what the catalog declares. Regenerate them: go run ./tools/codegen -catalog " + git diff -- catalog_gen.go mathops_meos_gen.go exit 1 fi - echo "the committed table is what the catalog of MobilityDB master declares" + echo "the committed tables are what the catalog of MobilityDB master declares" # EVERY TEST THIS REPOSITORY CARRIES RUNS HERE, AND NONE OF THEM SKIPS. # A test skips when a precondition is missing from the job that collects it, @@ -102,12 +103,12 @@ jobs: # The vendored OGC schema bundle is compared against what OGC publishes. MFAPI_SCHEMA_FRESHNESS: "1" # The floor is every result line `go test -v` writes, subtests included: - # 84 top-level tests and 64 subtests. Subtests belong in it because a + # 86 top-level tests and 64 subtests. Subtests belong in it because a # t.Run whose body skips is as unrun as its parent would be, and deleting # a case from a table-driven test moves nothing else. Raise it when the # suite grows. Lowering it belongs in the same commit as the removal it # accounts for, because deleting a test is what a skip refusal cannot see. - MFAPI_TEST_FLOOR: "148" + MFAPI_TEST_FLOOR: "150" steps: - uses: actions/checkout@v4 diff --git a/GENERATION.md b/GENERATION.md index f44db44..db519fd 100644 --- a/GENERATION.md +++ b/GENERATION.md @@ -7,14 +7,22 @@ MobilityDB master, regenerates against it, and refuses a difference. ## What is generated -`catalog_gen.go` is the table of MEOS temporal types: for each one, its name, -its base type, its bounding box, the token `asMFJSON` writes for it, whether it -is spatial, whether it is a number, and whether MEOS interpolates it linearly. +`catalog_gen.go` holds two tables. `temporalTypes` is the MEOS temporal types: +for each one, its name, its base type, its bounding box, the token `asMFJSON` +writes for it, whether it is spatial, whether it is a number, and whether MEOS +interpolates it linearly. `mathOps` is the MEOS functions that take a temporal +number and answer one, each with the SQL name it carries on every engine, the +MEOS C symbol behind it, and the C type of its one further parameter. + +`mathops_meos_gen.go` is the cgo call for each of those operations, behind the +`meos` build tag. It is what keeps a C symbol out of hand-written code: a MEOS +rename becomes a compile error rather than a wrong call. Run it against a catalog: ```bash -go run ./tools/codegen -catalog -o catalog_gen.go +go run ./tools/codegen -catalog \ + -o catalog_gen.go -o-meos mathops_meos_gen.go ``` ## Where the facts come from @@ -43,6 +51,16 @@ generator. ## What the tier adds, and what holds it complete +Which operations are POINTWISE is the tier's, because MEOS does not state it. +The catalog places `derivative`, `trend` and `angularDifference` in the same +group and the same category as `sin` and `abs`, since all five transform a +temporal number's values; only the first three do it by walking segments, and a +stream record is one instant with no neighbour to read. `liftedOps` in +`stream.go` declares the pointwise subset by SQL name, and `catalog_test.go` +checks every entry against `mathOps`: the name must be one the catalog states, +it must name exactly one function, and the query supplies the further parameter +exactly when the catalog gives it as a double. + Two things are the standard's rather than MEOS's, so they are stated in `main.go` next to the code that uses them: diff --git a/catalog_gen.go b/catalog_gen.go index 0b39f75..68a1f09 100644 --- a/catalog_gen.go +++ b/catalog_gen.go @@ -60,3 +60,50 @@ var temporalTypeByName = func() map[string]*TemporalType { } return m }() + +// MathOp is one MEOS function that takes a temporal number and answers one. +// SQLName is the name the operation carries on every engine, Symbol the MEOS C +// symbol behind it, and Arg the C type of its one further parameter, empty when +// it takes none. +type MathOp struct { + SQLName string + Symbol string + Arg string +} + +// mathOps is every such function the catalog states, ordered by SQL name. +var mathOps = []MathOp{ + {SQLName: "abs", Symbol: "tnumber_abs", Arg: ""}, + {SQLName: "angularDifference", Symbol: "tnumber_angular_difference", Arg: ""}, + {SQLName: "ceil", Symbol: "tfloat_ceil", Arg: ""}, + {SQLName: "cos", Symbol: "tfloat_cos", Arg: ""}, + {SQLName: "degrees", Symbol: "tfloat_degrees", Arg: "bool"}, + {SQLName: "derivative", Symbol: "temporal_derivative", Arg: ""}, + {SQLName: "exp", Symbol: "tfloat_exp", Arg: ""}, + {SQLName: "floor", Symbol: "tfloat_floor", Arg: ""}, + {SQLName: "ln", Symbol: "tfloat_ln", Arg: ""}, + {SQLName: "log10", Symbol: "tfloat_log10", Arg: ""}, + {SQLName: "radians", Symbol: "tfloat_radians", Arg: ""}, + {SQLName: "round", Symbol: "temporal_round", Arg: "int"}, + {SQLName: "scaleTime", Symbol: "temporal_scale_time", Arg: "const Interval *"}, + {SQLName: "setInterp", Symbol: "temporal_set_interp", Arg: "interpType"}, + {SQLName: "shiftTime", Symbol: "temporal_shift_time", Arg: "const Interval *"}, + {SQLName: "sin", Symbol: "tfloat_sin", Arg: ""}, + {SQLName: "tAdd", Symbol: "add_tfloat_float", Arg: "double"}, + {SQLName: "tDiv", Symbol: "div_tfloat_float", Arg: "double"}, + {SQLName: "tMul", Symbol: "mul_tfloat_float", Arg: "double"}, + {SQLName: "tSub", Symbol: "sub_tfloat_float", Arg: "double"}, + {SQLName: "tan", Symbol: "tfloat_tan", Arg: ""}, + {SQLName: "trend", Symbol: "tnumber_trend", Arg: ""}, +} + +// mathOpBySQLName indexes mathOps by the SQL name. A name several functions +// carry keeps them all, so a caller resolves it rather than reading whichever +// entry a map happened to hold. +var mathOpBySQLName = func() map[string][]*MathOp { + m := map[string][]*MathOp{} + for i := range mathOps { + m[mathOps[i].SQLName] = append(m[mathOps[i].SQLName], &mathOps[i]) + } + return m +}() diff --git a/catalog_test.go b/catalog_test.go index 7ab96e0..149bc2c 100644 --- a/catalog_test.go +++ b/catalog_test.go @@ -157,3 +157,61 @@ func TestValueColumnsAreOnePerScalarType(t *testing.T) { t.Errorf("the value columns are not in a stable order: %v", names) } } + +// Every operation the streaming tier lifts must be one the catalog states, with +// the shape the entry claims. A MEOS rename, a removal or a changed parameter +// then fails the build rather than a request at run time. +func TestEveryLiftedOpIsInTheCatalog(t *testing.T) { + for name, info := range liftedOps { + if info.sqlName == "" { + t.Errorf("the lifted operation %q names no SQL function", name) + continue + } + ops := mathOpBySQLName[info.sqlName] + if len(ops) == 0 { + t.Errorf("the lifted operation %q names the SQL function %q, which the catalog does not state over a temporal number", name, info.sqlName) + continue + } + if len(ops) > 1 { + var syms []string + for _, op := range ops { + syms = append(syms, op.Symbol) + } + sort.Strings(syms) + t.Errorf("the SQL name %q is carried by %v, so it does not name one operation", info.sqlName, syms) + continue + } + // The query supplies the further parameter exactly when the catalog + // gives it as a double. Where the catalog gives another type, as it + // does for the degrees normalisation flag, the tier passes a constant + // and the query supplies nothing. + if want := ops[0].Arg == "double"; info.needsArg != want { + t.Errorf("the lifted operation %q takes an argument from the query = %t, and the catalog gives %s a parameter of %q", + name, info.needsArg, ops[0].Symbol, ops[0].Arg) + } + } +} + +// The catalog states more operations over a temporal number than the tier +// lifts, and that is the point of holding the two apart: the ones left out are +// left out for a reason a reader can check. +func TestTheCatalogStatesMoreThanTheTierLifts(t *testing.T) { + if len(mathOps) < len(liftedOps) { + t.Fatalf("the catalog states %d operations and the tier lifts %d, so the tier lifts one the catalog does not", len(mathOps), len(liftedOps)) + } + lifted := map[string]bool{} + for _, info := range liftedOps { + lifted[info.sqlName] = true + } + // These three transform a temporal number's values by walking its segments, + // so on a stream record, which is one instant, they have no neighbour to + // read. They are in the catalog and must stay out of the lifted set. + for _, name := range []string{"derivative", "trend", "angularDifference"} { + if len(mathOpBySQLName[name]) == 0 { + t.Errorf("the catalog no longer states %q, so this test no longer measures anything", name) + } + if lifted[name] { + t.Errorf("%q is lifted, and its value at an instant is not a function of the value at that instant", name) + } + } +} diff --git a/mathops_meos_gen.go b/mathops_meos_gen.go new file mode 100644 index 0000000..b338b83 --- /dev/null +++ b/mathops_meos_gen.go @@ -0,0 +1,67 @@ +//go:build meos + +// Code generated by tools/codegen from the MEOS-API catalog. DO NOT EDIT. +// +// Regenerate with: +// +// go run ./tools/codegen -catalog -o-meos mathops_meos_gen.go +// +// One case per operation the catalog states over a temporal number, calling the +// MEOS symbol the catalog names. An operation whose further parameter cannot be +// filled from the single value a query carries gets no case. + +package main + +/* +#cgo pkg-config: meos +#include +*/ +import "C" + +// callMathOp applies the operation named by its SQL name to a temporal number, +// filling any further parameter from arg. It answers false when the operation +// is not one this dispatch serves, which the caller reports as an unknown +// operation rather than as a failed call. +func callMathOp(sqlName string, temp *C.Temporal, arg float64) (*C.Temporal, bool) { + switch sqlName { + case "abs": + return C.tnumber_abs(temp), true + case "angularDifference": + return C.tnumber_angular_difference(temp), true + case "ceil": + return C.tfloat_ceil(temp), true + case "cos": + return C.tfloat_cos(temp), true + case "degrees": + return C.tfloat_degrees(temp, arg != 0), true + case "derivative": + return C.temporal_derivative(temp), true + case "exp": + return C.tfloat_exp(temp), true + case "floor": + return C.tfloat_floor(temp), true + case "ln": + return C.tfloat_ln(temp), true + case "log10": + return C.tfloat_log10(temp), true + case "radians": + return C.tfloat_radians(temp), true + case "round": + return C.temporal_round(temp, C.int(arg)), true + case "sin": + return C.tfloat_sin(temp), true + case "tAdd": + return C.add_tfloat_float(temp, C.double(arg)), true + case "tDiv": + return C.div_tfloat_float(temp, C.double(arg)), true + case "tMul": + return C.mul_tfloat_float(temp, C.double(arg)), true + case "tSub": + return C.sub_tfloat_float(temp, C.double(arg)), true + case "tan": + return C.tfloat_tan(temp), true + case "trend": + return C.tnumber_trend(temp), true + } + return nil, false +} diff --git a/stream.go b/stream.go index cf329b2..36a26d7 100644 --- a/stream.go +++ b/stream.go @@ -200,31 +200,47 @@ func engineFor(name string) (StreamEngine, error) { return e, nil } -// opInfo describes a lifted scalar operation exposed by the streaming tier. The -// engine maps the name to the MEOS function; the control plane only validates. +// opInfo describes a lifted scalar operation exposed by the streaming tier. +// sqlName is the operation's name on every MEOS engine, which is what ties the +// entry to the catalog: catalog_gen.go states the MEOS symbol behind that name +// and the further parameter it takes. needsArg says the query supplies that +// parameter, which is so exactly when the catalog gives it as a double; where +// the catalog gives another type the tier passes a constant of its own. type opInfo struct { + sqlName string needsArg bool desc string } // liftedOps is the catalogue of scalar operations a continuous transform can -// apply to a tfloat stream. Every entry maps to a MEOS lifted temporal function. +// apply to a tfloat stream. It is the POINTWISE subset of the operations +// catalog_gen.go states: an operation whose value at an instant is a function +// of the value at that instant alone. A stream record is one instant, so an +// operation reading its neighbours has nothing to read. +// +// That property is the one thing about these operations MEOS does not state. +// The catalog places derivative, trend and angularDifference in the same group +// and the same category as sin and abs, because all five transform a temporal +// number's values; only the first three do it by walking segments. So the +// SUBSET is declared here and everything else about each entry is checked +// against the catalog by catalog_test.go: a MEOS rename, a removal, or a change +// of parameter fails the build rather than a request. var liftedOps = map[string]opInfo{ - "ln": {false, "natural logarithm"}, - "exp": {false, "exponential"}, - "log10": {false, "base-10 logarithm"}, - "ceil": {false, "ceiling"}, - "floor": {false, "floor"}, - "abs": {false, "absolute value"}, - "degrees": {false, "radians to degrees"}, - "radians": {false, "degrees to radians"}, - "sin": {false, "sine"}, - "cos": {false, "cosine"}, - "tan": {false, "tangent"}, - "add": {true, "add a scalar"}, - "sub": {true, "subtract a scalar"}, - "mul": {true, "multiply by a scalar"}, - "div": {true, "divide by a scalar"}, + "ln": {"ln", false, "natural logarithm"}, + "exp": {"exp", false, "exponential"}, + "log10": {"log10", false, "base-10 logarithm"}, + "ceil": {"ceil", false, "ceiling"}, + "floor": {"floor", false, "floor"}, + "abs": {"abs", false, "absolute value"}, + "degrees": {"degrees", false, "radians to degrees"}, + "radians": {"radians", false, "degrees to radians"}, + "sin": {"sin", false, "sine"}, + "cos": {"cos", false, "cosine"}, + "tan": {"tan", false, "tangent"}, + "add": {"tAdd", true, "add a scalar"}, + "sub": {"tSub", true, "subtract a scalar"}, + "mul": {"tMul", true, "multiply by a scalar"}, + "div": {"tDiv", true, "divide by a scalar"}, } func supportedOps() string { diff --git a/stream_engine_meos.go b/stream_engine_meos.go index fc82e35..f79b50f 100644 --- a/stream_engine_meos.go +++ b/stream_engine_meos.go @@ -291,41 +291,14 @@ func liftInstant(op string, arg float64, in Instant) (Instant, error) { } defer C.free(unsafe.Pointer(temp)) - var res *C.Temporal - switch op { - case "ln": - res = C.tfloat_ln(temp) - case "exp": - res = C.tfloat_exp(temp) - case "log10": - res = C.tfloat_log10(temp) - case "ceil": - res = C.tfloat_ceil(temp) - case "floor": - res = C.tfloat_floor(temp) - case "abs": - res = C.tnumber_abs(temp) - case "degrees": - res = C.tfloat_degrees(temp, false) - case "radians": - res = C.tfloat_radians(temp) - case "sin": - res = C.tfloat_sin(temp) - case "cos": - res = C.tfloat_cos(temp) - case "tan": - res = C.tfloat_tan(temp) - case "add": - res = C.add_tfloat_float(temp, C.double(arg)) - case "sub": - res = C.sub_tfloat_float(temp, C.double(arg)) - case "mul": - res = C.mul_tfloat_float(temp, C.double(arg)) - case "div": - res = C.div_tfloat_float(temp, C.double(arg)) - default: + info, lifted := liftedOps[op] + if !lifted { return Instant{}, fmt.Errorf("unknown operation %q", op) } + res, served := callMathOp(info.sqlName, temp, arg) + if !served { + return Instant{}, fmt.Errorf("the operation %q names the MEOS function %q, which this build does not call", op, info.sqlName) + } if res == nil { return Instant{}, fmt.Errorf("operation %q produced no result (domain error?)", op) } diff --git a/stream_meos_test.go b/stream_meos_test.go index 13d30dd..224acfc 100644 --- a/stream_meos_test.go +++ b/stream_meos_test.go @@ -40,6 +40,20 @@ func TestMeosTransform(t *testing.T) { {"cos", 0, 0, 1}, {"tan", 0, math.Pi / 4, 1}, } + // Every lifted operation is measured here, and that is what makes this an + // equivalence check rather than a sample: the dispatch it exercises names + // the MEOS symbol per operation, so an operation nobody measures could call + // the wrong one and still pass. + covered := map[string]bool{} + for _, c := range cases { + covered[c.op] = true + } + for op := range liftedOps { + if !covered[op] { + t.Errorf("the lifted operation %q is not measured here, so nothing checks which MEOS function it calls", op) + } + } + for _, c := range cases { ctx, cancel := context.WithCancel(context.Background()) src := make(chan Instant, 1) diff --git a/tools/codegen/main.go b/tools/codegen/main.go index d3b4dac..9c1ab84 100644 --- a/tools/codegen/main.go +++ b/tools/codegen/main.go @@ -28,11 +28,34 @@ import ( "go/format" "os" "sort" + "strings" ) // catalog is the part of meos-idl.json this generator reads. type catalog struct { TemporalTypes map[string]temporalType `json:"temporalTypes"` + Functions []function `json:"functions"` +} + +// function is the part of a catalog function entry the math-operation table +// reads: the C symbol, the SQL name that is the cross-engine invariant, the +// Doxygen group and category that place it, and the parameter C types. +type function struct { + Name string `json:"name"` + Group string `json:"group"` + Category string `json:"category"` + SQLName string `json:"sqlfn"` + Params []param `json:"params"` + Returns ctype `json:"returnType"` +} + +type param struct { + Name string `json:"name"` + CType string `json:"cType"` +} + +type ctype struct { + C string `json:"c"` } // temporalType is one entry of the catalog's temporalTypes registry. MFJSON is @@ -50,6 +73,7 @@ type temporalType struct { func main() { path := flag.String("catalog", "meos-idl.json", "the MEOS-API catalog") out := flag.String("o", "catalog_gen.go", "file to write") + outMeos := flag.String("o-meos", "mathops_meos_gen.go", "file to write the cgo dispatch to") flag.Parse() raw, err := os.ReadFile(*path) @@ -65,14 +89,29 @@ func main() { "MEOS-API that states one, or it is not a MEOS-API catalog", *path)) } - src, err := render(c.TemporalTypes) + ops := mathOperations(c.Functions) + if len(ops) == 0 { + fail(fmt.Errorf("%s names no temporal math operation: the groups this reads, "+ + "meos_temporal_math and meos_temporal_transf, are not where they were", *path)) + } + + src, err := render(c.TemporalTypes, ops) if err != nil { fail(err) } if err := os.WriteFile(*out, src, 0o644); err != nil { fail(err) } - fmt.Fprintf(os.Stderr, "codegen: %d temporal types written to %s\n", len(c.TemporalTypes), *out) + + dispatch, called, err := renderDispatch(ops) + if err != nil { + fail(err) + } + if err := os.WriteFile(*outMeos, dispatch, 0o644); err != nil { + fail(err) + } + fmt.Fprintf(os.Stderr, "codegen: %d temporal types and %d math operations written to %s, "+ + "%d of them callable from %s\n", len(c.TemporalTypes), len(ops), *out, called, *outMeos) } func fail(err error) { @@ -80,7 +119,145 @@ func fail(err error) { os.Exit(1) } -func render(types map[string]temporalType) ([]byte, error) { +// mathGroups are the two Doxygen groups MEOS puts a temporal number's value +// transformations in. +var mathGroups = map[string]bool{ + "meos_temporal_math": true, + "meos_temporal_transf": true, +} + +// mathPrefixes are the type classes whose functions accept a tfloat, which is +// the value a stream record carries. A MEOS function's leading token names the +// class it is generic over, so tfloat_, tnumber_ and temporal_ all admit one +// while tint_ and tbigint_ do not. +var mathPrefixes = map[string]bool{ + "tfloat": true, + "tnumber": true, + "temporal": true, +} + +// mathOperation is one MEOS function over a temporal number that the tier can +// apply to a stream record. +type mathOperation struct { + SQLName string // the SQL name, which is the same on every engine + Symbol string // the MEOS C symbol + Arg string // the C type of its one further parameter, empty when it takes none +} + +// mathOperations selects, from the catalog, the functions that take a temporal +// number and answer one. The selection is the function's own metadata and +// nothing else: +// +// - the Doxygen group places it among the value transformations; +// - the category is transformation, which is what separates a value +// transformation from a conversion (temporal_as_tinstant) and from an +// accessor (tnumber_delta_value); +// - it takes a Temporal and answers a Temporal; +// - its leading name token names a class that admits a tfloat, or it is the +// arithmetic form _tfloat_float; +// - it takes at most one further parameter, and that parameter is not itself +// a Temporal, since a stream record carries one value and not two. +func mathOperations(fns []function) []mathOperation { + var out []mathOperation + for _, f := range fns { + if !mathGroups[f.Group] || f.Category != "transformation" || f.SQLName == "" { + continue + } + if !strings.Contains(f.Returns.C, "Temporal *") { + continue + } + if len(f.Params) == 0 || !strings.Contains(f.Params[0].CType, "Temporal *") { + continue + } + rest := f.Params[1:] + if len(rest) > 1 || (len(rest) == 1 && strings.Contains(rest[0].CType, "Temporal *")) { + continue + } + if !mathPrefixes[strings.SplitN(f.Name, "_", 2)[0]] && !strings.HasSuffix(f.Name, "_tfloat_float") { + continue + } + arg := "" + if len(rest) == 1 { + arg = rest[0].CType + } + out = append(out, mathOperation{SQLName: f.SQLName, Symbol: f.Name, Arg: arg}) + } + sort.Slice(out, func(i, j int) bool { + if out[i].SQLName != out[j].SQLName { + return out[i].SQLName < out[j].SQLName + } + return out[i].Symbol < out[j].Symbol + }) + return out +} + +// argConversions turn the one float64 a continuous query carries into the C +// value a MEOS function's further parameter takes. A parameter type absent from +// this map cannot be filled from a query, so the operation carrying it gets no +// case and reaches the caller as one the dispatch does not serve. +var argConversions = map[string]string{ + "": "", + "double": "C.double(arg)", + "int": "C.int(arg)", + "bool": "arg != 0", +} + +// renderDispatch writes the cgo call for each operation, which is what keeps +// the C symbol behind an operation out of hand-written code: the symbol comes +// from the catalog, and a MEOS rename becomes a compile error here rather than +// a wrong call. It answers the source and how many operations it serves. +func renderDispatch(ops []mathOperation) ([]byte, int, error) { + var b bytes.Buffer + b.WriteString(`//go:build meos + +// Code generated by tools/codegen from the MEOS-API catalog. DO NOT EDIT. +// +// Regenerate with: +// +// go run ./tools/codegen -catalog -o-meos mathops_meos_gen.go +// +// One case per operation the catalog states over a temporal number, calling the +// MEOS symbol the catalog names. An operation whose further parameter cannot be +// filled from the single value a query carries gets no case. + +package main + +/* +#cgo pkg-config: meos +#include +*/ +import "C" + +// callMathOp applies the operation named by its SQL name to a temporal number, +// filling any further parameter from arg. It answers false when the operation +// is not one this dispatch serves, which the caller reports as an unknown +// operation rather than as a failed call. +func callMathOp(sqlName string, temp *C.Temporal, arg float64) (*C.Temporal, bool) { + switch sqlName { +`) + called := 0 + for _, op := range ops { + conv, ok := argConversions[op.Arg] + if !ok { + continue + } + called++ + fmt.Fprintf(&b, "\tcase %q:\n", op.SQLName) + if conv == "" { + fmt.Fprintf(&b, "\t\treturn C.%s(temp), true\n", op.Symbol) + } else { + fmt.Fprintf(&b, "\t\treturn C.%s(temp, %s), true\n", op.Symbol, conv) + } + } + b.WriteString(` } + return nil, false +} +`) + src, err := format.Source(b.Bytes()) + return src, called, err +} + +func render(types map[string]temporalType, ops []mathOperation) ([]byte, error) { names := make([]string, 0, len(types)) for name := range types { names = append(names, name) @@ -133,6 +310,35 @@ var temporalTypeByName = func() map[string]*TemporalType { } return m }() + +// MathOp is one MEOS function that takes a temporal number and answers one. +// SQLName is the name the operation carries on every engine, Symbol the MEOS C +// symbol behind it, and Arg the C type of its one further parameter, empty when +// it takes none. +type MathOp struct { + SQLName string + Symbol string + Arg string +} + +// mathOps is every such function the catalog states, ordered by SQL name. +var mathOps = []MathOp{ +`) + for _, op := range ops { + fmt.Fprintf(&b, "\t{SQLName: %q, Symbol: %q, Arg: %q},\n", op.SQLName, op.Symbol, op.Arg) + } + b.WriteString(`} + +// mathOpBySQLName indexes mathOps by the SQL name. A name several functions +// carry keeps them all, so a caller resolves it rather than reading whichever +// entry a map happened to hold. +var mathOpBySQLName = func() map[string][]*MathOp { + m := map[string][]*MathOp{} + for i := range mathOps { + m[mathOps[i].SQLName] = append(m[mathOps[i].SQLName], &mathOps[i]) + } + return m +}() `) return format.Source(b.Bytes()) }