Added subscript support for long var interpolation

- Modified long var interpolation to support dot lookups
- Added a time:from-unix function and added time.Time as an object
This commit is contained in:
Leon Mika 2025-01-18 09:54:21 +11:00
parent d3938aec83
commit ca95ac7008
6 changed files with 239 additions and 68 deletions

View file

@ -13,13 +13,24 @@ type astStringStringSpan struct {
Chars *string `parser:"@SingleChar"`
}
type astLongIdentDotSuffix struct {
KeyName *string `parser:"@LIIdent"`
Pipeline *astPipeline `parser:"| LILp @@ RP"`
}
type astLongIdent struct {
Pos lexer.Position
VarName string `parser:"@LIIdent"`
DotSuffix []astLongIdentDotSuffix `parser:"( LIDot @@ )*"`
}
type astDoubleStringSpan struct {
Pos lexer.Position
Chars *string `parser:"@Char"`
Escaped *string `parser:"| @Escaped"`
IdentRef *string `parser:"| @IdentRef"`
LongIdentRef *string `parser:"| @LongIdentRef"`
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
Chars *string `parser:"@Char"`
Escaped *string `parser:"| @Escaped"`
IdentRef *string `parser:"| @IdentRef"`
LongIdentRef *astLongIdent `parser:"| LongIdentRef @@ LIEnd"`
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
}
type astDoubleString struct {
@ -134,10 +145,16 @@ var scanner = lexer.MustStateful(lexer.Rules{
{"Escaped", `\\.`, nil},
{"StringEnd", `"`, lexer.Pop()},
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
{"LongIdentRef", `\$[{][^}]*[}]`, nil},
{"LongIdentRef", `\$[{]`, lexer.Push("LongIdent")},
{"StartSubExpr", `\$[(]`, lexer.Push("Root")},
{"Char", `[^$"\\]+`, nil},
},
"LongIdent": {
{"LIIdent", `[-]*[a-zA-Z_][\w-]*`, nil},
{"LIDot", `[.]`, nil},
{"LILp", `\(`, lexer.Push("Root")},
{"LIEnd", `\}`, lexer.Pop()},
},
"SingleString": {
{"SingleStringEnd", `'`, lexer.Pop()},
{"SingleChar", `[^']+`, nil},

26
ucl/builtins/time.go Normal file
View file

@ -0,0 +1,26 @@
package builtins
import (
"context"
"time"
"ucl.lmika.dev/ucl"
)
func Time() ucl.Module {
return ucl.Module{
Name: "time",
Builtins: map[string]ucl.BuiltinHandler{
"from-unix": timeFromUnix,
},
}
}
func timeFromUnix(ctx context.Context, args ucl.CallArgs) (any, error) {
var ux int
if err := args.Bind(&ux); err != nil {
return nil, err
}
return time.Unix(int64(ux), 0).UTC(), nil
}

37
ucl/builtins/time_test.go Normal file
View file

@ -0,0 +1,37 @@
package builtins_test
import (
"context"
"github.com/stretchr/testify/assert"
"testing"
"time"
"ucl.lmika.dev/ucl"
"ucl.lmika.dev/ucl/builtins"
)
func TestTime_FromUnix(t *testing.T) {
tests := []struct {
desc string
eval string
want any
wantErr bool
}{
{desc: "from unix 1", eval: `time:from-unix 0`, want: time.Unix(0, 0).UTC()},
{desc: "from unix 2", eval: `time:from-unix 0 | cat`, want: "1970-01-01T00:00:00Z"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
inst := ucl.New(
ucl.WithModule(builtins.Time()),
)
res, err := inst.Eval(context.Background(), tt.eval)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
}
})
}
}

View file

@ -3,6 +3,7 @@ package ucl
import (
"context"
"errors"
"fmt"
"strings"
)
@ -299,10 +300,11 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
sb.WriteString(v.String())
}
case n.LongIdentRef != nil:
identVal := (*n.LongIdentRef)[2 : len(*n.LongIdentRef)-1]
if v, ok := ec.getVar(identVal); ok && v != nil {
sb.WriteString(v.String())
v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef)
if err != nil {
return nil, err
}
sb.WriteString(v)
case n.SubExpr != nil:
res, err := e.evalPipeline(ctx, ec, n.SubExpr)
if err != nil {
@ -316,6 +318,38 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
return StringObject(sb.String()), nil
}
func (e evaluator) interpolateLongIdent(ctx context.Context, ec *evalCtx, n *astLongIdent) (_ string, err error) {
res, ok := ec.getVar(n.VarName)
if !ok {
return "", nil
}
for _, dot := range n.DotSuffix {
if res == nil {
return "", errorWithPos{fmt.Errorf("attempt to get field from nil value '%v'", n.VarName), n.Pos}
}
var idx Object
if dot.KeyName != nil {
idx = StringObject(*dot.KeyName)
} else {
idx, err = e.evalPipeline(ctx, ec, dot.Pipeline)
if err != nil {
return "", err
}
}
res, err = indexLookup(ctx, res, idx)
if err != nil {
return "", err
}
}
if res == nil {
return "", nil
}
return res.String(), nil
}
func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
pipelineRes, err := e.evalPipeline(ctx, ec, n)
if err != nil {

View file

@ -25,11 +25,15 @@ func TestInst_Eval(t *testing.T) {
{desc: "interpolate string 1", expr: `set what "world" ; firstarg "hello $what"`, want: "hello world"},
{desc: "interpolate string 2", expr: `set what "world" ; set when "now" ; firstarg "$when, hello $what"`, want: "now, hello world"},
{desc: "interpolate string 3", expr: `set what "world" ; set when "now" ; firstarg "${when}, hello ${what}"`, want: "now, hello world"},
{desc: "interpolate string 4", expr: `set "crazy var" "unknown" ; firstarg "hello ${crazy var}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
{desc: "interpolate string 6", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
{desc: "interpolate string 7", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
{desc: "interpolate string 8", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
{desc: "interpolate string 4", expr: `set crazy [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
{desc: "interpolate string 6", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
{desc: "interpolate string 7", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 2 | sub (sub 2 1) | sub 1)}"`, want: "hello hither"},
{desc: "interpolate string 8", expr: `set words ["old": ["hither" "thither" "yonder"] "new": ["near" "far"]] ; firstarg "hello ${words.old.(2)}"`, want: "hello yonder"},
{desc: "interpolate string 9", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"},
{desc: "interpolate string 10", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"},
{desc: "interpolate string 11", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"},
{desc: "interpolate string 12", expr: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"},
// Sub-expressions
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},

View file

@ -7,34 +7,35 @@ import (
"reflect"
"strconv"
"strings"
"time"
"github.com/lmika/gopkgs/fp/slices"
)
type object interface {
type Object interface {
String() string
Truthy() bool
}
type listable interface {
type Listable interface {
Len() int
Index(i int) object
Index(i int) Object
}
type hashable interface {
Len() int
Value(k string) object
Each(func(k string, v object) error) error
Value(k string) Object
Each(func(k string, v Object) error) error
}
type listObject []object
type listObject []Object
func (lo *listObject) Append(o object) {
func (lo *listObject) Append(o Object) {
*lo = append(*lo, o)
}
func (s listObject) String() string {
return fmt.Sprintf("%v", []object(s))
return fmt.Sprintf("%v", []Object(s))
}
func (s listObject) Truthy() bool {
@ -45,11 +46,11 @@ func (s listObject) Len() int {
return len(s)
}
func (s listObject) Index(i int) object {
func (s listObject) Index(i int) Object {
return s[i]
}
type hashObject map[string]object
type hashObject map[string]Object
func (s hashObject) String() string {
if len(s) == 0 {
@ -78,11 +79,11 @@ func (s hashObject) Len() int {
return len(s)
}
func (s hashObject) Value(k string) object {
func (s hashObject) Value(k string) Object {
return s[k]
}
func (s hashObject) Each(fn func(k string, v object) error) error {
func (s hashObject) Each(fn func(k string, v Object) error) error {
for k, v := range s {
if err := fn(k, v); err != nil {
return err
@ -91,13 +92,13 @@ func (s hashObject) Each(fn func(k string, v object) error) error {
return nil
}
type strObject string
type StringObject string
func (s strObject) String() string {
func (s StringObject) String() string {
return string(s)
}
func (s strObject) Truthy() bool {
func (s StringObject) Truthy() bool {
return string(s) != ""
}
@ -124,16 +125,28 @@ func (b boolObject) Truthy() bool {
return bool(b)
}
func toGoValue(obj object) (interface{}, bool) {
type timeObject time.Time
func (t timeObject) String() string {
return time.Time(t).Format(time.RFC3339)
}
func (t timeObject) Truthy() bool {
return !time.Time(t).IsZero()
}
func toGoValue(obj Object) (interface{}, bool) {
switch v := obj.(type) {
case nil:
return nil, true
case strObject:
case StringObject:
return string(v), true
case intObject:
return int(v), true
case boolObject:
return bool(v), true
case timeObject:
return time.Time(v), true
case listObject:
xs := make([]interface{}, 0, len(v))
for _, va := range v {
@ -157,42 +170,62 @@ func toGoValue(obj object) (interface{}, bool) {
case proxyObject:
return v.p, true
case listableProxyObject:
return v.v.Interface(), true
return v.orig.Interface(), true
case structProxyObject:
return v.v.Interface(), true
return v.orig.Interface(), true
}
return nil, false
}
func fromGoValue(v any) (object, error) {
func fromGoValue(v any) (Object, error) {
switch t := v.(type) {
case Object:
return t, nil
case nil:
return nil, nil
case string:
return strObject(t), nil
return StringObject(t), nil
case int:
return intObject(t), nil
case bool:
return boolObject(t), nil
case time.Time:
return timeObject(t), nil
}
return fromGoReflectValue(reflect.ValueOf(v))
}
func fromGoReflectValue(resVal reflect.Value) (Object, error) {
if !resVal.IsValid() {
return nil, nil
}
resVal := reflect.ValueOf(v)
switch resVal.Kind() {
case reflect.Slice:
return listableProxyObject{resVal}, nil
return listableProxyObject{v: resVal, orig: resVal}, nil
case reflect.Struct:
return newStructProxyObject(resVal), nil
return newStructProxyObject(resVal, resVal), nil
case reflect.Pointer:
switch resVal.Elem().Kind() {
case reflect.Slice:
return listableProxyObject{v: resVal.Elem(), orig: resVal}, nil
case reflect.Struct:
return newStructProxyObject(resVal.Elem(), resVal), nil
}
return fromGoReflectValue(resVal.Elem())
}
return proxyObject{v}, nil
return proxyObject{resVal.Interface()}, nil
}
type macroArgs struct {
eval evaluator
ec *evalCtx
hasPipe bool
pipeArg object
pipeArg Object
ast *astCmd
argShift int
}
@ -239,7 +272,7 @@ func (ma *macroArgs) shiftIdent(ctx context.Context) (string, bool) {
return "", false
}
func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) {
func (ma macroArgs) evalArg(ctx context.Context, n int) (Object, error) {
if n >= len(ma.ast.Args[ma.argShift:]) {
return nil, errors.New("not enough arguments") // FIX
}
@ -247,7 +280,7 @@ func (ma macroArgs) evalArg(ctx context.Context, n int) (object, error) {
return ma.eval.evalDot(ctx, ma.ec, ma.ast.Args[ma.argShift+n])
}
func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushScope bool) (object, error) {
func (ma macroArgs) evalBlock(ctx context.Context, n int, args []Object, pushScope bool) (Object, error) {
obj, err := ma.evalArg(ctx, n)
if err != nil {
return nil, err
@ -266,7 +299,7 @@ func (ma macroArgs) evalBlock(ctx context.Context, n int, args []object, pushSco
}
return ma.eval.evalBlock(ctx, ec, v.block)
case strObject:
case StringObject:
iv := ma.ec.lookupInvokable(string(v))
if iv == nil {
return nil, errors.New("'" + string(v) + "' is not invokable")
@ -297,7 +330,7 @@ type invocationArgs struct {
eval evaluator
inst *Inst
ec *evalCtx
args []object
args []Object
kwargs map[string]*listObject
}
@ -340,7 +373,7 @@ func (ia invocationArgs) invokableArg(i int) (invokable, error) {
switch v := ia.args[i].(type) {
case invokable:
return v, nil
case strObject:
case StringObject:
iv := ia.ec.lookupInvokable(string(v))
if iv == nil {
return nil, errors.New("'" + string(v) + "' is not invokable")
@ -350,7 +383,7 @@ func (ia invocationArgs) invokableArg(i int) (invokable, error) {
return nil, errors.New("expected an invokable arg")
}
func (ia invocationArgs) fork(args []object) invocationArgs {
func (ia invocationArgs) fork(args []Object) invocationArgs {
return invocationArgs{
eval: ia.eval,
inst: ia.inst,
@ -373,27 +406,28 @@ func (ia invocationArgs) shift(i int) invocationArgs {
}
}
// invokable is an object that can be executed as a command
// invokable is an Object that can be executed as a command
type invokable interface {
invoke(ctx context.Context, args invocationArgs) (object, error)
invoke(ctx context.Context, args invocationArgs) (Object, error)
}
type macroable interface {
invokeMacro(ctx context.Context, args macroArgs) (object, error)
invokeMacro(ctx context.Context, args macroArgs) (Object, error)
}
type pipeInvokable interface {
invokable
}
type invokableFunc func(ctx context.Context, args invocationArgs) (object, error)
type invokableFunc func(ctx context.Context, args invocationArgs) (Object, error)
func (i invokableFunc) invoke(ctx context.Context, args invocationArgs) (object, error) {
func (i invokableFunc) invoke(ctx context.Context, args invocationArgs) (Object, error) {
return i(ctx, args)
}
type blockObject struct {
block *astBlock
block *astBlock
closedEC *evalCtx
}
func (bo blockObject) String() string {
@ -404,8 +438,8 @@ func (bo blockObject) Truthy() bool {
return len(bo.block.Statements) > 0
}
func (bo blockObject) invoke(ctx context.Context, args invocationArgs) (object, error) {
ec := args.ec.fork()
func (bo blockObject) invoke(ctx context.Context, args invocationArgs) (Object, error) {
ec := bo.closedEC.fork()
for i, n := range bo.block.Names {
if i < len(args.args) {
ec.setOrDefineVar(n, args.args[i])
@ -415,13 +449,13 @@ func (bo blockObject) invoke(ctx context.Context, args invocationArgs) (object,
return args.eval.evalBlock(ctx, ec, bo.block)
}
type macroFunc func(ctx context.Context, args macroArgs) (object, error)
type macroFunc func(ctx context.Context, args macroArgs) (Object, error)
func (i macroFunc) invokeMacro(ctx context.Context, args macroArgs) (object, error) {
func (i macroFunc) invokeMacro(ctx context.Context, args macroArgs) (Object, error) {
return i(ctx, args)
}
func isTruthy(obj object) bool {
func isTruthy(obj Object) bool {
if obj == nil {
return false
}
@ -441,7 +475,8 @@ func (p proxyObject) Truthy() bool {
}
type listableProxyObject struct {
v reflect.Value
v reflect.Value
orig reflect.Value
}
func (p listableProxyObject) String() string {
@ -456,7 +491,7 @@ func (p listableProxyObject) Len() int {
return p.v.Len()
}
func (p listableProxyObject) Index(i int) object {
func (p listableProxyObject) Index(i int) Object {
e, err := fromGoValue(p.v.Index(i).Interface())
if err != nil {
return nil
@ -465,14 +500,16 @@ func (p listableProxyObject) Index(i int) object {
}
type structProxyObject struct {
v reflect.Value
vf []reflect.StructField
v reflect.Value
orig reflect.Value
vf []reflect.StructField
}
func newStructProxyObject(v reflect.Value) structProxyObject {
func newStructProxyObject(v reflect.Value, orig reflect.Value) structProxyObject {
return structProxyObject{
v: v,
vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }),
v: v,
orig: orig,
vf: slices.Filter(reflect.VisibleFields(v.Type()), func(t reflect.StructField) bool { return t.IsExported() }),
}
}
@ -488,7 +525,7 @@ func (s structProxyObject) Len() int {
return len(s.vf)
}
func (s structProxyObject) Value(k string) object {
func (s structProxyObject) Value(k string) Object {
f := s.v.FieldByName(k)
if !f.IsValid() {
return nil
@ -508,7 +545,7 @@ func (s structProxyObject) Value(k string) object {
return e
}
func (s structProxyObject) Each(fn func(k string, v object) error) error {
func (s structProxyObject) Each(fn func(k string, v Object) error) error {
for _, f := range s.vf {
v, err := fromGoValue(s.v.FieldByName(f.Name).Interface())
if err != nil {
@ -522,9 +559,25 @@ func (s structProxyObject) Each(fn func(k string, v object) error) error {
return nil
}
type OpaqueObject struct {
v any
}
func Opaque(v any) OpaqueObject {
return OpaqueObject{v: v}
}
func (p OpaqueObject) String() string {
return fmt.Sprintf("opaque{%T}", p.v)
}
func (p OpaqueObject) Truthy() bool {
return p.v != nil
}
type errBreak struct {
isCont bool
ret object
ret Object
}
func (e errBreak) Error() string {
@ -535,7 +588,7 @@ func (e errBreak) Error() string {
}
type errReturn struct {
ret object
ret Object
}
func (e errReturn) Error() string {