blob: 725031abe5a3b7ccb74aca0d5e7acf504841c583 [file] [log] [blame]
// Copyright 2019 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package typed
import (
"fmt"
"go.starlark.net/starlark"
"go.starlark.net/syntax"
)
// List is a Starlark value that looks like a regular list, but implements type
// checks and implicit conversions when assigning elements.
//
// Differences from regular lists:
// * Not comparable by value to regular lists, only to other typed lists.
// go.starlark.net has no way to express that typed lists should be
// comparable to regular lists.
// * Only == and != comparison operators are supported.
// * `l += ...` is same as `l = l + ...`, not `l.extend(...)`
type List struct {
itemT Converter // defines the type of elements
list *starlark.List // the underlying untyped list
}
// Note that these interfaces intersect, so we can't mush them into a single
// interface to check *List conforms it.
var (
_ starlark.Value = (*List)(nil)
_ starlark.Sequence = (*List)(nil)
_ starlark.Indexable = (*List)(nil)
_ starlark.HasBinary = (*List)(nil)
_ starlark.HasAttrs = (*List)(nil)
_ starlark.HasSetIndex = (*List)(nil)
_ starlark.Comparable = (*List)(nil)
)
// NewList returns a list with given elements, converted to necessary type
// through 'item' converter.
//
// All subsequently added elements will be converter through this converter too.
func NewList(item Converter, elems []starlark.Value) (*List, error) {
l := &List{itemT: item}
vals := make([]starlark.Value, len(elems))
for i, e := range elems {
var err error
if vals[i], err = l.conv(e, i); err != nil {
return nil, err
}
}
l.list = starlark.NewList(vals)
return l, nil
}
// AsTypedList allocates a new list<t>, and copies it from 'x'.
//
// Returns an error if 'x' is not an iterable or some of its elements can't be
// converted to 't'.
func AsTypedList(t Converter, x starlark.Value) (*List, error) {
it := starlark.Iterate(x)
if it == nil {
return nil, fmt.Errorf("got %s, want an iterable", x.Type())
}
defer it.Done()
var vals []starlark.Value
if l := starlark.Len(x); l > 0 {
vals = make([]starlark.Value, 0, l)
}
var itm starlark.Value
for it.Next(&itm) {
vals = append(vals, itm)
}
return NewList(t, vals)
}
// conv calls Convert, annotating the error with item's index if idx >= 0.
func (l *List) conv(v starlark.Value, idx int) (starlark.Value, error) {
switch v, err := l.itemT.Convert(v); {
case err == nil:
return v, nil
case idx >= 0:
return nil, fmt.Errorf("item #%d: %s", idx, err)
default:
return nil, err
}
}
// Converter returns the type converter used by this list.
func (l *List) Converter() Converter { return l.itemT }
// Binary implements 'l <op> y' (or 'y <op> l', depending on 'side').
//
// Note that *starlark.List has its binary operators implement natively by the
// Starlark evaluator, not via HasBinary interface.
func (l *List) Binary(op syntax.Token, y starlark.Value, side starlark.Side) (starlark.Value, error) {
switch op {
case syntax.IN, syntax.NOT_IN:
return starlark.Binary(op, y, l.list)
case syntax.STAR: // 2*[1,2] => [1,2,1,2], also typed
if _, ok := y.(starlark.Int); !ok {
goto unsupported
}
return l.asTypedList(starlark.Binary(op, l.list, y))
case syntax.PLUS:
// Convert regular lists to typed lists first.
if yl, ok := y.(*starlark.List); ok {
vals := make([]starlark.Value, yl.Len())
for i := 0; i < yl.Len(); i++ {
vals[i] = yl.Index(i)
}
asTyped, err := NewList(l.itemT, vals)
if err != nil {
return nil, err
}
return l.Binary(op, asTyped, side)
}
// When adding typed lists their types must match.
if yl, ok := y.(*List); ok {
if yl.itemT != l.itemT {
goto unsupported
}
lhs, rhs := maybeSwap(l.list, yl.list, side)
return l.asTypedList(starlark.Binary(op, lhs, rhs))
}
}
unsupported:
return nil, fmt.Errorf("unknown binary op: %s %s %s", l.Type(), op, y.Type())
}
func (l *List) asTypedList(v starlark.Value, err error) (starlark.Value, error) {
if err != nil {
return nil, err
}
if asL, ok := v.(*starlark.List); ok {
return &List{itemT: l.itemT, list: asL}, nil
}
return nil, fmt.Errorf("expected a list, but got %q", v.Type())
}
func maybeSwap(l, r starlark.Value, side starlark.Side) (starlark.Value, starlark.Value) {
if side == starlark.Left {
return l, r
}
return r, l
}
// starlark.List methods we reimplement or proxy.
func (l *List) Append(v starlark.Value) error {
v, err := l.conv(v, -1)
if err != nil {
return err
}
return l.list.Append(v)
}
func (l *List) AttrNames() []string {
// Note: we hardcode the list of methods (rather than call l.list.AttrNames)
// to be able to notice (via the failing test) when go.starlark.net adds some
// new methods that we might need to "wrap" in Attr(...).
return []string{
"append",
"clear",
"extend",
"index",
"insert",
"pop",
"remove",
}
}
func (l *List) Attr(name string) (starlark.Value, error) {
if name == "append" {
return l.implAppend(), nil // fully reimplemented, no need for orig
}
orig, err := l.list.Attr(name)
if err != nil {
return nil, err
}
switch name {
case "extend":
return l.implExtend(orig.(*starlark.Builtin)), nil
case "insert":
return l.implInsert(orig.(*starlark.Builtin)), nil
default:
return orig, nil // no need to wrap
}
}
func (l *List) SetIndex(i int, v starlark.Value) error {
// Note: 'i' is never negative here, the Starlark evaluator deals with
// negative indexes before calling SetIndex.
v, err := l.conv(v, i)
if err != nil {
return err
}
return l.list.SetIndex(i, v)
}
func (l *List) Slice(start, end, step int) starlark.Value {
return &List{
itemT: l.itemT,
list: l.list.Slice(start, end, step).(*starlark.List),
}
}
func (l *List) Type() string {
return fmt.Sprintf("list<%s>", l.itemT.Type())
}
func (l *List) String() string {
return fmt.Sprintf("list<%s>(%s)", l.itemT.Type(), l.list.String())
}
func (l *List) CompareSameType(op syntax.Token, y starlark.Value, depth int) (bool, error) {
switch op {
case syntax.EQL:
return listsEqual(l, y.(*List), depth)
case syntax.NEQ:
eq, err := listsEqual(l, y.(*List), depth)
return !eq, err
default:
return false, fmt.Errorf("%q is not implemented for %s", op, l.Type())
}
}
func listsEqual(l, r *List, depth int) (bool, error) {
if l.itemT != r.itemT {
return false, nil
}
return l.list.CompareSameType(syntax.EQL, r.list, depth)
}
// starlark.List methods delegated without any modifications.
func (l *List) Clear() error { return l.list.Clear() }
func (l *List) Freeze() { l.list.Freeze() }
func (l *List) Hash() (uint32, error) { return l.list.Hash() }
func (l *List) Index(i int) starlark.Value { return l.list.Index(i) }
func (l *List) Iterate() starlark.Iterator { return l.list.Iterate() }
func (l *List) Len() int { return l.list.Len() }
func (l *List) Truth() starlark.Bool { return l.list.Truth() }
// Reimplemented builtins.
// implAppend reimplements 'append' via Append(...).
func (l *List) implAppend() *starlark.Builtin {
return starlark.NewBuiltin("append", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var object starlark.Value
if err := starlark.UnpackPositionalArgs("append", args, kwargs, 1, &object); err != nil {
return nil, err
}
if err := l.Append(object); err != nil {
return nil, fmt.Errorf("append: %s", err)
}
return starlark.None, nil
})
}
// implExtend reimplements 'extend' via Append(...).
//
// Uses original extend() for the fast path to optimize memory allocations.
func (l *List) implExtend(orig *starlark.Builtin) *starlark.Builtin {
return starlark.NewBuiltin("extend", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var iterable starlark.Iterable
if err := starlark.UnpackPositionalArgs("extend", args, kwargs, 1, &iterable); err != nil {
return nil, err
}
// Fast path: the iterable has the exact same type as us, we can skip the
// conversion completely, since per Convert idempotency property this will
// be a noop anyway. By calling original 'extend' implementation we are
// basically doing one big memcpy(...) instead of adding items one by one.
if it, ok := iterable.(*List); ok && it.itemT == l.itemT {
return orig.CallInternal(th, starlark.Tuple{it.list}, nil)
}
// Slow path: append items one by one.
iter := iterable.Iterate()
defer iter.Done()
var x starlark.Value
for i := 0; iter.Next(&x); i++ {
if err := l.Append(x); err != nil {
return nil, fmt.Errorf("extend: item #%d: %s", i, err)
}
}
return starlark.None, nil
})
}
// implInsert implements 'insert' by wrapping the original 'insert'.
//
// It's not easily reimplementable, since starlark.List doesn't expose its
// internal list in a simple way.
func (l *List) implInsert(orig *starlark.Builtin) *starlark.Builtin {
return starlark.NewBuiltin("insert", func(th *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var index starlark.Value
var object starlark.Value
if err := starlark.UnpackPositionalArgs("insert", args, kwargs, 2, &index, &object); err != nil {
return nil, err
}
x, err := l.conv(object, -1)
if err != nil {
return nil, fmt.Errorf("insert: %s", err)
}
return orig.CallInternal(th, starlark.Tuple{index, x}, nil)
})
}