ucl/ucl/builtins.go
Leon Mika 934252e1bb Added strs:split and fixed closures
Closures now properly close over the context in which it was created
2025-01-13 21:37:54 +11:00

986 lines
20 KiB
Go

package ucl
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
)
func echoBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
echoPrinter := args.inst.echoPrinter
if echoPrinter != nil {
convertedArgs := make([]interface{}, len(args.args))
for i, arg := range args.args {
if convArg, ok := toGoValue(arg); ok {
convertedArgs[i] = convArg
} else {
convertedArgs[i] = arg
}
}
return nil, echoPrinter(ctx, args.inst.out, convertedArgs)
}
if len(args.args) == 0 {
if _, err := fmt.Fprintln(args.inst.Out()); err != nil {
return nil, err
}
return nil, nil
}
var line strings.Builder
for _, arg := range args.args {
if s, ok := arg.(fmt.Stringer); ok {
line.WriteString(s.String())
}
}
if _, err := fmt.Fprintln(args.inst.Out(), line.String()); err != nil {
return nil, err
}
return nil, nil
}
func addBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return intObject(0), nil
}
n := 0
for i, a := range args.args {
switch t := a.(type) {
case intObject:
n += int(t)
case StringObject:
v, err := strconv.Atoi(string(t))
if err != nil {
return nil, fmt.Errorf("arg %v of 'add' not convertable to an int", i)
}
n += v
default:
return nil, fmt.Errorf("arg %v of 'add' not convertable to an int", i)
}
}
return intObject(n), nil
}
func subBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return intObject(0), nil
}
n := 0
for i, a := range args.args {
var p int
switch t := a.(type) {
case intObject:
p = int(t)
case StringObject:
v, err := strconv.Atoi(string(t))
if err != nil {
return nil, fmt.Errorf("arg %v of 'sub' not convertable to an int", i)
}
p = v
default:
return nil, fmt.Errorf("arg %v of 'sub' not convertable to an int", i)
}
if i == 0 {
n = p
} else {
n -= p
}
}
return intObject(n), nil
}
func mupBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return intObject(1), nil
}
n := 1
for i, a := range args.args {
switch t := a.(type) {
case intObject:
n *= int(t)
case StringObject:
v, err := strconv.Atoi(string(t))
if err != nil {
return nil, fmt.Errorf("arg %v of 'mup' not convertable to an int", i)
}
n *= v
default:
return nil, fmt.Errorf("arg %v of 'mup' not convertable to an int", i)
}
}
return intObject(n), nil
}
func divBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return intObject(1), nil
}
n := 1
for i, a := range args.args {
var p int
switch t := a.(type) {
case intObject:
p = int(t)
case StringObject:
v, err := strconv.Atoi(string(t))
if err != nil {
return nil, fmt.Errorf("arg %v of 'div' not convertable to an int", i)
}
p = v
default:
return nil, fmt.Errorf("arg %v of 'div' not convertable to an int", i)
}
if i == 0 {
n = p
} else {
n /= p
}
}
return intObject(n), nil
}
func modBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) == 0 {
return intObject(0), nil
}
n := 0
for i, a := range args.args {
var p int
switch t := a.(type) {
case intObject:
p = int(t)
case StringObject:
v, err := strconv.Atoi(string(t))
if err != nil {
return nil, fmt.Errorf("arg %v of 'mod' not convertable to an int", i)
}
p = v
default:
return nil, fmt.Errorf("arg %v of 'mod' not convertable to an int", i)
}
if i == 0 {
n = p
} else {
n %= p
}
}
return intObject(n), nil
}
func setBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
name, err := args.stringArg(0)
if err != nil {
return nil, err
}
newVal := args.args[1]
args.ec.setOrDefineVar(name, newVal)
return newVal, nil
}
func toUpperBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
sarg, err := args.stringArg(0)
if err != nil {
return nil, err
}
return StringObject(strings.ToUpper(sarg)), nil
}
func eqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
l := args.args[0]
r := args.args[1]
return boolObject(objectsEqual(l, r)), nil
}
func neBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
l := args.args[0]
r := args.args[1]
return boolObject(!objectsEqual(l, r)), nil
}
func ltBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
isLess, err := objectsLessThan(args.args[0], args.args[1])
if err != nil {
return nil, err
}
return boolObject(isLess), nil
}
func leBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
isLess, err := objectsLessThan(args.args[0], args.args[1])
if err != nil {
return nil, err
}
return boolObject(isLess || objectsEqual(args.args[0], args.args[1])), nil
}
func gtBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
isGreater, err := objectsLessThan(args.args[1], args.args[0])
if err != nil {
return nil, err
}
return boolObject(isGreater), nil
}
func geBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
isGreater, err := objectsLessThan(args.args[1], args.args[0])
if err != nil {
return nil, err
}
return boolObject(isGreater || objectsEqual(args.args[0], args.args[1])), nil
}
func andBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
for _, a := range args.args {
if a == nil || !a.Truthy() {
return boolObject(false), nil
}
}
return args.args[len(args.args)-1], nil
}
func orBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
for _, a := range args.args {
if a != nil && a.Truthy() {
return a, nil
}
}
return boolObject(false), nil
}
func notBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
return boolObject(!args.args[0].Truthy()), nil
}
var errObjectsNotEqual = errors.New("objects not equal")
func objectsEqual(l, r Object) bool {
if l == nil || r == nil {
return l == nil && r == nil
}
switch lv := l.(type) {
case StringObject:
if rv, ok := r.(StringObject); ok {
return lv == rv
}
case intObject:
if rv, ok := r.(intObject); ok {
return lv == rv
}
case boolObject:
if rv, ok := r.(boolObject); ok {
return lv == rv
}
case Listable:
rv, ok := r.(Listable)
if !ok {
return false
}
if lv.Len() != rv.Len() {
return false
}
for i := 0; i < lv.Len(); i++ {
if !objectsEqual(lv.Index(i), rv.Index(i)) {
return false
}
}
return true
case hashable:
rv, ok := r.(hashable)
if !ok {
return false
}
if lv.Len() != rv.Len() {
return false
}
if err := lv.Each(func(k string, lkv Object) error {
rkv := rv.Value(k)
if rkv == nil {
return errObjectsNotEqual
} else if !objectsEqual(lkv, rkv) {
return errObjectsNotEqual
}
return nil
}); err != nil {
return false
}
return true
case OpaqueObject:
rv, ok := r.(OpaqueObject)
if !ok {
return false
}
return lv.v == rv.v
}
return false
}
func objectsLessThan(l, r Object) (bool, error) {
switch lv := l.(type) {
case StringObject:
if rv, ok := r.(StringObject); ok {
return lv < rv, nil
}
case intObject:
if rv, ok := r.(intObject); ok {
return lv < rv, nil
}
}
return false, errors.New("objects are not comparable")
}
func strBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
if args.args[0] == nil {
return StringObject(""), nil
}
return StringObject(args.args[0].String()), nil
}
func intBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
if args.args[0] == nil {
return intObject(0), nil
}
switch v := args.args[0].(type) {
case intObject:
return v, nil
case StringObject:
i, err := strconv.Atoi(string(v))
if err != nil {
return nil, errors.New("cannot convert to int")
}
return intObject(i), nil
case boolObject:
if v {
return intObject(1), nil
}
return intObject(0), nil
}
return nil, errors.New("cannot convert to int")
}
func concatBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
var sb strings.Builder
for _, a := range args.args {
if a == nil {
continue
}
sb.WriteString(a.String())
}
return StringObject(sb.String()), nil
}
func callBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
inv, ok := args.args[0].(invokable)
if !ok {
return nil, errors.New("expected invokable")
}
return inv.invoke(ctx, args.shift(1))
}
func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
switch v := args.args[0].(type) {
case StringObject:
return intObject(len(string(v))), nil
case Listable:
return intObject(v.Len()), nil
case hashable:
return intObject(v.Len()), nil
}
return intObject(0), nil
}
func indexLookup(ctx context.Context, obj, elem Object) (Object, error) {
switch v := obj.(type) {
case Listable:
intIdx, ok := elem.(intObject)
if !ok {
return nil, nil
}
if int(intIdx) >= 0 && int(intIdx) < v.Len() {
return v.Index(int(intIdx)), nil
}
return nil, nil
case hashable:
strIdx, ok := elem.(StringObject)
if !ok {
return nil, errors.New("expected string for hashable")
}
return v.Value(string(strIdx)), nil
}
return nil, nil
}
func indexBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
val := args.args[0]
for _, idx := range args.args[1:] {
newVal, err := indexLookup(ctx, val, idx)
if err != nil {
return nil, err
}
val = newVal
}
return val, nil
}
func keysBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
val := args.args[0]
switch v := val.(type) {
case hashable:
keys := make(listObject, 0, v.Len())
if err := v.Each(func(k string, _ Object) error {
keys = append(keys, StringObject(k))
return nil
}); err != nil {
return nil, err
}
return keys, nil
}
return nil, nil
}
func mapBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
inv, err := args.invokableArg(1)
if err != nil {
return nil, err
}
switch t := args.args[0].(type) {
case Listable:
l := t.Len()
newList := listObject{}
for i := 0; i < l; i++ {
v := t.Index(i)
m, err := inv.invoke(ctx, args.fork([]Object{v}))
if err != nil {
return nil, err
}
newList = append(newList, m)
}
return newList, nil
}
return nil, errors.New("expected listable")
}
func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(2); err != nil {
return nil, err
}
inv, err := args.invokableArg(1)
if err != nil {
return nil, err
}
switch t := args.args[0].(type) {
case Listable:
l := t.Len()
newList := listObject{}
for i := 0; i < l; i++ {
v := t.Index(i)
m, err := inv.invoke(ctx, args.fork([]Object{v}))
if err != nil {
return nil, err
} else if m.Truthy() {
newList = append(newList, v)
}
}
return newList, nil
case hashable:
newHash := hashObject{}
if err := t.Each(func(k string, v Object) error {
if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil {
return err
} else if m.Truthy() {
newHash[k] = v
}
return nil
}); err != nil {
return nil, err
}
return newHash, nil
}
return nil, errors.New("expected listable")
}
func reduceBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
var err error
if err = args.expectArgn(2); err != nil {
return nil, err
}
var (
accum Object
setFirst bool
block invokable
)
if len(args.args) == 3 {
accum = args.args[1]
block, err = args.invokableArg(2)
if err != nil {
return nil, err
}
} else {
setFirst = true
block, err = args.invokableArg(1)
if err != nil {
return nil, err
}
}
switch t := args.args[0].(type) {
case Listable:
l := t.Len()
for i := 0; i < l; i++ {
v := t.Index(i)
if setFirst {
accum = v
setFirst = false
continue
}
newAccum, err := block.invoke(ctx, args.fork([]Object{v, accum}))
if err != nil {
return nil, err
}
accum = newAccum
}
return accum, nil
case hashable:
// TODO: should raise error?
if err := t.Each(func(k string, v Object) error {
newAccum, err := block.invoke(ctx, args.fork([]Object{StringObject(k), v, accum}))
if err != nil {
return err
}
accum = newAccum
return nil
}); err != nil {
return nil, err
}
return accum, nil
}
return nil, errors.New("expected listable")
}
func firstBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if err := args.expectArgn(1); err != nil {
return nil, err
}
switch t := args.args[0].(type) {
case Listable:
if t.Len() == 0 {
return nil, nil
}
return t.Index(0), nil
}
return nil, errors.New("expected listable")
}
type seqObject struct {
from int
to int
inclusive bool
}
func (s seqObject) String() string {
return fmt.Sprintf("%d:%d", s.from, s.to)
}
func (s seqObject) Truthy() bool {
return s.from != s.to || s.inclusive
}
func (s seqObject) Len() int {
var l int
if s.from > s.to {
l = s.from - s.to
} else {
l = s.to - s.from
}
if s.inclusive {
l += 1
}
return l
}
func (s seqObject) Index(i int) Object {
l := s.Len()
if i < 0 || i > l {
return nil
}
if s.from > s.to {
return intObject(s.from - i)
}
return intObject(s.from + i)
}
func seqBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
inclusive := false
if inc, ok := args.kwargs["inc"]; ok {
inclusive = (inc.Len() == 0) || inc.Truthy()
}
switch len(args.args) {
case 1:
n, err := args.intArg(0)
if err != nil {
return nil, err
}
return seqObject{from: 0, to: n, inclusive: inclusive}, nil
case 2:
f, err := args.intArg(0)
if err != nil {
return nil, err
}
t, err := args.intArg(1)
if err != nil {
return nil, err
}
return seqObject{from: f, to: t, inclusive: inclusive}, nil
default:
return nil, errors.New("expected either 1 or 2 arguments")
}
}
func ifBuiltin(ctx context.Context, args macroArgs) (Object, error) {
if args.nargs() < 2 {
return nil, errors.New("need at least 2 arguments")
}
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
return args.evalBlock(ctx, 1, nil, false)
} else if err != nil {
return nil, err
}
args.shift(2)
for args.identIs(ctx, 0, "elif") {
args.shift(1)
if args.nargs() < 2 {
return nil, errors.New("need at least 2 arguments")
}
if guard, err := args.evalArg(ctx, 0); err == nil && isTruthy(guard) {
return args.evalBlock(ctx, 1, nil, false)
} else if err != nil {
return nil, err
}
args.shift(2)
}
if args.identIs(ctx, 0, "else") && args.nargs() > 1 {
return args.evalBlock(ctx, 1, nil, false)
} else if args.nargs() == 0 {
// no elif or else
return nil, nil
}
return nil, errors.New("malformed if-elif-else")
}
func tryBuiltin(ctx context.Context, args macroArgs) (_ Object, fnErr error) {
if args.nargs() < 1 {
return nil, errors.New("need at least 1 arguments")
}
var currError errObject
defer func() {
if errors.As(fnErr, &errBadUsage{}) {
return
}
if args.nargs() >= 2 && args.identIs(ctx, args.nargs()-2, "finally") {
var blockArgs []Object = nil
if fnErr != nil {
blockArgs = []Object{errObject{err: fnErr}}
}
_, err := args.evalBlock(ctx, args.nargs()-1, blockArgs, false)
if err != nil && fnErr == nil {
fnErr = err
}
}
}()
res, err := args.evalBlock(ctx, 0, nil, false)
args.shift(1)
if err == nil {
return res, nil
}
currError = errObject{err: err}
for args.identIs(ctx, 0, "catch") {
args.shift(1)
if args.nargs() < 1 {
return nil, errors.New("need at least 1 arguments")
}
res, err := args.evalBlock(ctx, 0, []Object{currError}, false)
if err == nil {
return res, nil
}
currError = errObject{err: err}
args.shift(1)
}
if args.identIs(ctx, 0, "finally") && args.nargs() > 2 {
return nil, tooManyFinallyBlocksError(args.ast.Pos)
}
return nil, currError.err
}
func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) {
var (
items Object
blockIdx int
err error
)
if !args.hasPipe {
if args.nargs() < 2 {
return nil, errors.New("need at least 2 arguments")
}
items, err = args.evalArg(ctx, 0)
if err != nil {
return nil, err
}
blockIdx = 1
} else {
if args.nargs() < 1 {
return nil, errors.New("need at least 1 argument")
}
items = args.pipeArg
blockIdx = 0
}
var (
last Object
breakErr errBreak
)
switch t := items.(type) {
case Listable:
l := t.Len()
for i := 0; i < l; i++ {
v := t.Index(i)
last, err = args.evalBlock(ctx, blockIdx, []Object{v}, true) // TO INCLUDE: the index
if err != nil {
if errors.As(err, &breakErr) {
if !breakErr.isCont {
return breakErr.ret, nil
}
} else {
return nil, err
}
}
}
case hashable:
err := t.Each(func(k string, v Object) error {
last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, true)
return err
})
if errors.As(err, &breakErr) {
if !breakErr.isCont {
return breakErr.ret, nil
}
} else {
return nil, err
}
}
return last, nil
}
func breakBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) < 1 {
return nil, errBreak{}
}
return nil, errBreak{ret: args.args[0]}
}
func continueBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return nil, errBreak{isCont: true}
}
func returnBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
if len(args.args) < 1 {
return nil, errReturn{}
}
return nil, errReturn{ret: args.args[0]}
}
func procBuiltin(ctx context.Context, args macroArgs) (Object, error) {
if args.nargs() < 1 {
return nil, errors.New("need at least one arguments")
}
var procName string
if args.nargs() == 2 {
name, ok := args.shiftIdent(ctx)
if !ok {
return nil, errors.New("malformed procedure: expected identifier as first argument")
}
procName = name
}
block, err := args.evalArg(ctx, 0)
if err != nil {
return nil, err
}
blockObj, ok := block.(blockObject)
if !ok {
return nil, fmt.Errorf("malformed procedure: expected block object, was %v", block.String())
}
obj := procObject{args.eval, args.ec, blockObj.block}
if procName != "" {
args.ec.addCmd(procName, obj)
}
return obj, nil
}
type procObject struct {
eval evaluator
ec *evalCtx
block *astBlock
}
func (b procObject) String() string {
return "(proc)"
}
func (b procObject) Truthy() bool {
return true
}
func (b procObject) invoke(ctx context.Context, args invocationArgs) (Object, error) {
newEc := b.ec.fork()
for i, name := range b.block.Names {
if i < len(args.args) {
newEc.setOrDefineVar(name, args.args[i])
} else {
newEc.setOrDefineVar(name, nil)
}
}
res, err := b.eval.evalBlock(ctx, newEc, b.block)
if err != nil {
var er errReturn
if errors.As(err, &er) {
return er.ret, nil
}
return nil, err
}
return res, nil
}