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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions book/src/super-sql/expressions/dot.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,19 @@ field name of a record or a string key of a map.

Dereferenceable values include [records](../types/record.md),
[maps](../types/map.md) with keys of type string, and
[type values](../types/type.md) that are of type record.
[type values](../types/type.md).

The result of a dot expression is
* the value of the indicated field for a record type,
* the value of the indicated entry for a map with string keys, or
* the type value of the indicated field for a record type value.
* the type value of the indicated field for a type value.

When navigating a type value, the dot operator looks through named,
optional, fusion, and error types and into the element type of
an array or set and the value type of a map, e.g., `<[{a:int64}]>.a`
is `<int64>`. For a union type, the result is the union of the field's
types in the union's types that have the field, e.g.,
`<{a:int64}|{a:string}|int64>.a` is `<int64|string>`.

When a field or key is referenced in a dereferenceable type but that
field or key is not present, then `error("missing")` is the result.
Expand Down Expand Up @@ -65,6 +72,27 @@ map{"x":{y:1},"y":2}

---

_Navigate into a type value_

```mdtest-spq
# spq
values this.a.b
# input
<{a:{b:string}}>
<{a:[{b:ip}]}>
<{a:map{string:{b:int64}}}>
<{a:{b:int64}|{b:string}|{c:bool}}>
<{a:int64}>
# expected output
<string>
<ip>
<int64>
<int64|string>
error({message:"no such field 'b'",on:<int64>})
```

---

_Use backtick quotes or index syntax for identifiers with special characters_

```mdtest-spq
Expand Down
4 changes: 4 additions & 0 deletions compiler/semantic/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,10 @@ func (c *checker) deref(loc ast.Node, inType super.Type, field string, noneish b
return c.unknown
}
return typ
case *super.TypeOfType:
// A field of a type value is a type value (or an error if the
// type has no such field, which we can't know statically).
return super.TypeType
case *super.TypeOfMissing:
// A field of a missing value is missing too, so leave the missing
// in the type for whatever uses it to catch.
Expand Down
90 changes: 74 additions & 16 deletions runtime/expr/dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,7 @@ func (d *DotExpr) eval(vecs ...vector.Any) vector.Any {
return unripped
}, vector.AddNoRip(out), out)
case *vector.TypeValue:
var errs []uint32
typvals := vector.NewTypeValueEmpty()
for i := range vec.Len() {
typ := vec.Value(i)
if typ, ok := super.TypeUnder(typ).(*super.TypeRecord); ok {
if typ, ok := typ.TypeOfField(d.key); ok {
typvals.Append(typ)
continue
}
}
errs = append(errs, i)
}
if len(errs) > 0 {
return vector.NewCombinedError(d.sctx, fmt.Sprintf("no such field '%s'", sup.QuotedName(d.key)), typvals, vec, errs)
}
return typvals
return d.derefTypeValue(vec)
case *vector.Map:
keyVec := vector.NewConstString(d.key, vec.Len())
return indexMap(d.sctx, vec, keyVec)
Expand Down Expand Up @@ -172,6 +157,79 @@ func (d *DotExpr) noneOfField(vec *vector.Option) vector.Any {
return vector.NewNone(vec.Len())
}

// derefTypeValue returns the type of field d.key for each type value in vec.
// See derefType.
func (d *DotExpr) derefTypeValue(vec *vector.TypeValue) vector.Any {
var errs []uint32
typvals := vector.NewTypeValueEmpty()
for i := range vec.Len() {
if typ := d.derefType(vec.Value(i)); typ != nil {
typvals.Append(typ)
continue
}
errs = append(errs, i)
}
if len(errs) > 0 {
msg := fmt.Sprintf("no such field '%s'", sup.QuotedName(d.key))
if len(errs) == int(vec.Len()) {
return vector.NewWrappedError(d.sctx, msg, vec)
}
return vector.NewCombinedError(d.sctx, msg, typvals, vec, errs)
}
return typvals
}

// derefType returns the type of field d.key in typ or nil if there is no
// such field. The dot navigates through named, option, fusion, and error
// types and into the element types of arrays and sets and the value type of
// maps, so, e.g., <[{a:{b:string}}]>.a.b is <string>. For a union, the
// result is the union of the field's types in the union's types that have
// the field.
func (d *DotExpr) derefType(typ super.Type) super.Type {
switch typ := typ.(type) {
case *super.TypeNamed:
return d.derefType(typ.Type)
case *super.TypeRecord:
if t, ok := typ.TypeOfField(d.key); ok {
return t
}
case *super.TypeArray:
return d.derefType(typ.Type)
case *super.TypeSet:
return d.derefType(typ.Type)
case *super.TypeMap:
return d.derefType(typ.ValType)
case *super.TypeUnion:
var types []super.Type
for _, t := range typ.Types {
out := d.derefType(t)
if out == nil {
continue
}
if u, ok := out.(*super.TypeUnion); ok {
types = append(types, u.Types...)
} else {
types = append(types, out)
}
}
types = super.UniqueTypes(types)
switch len(types) {
case 0:
return nil
case 1:
return types[0]
}
return d.sctx.MustLookupTypeUnion(types)
case *super.TypeFusion:
return d.derefType(typ.Type)
case *super.TypeOption:
return d.derefType(typ.Type)
case *super.TypeError:
return d.derefType(typ.Type)
}
return nil
}

func (d *DotExpr) noneErr(vec vector.Any) *vector.Error {
return vector.NewStringError(d.sctx, fmt.Sprintf("cannot access field '%s' of none (use ?. or ok())", d.key), vec.Len())
}
Expand Down
92 changes: 92 additions & 0 deletions runtime/ztests/expr/dot-type.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Dot navigation into type values.

spq: values <{a:int64,b:{c:string}}>.b.c, <{a:int64}>.a == <int64>

output: |
<string>
true

---

# The dot looks through named, option, fusion, and error types.
spq: |
type foo={a:int64}
values <foo>.a, <option({a:string})>.a, <fusion({a:ip})>.a, <error({a:bool})>.a

output: |
<int64>
<string>
<ip>
<bool>

---

# The dot navigates into array and set element types and map value types.
spq: values <[{a:int64}]>.a, <set[{a:string}]>.a, <map{string:{a:ip}}>.a, <{a:[{b:{c:bool}}]}>.a.b.c

output: |
<int64>
<string>
<ip>
<bool>

---

# For a union, the result is the union of the field's types in the union's
# types that have the field.
spq: |
values
<{a:int64}|{a:string}>.a,
<{a:int64}|{b:string}>.a,
<{a:int64}|{a:int64,b:string}>.a,
<{a:int64|string}|{a:bool}>.a,
<[{a:int64}]|{a:string}|int64>.a

output: |
<int64|string>
<int64>
<int64>
<int64|bool|string>
<int64|string>

---

# A type without the field is an error, which ok() turns into none.
spq: values <{a:int64}>.z, <int64|string>.a, <[int64]>.a, ok(<{a:int64}>.z)

output: |
error({message:"no such field 'z'",on:<{a:int64}>})
error({message:"no such field 'a'",on:<int64|string>})
error({message:"no such field 'a'",on:<[int64]>})
none

---

# Type values from the input, some with and some without the field.
spq: values t.a, ok(t.a) ?? <null>

input: |
{t:<{a:int64}>}
{t:<[{a:string}]>}
{t:<int64>}

output: |
<int64>
<int64>
<string>
<string>
error({message:"no such field 'a'",on:<int64>})
<null>

---

# Explore the types of the input values.
spq: values typeof(this).b.c

input: |
{a:1,b:{c:"x"}}
{a:2,b:[{c:10.0.0.1}]}

output: |
<string>
<ip>
2 changes: 2 additions & 0 deletions type.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func (k Kind) String() string {
return "enum"
case DecimalKind:
return "decimal"
case FusionKind:
return "fusion"
case OptionKind:
return "option"
case ErrorKind:
Expand Down
Loading