skylarktime: a sketch of a time package

Work in progress

Fixes #17

Change-Id: Ia4bd6b28b24168d996dc5f159a05d2b153e1fede
diff --git a/skylarktime/testdata/time.sky b/skylarktime/testdata/time.sky
new file mode 100644
index 0000000..05faf16
--- /dev/null
+++ b/skylarktime/testdata/time.sky
@@ -0,0 +1,31 @@
+
+load("assert.sky", "assert")
+load("time.sky", "time")
+
+assert.eq(type(time.now()), "time.time")
+
+year = 365.25 * 24 * time.hour
+# assert.eq((time.now() - time.zero) / year, 292.271) # years since zero, approx
+
+# The clock is set to Sat Mar  7 11:06:39 PST 2015.
+assert.eq(time.now().year, 2015)
+assert.eq(time.now().month, "March")
+assert.eq(time.now().day, 7)
+assert.eq(time.now().hour, 11)
+assert.eq(time.now(), time.time("Sat Mar  7 11:06:39 PST 2015")) # TODO: decide on formats
+
+assert.eq(str(300 * time.millisecond), "300ms")
+assert.eq(str(135 * time.second + 2 * time.minute), "4m15s")
+assert.eq(str(time.second * 135 + time.minute * 2), "4m15s")
+assert.eq(str(time.hour * 2), "2h0m0s")
+assert.eq(str(time.hour * 2.0), "2h0m0s")
+assert.eq((time.hour * 1.25 - 15 * time.minute) / time.second, 3600)
+assert.eq(str(time.hour / 2), "30m0s")
+assert.eq(str(time.hour / 2.0), "30m0s")
+assert.fails(lambda: time.hour // 2, "unknown binary op: time.duration // int")
+assert.fails(lambda: time.hour // 2.0, "unknown binary op: time.duration // float")
+
+assert.eq(time.duration("300.0ms"), 300 * time.millisecond)
+assert.eq(time.duration("300.0ms"), 300.0 * time.millisecond)
+assert.eq(type(time.duration("1s")), "time.duration")
+assert.eq(str(time.duration("1.00s")), "1s")
diff --git a/skylarktime/time.go b/skylarktime/time.go
new file mode 100644
index 0000000..abf0d1c
--- /dev/null
+++ b/skylarktime/time.go
@@ -0,0 +1,383 @@
+// Package time provides time.Time wrappers for the skylark embedded language.
+// See https://github.com/google/skylark/issues/17.
+package skylarktime
+
+import (
+	"errors"
+	"sync"
+	"time"
+
+	"github.com/google/skylark"
+	"github.com/google/skylark/skylarkstruct"
+	"github.com/google/skylark/syntax"
+)
+
+/*
+TODO: provide Skylark module documentation.
+
+module time
+
+functions
+        duration(string) duration                               # parse a duration
+        location(string) location                               # parse a location
+        time(string, format=..., location=...) time             # parse a time
+        now() time # implementations would be able to make this a constant
+        zero time # a constant
+
+type duration
+operators
+        duration - time = duration
+        duration + time = time
+        duration == duration
+        duration < duration
+fields
+        hour int
+        minute int
+        nanosecond int
+        second int
+
+type time
+operators
+        time == time
+        time < time
+        time + duration = time
+        time - duration = time
+        time - time = duration
+fields
+        year int
+        month int
+        day int
+        hour int
+        minute int
+        second int
+        nanosecond int
+
+TODO:
+- unix time_t conversions
+- timezone stuff
+- strftime formatting
+- constructor from 6 components + location
+
+*/
+
+// TODO: the Skylark module system is poor.
+
+var (
+	once       sync.Once
+	timeModule skylark.StringDict
+	timeErr    error
+)
+
+// LoadTimeModule loads the time module.
+// It is concurrency-safe and idempotent.
+func LoadTimeModule() (skylark.StringDict, error) {
+	once.Do(func() {
+		timeModule = skylark.StringDict{
+			"time": skylarkstruct.FromStringDict(
+				skylarkstruct.Default,
+				skylark.StringDict{
+					"now":         skylark.NewBuiltin("error", now),
+					"zero":        Time(time.Time{}),
+					"duration":    skylark.NewBuiltin("duration", parseDuration),
+					"location":    skylark.NewBuiltin("location", parseLocation),
+					"time":        skylark.NewBuiltin("time", parseTime),
+					"nanosecond":  Duration(time.Nanosecond),
+					"microsecond": Duration(time.Microsecond),
+					"millisecond": Duration(time.Millisecond),
+					"second":      Duration(time.Second),
+					"minute":      Duration(time.Minute),
+					"hour":        Duration(time.Hour),
+				},
+			),
+		}
+	})
+	return timeModule, nil
+}
+
+// Now is the function called by Skylark's time.now function.
+// By default it is Go's time function, but a client
+// may install an alternative function that, for example,
+// returns a constant to ensure deterministic execution.
+//
+// TODO(adonovan): make this a thread-local variable.
+var Now = time.Now
+
+// now returns the current time.
+func now(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+	if len(args) > 0 || len(kwargs) != 0 {
+		return nil, errors.New("now: unexpected arguments")
+	}
+	return Time(Now()), nil
+}
+
+// Delta returns a duration created from kwargs.  Expected "hours", "minutes",
+// "seconds", "milliseconds", or "nanoseconds", and assigned an int.
+func Delta(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+	if len(args) != 0 {
+		return nil, errors.New("too many args")
+	}
+	var d time.Duration
+	for _, t := range kwargs {
+		if len(t) != 2 {
+			panic("invalid kwarg")
+		}
+		s, ok := t[0].(skylark.String)
+		if !ok {
+			panic("invalid kwarg name")
+		}
+		i, ok := t[1].(skylark.Int)
+		if !ok {
+			return nil, errors.New("invalid value for timedelta arg, must be int")
+		}
+		v, ok := i.Int64()
+		if !ok {
+			return nil, errors.New("numeric value overflows int64")
+		}
+		switch s {
+		case "hours":
+			d += time.Hour * time.Duration(v)
+		case "minutes":
+			d += time.Minute * time.Duration(v)
+		case "seconds":
+			d += time.Second * time.Duration(v)
+		case "milliseconds":
+			d += time.Millisecond * time.Duration(v)
+		case "nanoseconds":
+			d += time.Nanosecond * time.Duration(v)
+		default:
+			return nil, errors.New("invalid duration unit: " + string(s))
+		}
+	}
+	return Duration(d), nil
+}
+
+// Time is the type of a Skylark time.Time.
+type Time time.Time
+
+var (
+	_ skylark.Value      = Time{}
+	_ skylark.Comparable = Time{}
+	_ skylark.HasAttrs   = Time{}
+	_ skylark.HasBinary  = Time{}
+)
+
+func (t Time) String() string        { return time.Time(t).String() }
+func (t Time) Type() string          { return "time.time" }
+func (t Time) Freeze()               {} // immutable
+func (t Time) Truth() skylark.Bool   { return skylark.Bool(!time.Time(t).IsZero()) }
+func (t Time) Hash() (uint32, error) { return uint32(time.Time(t).Unix()), nil } // TODO not robust
+
+func (t Time) CompareSameType(op syntax.Token, y_ skylark.Value, depth int) (bool, error) {
+	x := time.Time(t)
+	y := time.Time(y_.(Time))
+	switch op {
+	case syntax.EQL:
+		return x.Equal(y), nil
+	case syntax.NEQ:
+		return !x.Equal(y), nil
+	case syntax.LE:
+		return !y.Before(x), nil
+	case syntax.LT:
+		return x.Before(y), nil
+	case syntax.GE:
+		return !y.After(x), nil
+	case syntax.GT:
+		return x.After(y), nil
+	}
+	panic(op)
+}
+
+func (t Time) AttrNames() []string {
+	return []string{"year", "month", "day", "hour", "minute", "second", "nanosecond"}
+}
+
+func (t Time) Attr(name string) (skylark.Value, error) {
+	x := time.Time(t)
+	switch name {
+	case "year":
+		return skylark.MakeInt(x.Year()), nil
+	case "month":
+		return skylark.String(x.Month().String()), nil
+	case "day":
+		return skylark.MakeInt(x.Day()), nil
+	case "hour":
+		return skylark.MakeInt(x.Hour()), nil
+	case "minute":
+		return skylark.MakeInt(x.Minute()), nil
+	case "second":
+		return skylark.MakeInt(x.Second()), nil
+	case "nanosecond":
+		return skylark.MakeInt(x.Nanosecond()), nil
+	}
+	return nil, nil // no such attribute
+}
+
+func parseTime(_ *skylark.Thread, b *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+	var s string
+	if err := skylark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &s); err != nil {
+		return nil, err
+	}
+	t, err := time.Parse(time.UnixDate, s) // TODO
+	if err != nil {
+		return nil, err
+	}
+	return Time(t), nil
+}
+
+func (t Time) Binary(op syntax.Token, y skylark.Value, side skylark.Side) (skylark.Value, error) {
+	if side == skylark.Right {
+		return nil, nil
+	}
+	if time2, ok := y.(Time); ok {
+		return t.binaryTime(op, time2)
+	}
+	if d, ok := y.(Duration); ok {
+		return t.binaryDuration(op, d)
+	}
+
+	return nil, nil
+}
+
+func (t Time) binaryTime(op syntax.Token, y Time) (skylark.Value, error) {
+	switch op {
+	case syntax.MINUS:
+		return Duration(time.Time(t).Sub(time.Time(y))), nil
+	}
+	return nil, nil
+}
+
+func (t Time) binaryDuration(op syntax.Token, y Duration) (skylark.Value, error) {
+	switch op {
+	case syntax.MINUS:
+		return Time(time.Time(t).Add(-time.Duration(y))), nil
+	case syntax.PLUS:
+		return Time(time.Time(t).Add(time.Duration(y))), nil
+	}
+	return nil, nil
+}
+
+type Duration time.Duration
+
+var (
+	_ skylark.Value      = Duration(0)
+	_ skylark.Comparable = Duration(0)
+	_ skylark.HasBinary  = Duration(0)
+)
+
+func (d Duration) String() string        { return time.Duration(d).String() }
+func (d Duration) Type() string          { return "time.duration" }
+func (d Duration) Freeze()               {} // immutable
+func (d Duration) Truth() skylark.Bool   { return d != 0 }
+func (d Duration) Hash() (uint32, error) { return skylark.MakeInt64(int64(d)).Hash() }
+
+func (x Duration) CompareSameType(op syntax.Token, y_ skylark.Value, depth int) (bool, error) {
+	y := y_.(Duration)
+	switch op {
+	case syntax.EQL:
+		return x == y, nil
+	case syntax.NEQ:
+		return x != y, nil
+	case syntax.LE:
+		return x <= y, nil
+	case syntax.LT:
+		return x < y, nil
+	case syntax.GE:
+		return x >= y, nil
+	case syntax.GT:
+		return x > y, nil
+	}
+	panic(op)
+}
+
+//         duration + duration = duration
+//         duration - duration = duration
+//         duration / duration = float
+//         duration + time     = time
+//         duration * number   = duration
+//         number * duration   = duration
+//         duration / number   = duration
+//
+//         time - time = duration
+//         time + duration = time
+//         time - duration = time
+func (x Duration) Binary(op syntax.Token, y_ skylark.Value, side skylark.Side) (skylark.Value, error) {
+	if side == skylark.Left {
+		// duration op y
+		switch y := y_.(type) {
+		case Duration:
+			// duration + duration
+			// duration - duration
+			// duration / duration
+			switch op {
+			case syntax.PLUS:
+				return Duration(x + y), nil
+			case syntax.MINUS:
+				return Duration(x - y), nil
+			case syntax.SLASH:
+				return skylark.Float(x) / skylark.Float(y), nil
+			}
+		case Time:
+			// duration + time = time
+			if op == syntax.PLUS {
+				return Time(time.Time(y).Add(time.Duration(x))), nil
+			}
+		case skylark.Int, skylark.Float:
+			// (double x//y not supported)
+			if op == syntax.STAR || op == syntax.SLASH {
+				return scaleDuration(x, y, op == syntax.SLASH)
+			}
+		}
+	} else {
+		// y op duration
+		// We need handle only cases not covered by side==Left.
+		switch y := y_.(type) {
+		case skylark.Int, skylark.Float:
+			if op == syntax.STAR {
+				// duration * number = duration
+				return scaleDuration(x, y, false)
+			}
+		}
+	}
+
+	return nil, nil
+}
+
+// duration * k = duration
+// k * duration = duration
+func scaleDuration(x Duration, y skylark.Value, divide bool) (skylark.Value, error) {
+	switch y := y.(type) {
+	case skylark.Int:
+		// TODO: check for overflow
+		// TODO: handle Uint64, bigint.
+		if y, ok := y.Int64(); ok {
+			if divide {
+				return x / Duration(y), nil
+			} else {
+				return x * Duration(y), nil
+			}
+		}
+	case skylark.Float:
+		if divide {
+			return Duration(float64(x) / float64(y)), nil
+		} else {
+			return Duration(float64(x) * float64(y)), nil
+		}
+	}
+	return nil, nil
+}
+
+func parseDuration(_ *skylark.Thread, b *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+	var s string
+	if err := skylark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &s); err != nil {
+		return nil, err
+	}
+	d, err := time.ParseDuration(s)
+	if err != nil {
+		return nil, err
+	}
+	return Duration(d), nil
+}
+
+func parseLocation(_ *skylark.Thread, _ *skylark.Builtin, args skylark.Tuple, kwargs []skylark.Tuple) (skylark.Value, error) {
+	return nil, nil
+}
diff --git a/skylarktime/time.sky b/skylarktime/time.sky
new file mode 100644
index 0000000..6c10c83
--- /dev/null
+++ b/skylarktime/time.sky
@@ -0,0 +1,5 @@
+#  the time module
+
+time = struct(
+    now = now,
+)
diff --git a/skylarktime/time_test.go b/skylarktime/time_test.go
new file mode 100644
index 0000000..6ca382c
--- /dev/null
+++ b/skylarktime/time_test.go
@@ -0,0 +1,81 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package skylarktime_test
+
+import (
+	"log"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"github.com/google/skylark"
+	"github.com/google/skylark/internal/chunkedfile"
+	"github.com/google/skylark/resolve"
+	"github.com/google/skylark/skylarktest"
+	"github.com/google/skylark/skylarktime"
+)
+
+func init() {
+	// The tests make extensive use of these not-yet-standard features.
+	resolve.AllowLambda = true
+	resolve.AllowNestedDef = true
+	resolve.AllowFloat = true
+	resolve.AllowSet = true
+
+	// Fake the clock for test determinism.
+	now, err := time.Parse(time.UnixDate, "Sat Mar  7 11:06:39 PST 2015")
+	if err != nil {
+		log.Fatal(err)
+	}
+	skylarktime.Now = func() time.Time { return now }
+}
+
+func TestExecFile(t *testing.T) {
+	testdata := skylarktest.DataFile("skylark/skylarktime", ".")
+	thread := &skylark.Thread{Load: load}
+	skylarktest.SetReporter(thread, t)
+	for _, file := range []string{
+		"testdata/time.sky",
+	} {
+		filename := filepath.Join(testdata, file)
+		for _, chunk := range chunkedfile.Read(filename, t) {
+			_, err := skylark.ExecFile(thread, filename, chunk.Source, nil)
+			switch err := err.(type) {
+			case *skylark.EvalError:
+				found := false
+				for _, fr := range err.Stack() {
+					posn := fr.Position()
+					if posn.Filename() == filename {
+						chunk.GotError(int(posn.Line), err.Error())
+						found = true
+						break
+					}
+				}
+				if !found {
+					t.Error(err.Backtrace())
+				}
+			case nil:
+				// success
+			default:
+				t.Error(err)
+			}
+			chunk.Done()
+		}
+	}
+}
+
+// load implements the 'load' operation as used in the evaluator tests.
+func load(thread *skylark.Thread, module string) (skylark.StringDict, error) {
+	if module == "assert.sky" {
+		return skylarktest.LoadAssertModule()
+	}
+	if module == "time.sky" {
+		return skylarktime.LoadTimeModule()
+	}
+
+	// TODO(adonovan): test load() using this execution path.
+	filename := filepath.Join(filepath.Dir(thread.Caller().Position().Filename()), module)
+	return skylark.ExecFile(thread, filename, nil, nil)
+}