diff --git a/cmd/flux/main.go b/cmd/flux/main.go index d0d3f9d584..9dc5418b6e 100644 --- a/cmd/flux/main.go +++ b/cmd/flux/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "fmt" "io/ioutil" "os" @@ -23,7 +24,7 @@ var flags struct { ExecScript bool Trace string Format string - Features []string + Features string } func runE(cmd *cobra.Command, args []string) error { @@ -54,8 +55,10 @@ func runE(cmd *cobra.Command, args []string) error { defer span.Finish() flagger := executetest.TestFlagger{} - for _, feature := range flags.Features { - flagger[feature] = true + if len(flags.Features) != 0 { + if err := json.Unmarshal([]byte(flags.Features), &flagger); err != nil { + return errors.Newf(codes.Invalid, "Unable to unmarshal features as json: %s", err) + } } ctx = feature.Dependency{Flagger: flagger}.Inject(ctx) @@ -115,7 +118,7 @@ func main() { fluxCmd.Flags().StringVar(&flags.Trace, "trace", "", "Trace query execution") fluxCmd.Flags().StringVarP(&flags.Format, "format", "", "cli", "Output format one of: cli,csv. Defaults to cli") fluxCmd.Flag("trace").NoOptDefVal = "jaeger" - fluxCmd.Flags().StringSliceVar(&flags.Features, "feature", nil, "Adds a boolean feature flag. See internal/feature/flags.yml for a list of the current features") + fluxCmd.Flags().StringVar(&flags.Features, "feature", "", "JSON object specifying the features to execute with. See internal/feature/flags.yml for a list of the current features") fmtCmd := &cobra.Command{ Use: "fmt", diff --git a/docs/SPEC.md b/docs/SPEC.md index ad9fd4101a..ab4aab8aa4 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -503,6 +503,25 @@ Note that an empty string is distinct from a _null_ value. The length of a string is its size in bytes, not the number of characters, since a single character may be multiple bytes. +##### Label types (Upcoming/Feature flagged) + +A _label type_ represents the name of a record field. +String literals may be treated as a label type instead of a `string` when used in a context that +expects a label type. + +``` +"a" // Can be treated as Label("a") +"xyz" // Can be treated as Label("xyz") +``` + +In effect, this allows functions accepting a record and a label to refer to specific properties of +the record. + +``` +// "mycolumn" is treated as Label("mycolumn") for when passed to `mean` +mean(column: "mycolumn") // Calculates the mean of `mycolumn` +``` + ##### Regular expression types A _regular expression type_ represents the set of all patterns for regular expressions. @@ -523,11 +542,9 @@ The length of an array is the number of elements in the array. ##### Record types An _record type_ represents a set of unordered key and value pairs. -The key must always be a string. +The key can be a string or a [type variable](<#Type variables>). The value may be any other type, and need not be the same as other values within the record. -Keys on a record may only be referenced statically. - Type inference will determine the properties that are present on a record. If type inference determines all the properties on a record it is said to be bounded. Not all keys may be known on the type of a record in which case the record is said to be unbounded. @@ -555,6 +572,11 @@ The generated values may be of any other type but must all be the same type. Flux functions can be polymorphic, meaning they can be applied to arguments of different types. Flux supports parametric, record, and ad hoc polymorphism. +##### Type variables + +Polymorphism are represented via "type variables" which are specified with a single uppercase +letter (`A`, `B`, etc). + ##### Parametric Polymorphism Parametric polymorphism is the notion that a function can be applied uniformly to arguments of any type. diff --git a/embed/stdlib/contrib/RohanSreerama5/naiveBayesClassifier.fc b/embed/stdlib/contrib/RohanSreerama5/naiveBayesClassifier.fc index 04dcf20c20..830ea83b3e 100644 Binary files a/embed/stdlib/contrib/RohanSreerama5/naiveBayesClassifier.fc and b/embed/stdlib/contrib/RohanSreerama5/naiveBayesClassifier.fc differ diff --git a/embed/stdlib/contrib/anaisdg/anomalydetection.fc b/embed/stdlib/contrib/anaisdg/anomalydetection.fc index 58a914e953..d4da3c440f 100644 Binary files a/embed/stdlib/contrib/anaisdg/anomalydetection.fc and b/embed/stdlib/contrib/anaisdg/anomalydetection.fc differ diff --git a/embed/stdlib/contrib/anaisdg/statsmodels.fc b/embed/stdlib/contrib/anaisdg/statsmodels.fc index 3257a9dab8..60ddb7d096 100644 Binary files a/embed/stdlib/contrib/anaisdg/statsmodels.fc and b/embed/stdlib/contrib/anaisdg/statsmodels.fc differ diff --git a/embed/stdlib/contrib/bonitoo-io/alerta.fc b/embed/stdlib/contrib/bonitoo-io/alerta.fc index 07a571ed52..a57e84d6f2 100644 Binary files a/embed/stdlib/contrib/bonitoo-io/alerta.fc and b/embed/stdlib/contrib/bonitoo-io/alerta.fc differ diff --git a/embed/stdlib/contrib/bonitoo-io/servicenow.fc b/embed/stdlib/contrib/bonitoo-io/servicenow.fc index 9174b04500..c7f967acaf 100644 Binary files a/embed/stdlib/contrib/bonitoo-io/servicenow.fc and b/embed/stdlib/contrib/bonitoo-io/servicenow.fc differ diff --git a/embed/stdlib/contrib/bonitoo-io/tickscript.fc b/embed/stdlib/contrib/bonitoo-io/tickscript.fc index f456b9feac..6e5faff999 100644 Binary files a/embed/stdlib/contrib/bonitoo-io/tickscript.fc and b/embed/stdlib/contrib/bonitoo-io/tickscript.fc differ diff --git a/embed/stdlib/contrib/bonitoo-io/victorops.fc b/embed/stdlib/contrib/bonitoo-io/victorops.fc index 7244464798..4a9c3892a4 100644 Binary files a/embed/stdlib/contrib/bonitoo-io/victorops.fc and b/embed/stdlib/contrib/bonitoo-io/victorops.fc differ diff --git a/embed/stdlib/contrib/bonitoo-io/zenoss.fc b/embed/stdlib/contrib/bonitoo-io/zenoss.fc index 77a3f4f016..6f71a95c75 100644 Binary files a/embed/stdlib/contrib/bonitoo-io/zenoss.fc and b/embed/stdlib/contrib/bonitoo-io/zenoss.fc differ diff --git a/embed/stdlib/contrib/chobbs/discord.fc b/embed/stdlib/contrib/chobbs/discord.fc index 1aa83df2b4..f1d8ac7edd 100644 Binary files a/embed/stdlib/contrib/chobbs/discord.fc and b/embed/stdlib/contrib/chobbs/discord.fc differ diff --git a/embed/stdlib/contrib/jsternberg/aggregate.fc b/embed/stdlib/contrib/jsternberg/aggregate.fc index b24e5680b9..094dc940a4 100644 Binary files a/embed/stdlib/contrib/jsternberg/aggregate.fc and b/embed/stdlib/contrib/jsternberg/aggregate.fc differ diff --git a/embed/stdlib/contrib/jsternberg/influxdb.fc b/embed/stdlib/contrib/jsternberg/influxdb.fc index 7f9589a9e7..cac5830c63 100644 Binary files a/embed/stdlib/contrib/jsternberg/influxdb.fc and b/embed/stdlib/contrib/jsternberg/influxdb.fc differ diff --git a/embed/stdlib/contrib/rhajek/bigpanda.fc b/embed/stdlib/contrib/rhajek/bigpanda.fc index 60250dc757..328ad34c86 100644 Binary files a/embed/stdlib/contrib/rhajek/bigpanda.fc and b/embed/stdlib/contrib/rhajek/bigpanda.fc differ diff --git a/embed/stdlib/contrib/sranka/opsgenie.fc b/embed/stdlib/contrib/sranka/opsgenie.fc index aa8cd1aebc..5d79fe0958 100644 Binary files a/embed/stdlib/contrib/sranka/opsgenie.fc and b/embed/stdlib/contrib/sranka/opsgenie.fc differ diff --git a/embed/stdlib/contrib/sranka/sensu.fc b/embed/stdlib/contrib/sranka/sensu.fc index 0804378cc8..48d1ff8d78 100644 Binary files a/embed/stdlib/contrib/sranka/sensu.fc and b/embed/stdlib/contrib/sranka/sensu.fc differ diff --git a/embed/stdlib/contrib/sranka/teams.fc b/embed/stdlib/contrib/sranka/teams.fc index 13a0d8a4f9..9768425b09 100644 Binary files a/embed/stdlib/contrib/sranka/teams.fc and b/embed/stdlib/contrib/sranka/teams.fc differ diff --git a/embed/stdlib/contrib/sranka/telegram.fc b/embed/stdlib/contrib/sranka/telegram.fc index 23a017aac7..10bd5d383e 100644 Binary files a/embed/stdlib/contrib/sranka/telegram.fc and b/embed/stdlib/contrib/sranka/telegram.fc differ diff --git a/embed/stdlib/contrib/sranka/webexteams.fc b/embed/stdlib/contrib/sranka/webexteams.fc index 9b9036ad7c..464fe10c19 100644 Binary files a/embed/stdlib/contrib/sranka/webexteams.fc and b/embed/stdlib/contrib/sranka/webexteams.fc differ diff --git a/embed/stdlib/date.fc b/embed/stdlib/date.fc index 3cfa4f6c35..a95fdbe416 100644 Binary files a/embed/stdlib/date.fc and b/embed/stdlib/date.fc differ diff --git a/embed/stdlib/experimental.fc b/embed/stdlib/experimental.fc index 1ef85f73fb..12457958e7 100644 Binary files a/embed/stdlib/experimental.fc and b/embed/stdlib/experimental.fc differ diff --git a/embed/stdlib/experimental/aggregate.fc b/embed/stdlib/experimental/aggregate.fc index ffe6016b37..4c64e4d491 100644 Binary files a/embed/stdlib/experimental/aggregate.fc and b/embed/stdlib/experimental/aggregate.fc differ diff --git a/embed/stdlib/experimental/array.fc b/embed/stdlib/experimental/array.fc index a8ac8e8dba..04d7568d15 100644 Binary files a/embed/stdlib/experimental/array.fc and b/embed/stdlib/experimental/array.fc differ diff --git a/embed/stdlib/experimental/csv.fc b/embed/stdlib/experimental/csv.fc index 826f413aa5..a4ceee87f8 100644 Binary files a/embed/stdlib/experimental/csv.fc and b/embed/stdlib/experimental/csv.fc differ diff --git a/embed/stdlib/experimental/geo.fc b/embed/stdlib/experimental/geo.fc index c77e395095..3140a4e875 100644 Binary files a/embed/stdlib/experimental/geo.fc and b/embed/stdlib/experimental/geo.fc differ diff --git a/embed/stdlib/experimental/http/requests.fc b/embed/stdlib/experimental/http/requests.fc index e816fd7344..0f493c9fbc 100644 Binary files a/embed/stdlib/experimental/http/requests.fc and b/embed/stdlib/experimental/http/requests.fc differ diff --git a/embed/stdlib/experimental/oee.fc b/embed/stdlib/experimental/oee.fc index d3c0bf8472..da526e5455 100644 Binary files a/embed/stdlib/experimental/oee.fc and b/embed/stdlib/experimental/oee.fc differ diff --git a/embed/stdlib/experimental/prometheus.fc b/embed/stdlib/experimental/prometheus.fc index eaf439e01a..da4f8210ca 100644 Binary files a/embed/stdlib/experimental/prometheus.fc and b/embed/stdlib/experimental/prometheus.fc differ diff --git a/embed/stdlib/experimental/query.fc b/embed/stdlib/experimental/query.fc index 2f5fd41b19..7b6f9a19d4 100644 Binary files a/embed/stdlib/experimental/query.fc and b/embed/stdlib/experimental/query.fc differ diff --git a/embed/stdlib/experimental/universe.fc b/embed/stdlib/experimental/universe.fc new file mode 100644 index 0000000000..e45a384100 Binary files /dev/null and b/embed/stdlib/experimental/universe.fc differ diff --git a/embed/stdlib/experimental/usage.fc b/embed/stdlib/experimental/usage.fc index 7cbff997a2..cc29d20d5a 100644 Binary files a/embed/stdlib/experimental/usage.fc and b/embed/stdlib/experimental/usage.fc differ diff --git a/embed/stdlib/http.fc b/embed/stdlib/http.fc index 74385e2d4c..9831b829a6 100644 Binary files a/embed/stdlib/http.fc and b/embed/stdlib/http.fc differ diff --git a/embed/stdlib/influxdata/influxdb/monitor.fc b/embed/stdlib/influxdata/influxdb/monitor.fc index fbde8bc0ae..070a46d210 100644 Binary files a/embed/stdlib/influxdata/influxdb/monitor.fc and b/embed/stdlib/influxdata/influxdb/monitor.fc differ diff --git a/embed/stdlib/influxdata/influxdb/sample.fc b/embed/stdlib/influxdata/influxdb/sample.fc index 21718a0345..a880d6a5b4 100644 Binary files a/embed/stdlib/influxdata/influxdb/sample.fc and b/embed/stdlib/influxdata/influxdb/sample.fc differ diff --git a/embed/stdlib/influxdata/influxdb/schema.fc b/embed/stdlib/influxdata/influxdb/schema.fc index 16064ee850..4801452382 100644 Binary files a/embed/stdlib/influxdata/influxdb/schema.fc and b/embed/stdlib/influxdata/influxdb/schema.fc differ diff --git a/embed/stdlib/influxdata/influxdb/tasks.fc b/embed/stdlib/influxdata/influxdb/tasks.fc index a4668a2e42..95b148b033 100644 Binary files a/embed/stdlib/influxdata/influxdb/tasks.fc and b/embed/stdlib/influxdata/influxdb/tasks.fc differ diff --git a/embed/stdlib/influxdata/influxdb/v1.fc b/embed/stdlib/influxdata/influxdb/v1.fc index 69342526d9..92a3bf5dda 100644 Binary files a/embed/stdlib/influxdata/influxdb/v1.fc and b/embed/stdlib/influxdata/influxdb/v1.fc differ diff --git a/embed/stdlib/internal/location.fc b/embed/stdlib/internal/location.fc index e58a5829ad..72c55821ab 100644 Binary files a/embed/stdlib/internal/location.fc and b/embed/stdlib/internal/location.fc differ diff --git a/embed/stdlib/internal/promql.fc b/embed/stdlib/internal/promql.fc index 82a1148a24..85a2722b16 100644 Binary files a/embed/stdlib/internal/promql.fc and b/embed/stdlib/internal/promql.fc differ diff --git a/embed/stdlib/pagerduty.fc b/embed/stdlib/pagerduty.fc index 527635179a..09fc30a5ca 100644 Binary files a/embed/stdlib/pagerduty.fc and b/embed/stdlib/pagerduty.fc differ diff --git a/embed/stdlib/pushbullet.fc b/embed/stdlib/pushbullet.fc index 71ea88f102..04c1bb893a 100644 Binary files a/embed/stdlib/pushbullet.fc and b/embed/stdlib/pushbullet.fc differ diff --git a/embed/stdlib/sampledata.fc b/embed/stdlib/sampledata.fc index 9c7fe099d3..2527cc64be 100644 Binary files a/embed/stdlib/sampledata.fc and b/embed/stdlib/sampledata.fc differ diff --git a/embed/stdlib/slack.fc b/embed/stdlib/slack.fc index c24464be48..5858586078 100644 Binary files a/embed/stdlib/slack.fc and b/embed/stdlib/slack.fc differ diff --git a/embed/stdlib/testing.fc b/embed/stdlib/testing.fc index 8deaf5431a..80daa0165b 100644 Binary files a/embed/stdlib/testing.fc and b/embed/stdlib/testing.fc differ diff --git a/embed/stdlib/timezone.fc b/embed/stdlib/timezone.fc index 94b02eb5c9..4fcbe61193 100644 Binary files a/embed/stdlib/timezone.fc and b/embed/stdlib/timezone.fc differ diff --git a/embed/stdlib/universe.fc b/embed/stdlib/universe.fc index 4a0c1621ae..9857e8562f 100644 Binary files a/embed/stdlib/universe.fc and b/embed/stdlib/universe.fc differ diff --git a/internal/fbsemantic/semantic.fbs b/internal/fbsemantic/semantic.fbs index d21dc3559b..aa9924bd1c 100644 --- a/internal/fbsemantic/semantic.fbs +++ b/internal/fbsemantic/semantic.fbs @@ -99,9 +99,18 @@ table Argument { optional:bool; } +union RecordLabel { + Concrete, + Var, +} + +table Concrete { + id:string /*(required)*/; +} + table Prop { - k:string /*(required)*/; - v:MonoType /*(required)*/; + k:RecordLabel /*(required)*/; + v:MonoType /*(required)*/; } table PolyType { @@ -118,6 +127,7 @@ enum Kind : ubyte { Numeric, Comparable, Equatable, + Label, Nullable, Record, Negatable, diff --git a/internal/fbsemantic/semantic_generated.go b/internal/fbsemantic/semantic_generated.go index 54fc0a2361..0081a7efe8 100644 --- a/internal/fbsemantic/semantic_generated.go +++ b/internal/fbsemantic/semantic_generated.go @@ -119,6 +119,33 @@ func (v CollectionType) String() string { return "CollectionType(" + strconv.FormatInt(int64(v), 10) + ")" } +type RecordLabel byte + +const ( + RecordLabelNONE RecordLabel = 0 + RecordLabelConcrete RecordLabel = 1 + RecordLabelVar RecordLabel = 2 +) + +var EnumNamesRecordLabel = map[RecordLabel]string{ + RecordLabelNONE: "NONE", + RecordLabelConcrete: "Concrete", + RecordLabelVar: "Var", +} + +var EnumValuesRecordLabel = map[string]RecordLabel{ + "NONE": RecordLabelNONE, + "Concrete": RecordLabelConcrete, + "Var": RecordLabelVar, +} + +func (v RecordLabel) String() string { + if s, ok := EnumNamesRecordLabel[v]; ok { + return s + } + return "RecordLabel(" + strconv.FormatInt(int64(v), 10) + ")" +} + type Kind byte const ( @@ -129,11 +156,12 @@ const ( KindNumeric Kind = 4 KindComparable Kind = 5 KindEquatable Kind = 6 - KindNullable Kind = 7 - KindRecord Kind = 8 - KindNegatable Kind = 9 - KindTimeable Kind = 10 - KindStringable Kind = 11 + KindLabel Kind = 7 + KindNullable Kind = 8 + KindRecord Kind = 9 + KindNegatable Kind = 10 + KindTimeable Kind = 11 + KindStringable Kind = 12 ) var EnumNamesKind = map[Kind]string{ @@ -144,6 +172,7 @@ var EnumNamesKind = map[Kind]string{ KindNumeric: "Numeric", KindComparable: "Comparable", KindEquatable: "Equatable", + KindLabel: "Label", KindNullable: "Nullable", KindRecord: "Record", KindNegatable: "Negatable", @@ -159,6 +188,7 @@ var EnumValuesKind = map[string]Kind{ "Numeric": KindNumeric, "Comparable": KindComparable, "Equatable": KindEquatable, + "Label": KindLabel, "Nullable": KindNullable, "Record": KindRecord, "Negatable": KindNegatable, @@ -1371,6 +1401,51 @@ func ArgumentEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() } +type Concrete struct { + _tab flatbuffers.Table +} + +func GetRootAsConcrete(buf []byte, offset flatbuffers.UOffsetT) *Concrete { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &Concrete{} + x.Init(buf, n+offset) + return x +} + +func GetSizePrefixedRootAsConcrete(buf []byte, offset flatbuffers.UOffsetT) *Concrete { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &Concrete{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func (rcv *Concrete) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *Concrete) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *Concrete) Id() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func ConcreteStart(builder *flatbuffers.Builder) { + builder.StartObject(1) +} +func ConcreteAddId(builder *flatbuffers.Builder, id flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(id), 0) +} +func ConcreteEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} + type Prop struct { _tab flatbuffers.Table } @@ -1398,16 +1473,29 @@ func (rcv *Prop) Table() flatbuffers.Table { return rcv._tab } -func (rcv *Prop) K() []byte { +func (rcv *Prop) KType() RecordLabel { o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) if o != 0 { - return rcv._tab.ByteVector(o + rcv._tab.Pos) + return RecordLabel(rcv._tab.GetByte(o + rcv._tab.Pos)) } - return nil + return 0 } -func (rcv *Prop) VType() MonoType { +func (rcv *Prop) MutateKType(n RecordLabel) bool { + return rcv._tab.MutateByteSlot(4, byte(n)) +} + +func (rcv *Prop) K(obj *flatbuffers.Table) bool { o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + rcv._tab.Union(obj, o) + return true + } + return false +} + +func (rcv *Prop) VType() MonoType { + o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) if o != 0 { return MonoType(rcv._tab.GetByte(o + rcv._tab.Pos)) } @@ -1415,11 +1503,11 @@ func (rcv *Prop) VType() MonoType { } func (rcv *Prop) MutateVType(n MonoType) bool { - return rcv._tab.MutateByteSlot(6, byte(n)) + return rcv._tab.MutateByteSlot(8, byte(n)) } func (rcv *Prop) V(obj *flatbuffers.Table) bool { - o := flatbuffers.UOffsetT(rcv._tab.Offset(8)) + o := flatbuffers.UOffsetT(rcv._tab.Offset(10)) if o != 0 { rcv._tab.Union(obj, o) return true @@ -1428,16 +1516,19 @@ func (rcv *Prop) V(obj *flatbuffers.Table) bool { } func PropStart(builder *flatbuffers.Builder) { - builder.StartObject(3) + builder.StartObject(4) +} +func PropAddKType(builder *flatbuffers.Builder, kType RecordLabel) { + builder.PrependByteSlot(0, byte(kType), 0) } func PropAddK(builder *flatbuffers.Builder, k flatbuffers.UOffsetT) { - builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(k), 0) + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(k), 0) } func PropAddVType(builder *flatbuffers.Builder, vType MonoType) { - builder.PrependByteSlot(1, byte(vType), 0) + builder.PrependByteSlot(2, byte(vType), 0) } func PropAddV(builder *flatbuffers.Builder, v flatbuffers.UOffsetT) { - builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(v), 0) + builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(v), 0) } func PropEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { return builder.EndObject() diff --git a/internal/feature/flags.go b/internal/feature/flags.go index d6243c205c..10d2796be9 100644 --- a/internal/feature/flags.go +++ b/internal/feature/flags.go @@ -173,6 +173,18 @@ func VectorizeOperators() BoolFlag { return vectorizeOperators } +var labelPolymorphism = feature.MakeBoolFlag( + "Label polymorphism", + "labelPolymorphism", + "Markus Westerlind", + false, +) + +// LabelPolymorphism - Enables label polymorphism in the type system +func LabelPolymorphism() BoolFlag { + return labelPolymorphism +} + // Inject will inject the Flagger into the context. func Inject(ctx context.Context, flagger Flagger) context.Context { return feature.Inject(ctx, flagger) @@ -192,6 +204,7 @@ var all = []Flag{ setFinalizerMemoryTracking, vectorizeAddition, vectorizeOperators, + labelPolymorphism, } var byKey = map[string]Flag{ @@ -208,6 +221,7 @@ var byKey = map[string]Flag{ "setFinalizerMemoryTracking": setFinalizerMemoryTracking, "vectorizeAddition": vectorizeAddition, "vectorizeOperators": vectorizeOperators, + "labelPolymorphism": labelPolymorphism, } // Flags returns all feature flags. diff --git a/internal/feature/flags.yml b/internal/feature/flags.yml index f1c7abd31c..bd7db0fa70 100644 --- a/internal/feature/flags.yml +++ b/internal/feature/flags.yml @@ -87,3 +87,9 @@ key: vectorizeOperators default: false contact: Markus Westerlind + +- name: Label polymorphism + description: Enables label polymorphism in the type system + key: labelPolymorphism + default: false + contact: Markus Westerlind diff --git a/libflux/Polymorphic_Labels.md b/libflux/Polymorphic_Labels.md new file mode 100644 index 0000000000..50d14283f2 --- /dev/null +++ b/libflux/Polymorphic_Labels.md @@ -0,0 +1,155 @@ +# Polymorphic record labels + +## Motivation + +We have many operations that accept a column name as a parameter and uses that to operate on that specific record field (and in some cases multiple fields). + + +```flux +builtin fill : (<-tables: stream[A], ?column: string, ?value: B, ?usePrevious: bool) => stream[C] + where + A: Record, + C: Record +``` + +However as we can see from `fill`'s signature, the type system is wholly unaware of whether the incoming record (`A`) has the `column`. It also doesn't know what the shape of the returned record (`C`) is either. To fix this issue we need a way to connect the `column` value with the input (and output records). As a solution I propose we add "polymorphic record labels" which would let us define `fill` as follows. + +```flux +builtin fill : (<-tables: stream[{ A with 'column: B }], ?column: 'column, ?value: B, ?usePrevious: bool) => stream[{ A with 'column: B }] + where A: Record +``` + +`'column` in this case indicates a polymorphic label which would allow `fill` to be called as normal, however when passing a "label" (a string) that is known at compile time it lets us enforce the typing of the input and output records. + +```flux + +// 'column is inferred to `a` which propagates to the input and output records, making them `{ A with a: B }` +[{ a: 1 }] |> fill(column: "a", value: 0) + +// ERROR: the input record lacks the field `a` +[{}] |> fill(column: "a", value: 0) +``` + + +## Explanation + +Polymorphic labels adds the ability to define record types where the field names (labels) can be a variable instead of a literal name in a similar way that type variables can exist in a type. + +``` +{ 'col: string } vs { some_column: string } +(x: A) => A vs (x: string) => string +``` + +The types that fit into a record field implement the new `Label` kind to ensure that unexpected types like `Int` are an error when used in a record field. + +To make the change as transparent as possible string literals now get the new `Label("literal")` type instead of `String` making `"abc": Literal("abc")`. While this means that two different string literals will have different types we must still treat `Label` types as strings in most cases. Consider + +```flux +if b then "a" else "b" +``` + +The branches will have the `Label("a")` and `Label("b")` respectively, which we still need to unify successfully and the resulting type of the expression should still be a `string`. As long as we limit the uses of label types to direct uses in calls (like in `fill` above). We may keep the same type checking behaviour by treating `Label` as a `String` types in every instance except when type checking function calls, in which case we specialize unification to a subsumption check where we allow some limited sub-typing. + +```flux +// The original fill call is allowed such that `'column` is inferred to be `Label("a")` +fill(column: "a", value: 0) + +// We should (probably) still allow dynamic strings for backwards compatibility sake so the `string` type +// also implements the `Label` Kind +c = "a" + "" +fill(column: c, value: 0) + +builtin add : (x: A, y: A) => A where A: Addable + +// This must still be allowed, however a naive implementation would first infer `A <=> Label("a")` +// and then fail to unify `Label("a") <=> Label("b")`. If we keep treating `Label(..)` as a string except in cases where it unifies to a variable that actually has the `Label` type. +add(x: "a", y: "b") + +builtin func : (opt: string) => int + +// This will work since `Label("a")` is a sub type of `string` +func(opt: "a") + +// Possible extension where we only allow some specific labels to be passed in +builtin func : (opt: "option1" | "option2") => int + +// "option1" is an allowed option +func(opt: "option1") +// However "option3" is not allowed +func(opt: "option3") +// However passing in a dynamic string could be disallowed (`string` is to general) +o = "option" + "" +func(opt: o) +``` + +Row polymorphism introduces another wrinkle to the implementation. When fields aren't necessarily know at the point that `unify` are called we aren't able to match the field of either side. + +```flux +// Unifying know fields works regardless of the order that they are defined in +{ a: int, b: string } <=> { b: string, a: int } + +// Should 'column unify against `a` or `b` field on the left side? +{ 'column: A } <=> { b: string, a: int } +``` + +There may be a consistent way to unify these records in the face of type variables, however an easy workaround would be to delay the unification of records with unknown fields until they have been resolved, at which point they can unify normally. If there is a field that is still unknown when type checking is done we can designate that as an unbound variable error. + +```flux +builtin badFill : (<-tables: stream[{ A with 'column: B }], ?value: B) => stream[{ A with 'column: B }] + where A: Record + +// There is no way to determine what `'column` should be, so we must error +badFill() +``` + +## Extensions + +### String labels + +For backwards compatibility's sake we may want to still allow dynamic string values to be passed in place of a static label. + +```flux +c = "a" + "" +fill(column: c, value: 0) +// Will give a type like +// (<-tables: stream[{ A with string: int }], column: string, ?value: int) => stream[{ A with string: int }] +// where the "string" field is the string type, not a field named "string" +``` + +Since a field that is a string type doesn't make much sense we need some other semantics for this. The simplest way would be to just omit the field from the record, which would give a type like. + +```flux +(<-tables: stream[{ A with }], column: string, ?value: int) => stream[{ A with }] +``` + +However this poses a problem where the returned records does not indicate that they have changed in any way which could cause errors in latter transformations if they try to operate on fields that does not exist on the type. + +An alternative may be to convert `string` typed labels get turned into a special `dynamic` field which acts as a catch-all. When unifying against a dynamic field any field is allowed to match. + +```flux +// This record holds some unknown field of type int +(<-tables: stream[{ A with *: int }], column: string, ?value: int) => stream[{ A with *:int }] +``` + +The semantics of these may be complex though, as the dynamic field mustn't "swallow" fields. + +```flux +c = "a" + "" +[{ }] + |> fill(column: c, value: 0) + // r should be `{ *: int, b: A }` the dynamic field should not swallow `b` and be `{ *: int }` + // if that makes sense + |> map(fn: (r) => { r with r.b }) +``` + +## Alternatives + +String literals serving double duty as `Label` and `String` types may complicate the internals to much. An alternative could be to make labels a separate syntactic element. + +```flux +.a // Label(a) +"a" // String +fill(column: .a, value: 0) +``` + +This may complicate things for users but it avoids the subtyping issues during typechecking. diff --git a/libflux/flux-core/Cargo.toml b/libflux/flux-core/Cargo.toml index c7144190ef..6cb1963cfc 100644 --- a/libflux/flux-core/Cargo.toml +++ b/libflux/flux-core/Cargo.toml @@ -32,7 +32,8 @@ codespan-reporting = "0.11" csv = { version = "1.1", optional = true } derivative = "2.1.1" derive_more = { version = "0.99.17", default-features = false, features = [ - "display" + "display", + "from" ] } ena = "0.14" env_logger = "0.9" diff --git a/libflux/flux-core/src/bin/fluxdoc.rs b/libflux/flux-core/src/bin/fluxdoc.rs index d9712f509b..8c8e7094ef 100644 --- a/libflux/flux-core/src/bin/fluxdoc.rs +++ b/libflux/flux-core/src/bin/fluxdoc.rs @@ -297,7 +297,11 @@ impl<'a> example::Executor for CLIExecutor<'a> { write!(tmpfile.reopen()?, "{}", code)?; let mut cmd = Command::new(self.path); - cmd.arg("--format").arg("csv").arg(tmpfile.path()); + cmd.arg("--format") + .arg("csv") + .arg(tmpfile.path()) + .arg("--feature") + .arg(r#"{"labelPolymorphism": true}"#); log::debug!("Executing {:?}", cmd); let output = cmd .output() diff --git a/libflux/flux-core/src/errors.rs b/libflux/flux-core/src/errors.rs index c25c73888d..3c2307097f 100644 --- a/libflux/flux-core/src/errors.rs +++ b/libflux/flux-core/src/errors.rs @@ -178,6 +178,15 @@ pub struct Located { pub error: E, } +impl From for Located { + fn from(error: E) -> Self { + Self { + location: Default::default(), + error, + } + } +} + impl Located { pub(crate) fn map(self, f: impl FnOnce(E) -> F) -> Located { Located { diff --git a/libflux/flux-core/src/parser/mod.rs b/libflux/flux-core/src/parser/mod.rs index f5088ae6fb..d496fbd66a 100644 --- a/libflux/flux-core/src/parser/mod.rs +++ b/libflux/flux-core/src/parser/mod.rs @@ -1528,12 +1528,11 @@ impl<'input> Parser<'input> { fn parse_property_key(&mut self) -> PropertyKey { let t = self.expect_one_of(&[TokenType::Ident, TokenType::String]); match t.tok { - TokenType::Ident => PropertyKey::Identifier(Identifier { + TokenType::String => PropertyKey::StringLit(self.new_string_literal(t)), + _ => PropertyKey::Identifier(Identifier { base: self.base_node_from_token(&t), name: t.lit, }), - TokenType::String => PropertyKey::StringLit(self.new_string_literal(t)), - _ => unreachable!(), } } fn parse_identifier(&mut self) -> Identifier { diff --git a/libflux/flux-core/src/semantic/bootstrap.rs b/libflux/flux-core/src/semantic/bootstrap.rs index 52271cb551..07ec2319c8 100644 --- a/libflux/flux-core/src/semantic/bootstrap.rs +++ b/libflux/flux-core/src/semantic/bootstrap.rs @@ -20,7 +20,9 @@ use crate::{ import::{Importer, Packages}, nodes::{self, Package, Symbol}, sub::{Substitutable, Substituter}, - types::{MonoType, PolyType, PolyTypeHashMap, Record, SemanticMap, Tvar, TvarKinds}, + types::{ + MonoType, PolyType, PolyTypeHashMap, Record, RecordLabel, SemanticMap, Tvar, TvarKinds, + }, Analyzer, PackageExports, }, }; @@ -311,7 +313,15 @@ fn add_record_to_map( } } env.insert( - field.k.clone().into(), + match &field.k { + RecordLabel::Concrete(s) => s.clone().into(), + RecordLabel::BoundVariable(_) | RecordLabel::Variable(_) => { + bail!("Record contains variable labels") + } + RecordLabel::Error => { + bail!("Record contains type error") + } + }, PolyType { vars: new_vars, cons: new_cons, diff --git a/libflux/flux-core/src/semantic/convert.rs b/libflux/flux-core/src/semantic/convert.rs index 57fe37d31c..cb7c3f94e6 100644 --- a/libflux/flux-core/src/semantic/convert.rs +++ b/libflux/flux-core/src/semantic/convert.rs @@ -578,12 +578,22 @@ impl<'a> Converter<'a> { } }; for prop in &rec.properties { - let name = match &prop.name { - ast::PropertyKey::Identifier(id) => &id.name, - ast::PropertyKey::StringLit(lit) => &lit.value, - }; let property = types::Property { - k: types::Label::from(self.symbols.lookup(name)), + k: match &prop.name { + ast::PropertyKey::Identifier(id) => { + if id.name.len() == 1 && id.name.starts_with(char::is_uppercase) { + let tvar = *tvars + .entry(id.name.clone()) + .or_insert_with(|| self.sub.fresh()); + types::RecordLabel::BoundVariable(tvar) + } else { + types::Label::from(self.symbols.lookup(&id.name)).into() + } + } + ast::PropertyKey::StringLit(lit) => { + types::Label::from(self.symbols.lookup(&lit.value)).into() + } + }, v: self.convert_monotype(&prop.monotype, tvars), }; r = MonoType::from(types::Record::Extension { @@ -1179,6 +1189,7 @@ impl<'a> Converter<'a> { StringLit { loc: lit.base.location.clone(), value: lit.value.clone(), + typ: None, } } @@ -1384,6 +1395,7 @@ mod tests { path: StringLit { loc: b.location.clone(), value: "path/foo".to_string(), + typ: None, }, alias: None, import_symbol: symbols["foo"].clone(), @@ -1393,6 +1405,7 @@ mod tests { path: StringLit { loc: b.location.clone(), value: "path/bar".to_string(), + typ: None, }, alias: Some(Identifier { loc: b.location.clone(), @@ -1702,6 +1715,7 @@ mod tests { value: Expression::StringLit(StringLit { loc: b.location.clone(), value: "foo".to_string(), + typ: None, }), }, Property { @@ -1743,6 +1757,7 @@ mod tests { value: Expression::StringLit(StringLit { loc: b.location.clone(), value: "0 2 * * *".to_string(), + typ: None, }), }, Property { @@ -1798,6 +1813,7 @@ mod tests { init: Expression::StringLit(StringLit { loc: b.location.clone(), value: "Warning".to_string(), + typ: None, }), }), }))], @@ -2672,13 +2688,13 @@ mod tests { #[test] fn test_convert_monotype_record() { - let monotype = Parser::new("{ A with B: int }").parse_monotype(); + let monotype = Parser::new("{ A with b: int }").parse_monotype(); let mut m = BTreeMap::::new(); let got = convert_monotype(&monotype, &mut m, &mut sub::Substitution::default()).unwrap(); let want = MonoType::from(types::Record::Extension { head: types::Property { - k: types::Label::from("B"), + k: types::RecordLabel::from("b"), v: MonoType::INT, }, tail: MonoType::BoundVar(Tvar(0)), diff --git a/libflux/flux-core/src/semantic/flatbuffers/semantic_generated.rs b/libflux/flux-core/src/semantic/flatbuffers/semantic_generated.rs index b5f9b87aad..b3f3277ce7 100644 --- a/libflux/flux-core/src/semantic/flatbuffers/semantic_generated.rs +++ b/libflux/flux-core/src/semantic/flatbuffers/semantic_generated.rs @@ -356,6 +356,102 @@ pub mod fbsemantic { } impl flatbuffers::SimpleToVerifyInSlice for CollectionType {} + #[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." + )] + pub const ENUM_MIN_RECORD_LABEL: u8 = 0; + #[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." + )] + pub const ENUM_MAX_RECORD_LABEL: u8 = 2; + #[deprecated( + since = "2.0.0", + note = "Use associated constants instead. This will no longer be generated in 2021." + )] + #[allow(non_camel_case_types)] + pub const ENUM_VALUES_RECORD_LABEL: [RecordLabel; 3] = + [RecordLabel::NONE, RecordLabel::Concrete, RecordLabel::Var]; + + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] + #[repr(transparent)] + pub struct RecordLabel(pub u8); + #[allow(non_upper_case_globals)] + impl RecordLabel { + pub const NONE: Self = Self(0); + pub const Concrete: Self = Self(1); + pub const Var: Self = Self(2); + + pub const ENUM_MIN: u8 = 0; + pub const ENUM_MAX: u8 = 2; + pub const ENUM_VALUES: &'static [Self] = &[Self::NONE, Self::Concrete, Self::Var]; + /// Returns the variant's name or "" if unknown. + pub fn variant_name(self) -> Option<&'static str> { + match self { + Self::NONE => Some("NONE"), + Self::Concrete => Some("Concrete"), + Self::Var => Some("Var"), + _ => None, + } + } + } + impl std::fmt::Debug for RecordLabel { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if let Some(name) = self.variant_name() { + f.write_str(name) + } else { + f.write_fmt(format_args!("", self.0)) + } + } + } + impl<'a> flatbuffers::Follow<'a> for RecordLabel { + type Inner = Self; + #[inline] + fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + let b = unsafe { flatbuffers::read_scalar_at::(buf, loc) }; + Self(b) + } + } + + impl flatbuffers::Push for RecordLabel { + type Output = RecordLabel; + #[inline] + fn push(&self, dst: &mut [u8], _rest: &[u8]) { + unsafe { + flatbuffers::emplace_scalar::(dst, self.0); + } + } + } + + impl flatbuffers::EndianScalar for RecordLabel { + #[inline] + fn to_little_endian(self) -> Self { + let b = u8::to_le(self.0); + Self(b) + } + #[inline] + #[allow(clippy::wrong_self_convention)] + fn from_little_endian(self) -> Self { + let b = u8::from_le(self.0); + Self(b) + } + } + + impl<'a> flatbuffers::Verifiable for RecordLabel { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, + pos: usize, + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + u8::run_verifier(v, pos) + } + } + + impl flatbuffers::SimpleToVerifyInSlice for RecordLabel {} + pub struct RecordLabelUnionTableOffset {} + #[deprecated( since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021." @@ -365,13 +461,13 @@ pub mod fbsemantic { since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021." )] - pub const ENUM_MAX_KIND: u8 = 11; + pub const ENUM_MAX_KIND: u8 = 12; #[deprecated( since = "2.0.0", note = "Use associated constants instead. This will no longer be generated in 2021." )] #[allow(non_camel_case_types)] - pub const ENUM_VALUES_KIND: [Kind; 12] = [ + pub const ENUM_VALUES_KIND: [Kind; 13] = [ Kind::Addable, Kind::Basic, Kind::Subtractable, @@ -379,6 +475,7 @@ pub mod fbsemantic { Kind::Numeric, Kind::Comparable, Kind::Equatable, + Kind::Label, Kind::Nullable, Kind::Record, Kind::Negatable, @@ -398,14 +495,15 @@ pub mod fbsemantic { pub const Numeric: Self = Self(4); pub const Comparable: Self = Self(5); pub const Equatable: Self = Self(6); - pub const Nullable: Self = Self(7); - pub const Record: Self = Self(8); - pub const Negatable: Self = Self(9); - pub const Timeable: Self = Self(10); - pub const Stringable: Self = Self(11); + pub const Label: Self = Self(7); + pub const Nullable: Self = Self(8); + pub const Record: Self = Self(9); + pub const Negatable: Self = Self(10); + pub const Timeable: Self = Self(11); + pub const Stringable: Self = Self(12); pub const ENUM_MIN: u8 = 0; - pub const ENUM_MAX: u8 = 11; + pub const ENUM_MAX: u8 = 12; pub const ENUM_VALUES: &'static [Self] = &[ Self::Addable, Self::Basic, @@ -414,6 +512,7 @@ pub mod fbsemantic { Self::Numeric, Self::Comparable, Self::Equatable, + Self::Label, Self::Nullable, Self::Record, Self::Negatable, @@ -430,6 +529,7 @@ pub mod fbsemantic { Self::Numeric => Some("Numeric"), Self::Comparable => Some("Comparable"), Self::Equatable => Some("Equatable"), + Self::Label => Some("Label"), Self::Nullable => Some("Nullable"), Self::Record => Some("Record"), Self::Negatable => Some("Negatable"), @@ -4151,6 +4251,103 @@ pub mod fbsemantic { ds.finish() } } + pub enum ConcreteOffset {} + #[derive(Copy, Clone, PartialEq)] + + pub struct Concrete<'a> { + pub _tab: flatbuffers::Table<'a>, + } + + impl<'a> flatbuffers::Follow<'a> for Concrete<'a> { + type Inner = Concrete<'a>; + #[inline] + fn follow(buf: &'a [u8], loc: usize) -> Self::Inner { + Self { + _tab: flatbuffers::Table { buf, loc }, + } + } + } + + impl<'a> Concrete<'a> { + #[inline] + pub fn init_from_table(table: flatbuffers::Table<'a>) -> Self { + Concrete { _tab: table } + } + #[allow(unused_mut)] + pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( + _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, + args: &'args ConcreteArgs<'args>, + ) -> flatbuffers::WIPOffset> { + let mut builder = ConcreteBuilder::new(_fbb); + if let Some(x) = args.id { + builder.add_id(x); + } + builder.finish() + } + + pub const VT_ID: flatbuffers::VOffsetT = 4; + + #[inline] + pub fn id(&self) -> Option<&'a str> { + self._tab + .get::>(Concrete::VT_ID, None) + } + } + + impl flatbuffers::Verifiable for Concrete<'_> { + #[inline] + fn run_verifier( + v: &mut flatbuffers::Verifier, + pos: usize, + ) -> Result<(), flatbuffers::InvalidFlatbuffer> { + use self::flatbuffers::Verifiable; + v.visit_table(pos)? + .visit_field::>(&"id", Self::VT_ID, false)? + .finish(); + Ok(()) + } + } + pub struct ConcreteArgs<'a> { + pub id: Option>, + } + impl<'a> Default for ConcreteArgs<'a> { + #[inline] + fn default() -> Self { + ConcreteArgs { id: None } + } + } + pub struct ConcreteBuilder<'a: 'b, 'b> { + fbb_: &'b mut flatbuffers::FlatBufferBuilder<'a>, + start_: flatbuffers::WIPOffset, + } + impl<'a: 'b, 'b> ConcreteBuilder<'a, 'b> { + #[inline] + pub fn add_id(&mut self, id: flatbuffers::WIPOffset<&'b str>) { + self.fbb_ + .push_slot_always::>(Concrete::VT_ID, id); + } + #[inline] + pub fn new(_fbb: &'b mut flatbuffers::FlatBufferBuilder<'a>) -> ConcreteBuilder<'a, 'b> { + let start = _fbb.start_table(); + ConcreteBuilder { + fbb_: _fbb, + start_: start, + } + } + #[inline] + pub fn finish(self) -> flatbuffers::WIPOffset> { + let o = self.fbb_.end_table(self.start_); + flatbuffers::WIPOffset::new(o.value()) + } + } + + impl std::fmt::Debug for Concrete<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut ds = f.debug_struct("Concrete"); + ds.field("id", &self.id()); + ds.finish() + } + } pub enum PropOffset {} #[derive(Copy, Clone, PartialEq)] @@ -4176,7 +4373,7 @@ pub mod fbsemantic { #[allow(unused_mut)] pub fn create<'bldr: 'args, 'args: 'mut_bldr, 'mut_bldr>( _fbb: &'mut_bldr mut flatbuffers::FlatBufferBuilder<'bldr>, - args: &'args PropArgs<'args>, + args: &'args PropArgs, ) -> flatbuffers::WIPOffset> { let mut builder = PropBuilder::new(_fbb); if let Some(x) = args.v { @@ -4186,17 +4383,25 @@ pub mod fbsemantic { builder.add_k(x); } builder.add_v_type(args.v_type); + builder.add_k_type(args.k_type); builder.finish() } - pub const VT_K: flatbuffers::VOffsetT = 4; - pub const VT_V_TYPE: flatbuffers::VOffsetT = 6; - pub const VT_V: flatbuffers::VOffsetT = 8; + pub const VT_K_TYPE: flatbuffers::VOffsetT = 4; + pub const VT_K: flatbuffers::VOffsetT = 6; + pub const VT_V_TYPE: flatbuffers::VOffsetT = 8; + pub const VT_V: flatbuffers::VOffsetT = 10; #[inline] - pub fn k(&self) -> Option<&'a str> { + pub fn k_type(&self) -> RecordLabel { + self._tab + .get::(Prop::VT_K_TYPE, Some(RecordLabel::NONE)) + .unwrap() + } + #[inline] + pub fn k(&self) -> Option> { self._tab - .get::>(Prop::VT_K, None) + .get::>>(Prop::VT_K, None) } #[inline] pub fn v_type(&self) -> MonoType { @@ -4209,6 +4414,26 @@ pub mod fbsemantic { self._tab .get::>>(Prop::VT_V, None) } + #[inline] + #[allow(non_snake_case)] + pub fn k_as_concrete(&self) -> Option> { + if self.k_type() == RecordLabel::Concrete { + self.k().map(Concrete::init_from_table) + } else { + None + } + } + + #[inline] + #[allow(non_snake_case)] + pub fn k_as_var(&self) -> Option> { + if self.k_type() == RecordLabel::Var { + self.k().map(Var::init_from_table) + } else { + None + } + } + #[inline] #[allow(non_snake_case)] pub fn v_as_basic(&self) -> Option> { @@ -4278,7 +4503,26 @@ pub mod fbsemantic { ) -> Result<(), flatbuffers::InvalidFlatbuffer> { use self::flatbuffers::Verifiable; v.visit_table(pos)? - .visit_field::>(&"k", Self::VT_K, false)? + .visit_union::( + &"k_type", + Self::VT_K_TYPE, + &"k", + Self::VT_K, + false, + |key, v, pos| match key { + RecordLabel::Concrete => v + .verify_union_variant::>( + "RecordLabel::Concrete", + pos, + ), + RecordLabel::Var => v + .verify_union_variant::>( + "RecordLabel::Var", + pos, + ), + _ => Ok(()), + }, + )? .visit_union::( &"v_type", Self::VT_V_TYPE, @@ -4323,15 +4567,17 @@ pub mod fbsemantic { Ok(()) } } - pub struct PropArgs<'a> { - pub k: Option>, + pub struct PropArgs { + pub k_type: RecordLabel, + pub k: Option>, pub v_type: MonoType, pub v: Option>, } - impl<'a> Default for PropArgs<'a> { + impl<'a> Default for PropArgs { #[inline] fn default() -> Self { PropArgs { + k_type: RecordLabel::NONE, k: None, v_type: MonoType::NONE, v: None, @@ -4344,7 +4590,12 @@ pub mod fbsemantic { } impl<'a: 'b, 'b> PropBuilder<'a, 'b> { #[inline] - pub fn add_k(&mut self, k: flatbuffers::WIPOffset<&'b str>) { + pub fn add_k_type(&mut self, k_type: RecordLabel) { + self.fbb_ + .push_slot::(Prop::VT_K_TYPE, k_type, RecordLabel::NONE); + } + #[inline] + pub fn add_k(&mut self, k: flatbuffers::WIPOffset) { self.fbb_ .push_slot_always::>(Prop::VT_K, k); } @@ -4376,7 +4627,33 @@ pub mod fbsemantic { impl std::fmt::Debug for Prop<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut ds = f.debug_struct("Prop"); - ds.field("k", &self.k()); + ds.field("k_type", &self.k_type()); + match self.k_type() { + RecordLabel::Concrete => { + if let Some(x) = self.k_as_concrete() { + ds.field("k", &x) + } else { + ds.field( + "k", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + RecordLabel::Var => { + if let Some(x) = self.k_as_var() { + ds.field("k", &x) + } else { + ds.field( + "k", + &"InvalidFlatbuffer: Union discriminant does not match value.", + ) + } + } + _ => { + let x: Option<()> = None; + ds.field("k", &x) + } + }; ds.field("v_type", &self.v_type()); match self.v_type() { MonoType::Basic => { diff --git a/libflux/flux-core/src/semantic/flatbuffers/types.rs b/libflux/flux-core/src/semantic/flatbuffers/types.rs index fd93212b8e..aff7f2833a 100644 --- a/libflux/flux-core/src/semantic/flatbuffers/types.rs +++ b/libflux/flux-core/src/semantic/flatbuffers/types.rs @@ -12,7 +12,7 @@ use crate::semantic::{ nodes::Symbol, import::Packages, types::{CollectionType, - Label, + RecordLabel, Collection, Dictionary, Function, @@ -139,6 +139,7 @@ impl From for Kind { fb::Kind::Numeric => Kind::Numeric, fb::Kind::Comparable => Kind::Comparable, fb::Kind::Equatable => Kind::Equatable, + fb::Kind::Label => Kind::Label, fb::Kind::Nullable => Kind::Nullable, fb::Kind::Record => Kind::Record, fb::Kind::Negatable => Kind::Negatable, @@ -159,17 +160,33 @@ impl From for fb::Kind { Kind::Numeric => fb::Kind::Numeric, Kind::Comparable => fb::Kind::Comparable, Kind::Equatable => fb::Kind::Equatable, + Kind::Label => fb::Kind::Label, Kind::Nullable => fb::Kind::Nullable, Kind::Record => fb::Kind::Record, Kind::Negatable => fb::Kind::Negatable, Kind::Timeable => fb::Kind::Timeable, Kind::Stringable => fb::Kind::Stringable, Kind::Basic => fb::Kind::Basic, - _ => unreachable!("Unknown Kind"), } } } +fn record_label_from_table(table: flatbuffers::Table, t: fb::RecordLabel) -> Option { + match t { + fb::RecordLabel::Var => { + let var = fb::Var::init_from_table(table); + Some(RecordLabel::BoundVariable(Tvar::from(var))) + } + fb::RecordLabel::Concrete => { + let concrete = fb::Concrete::init_from_table(table); + let id = concrete.id()?; + Some(RecordLabel::from(id)) + } + fb::RecordLabel::NONE => None, + _ => unreachable!("Unknown type from table"), + } +} + fn from_table(table: flatbuffers::Table, t: fb::MonoType) -> Option { match t { fb::MonoType::Basic => { @@ -186,12 +203,12 @@ fn from_table(table: flatbuffers::Table, t: fb::MonoType) -> Option { } fb::MonoType::Fun => { let opt: Option = fb::Fun::init_from_table(table).into(); - Some(MonoType::fun(opt?)) + Some(MonoType::from(opt?)) } fb::MonoType::Record => fb::Record::init_from_table(table).into(), fb::MonoType::Dict => { let opt: Option = fb::Dict::init_from_table(table).into(); - Some(MonoType::dict(opt?)) + Some(MonoType::from(opt?)) } fb::MonoType::NONE => None, _ => unreachable!("Unknown type from table"), @@ -265,7 +282,7 @@ impl From> for Option { impl From> for Option { fn from(t: fb::Prop) -> Option { Some(Property { - k: Label::from(t.k()?), + k: record_label_from_table(t.k()?, t.k_type())?, v: from_table(t.v()?, t.v_type())?, }) } @@ -479,6 +496,7 @@ pub fn build_type( match t { MonoType::Error => unreachable!(), MonoType::Builtin(typ) => build_basic_type(builder, typ), + MonoType::Label(_) => build_basic_type(builder, &BuiltinType::String), MonoType::BoundVar(tvr) | MonoType::Var(tvr) => { let offset = build_var(builder, *tvr); (offset.as_union_value(), fb::MonoType::Var) @@ -601,13 +619,25 @@ fn build_prop<'a>( builder: &mut flatbuffers::FlatBufferBuilder<'a>, prop: &Property, ) -> flatbuffers::WIPOffset> { - let (off, typ) = build_type(builder, &prop.v); - let k = builder.create_string(prop.k.as_symbol().full_name()); + let (off, v_type) = build_type(builder, &prop.v); + let (k, k_type) = match &prop.k { + RecordLabel::Variable(var) | RecordLabel::BoundVariable(var) => { + let concrete = build_var(builder, *var); + (concrete.as_union_value(), fb::RecordLabel::Var) + } + RecordLabel::Concrete(name) => { + let id = builder.create_string(name); + let concrete = fb::Concrete::create(builder, &fb::ConcreteArgs { id: Some(id) }); + (concrete.as_union_value(), fb::RecordLabel::Concrete) + } + RecordLabel::Error => unreachable!(), + }; fb::Prop::create( builder, &fb::PropArgs { + k_type, k: Some(k), - v_type: typ, + v_type, v: Some(off), }, ) diff --git a/libflux/flux-core/src/semantic/formatter/mod.rs b/libflux/flux-core/src/semantic/formatter/mod.rs index 7c4da154ec..8850806e6e 100644 --- a/libflux/flux-core/src/semantic/formatter/mod.rs +++ b/libflux/flux-core/src/semantic/formatter/mod.rs @@ -234,7 +234,7 @@ impl Formatter { } fn format_property_type(&mut self, n: &semantic::types::Property) { - self.write_string(&n.k); + self.write_string(&n.k.to_string()); self.write_string(": "); self.format_monotype(&n.v); } diff --git a/libflux/flux-core/src/semantic/fresh.rs b/libflux/flux-core/src/semantic/fresh.rs index 361c1f3928..0b5d5f6972 100644 --- a/libflux/flux-core/src/semantic/fresh.rs +++ b/libflux/flux-core/src/semantic/fresh.rs @@ -7,7 +7,7 @@ use crate::semantic::{ sub::{merge, merge3, merge4, merge_collect}, types::{ Collection, Dictionary, Function, Kind, Label, MonoType, MonoTypeVecMap, PolyType, - Property, Record, SemanticMap, Tvar, TvarMap, + Property, Record, RecordLabel, SemanticMap, Tvar, TvarMap, }, }; @@ -46,6 +46,18 @@ pub trait Fresh { Self: Sized; } +impl Fresh for RecordLabel { + fn fresh_ref(&self, f: &mut Fresher, sub: &mut TvarMap) -> Option { + match self { + RecordLabel::Variable(var) => var.fresh_ref(f, sub).map(RecordLabel::Variable), + RecordLabel::BoundVariable(var) => { + var.fresh_ref(f, sub).map(RecordLabel::BoundVariable) + } + RecordLabel::Concrete(_) | RecordLabel::Error => None, + } + } +} + impl Fresh for Label { fn fresh_ref(&self, _: &mut Fresher, _: &mut TvarMap) -> Option { None @@ -143,7 +155,7 @@ impl Fresh for PolyType { impl Fresh for MonoType { fn fresh_ref(&self, f: &mut Fresher, sub: &mut TvarMap) -> Option { match self { - MonoType::Error | MonoType::Builtin(_) => None, + MonoType::Error | MonoType::Builtin(_) | MonoType::Label(_) => None, MonoType::BoundVar(tvr) => tvr.fresh_ref(f, sub).map(MonoType::BoundVar), MonoType::Var(tvr) => tvr.fresh_ref(f, sub).map(MonoType::Var), MonoType::Collection(app) => app.fresh_ref(f, sub).map(MonoType::app), diff --git a/libflux/flux-core/src/semantic/infer.rs b/libflux/flux-core/src/semantic/infer.rs index 116ce196ef..d3bd738696 100644 --- a/libflux/flux-core/src/semantic/infer.rs +++ b/libflux/flux-core/src/semantic/infer.rs @@ -173,7 +173,7 @@ pub fn equal( ) -> Result>> { log::debug!("Constraint::Equal {:?}: {} <===> {}", loc.source, exp, act); exp.try_unify(act, sub).map_err(|error| { - log::debug!("Unify error: {} <=> {}", exp, act); + log::debug!("Unify error: {} <=> {} : {}", exp, act, error); Located { location: loc.clone(), diff --git a/libflux/flux-core/src/semantic/mod.rs b/libflux/flux-core/src/semantic/mod.rs index 8d7210a092..362a71b47d 100644 --- a/libflux/flux-core/src/semantic/mod.rs +++ b/libflux/flux-core/src/semantic/mod.rs @@ -45,7 +45,7 @@ use crate::{ infer::Constraints, nodes::Symbol, sub::Substitution, - types::{Label, MonoType, PolyType, PolyTypeHashMap, Property, Record}, + types::{MonoType, PolyType, PolyTypeHashMap, Property, Record, RecordLabel}, }, }; @@ -260,7 +260,7 @@ fn build_record( ); r = Record::Extension { head: Property { - k: Label::from(name.clone()), + k: RecordLabel::from(name.clone()), v: ty, }, tail: MonoType::record(r), @@ -298,14 +298,17 @@ impl Source for codespan_reporting::files::SimpleFile<&str, &str> { fn codespan_range(&self, location: &ast::SourceLocation) -> Range { (|| { let start = self - .line_range((), location.start.line as usize - 1) + .line_range((), (location.start.line as usize).saturating_sub(1)) .ok()? .start; let end = self - .line_range((), location.end.line as usize - 1) + .line_range((), (location.end.line as usize).saturating_sub(1)) .ok()? .start; - Some(start + location.start.column as usize - 1..end + location.end.column as usize - 1) + Some( + start + (location.start.column as usize).saturating_sub(1) + ..end + (location.end.column as usize).saturating_sub(1), + ) })() .unwrap_or_default() } @@ -406,6 +409,9 @@ pub enum Feature { VectorizeOperators, /// Enables vectorization VectorizedMap, + + /// Enables label polymorphism + LabelPolymorphism, } /// A set of configuration options for the behavior of an Analyzer. @@ -493,7 +499,13 @@ impl<'env, I: import::Importer> Analyzer<'env, I> { } self.env.enter_scope(); - let env = match nodes::infer_package(&mut sem_pkg, &mut self.env, sub, &mut self.importer) { + let env = match nodes::infer_package( + &mut sem_pkg, + &mut self.env, + sub, + &mut self.importer, + &self.config, + ) { Ok(()) => { let env = self.env.exit_scope(); PackageExports::try_from(env.values).unwrap_or_else(|err| { diff --git a/libflux/flux-core/src/semantic/nodes.rs b/libflux/flux-core/src/semantic/nodes.rs index c4242fffdf..9f6663691c 100644 --- a/libflux/flux-core/src/semantic/nodes.rs +++ b/libflux/flux-core/src/semantic/nodes.rs @@ -25,9 +25,10 @@ use crate::{ infer::{self, Constraint}, sub::{BindVars, Substitutable, Substituter, Substitution}, types::{ - self, Dictionary, Function, Kind, Label, MonoType, MonoTypeMap, PolyType, Tvar, - TvarKinds, + self, Dictionary, Function, Kind, Label, MonoType, MonoTypeMap, PolyType, RecordLabel, + Tvar, TvarKinds, }, + AnalyzerConfig, Feature, }, }; @@ -126,6 +127,7 @@ struct InferState<'a, 'env> { imports: HashMap, env: &'a mut Environment<'env>, errors: Errors, + config: &'a AnalyzerConfig, } impl InferState<'_, '_> { @@ -184,7 +186,7 @@ pub enum Statement { } impl Statement { - fn apply(self, sub: &Substitution) -> Self { + fn apply(self, sub: &dyn Substituter) -> Self { match self { Statement::Expr(stmt) => Statement::Expr(stmt.apply(sub)), Statement::Variable(stmt) => Statement::Variable(Box::new(stmt.apply(sub))), @@ -206,7 +208,7 @@ pub enum Assignment { } impl Assignment { - fn apply(self, sub: &Substitution) -> Self { + fn apply(self, sub: &dyn Substituter) -> Self { match self { Assignment::Variable(assign) => Assignment::Variable(assign.apply(sub)), Assignment::Member(assign) => Assignment::Member(assign.apply(sub)), @@ -262,7 +264,7 @@ impl Expression { Expression::StringExpr(_) => MonoType::STRING, Expression::Integer(_) => MonoType::INT, Expression::Float(_) => MonoType::FLOAT, - Expression::StringLit(_) => MonoType::STRING, + Expression::StringLit(lit) => lit.typ.clone().unwrap_or(MonoType::STRING), Expression::Duration(_) => MonoType::DURATION, Expression::Uint(_) => MonoType::UINT, Expression::Boolean(_) => MonoType::BOOL, @@ -315,7 +317,7 @@ impl Expression { Expression::StringExpr(e) => e.infer(infer), Expression::Integer(lit) => lit.infer(), Expression::Float(lit) => lit.infer(), - Expression::StringLit(lit) => lit.infer(), + Expression::StringLit(lit) => lit.infer(infer), Expression::Duration(lit) => lit.infer(), Expression::Uint(lit) => lit.infer(), Expression::Boolean(lit) => lit.infer(), @@ -324,7 +326,7 @@ impl Expression { Expression::Error(_) => Ok(()), } } - fn apply(self, sub: &Substitution) -> Self { + fn apply(self, sub: &dyn Substituter) -> Self { match self { Expression::Identifier(e) => Expression::Identifier(e.apply(sub)), Expression::Array(e) => Expression::Array(Box::new(e.apply(sub))), @@ -359,6 +361,7 @@ pub fn infer_package( env: &mut Environment<'_>, sub: &mut Substitution, importer: &mut T, + config: &AnalyzerConfig, ) -> std::result::Result<(), Errors> where T: Importer, @@ -369,15 +372,16 @@ where imports: Default::default(), env, errors: Errors::new(), + config, }; pkg.infer(&mut infer).map_err(|err| err.apply(infer.sub))?; - infer.env.apply_mut(infer.sub); + infer.env.apply_mut(&FinalizeTypes { sub: infer.sub }); if infer.errors.has_errors() { let sub = BindVars::new(infer.sub); for err in &mut infer.errors { - err.apply_mut(&sub); + err.apply_mut(&FinalizeTypes { sub: &sub }); } Err(infer.errors) } else { @@ -388,7 +392,27 @@ where /// Applies the substitution to the entire package. #[allow(missing_docs)] pub fn inject_pkg_types(pkg: Package, sub: &Substitution) -> Package { - pkg.apply(sub) + pkg.apply(&FinalizeTypes { sub }) +} + +struct FinalizeTypes<'a> { + sub: &'a dyn Substituter, +} + +impl Substituter for FinalizeTypes<'_> { + fn try_apply(&self, tvr: Tvar) -> Option { + self.sub.try_apply(tvr) + } + fn visit_type(&self, typ: &MonoType) -> Option { + match typ { + MonoType::Var(tvr) => { + let typ = self.sub.try_apply(*tvr)?; + Some(self.visit_type(&typ).unwrap_or(typ)) + } + MonoType::Label(_) => Some(MonoType::STRING), + _ => None, + } + } } #[derive(Debug, PartialEq, Clone)] @@ -407,7 +431,7 @@ impl Package { } Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.files = self.files.into_iter().map(|file| file.apply(sub)).collect(); self } @@ -458,7 +482,7 @@ impl File { infer.imports.clear(); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.body = self.body.into_iter().map(|stmt| stmt.apply(sub)).collect(); self } @@ -510,7 +534,7 @@ impl OptionStmt { } } } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.assignment = self.assignment.apply(sub); self } @@ -529,7 +553,7 @@ impl BuiltinStmt { infer.env.add(self.id.name.clone(), self.typ_expr.clone()); Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -546,7 +570,7 @@ impl TestStmt { fn infer(&mut self, infer: &mut InferState<'_, '_>) -> Result<()> { self.assignment.infer(infer) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.assignment = self.assignment.apply(sub); self } @@ -577,7 +601,7 @@ impl TestCaseStmt { } Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.body = self.body.into_iter().map(|stmt| stmt.apply(sub)).collect(); self } @@ -596,7 +620,7 @@ impl ExprStmt { self.expression.infer(infer)?; Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.expression = self.expression.apply(sub); self } @@ -615,7 +639,7 @@ impl ReturnStmt { fn infer(&mut self, infer: &mut InferState<'_, '_>) -> Result { self.argument.infer(infer) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.argument = self.argument.apply(sub); self } @@ -686,7 +710,7 @@ impl VariableAssgn { infer.env.add(self.id.name.clone(), p); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.init = self.init.apply(sub); self } @@ -702,7 +726,7 @@ pub struct MemberAssgn { } impl MemberAssgn { - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.member = self.member.apply(sub); self.init = self.init.apply(sub); self @@ -731,7 +755,7 @@ impl StringExpr { } Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.parts = self.parts.into_iter().map(|part| part.apply(sub)).collect(); self } @@ -745,7 +769,7 @@ pub enum StringExprPart { } impl StringExprPart { - fn apply(self, sub: &Substitution) -> Self { + fn apply(self, sub: &dyn Substituter) -> Self { match self { StringExprPart::Interpolated(part) => StringExprPart::Interpolated(part.apply(sub)), StringExprPart::Text(_) => self, @@ -770,7 +794,7 @@ pub struct InterpolatedPart { } impl InterpolatedPart { - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.expression = self.expression.apply(sub); self } @@ -806,7 +830,7 @@ impl ArrayExpr { self.typ = MonoType::arr(elt); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.elements = self .elements @@ -852,7 +876,7 @@ impl DictExpr { Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.elements = self .elements @@ -1025,7 +1049,7 @@ impl FunctionExpr { ds } #[allow(missing_docs)] - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.params = self .params @@ -1091,7 +1115,7 @@ impl Block { } } } - fn apply(self, sub: &Substitution) -> Self { + fn apply(self, sub: &dyn Substituter) -> Self { match self { Block::Variable(assign, next) => { Block::Variable(Box::new(assign.apply(sub)), Box::new(next.apply(sub))) @@ -1114,7 +1138,7 @@ pub struct FunctionParameter { } impl FunctionParameter { - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { match self.default { Some(e) => { self.default = Some(e.apply(sub)); @@ -1243,7 +1267,7 @@ impl BinaryExpr { Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.left = self.left.apply(sub); self.right = self.right.apply(sub); @@ -1290,50 +1314,38 @@ impl CallExpr { v: (p.type_of(), p.loc()), }); } + + let act = Function { + opt: MonoTypeMap::new(), + req, + pipe, + retn: (self.typ.clone(), &self.loc), + }; + match &*self.callee.type_of().apply_cow(infer.sub) { MonoType::Fun(func) => { - if let Err(err) = func.try_unify( - &Function { - opt: MonoTypeMap::new(), - req, - pipe, - retn: (self.typ.clone(), &self.loc), - }, - infer.sub, - ) { + if let Err(err) = func.try_subsume_with(&act, infer.sub, |error| Located { + location: self.loc.clone(), + error, + }) { + log::debug!( + "Unify error: {} <=> {} : {}", + func, + act.map(|(typ, _)| typ), + err + ); infer.errors.extend(err.into_iter().map(Error::from)); } } callee => { // Constrain the callee to be a Function. - infer.equal( - callee, - &MonoType::from(Function { - opt: MonoTypeMap::new(), - req: req.into_iter().map(|(k, (v, _))| (k, v)).collect(), - pipe: pipe.map(|prop| types::Property { - k: prop.k, - v: prop.v.0, - }), - // The return type of a function call is the type of the call itself. - // Remind that, when two functions are unified, their return types are unified too. - // As an example take: - // f = (a) => a + 1 - // f(a: 0) - // The return type of `f` is `int`. - // The return type of `f(a: 0)` is `t0` (a fresh type variable). - // Upon unification a substitution "t0 => int" is created, so that the compiler - // can infer that, for instance, `f(a: 0) + 1` is legal. - retn: self.typ.clone(), - }), - &self.loc, - ); + infer.equal(callee, &MonoType::from(act.map(|(typ, _)| typ)), &self.loc); } } Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.callee = self.callee.apply(sub); self.arguments = self @@ -1378,7 +1390,7 @@ impl ConditionalExpr { Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.test = self.test.apply(sub); self.consequent = self.consequent.apply(sub); self.alternate = self.alternate.apply(sub); @@ -1414,7 +1426,7 @@ impl LogicalExpr { ]); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.left = self.left.apply(sub); self.right = self.right.apply(sub); self @@ -1455,7 +1467,7 @@ impl MemberExpr { let r = { self.typ = MonoType::Var(infer.sub.fresh()); let head = types::Property { - k: Label::from(self.property.to_owned()), + k: RecordLabel::from(self.property.to_owned()), v: self.typ.to_owned(), }; let tail = MonoType::Var(infer.sub.fresh()); @@ -1469,7 +1481,7 @@ impl MemberExpr { }]); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.object = self.object.apply(sub); self @@ -1509,7 +1521,7 @@ impl IndexExpr { ]); Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.array = self.array.apply(sub); self.index = self.index.apply(sub); @@ -1544,7 +1556,7 @@ impl ObjectExpr { prop.value.infer(infer)?; r = MonoType::from(types::Record::Extension { head: types::Property { - k: Label::from(prop.key.name.clone()), + k: RecordLabel::from(prop.key.name.clone()), v: prop.value.type_of(), }, tail: r, @@ -1553,7 +1565,7 @@ impl ObjectExpr { self.typ = r; Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); if let Some(e) = self.with { self.with = Some(e.apply(sub)); @@ -1611,7 +1623,7 @@ impl UnaryExpr { } Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self.argument = self.argument.apply(sub); self @@ -1628,7 +1640,7 @@ pub struct Property { } impl Property { - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.value = self.value.apply(sub); self } @@ -1654,7 +1666,7 @@ impl IdentifierExpr { self.typ = t; Ok(()) } - fn apply(mut self, sub: &Substitution) -> Self { + fn apply(mut self, sub: &dyn Substituter) -> Self { self.typ = self.typ.apply(sub); self } @@ -1680,7 +1692,7 @@ impl BooleanLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1697,7 +1709,7 @@ impl IntegerLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1714,7 +1726,7 @@ impl FloatLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1731,7 +1743,7 @@ impl RegexpLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1742,13 +1754,18 @@ impl RegexpLit { pub struct StringLit { pub loc: ast::SourceLocation, pub value: String, + /// The (label) type if label types are enabled. + pub typ: Option, } impl StringLit { - fn infer(&mut self) -> Result { + fn infer(&mut self, infer: &mut InferState<'_, '_>) -> Result { + if infer.config.features.contains(&Feature::LabelPolymorphism) { + self.typ = Some(MonoType::Label(Label::from(self.value.as_str()))); + } Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1765,7 +1782,7 @@ impl UintLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1782,7 +1799,7 @@ impl DateTimeLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } @@ -1816,7 +1833,7 @@ impl DurationLit { fn infer(&mut self) -> Result { Ok(()) } - fn apply(self, _: &Substitution) -> Self { + fn apply(self, _: &dyn Substituter) -> Self { self } } diff --git a/libflux/flux-core/src/semantic/sub.rs b/libflux/flux-core/src/semantic/sub.rs index 7988509663..fac382ad30 100644 --- a/libflux/flux-core/src/semantic/sub.rs +++ b/libflux/flux-core/src/semantic/sub.rs @@ -1,15 +1,17 @@ //! Substitutions during type inference. -use std::{borrow::Cow, cell::RefCell, iter::FusedIterator}; +use std::{borrow::Cow, cell::RefCell, collections::BTreeMap, fmt, iter::FusedIterator}; use crate::semantic::types::{union, Error, MonoType, PolyType, SubstitutionMap, Tvar, TvarKinds}; +use ena::unify::UnifyKey; + /// A substitution defines a function that takes a monotype as input /// and returns a monotype as output. The output type is interpreted /// as being equivalent to the input type. /// /// Substitutions are idempotent. Given a substitution *s* and an input /// type *x*, we have *s*(*s*(*x*)) = *s*(*x*). -#[derive(Clone, Debug, Default)] +#[derive(Clone, Default)] pub struct Substitution { table: RefCell, // TODO Add `snapshot`/`rollback_to` for `TvarKinds` (like `ena::UnificationTable`) so that @@ -18,6 +20,37 @@ pub struct Substitution { cons: RefCell, } +impl fmt::Debug for Substitution { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut roots = BTreeMap::new(); + + let mut table = self.table.borrow_mut(); + + #[derive(Debug)] + struct Root { + variables: Vec, + #[allow(dead_code)] + value: T, + } + for i in 0..table.len() as u32 { + let i = Tvar::from_index(i); + let root = table.find(i); + let root_node = roots.entry(root).or_insert_with(|| Root { + variables: Vec::new(), + value: table.probe_value(root), + }); + if i != root { + root_node.variables.push(i); + } + } + + f.debug_struct("Substitution") + .field("table", &roots) + .field("cons", &*self.cons.borrow()) + .finish() + } +} + /// An implementation of a /// (Disjoint-set](https://en.wikipedia.org/wiki/Disjoint-set_data_structure) which is used to /// track which type variables are them same (unified) and which type they have unified to (if any) @@ -231,6 +264,12 @@ pub trait Substitutable { } } +impl Substitutable for String { + fn walk(&self, _sub: &dyn Substituter) -> Option { + None + } +} + impl Substitutable for Box where T: Substitutable, diff --git a/libflux/flux-core/src/semantic/tests.rs b/libflux/flux-core/src/semantic/tests.rs index f5bd96a2a6..fae15439db 100644 --- a/libflux/flux-core/src/semantic/tests.rs +++ b/libflux/flux-core/src/semantic/tests.rs @@ -53,18 +53,18 @@ fn parse_map(package: Option<&str>, m: HashMap<&str, &str>) -> PolyTypeHashMap Symbol::from(name), Some(package) => Symbol::from(name).with_package(package), }, - poly.unwrap(), - ); + poly, + ) }) .collect() } @@ -220,7 +220,7 @@ macro_rules! test_infer { config = $config; )? if let Err(e) = infer_types($src, env, imp, Some($exp), config) { - panic!("{}", e); + panic!("{}", e.pretty($src)); } }} } @@ -298,7 +298,7 @@ macro_rules! test_infer_err { /// ``` /// macro_rules! test_error_msg { - ( $(test: $test: ident,)? $(imp: $imp:expr,)? $(env: $env:expr,)? src: $src:expr $(,)?, expect: $expect:expr $(,)? ) => { + ( $(config: $config:expr,)? $(test: $test: ident,)? $(imp: $imp:expr,)? $(env: $env:expr,)? src: $src:expr $(,)?, expect: $expect:expr $(,)? ) => { $(#[test] fn $test() )? { #[allow(unused_mut, unused_assignments)] @@ -311,12 +311,17 @@ macro_rules! test_error_msg { $( env = $env; )? + #[allow(unused_mut, unused_assignments)] + let mut config = AnalyzerConfig::default(); + $( + config = $config; + )? match infer_types( $src, env, imp, None, - AnalyzerConfig::default(), + config, ) { Err(e) => { let got = e.pretty($src); @@ -400,6 +405,8 @@ macro_rules! package { }} } +mod labels; + #[test] fn dictionary_literals() { test_infer! { diff --git a/libflux/flux-core/src/semantic/tests/labels.rs b/libflux/flux-core/src/semantic/tests/labels.rs new file mode 100644 index 0000000000..629fc9ccb0 --- /dev/null +++ b/libflux/flux-core/src/semantic/tests/labels.rs @@ -0,0 +1,236 @@ +use super::*; + +use crate::semantic::Feature; + +#[test] +fn labels_simple() { + test_infer! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "fill" => "(<-tables: [{ A with B: C }], ?column: B, ?value: D) => [{ A with B: D }] + where B: Label + " + ], + src: r#" + x = [{ a: 1 }] |> fill(column: "a", value: "x") + y = [{ a: 1, b: ""}] |> fill(column: "b", value: 1.0) + b = "b" + z = [{ a: 1, b: ""}] |> fill(column: b, value: 1.0) + "#, + exp: map![ + "b" => "string", + "x" => "[{ a: string }]", + "y" => "[{ a: int, b: float }]", + "z" => "[{ a: int, b: float }]", + ], + } +} + +#[test] +fn labels_unbound() { + test_infer! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "f" => "(<-tables: [{ A with B: C }], ?value: D) => [{ A with B: D }] + where B: Label + " + ], + src: r#" + x = [{ a: 1, b: 2.0 }] |> f(value: "x") + "#, + exp: map![ + "x" => "[{A with a:int, B:string}] where B: Label", + ], + } +} + +#[test] +fn labels_dynamic_string() { + test_error_msg! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "fill" => "(<-tables: [{ A with B: C }], ?column: B, ?value: D) => [{ A with B: D }] + where B: Label + " + ], + src: r#" + column = "" + "a" + x = [{ a: 1 }] |> fill(column: column, value: "x") + "#, + expect: expect![[r#" + error: string is not Label (argument column) + ┌─ main:3:44 + │ + 3 │ x = [{ a: 1 }] |> fill(column: column, value: "x") + │ ^^^^^^ + + error: string is not a label + ┌─ main:3:31 + │ + 3 │ x = [{ a: 1 }] |> fill(column: column, value: "x") + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + "#]], + } +} + +#[test] +fn undefined_field() { + test_error_msg! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "fill" => "(<-tables: [{ A with B: C }], ?column: B, ?value: D) => [{ A with B: D }] + where B: Label + " + ], + src: r#" + x = [{ b: 1 }] |> fill(column: "a", value: "x") + "#, + expect: expect![[r#" + error: record is missing label a + ┌─ main:2:31 + │ + 2 │ x = [{ b: 1 }] |> fill(column: "a", value: "x") + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + "#]], + } +} + +#[test] +fn merge_labels_to_string() { + test_infer! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + src: r#" + x = if 1 == 1 then "a" else "b" + y = if 1 == 1 then "a" else "b" + "b" + z = ["a", "b"] + "#, + exp: map![ + "x" => "string", + "y" => "string", + "z" => "[string]", + ], + } +} + +#[test] +fn merge_labels_to_string_in_function() { + test_infer! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "same" => "(x: A, y: A) => A" + ], + src: r#" + x = same(x: "a", y: "b") + y = same(x: ["a"], y: ["b"]) + "#, + exp: map![ + "x" => "string", + "y" => "[string]", + ], + } +} + +#[test] +fn attempt_to_use_label_polymorphism_without_feature() { + test_error_msg! { + env: map![ + "columns" => "(table: A, ?column: C) => { C: string } where A: Record, C: Label", + ], + src: r#" + x = columns(table: { a: 1, b: "b" }, column: "abc") + y = x.abc + "#, + expect: expect![[r#" + error: string is not Label (argument column) + ┌─ main:2:58 + │ + 2 │ x = columns(table: { a: 1, b: "b" }, column: "abc") + │ ^^^^^ + + error: record is missing label abc + ┌─ main:3:17 + │ + 3 │ y = x.abc + │ ^ + + "#]], + } +} +#[test] +fn columns() { + test_infer! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "stream" => "stream[{ a: int }]", + "map" => "(<-tables: stream[A], fn: (r: A) => B) => stream[B]" + ], + imp: map![ + "experimental/universe" => package![ + "fill" => "(<-tables: stream[{A with C: B}], ?column: C, ?value: B, ?usePrevious: bool) => stream[{A with C: B}] + where + A: Record, + C: Label", + "columns" => "(<-tables: stream[A], ?column: C) => stream[{ C: string }] where A: Record, C: Label", + ], + ], + src: r#" + import "experimental/universe" + + x = stream + |> universe.columns(column: "abc") + |> map(fn: (r) => ({ x: r.abc })) + "#, + exp: map![ + "x" => "stream[{ x: string }]", + ], + } +} + +#[test] +fn optional_label() { + test_error_msg! { + config: AnalyzerConfig{ + features: vec![Feature::LabelPolymorphism], + ..AnalyzerConfig::default() + }, + env: map![ + "columns" => "(table: A, ?column: C) => { C: string } where A: Record, C: Label", + ], + src: r#" + x = columns(table: { a: 1, b: "b" }) + y = x.abc + "#, + // TODO This fails because `column` is not specified but it ought to provide a better error + expect: expect![[r#" + error: record is missing label abc + ┌─ main:3:17 + │ + 3 │ y = x.abc + │ ^ + + "#]], + } +} diff --git a/libflux/flux-core/src/semantic/types.rs b/libflux/flux-core/src/semantic/types.rs index 64378be33f..772cd45f1e 100644 --- a/libflux/flux-core/src/semantic/types.rs +++ b/libflux/flux-core/src/semantic/types.rs @@ -1,6 +1,7 @@ //! Semantic representations of types. use std::{ + borrow::Cow, cell::Cell, cmp, collections::{BTreeMap, BTreeSet}, @@ -28,20 +29,146 @@ pub type SemanticMap = BTreeMap; #[allow(missing_docs)] pub type SemanticMapIter<'a, K, V> = std::collections::btree_map::Iter<'a, K, V>; +trait Matcher { + fn match_types( + &self, + unifier: &mut Unifier<'_, E>, + expected: &MonoType, + actual: &MonoType, + ) -> MonoType; +} + +struct Unify; + +impl Matcher for Unify { + fn match_types( + &self, + unifier: &mut Unifier<'_, Error>, + expected: &MonoType, + actual: &MonoType, + ) -> MonoType { + // Normally we just treat any label as a string. This effectively ensures that all + // string literals are still treated as strings. + let expected = match expected { + MonoType::Label(_) => &MonoType::STRING, + _ => expected, + }; + let actual = match actual { + MonoType::Label(_) => &MonoType::STRING, + _ => actual, + }; + expected.unify_inner(actual, unifier) + } +} + +struct Subsume; + +impl Matcher for Subsume { + fn match_types( + &self, + unifier: &mut Unifier<'_, Error>, + expected: &MonoType, + actual: &MonoType, + ) -> MonoType { + // When a label is unified to a type variable that has the `Label` kind we preserve the + // label. Otherwise we translate the label to `string`, same as during normal unification. + fn translate_label<'a>( + unifier: &mut Unifier<'_, Error>, + maybe_label: &'a MonoType, + maybe_var: &'a MonoType, + ) -> Cow<'a, MonoType> { + match maybe_var { + MonoType::Var(v) + if !unifier + .sub + .cons() + .get(v) + .map_or(false, |kinds| kinds.contains(&Kind::Label)) => + { + struct ReplaceLabels; + impl Substituter for ReplaceLabels { + fn try_apply(&self, _: Tvar) -> Option { + None + } + fn visit_type(&self, typ: &MonoType) -> Option { + match typ { + MonoType::Label(_) => Some(MonoType::STRING), + _ => None, + } + } + } + maybe_label + .visit(&ReplaceLabels) + .map(Cow::Owned) + .unwrap_or_else(|| Cow::Borrowed(maybe_label)) + } + _ => Cow::Borrowed(maybe_label), + } + } + let actual = translate_label(unifier, actual, expected); + let expected = translate_label(unifier, expected, &actual); + match (&*expected, &*actual) { + // Labels should be accepted anywhere that we expect a string + (MonoType::Builtin(BuiltinType::String), MonoType::Label(_)) => MonoType::STRING, + _ => expected.unify_inner(&actual, unifier), + } + } +} + struct Unifier<'a, E = Error> { sub: &'a mut Substitution, + // We must delay the inference of records with label variables until we have inferred + // the remaining context. + delayed_records: Vec<(Record, Record)>, errors: Errors, + matcher: &'a dyn Matcher, } impl<'a, E> Unifier<'a, E> { fn new(sub: &'a mut Substitution) -> Self { Unifier { sub, + delayed_records: Vec::new(), errors: Errors::new(), + matcher: &Unify, } } - fn finish(self, value: T) -> Result> { + fn new_subsume(sub: &'a mut Substitution) -> Self { + Unifier { + sub, + delayed_records: Vec::new(), + errors: Errors::new(), + matcher: &Subsume, + } + } + + fn sub_unifier(&mut self) -> Unifier<'_, F> { + Unifier { + sub: self.sub, + delayed_records: Vec::new(), + errors: Errors::new(), + matcher: self.matcher, + } + } + + fn finish( + mut self, + value: MonoType, + mk_error: impl Fn(Error) -> E, + ) -> Result> { + if !self.delayed_records.is_empty() { + let mut sub_unifier = Unifier::new(self.sub); + while let Some((expected, actual)) = self.delayed_records.pop() { + let expected = expected.apply(sub_unifier.sub); + let actual = actual.apply(sub_unifier.sub); + expected.unify_now(&actual, &mut sub_unifier); + } + + self.errors + .extend(sub_unifier.errors.into_iter().map(&mk_error)); + } + if self.errors.has_errors() { Err(self.errors) } else { @@ -211,6 +338,7 @@ pub enum Error { exp: String, act: String, }, + NotALabel(MonoType), } impl fmt::Display for Error { @@ -268,6 +396,9 @@ impl fmt::Display for Error { Error::MultiplePipeArguments { exp, act } => { write!(f, "expected pipe argument {} but found {}", exp, act) } + Error::NotALabel(typ) => { + write!(f, "{} is not a label", typ) + } } } } @@ -298,6 +429,7 @@ impl Substitutable for Error { .map(|e| Error::CannotUnifyArgument(x.clone(), e)), Error::CannotUnifyReturn { exp, act, cause } => apply3(exp, act, cause, sub) .map(|(exp, act, cause)| Error::CannotUnifyReturn { exp, act, cause }), + Error::NotALabel(t) => t.visit(sub).map(Error::NotALabel), Error::MissingLabel(_) | Error::ExtraLabel(_) | Error::MissingArgument(_) @@ -324,6 +456,7 @@ pub enum Kind { Comparable, Divisible, Equatable, + Label, Negatable, Nullable, Numeric, @@ -344,6 +477,7 @@ impl FromStr for Kind { "Numeric" => Kind::Numeric, "Comparable" => Kind::Comparable, "Equatable" => Kind::Equatable, + "Label" => Kind::Label, "Nullable" => Kind::Nullable, "Negatable" => Kind::Negatable, "Timeable" => Kind::Timeable, @@ -401,6 +535,8 @@ pub enum MonoType { #[display(fmt = "{}", _0)] Builtin(BuiltinType), + #[display(fmt = "\"{}\"", _0)] + Label(Label), #[display(fmt = "#{}", _0)] Var(Tvar), @@ -440,6 +576,7 @@ impl Serialize for MonoType { Regexp, Bytes, Var(Tvar), + Label(&'a Label), Arr(&'a MonoType), Dict(&'a Ptr), Record(&'a Ptr), @@ -469,6 +606,7 @@ impl Serialize for MonoType { CollectionType::Vector => MonoTypeSer::Vector(&p.arg), CollectionType::Stream => MonoTypeSer::Stream(&p.arg), }, + Self::Label(p) => MonoTypeSer::Label(p), Self::Dict(p) => MonoTypeSer::Dict(p), Self::Record(p) => MonoTypeSer::Record(p), Self::Fun(p) => MonoTypeSer::Fun(p), @@ -647,9 +785,11 @@ impl Substitutable for MonoType { fn walk(&self, sub: &dyn Substituter) -> Option { match self { - MonoType::Error | MonoType::Builtin(_) | MonoType::BoundVar(_) | MonoType::Var(_) => { - None - } + MonoType::Error + | MonoType::Builtin(_) + | MonoType::Label(_) + | MonoType::BoundVar(_) + | MonoType::Var(_) => None, MonoType::Collection(app) => app.visit(sub).map(MonoType::app), MonoType::Dict(dict) => dict.visit(sub).map(MonoType::dict), MonoType::Record(obj) => obj.visit(sub).map(MonoType::record), @@ -765,7 +905,23 @@ impl MonoType { let typ = self.unify(actual, &mut unifier); - unifier.finish(typ) + unifier.finish(typ, From::from) + } + + /// Performs subsumption on the type with another type. + /// If successful, results in a solution to the unification problem, + /// in the form of a substitution. If there is no solution to the + /// unification problem then unification fails and an error is reported. + pub fn try_subsume( + &self, // self represents the expected type + actual: &Self, + sub: &mut Substitution, + ) -> Result> { + let mut unifier = Unifier::new_subsume(sub); + + let typ = self.unify(actual, &mut unifier); + + unifier.finish(typ, From::from) } fn unify( @@ -774,11 +930,24 @@ impl MonoType { unifier: &mut Unifier<'_>, ) -> MonoType { log::debug!("Unify {} <=> {}", self, actual); + + unifier.matcher.match_types(unifier, self, actual) + } + + fn unify_inner( + &self, // self represents the expected type + actual: &Self, + unifier: &mut Unifier<'_>, + ) -> MonoType { match (self, actual) { // An error has already occurred so assume everything is ok here so that we do not // create additional, spurious errors (MonoType::Error, _) | (_, MonoType::Error) => (), + (MonoType::Builtin(exp), MonoType::Builtin(act)) => exp.unify(*act, unifier), + + (MonoType::Label(l), MonoType::Label(r)) if l == r => {} + (MonoType::Var(tv), MonoType::Var(tv2)) => { match (unifier.sub.try_apply(*tv), unifier.sub.try_apply(*tv2)) { (Some(self_), Some(actual)) => { @@ -805,16 +974,19 @@ impl MonoType { } None => tv.unify(t, unifier), }, + (MonoType::Collection(t), MonoType::Collection(s)) => t.unify(s, unifier), + (MonoType::Dict(t), MonoType::Dict(s)) => t.unify(s, unifier), + (MonoType::Record(t), MonoType::Record(s)) => t.unify(s, unifier), + (MonoType::Fun(t), MonoType::Fun(s)) => t.unify(s, unifier), - (exp, act) => { - unifier.errors.push(Error::CannotUnify { - exp: exp.clone(), - act: act.clone(), - }); - } + + (exp, act) => unifier.errors.push(Error::CannotUnify { + exp: exp.clone(), + act: act.clone(), + }), } self.clone() } @@ -827,6 +999,19 @@ impl MonoType { // TODO Should constrain bound vars as well, but we can't just store it in `cons` as // they would override constraints of free variables MonoType::BoundVar(_) => Ok(()), + MonoType::Label(_) => match with { + Kind::Addable + | Kind::Comparable + | Kind::Equatable + | Kind::Label + | Kind::Nullable + | Kind::Basic + | Kind::Stringable => Ok(()), + _ => Err(Error::CannotConstrain { + act: self.clone(), + exp: with, + }), + }, MonoType::Var(tvr) => { tvr.constrain(with, cons); Ok(()) @@ -840,7 +1025,9 @@ impl MonoType { fn contains(&self, tv: Tvar) -> bool { match self { - MonoType::Error | MonoType::Builtin(_) | MonoType::BoundVar(_) => false, + MonoType::Error | MonoType::Builtin(_) | MonoType::Label(_) | MonoType::BoundVar(_) => { + false + } MonoType::Var(tvr) => tv == *tvr, MonoType::Collection(app) => app.contains(tv), MonoType::Dict(dict) => dict.contains(tv), @@ -915,8 +1102,8 @@ impl ena::unify::UnifyKey for Tvar { } impl ena::unify::UnifyValue for MonoType { type Error = ena::unify::NoError; - fn unify_values(_value1: &Self, _value2: &Self) -> Result { - unreachable!("We should never unify two values with each other within the substitution. If we reach this we did not resolve the variable before unifying") + fn unify_values(value1: &Self, value2: &Self) -> Result { + unreachable!("We should never unify two values with each other within the substitution. If we reach this we did not resolve the variable before unifying {} <=> {}", value1, value2) } } @@ -1121,7 +1308,7 @@ impl fmt::Display for Record { } } -fn collect_record(record: &Record) -> (RefMonoTypeVecMap<'_, Label>, Option<&MonoType>) { +fn collect_record(record: &Record) -> (RefMonoTypeVecMap<'_, RecordLabel>, Option<&MonoType>) { let mut a = RefMonoTypeVecMap::new(); let mut fields = record.fields(); @@ -1200,6 +1387,22 @@ impl Record { // self represents the expected type. // fn unify(&self, actual: &Self, unifier: &mut Unifier<'_>) { + let has_variable_label = |r: &Record| { + r.fields().any(|prop| match prop.k { + RecordLabel::Variable(v) => unifier.sub.try_apply(v).is_none(), + RecordLabel::BoundVariable(_) | RecordLabel::Concrete(_) | RecordLabel::Error => { + false + } + }) + }; + if has_variable_label(self) || has_variable_label(actual) { + unifier.delayed_records.push((self.clone(), actual.clone())); + return; + } + self.unify_now(actual, unifier) + } + + fn unify_now(&self, actual: &Self, unifier: &mut Unifier<'_>) { match (self, actual) { (Record::Empty, Record::Empty) => (), ( @@ -1280,9 +1483,15 @@ impl Record { .. }, Record::Empty, - ) => { - unifier.errors.push(Error::MissingLabel(a.to_string())); - } + ) => match *a.apply_cow(unifier.sub) { + RecordLabel::Concrete(_) => unifier.errors.push(Error::MissingLabel(a.to_string())), + RecordLabel::BoundVariable(v) | RecordLabel::Variable(v) => { + let t = unifier.sub.apply(v); + t.unify(&MonoType::Error, unifier); + unifier.errors.push(Error::NotALabel(t)); + } + RecordLabel::Error => (), + }, // If we are expecting {} but find {a: u | r}, label `a` is extra. ( Record::Empty, @@ -1381,6 +1590,30 @@ impl<'a> Iterator for FieldIter<'a> { } } +fn merge_in_context( + exp: &MonoType, + act: &T, + unifier: &mut Unifier<'_, T::Error>, + mut context: impl FnMut(Error) -> Error, +) where + T: TypeLike, +{ + let mut sub_unifier = unifier.sub_unifier(); + exp.unify(act.typ(), &mut sub_unifier); + + let Unifier { + delayed_records, + errors, + .. + } = sub_unifier; + + unifier + .errors + .extend(errors.into_iter().map(|e| act.error(context(e)))); + + unifier.delayed_records.extend(delayed_records); +} + // Applies `context` to each error generated by unifying `exp` and `act` fn unify_in_context( exp: &MonoType, @@ -1390,10 +1623,7 @@ fn unify_in_context( ) where T: TypeLike, { - let mut sub_unifier = Unifier { - sub: unifier.sub, - errors: Errors::new(), - }; + let mut sub_unifier = Unifier::new(unifier.sub); exp.unify(act.typ(), &mut sub_unifier); unifier.errors.extend( @@ -1402,6 +1632,95 @@ fn unify_in_context( .into_iter() .map(|e| act.error(context(e))), ); + + unifier.delayed_records.extend(sub_unifier.delayed_records); +} + +/// Labels in records that are allowed be variables +#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Serialize)] +pub enum RecordLabel { + /// A variable label + Variable(Tvar), + /// A variable label + BoundVariable(Tvar), + /// A concrete label + Concrete(Label), + /// A type error occurred during type inference + Error, +} + +impl From