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,12 +13,23 @@ type astStringStringSpan struct {
Chars *string `parser:"@SingleChar"` 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 { type astDoubleStringSpan struct {
Pos lexer.Position Pos lexer.Position
Chars *string `parser:"@Char"` Chars *string `parser:"@Char"`
Escaped *string `parser:"| @Escaped"` Escaped *string `parser:"| @Escaped"`
IdentRef *string `parser:"| @IdentRef"` IdentRef *string `parser:"| @IdentRef"`
LongIdentRef *string `parser:"| @LongIdentRef"` LongIdentRef *astLongIdent `parser:"| LongIdentRef @@ LIEnd"`
SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"` SubExpr *astPipeline `parser:"| StartSubExpr @@ RP"`
} }
@ -134,10 +145,16 @@ var scanner = lexer.MustStateful(lexer.Rules{
{"Escaped", `\\.`, nil}, {"Escaped", `\\.`, nil},
{"StringEnd", `"`, lexer.Pop()}, {"StringEnd", `"`, lexer.Pop()},
{"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil}, {"IdentRef", `\$[-]*[a-zA-Z_][\w-]*`, nil},
{"LongIdentRef", `\$[{][^}]*[}]`, nil}, {"LongIdentRef", `\$[{]`, lexer.Push("LongIdent")},
{"StartSubExpr", `\$[(]`, lexer.Push("Root")}, {"StartSubExpr", `\$[(]`, lexer.Push("Root")},
{"Char", `[^$"\\]+`, nil}, {"Char", `[^$"\\]+`, nil},
}, },
"LongIdent": {
{"LIIdent", `[-]*[a-zA-Z_][\w-]*`, nil},
{"LIDot", `[.]`, nil},
{"LILp", `\(`, lexer.Push("Root")},
{"LIEnd", `\}`, lexer.Pop()},
},
"SingleString": { "SingleString": {
{"SingleStringEnd", `'`, lexer.Pop()}, {"SingleStringEnd", `'`, lexer.Pop()},
{"SingleChar", `[^']+`, nil}, {"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 ( import (
"context" "context"
"errors" "errors"
"fmt"
"strings" "strings"
) )
@ -299,10 +300,11 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
sb.WriteString(v.String()) sb.WriteString(v.String())
} }
case n.LongIdentRef != nil: case n.LongIdentRef != nil:
identVal := (*n.LongIdentRef)[2 : len(*n.LongIdentRef)-1] v, err := e.interpolateLongIdent(ctx, ec, n.LongIdentRef)
if v, ok := ec.getVar(identVal); ok && v != nil { if err != nil {
sb.WriteString(v.String()) return nil, err
} }
sb.WriteString(v)
case n.SubExpr != nil: case n.SubExpr != nil:
res, err := e.evalPipeline(ctx, ec, n.SubExpr) res, err := e.evalPipeline(ctx, ec, n.SubExpr)
if err != nil { if err != nil {
@ -316,6 +318,38 @@ func (e evaluator) interpolateDoubleQuotedString(ctx context.Context, ec *evalCt
return StringObject(sb.String()), nil 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) { func (e evaluator) evalSub(ctx context.Context, ec *evalCtx, n *astPipeline) (Object, error) {
pipelineRes, err := e.evalPipeline(ctx, ec, n) pipelineRes, err := e.evalPipeline(ctx, ec, n)
if err != nil { 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 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 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 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 4", expr: `set crazy [far: "unknown"] ; firstarg "hello ${crazy.far}"`, want: "hello unknown"},
{desc: "interpolate string 5", expr: `set what "world" ; firstarg "hello $($what)"`, want: "hello world"}, {desc: "interpolate string 5", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(1)}"`, want: "hello thither"},
{desc: "interpolate string 6", expr: `firstarg "hello $([1 2 3] | len)"`, want: "hello 3"}, {desc: "interpolate string 6", expr: `set oldWords ["hither" "thither" "yonder"] ; firstarg "hello ${oldWords.(add 1 1)}"`, want: "hello yonder"},
{desc: "interpolate string 7", expr: `firstarg "hello $(add (add 1 2) 3)"`, want: "hello 6"}, {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: `firstarg ("$(add 2 (add 1 1)) + $([1 2 3].(1) | cat ("$("")")) = $(("$(add 2 (4))"))")`, want: "4 + 2 = 6"}, {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 // Sub-expressions
{desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"}, {desc: "sub expression 1", expr: `firstarg (sjoin "hello")`, want: "hello"},

View file

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