Skip to content
This repository was archived by the owner on Mar 16, 2025. It is now read-only.

Commit 8e932ed

Browse files
committed
make variadics require at least 1 item, as that's most often what we need
1 parent 063a5f5 commit 8e932ed

9 files changed

Lines changed: 101 additions & 108 deletions

File tree

pkg/builtin/core.go

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@ func ifElseFunction(ctx types.Context, test bool, yes, no ast.Expression) (any,
3434
return result, err
3535
}
3636

37-
// (This signature ensures do is always called with 1 expression at least.)
38-
39-
func doFunction(ctx types.Context, arg ast.Expression, args ...ast.Expression) (any, error) {
40-
tupleCtx, result, err := eval.EvalExpression(ctx, arg)
41-
if err != nil {
42-
return nil, fmt.Errorf("argument #0: %w", err)
43-
}
37+
// NB: Variadic functions always require at least 1 argument in Rudi to match.
38+
func doFunction(ctx types.Context, args ...ast.Expression) (any, error) {
39+
var (
40+
tupleCtx = ctx
41+
result any
42+
err error
43+
)
4444

4545
// do not use evalArgs(), as we want to inherit the context between expressions
46-
for i, arg := range args {
46+
for _, arg := range args {
4747
tupleCtx, result, err = eval.EvalExpression(tupleCtx, arg)
4848
if err != nil {
49-
return nil, fmt.Errorf("argument #%d: %w", i+1, err)
49+
return nil, err
5050
}
5151
}
5252

@@ -121,28 +121,23 @@ func hasFunction(ctx types.Context, arg ast.Expression) (any, error) {
121121
}
122122

123123
// (default TEST:Expression FALLBACK:any)
124-
func defaultFunction(ctx types.Context, test ast.Expression, fallback ast.Expression) (any, error) {
125-
_, result, err := eval.EvalExpression(ctx, test)
126-
if err != nil {
127-
return nil, fmt.Errorf("argument #0: %w", err)
128-
}
129-
130-
// this function purposefully always uses humane coalescing for this check
131-
boolified, err := coalescing.NewHumane().ToBool(result)
124+
func defaultFunction(ctx types.Context, value any, fallback ast.Expression) (any, error) {
125+
// this function purposefully always uses humane coalescing, but only for this check
126+
boolified, err := coalescing.NewHumane().ToBool(value)
132127
if err != nil {
133128
return nil, fmt.Errorf("argument #0: %w", err)
134129
}
135130

136131
if boolified {
137-
return result, nil
132+
return value, nil
138133
}
139134

140-
_, result, err = eval.EvalExpression(ctx, fallback)
135+
_, value, err = eval.EvalExpression(ctx, fallback)
141136
if err != nil {
142137
return nil, fmt.Errorf("argument #1: %w", err)
143138
}
144139

145-
return result, nil
140+
return value, nil
146141
}
147142

148143
func tryFunction(ctx types.Context, test ast.Expression) (any, error) {
@@ -283,6 +278,10 @@ func isEmptyFunction(val bool) (any, error) {
283278
return !val, nil
284279
}
285280

286-
func errorFunction(ctx types.Context, format string, args ...any) (any, error) {
281+
func errorFunction(message string) (any, error) {
282+
return nil, errors.New(message)
283+
}
284+
285+
func fmtErrorFunction(format string, args ...any) (any, error) {
287286
return nil, fmt.Errorf(format, args...)
288287
}

pkg/builtin/functions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
"delete": functions.NewBuilder(deleteFunction).WithBangHandler(deleteBangHandler).WithDescription("removes a key from an object or an item from a vector").Build(),
2020
"do": functions.NewBuilder(doFunction).WithDescription("eval a sequence of statements where only one expression is valid").Build(),
2121
"empty?": functions.NewBuilder(isEmptyFunction).WithCoalescer(humaneCoalescer).WithDescription("returns true when the given value is empty-ish (0, false, null, \"\", ...)").Build(),
22-
"error": functions.NewBuilder(errorFunction).WithDescription("returns an error").Build(),
22+
"error": functions.NewBuilder(errorFunction, fmtErrorFunction).WithDescription("returns an error").Build(),
2323
"has?": functions.NewBuilder(hasFunction).WithDescription("returns true if the given symbol's path expression points to an existing value").Build(),
2424
"if": functions.NewBuilder(ifElseFunction, ifFunction).WithDescription("evaluate one of two expressions based on a condition").Build(),
2525
"set": functions.NewBuilder(setFunction).WithDescription("set a value in a variable/document, only really useful with ! modifier (set!)").Build(),

pkg/builtin/lists.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func stringLenFunction(s string) (any, error) {
2525
return len(s), nil
2626
}
2727

28+
// (append VEC ITEMS+)
2829
func appendToVectorFunction(base []any, args ...any) (any, error) {
2930
result := []any{}
3031
result = append(result, base...)
@@ -33,18 +34,22 @@ func appendToVectorFunction(base []any, args ...any) (any, error) {
3334
return result, nil
3435
}
3536

37+
// (append STR ITEMS+)
3638
func appendToStringFunction(base string, args ...string) (any, error) {
3739
return base + strings.Join(args, ""), nil
3840
}
3941

42+
// (prepend VEC ITEMS+)
4043
func prependToVectorFunction(base []any, args ...any) (any, error) {
4144
return append(args, base...), nil
4245
}
4346

47+
// (prepend STR ITEMS+)
4448
func prependToStringFunction(base string, args ...string) (any, error) {
4549
return strings.Join(args, "") + base, nil
4650
}
4751

52+
// (reverse STR)
4853
func reverseStringFunction(s string) (any, error) {
4954
// thank you https://stackoverflow.com/a/10030772
5055
result := []rune(s)
@@ -55,6 +60,7 @@ func reverseStringFunction(s string) (any, error) {
5560
return string(result), nil
5661
}
5762

63+
// (reverse VEC)
5864
func reverseVectorFunction(vec []any) (any, error) {
5965
// clone original data
6066
result := append([]any{}, vec...)
@@ -409,10 +415,12 @@ func decodeNamingVector(expr ast.Expression) (indexName string, valueName string
409415
return indexName, valueName, nil
410416
}
411417

418+
// (contains? STR STR)
412419
func stringContainsFunction(haystack string, needle string) (any, error) {
413420
return strings.Contains(haystack, needle), nil
414421
}
415422

423+
// (contains? VEC ITEM)
416424
func vectorContainsFunction(ctx types.Context, haystack []any, needle any) (any, error) {
417425
for _, val := range haystack {
418426
equal, err := equality.Equal(ctx.Coalesce(), val, needle)

pkg/builtin/logic.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ import (
1111
"go.xrstf.de/rudi/pkg/lang/ast"
1212
)
1313

14-
func andFunction(ctx types.Context, base bool, args ...ast.Expression) (any, error) {
15-
if !base {
16-
return false, nil
17-
}
18-
14+
func andFunction(ctx types.Context, args ...ast.Expression) (any, error) {
1915
for i, arg := range args {
2016
_, evaluated, err := eval.EvalExpression(ctx, arg)
2117
if err != nil {
@@ -35,11 +31,7 @@ func andFunction(ctx types.Context, base bool, args ...ast.Expression) (any, err
3531
return true, nil
3632
}
3733

38-
func orFunction(ctx types.Context, base bool, args ...ast.Expression) (any, error) {
39-
if base {
40-
return true, nil
41-
}
42-
34+
func orFunction(ctx types.Context, args ...ast.Expression) (any, error) {
4335
for i, arg := range args {
4436
_, evaluated, err := eval.EvalExpression(ctx, arg)
4537
if err != nil {

pkg/builtin/math.go

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,84 +9,71 @@ import (
99
"go.xrstf.de/rudi/pkg/lang/ast"
1010
)
1111

12-
func integerAddFunction(a, b int64, extra ...int64) (any, error) {
13-
sum := a + b
12+
func integerAddFunction(base int64, extra ...int64) (any, error) {
1413
for _, num := range extra {
15-
sum += num
14+
base += num
1615
}
1716

18-
return sum, nil
17+
return base, nil
1918
}
2019

21-
func numberAddFunction(a, b ast.Number, extra ...ast.Number) (any, error) {
22-
sum := a.MustToFloat() + b.MustToFloat()
20+
func numberAddFunction(base ast.Number, extra ...ast.Number) (any, error) {
21+
sum := base.MustToFloat()
2322
for _, num := range extra {
2423
sum += num.MustToFloat()
2524
}
2625

2726
return sum, nil
2827
}
2928

30-
func integerSubFunction(a, b int64, extra ...int64) (any, error) {
31-
diff := a - b
29+
func integerSubFunction(base int64, extra ...int64) (any, error) {
3230
for _, num := range extra {
33-
diff -= num
31+
base -= num
3432
}
3533

36-
return diff, nil
34+
return base, nil
3735
}
3836

39-
func numberSubFunction(a, b ast.Number, extra ...ast.Number) (any, error) {
40-
diff := a.MustToFloat() - b.MustToFloat()
37+
func numberSubFunction(base ast.Number, extra ...ast.Number) (any, error) {
38+
diff := base.MustToFloat()
4139
for _, num := range extra {
4240
diff -= num.MustToFloat()
4341
}
4442

4543
return diff, nil
4644
}
4745

48-
func integerMultFunction(a, b int64, extra ...int64) (any, error) {
49-
product := a * b
46+
func integerMultFunction(base int64, extra ...int64) (any, error) {
5047
for _, num := range extra {
51-
product *= num
48+
base *= num
5249
}
5350

54-
return product, nil
51+
return base, nil
5552
}
5653

57-
func numberMultFunction(a, b ast.Number, extra ...ast.Number) (any, error) {
58-
product := a.MustToFloat() * b.MustToFloat()
54+
func numberMultFunction(base ast.Number, extra ...ast.Number) (any, error) {
55+
product := base.MustToFloat()
5956
for _, num := range extra {
6057
product *= num.MustToFloat()
6158
}
6259

6360
return product, nil
6461
}
6562

66-
func integerDivFunction(a, b int64, extra ...int64) (any, error) {
67-
if b == 0 {
68-
return nil, errors.New("division by zero")
69-
}
70-
71-
result := a / b
72-
63+
func integerDivFunction(base int64, extra ...int64) (any, error) {
7364
for _, num := range extra {
7465
if num == 0 {
7566
return nil, errors.New("division by zero")
7667
}
7768

78-
result /= num
69+
base /= num
7970
}
8071

81-
return result, nil
72+
return base, nil
8273
}
8374

84-
func numberDivFunction(a, b ast.Number, extra ...ast.Number) (any, error) {
85-
if b.MustToFloat() == 0 {
86-
return nil, errors.New("division by zero")
87-
}
88-
89-
result := a.MustToFloat() / b.MustToFloat()
75+
func numberDivFunction(base ast.Number, extra ...ast.Number) (any, error) {
76+
result := base.MustToFloat()
9077

9178
for _, num := range extra {
9279
if num.MustToFloat() == 0 {

pkg/eval/functions/args_consumer.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,16 @@ func contextConsumer(ctx types.Context, args []cachedExpression) (asserted []any
222222
return []any{ctx}, args, nil
223223
}
224224

225+
// toVariadicConsumer wraps a singular consumer to consume all remaining args. In constrast to
226+
// Go, variadic arguments must have at least 1 item (i.e. calling func(foo string, a ...int) with
227+
// ("abc") only is invalid).
225228
func toVariadicConsumer(singleConsumer argsConsumer) argsConsumer {
226229
return func(ctx types.Context, args []cachedExpression) (asserted []any, remaining []cachedExpression, err error) {
230+
// at least one argument is required, otherwise variadics do not match
231+
if len(args) == 0 {
232+
return nil, nil, nil
233+
}
234+
227235
leftover := args
228236
result := []any{}
229237

pkg/eval/functions/args_consumer_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,33 @@ func TestExpressionConsumer(t *testing.T) {
221221
})
222222
}
223223
}
224+
225+
func TestVariadicConsumer(t *testing.T) {
226+
testcases := []argsConsumerTestcase{
227+
{
228+
name: "variadic request at least 1 item",
229+
args: []ast.Expression{},
230+
expected: nil,
231+
remaining: 0,
232+
},
233+
{
234+
name: "consume all strings",
235+
args: []ast.Expression{
236+
ast.String("foo"),
237+
ast.Bool(true),
238+
ast.Null{},
239+
},
240+
expected: []any{"foo", "true", ""},
241+
remaining: 0,
242+
},
243+
}
244+
245+
coalescer := coalescing.NewHumane()
246+
ctx := types.NewContext(types.Document{}, nil, nil, coalescer)
247+
248+
for _, tc := range testcases {
249+
t.Run(tc.name, func(t *testing.T) {
250+
tc.Test(t, ctx, toVariadicConsumer(stringConsumer))
251+
})
252+
}
253+
}

pkg/eval/functions/args_matcher_test.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ func TestNewArgsMatcherSignatures(t *testing.T) {
216216
fun: func(map[string][]any) (any, error) { panic("") },
217217
invalid: true,
218218
},
219+
{
220+
name: "accept only variadic arg",
221+
fun: func(...int64) (any, error) { panic("") },
222+
},
219223
{
220224
name: "accept variadic basic arg",
221225
fun: func(string, []any, ...int64) (any, error) { panic("") },
@@ -332,11 +336,10 @@ func TestArgsMatcher(t *testing.T) {
332336
expected: []any{ast.Shim{Value: 1}},
333337
},
334338
{
335-
name: "basic variadic parameter with no args",
336-
fun: func(...string) (any, error) { return nil, nil },
337-
args: []ast.Expression{},
338-
match: true,
339-
expected: []any{},
339+
name: "variadic parameters require at least 1 item",
340+
fun: func(...string) (any, error) { return nil, nil },
341+
args: []ast.Expression{},
342+
match: false,
340343
},
341344
{
342345
name: "basic variadic parameter",
@@ -353,11 +356,10 @@ func TestArgsMatcher(t *testing.T) {
353356
expected: []any{"foo", "bar"},
354357
},
355358
{
356-
name: "variadic can be empty",
357-
fun: func(string, ...string) (any, error) { return nil, nil },
358-
args: []ast.Expression{ast.String("foo")},
359-
match: true,
360-
expected: []any{"foo"},
359+
name: "variadic cannot be empty",
360+
fun: func(string, ...string) (any, error) { return nil, nil },
361+
args: []ast.Expression{ast.String("foo")},
362+
match: false,
361363
},
362364
{
363365
name: "variadic slices",

0 commit comments

Comments
 (0)