diff --git a/cmd/rudi/docs/data/functions/func.md.gz b/cmd/rudi/docs/data/functions/func.md.gz index 698ae61..3f433a9 100644 Binary files a/cmd/rudi/docs/data/functions/func.md.gz and b/cmd/rudi/docs/data/functions/func.md.gz differ diff --git a/docs/stdlib/rudifunc/func.md b/docs/stdlib/rudifunc/func.md index ab758c1..9ddfe92 100644 --- a/docs/stdlib/rudifunc/func.md +++ b/docs/stdlib/rudifunc/func.md @@ -23,7 +23,7 @@ embedding Rudi into other Go applications. `func!` creates a new function, which can be used in all subseqeuent statements in the same Rudi program. Function creation is bound to its parent scope, so defining a function within an `if` statement for example will make the function -available onside inside that statement, not globally. +available only inside that statement, not globally. Dynamically defined functions cannot overwrite statically defined functions, i.e. you cannot define a custom `concat` function using `func!` if `concat` already @@ -34,6 +34,10 @@ Since defining a new function is a side effect, `func` must always be used with the bang modifier (`func!`). The behaviour of Rudi programs that use `func` without the bang modifier is undefined. +Functions can contain an arbitrary number of expressions, which will be evaluated +in sequence and share a single context. This means user-defined functions form +a "sub-program" the same way the [`do`](core-do.md) function does. + ## Examples ``` @@ -56,12 +60,11 @@ inject a Go function statically into Rudi instead of defining it at runtime. ## Forms -### `(func! name:identifier params:vector body:expression)` ➜ `null` +### `(func! name:identifier params:vector body:expression…)` ➜ `null` * `name` is an identifier giving the function its name. * `params` is a vector containing identifiers that hold the parameter names. -* `body` is a single expression (use `do` for multiple statements) that forms - the function body. +* `body` is one or more expressions that form the function body. This form will create a new function called `name` with as many parameters as `params` has identifiers. `params` can be empty, but must otherwise contain only diff --git a/pkg/builtin/rudifunc/docs/func.md b/pkg/builtin/rudifunc/docs/func.md index ab758c1..9ddfe92 100644 --- a/pkg/builtin/rudifunc/docs/func.md +++ b/pkg/builtin/rudifunc/docs/func.md @@ -23,7 +23,7 @@ embedding Rudi into other Go applications. `func!` creates a new function, which can be used in all subseqeuent statements in the same Rudi program. Function creation is bound to its parent scope, so defining a function within an `if` statement for example will make the function -available onside inside that statement, not globally. +available only inside that statement, not globally. Dynamically defined functions cannot overwrite statically defined functions, i.e. you cannot define a custom `concat` function using `func!` if `concat` already @@ -34,6 +34,10 @@ Since defining a new function is a side effect, `func` must always be used with the bang modifier (`func!`). The behaviour of Rudi programs that use `func` without the bang modifier is undefined. +Functions can contain an arbitrary number of expressions, which will be evaluated +in sequence and share a single context. This means user-defined functions form +a "sub-program" the same way the [`do`](core-do.md) function does. + ## Examples ``` @@ -56,12 +60,11 @@ inject a Go function statically into Rudi instead of defining it at runtime. ## Forms -### `(func! name:identifier params:vector body:expression)` ➜ `null` +### `(func! name:identifier params:vector body:expression…)` ➜ `null` * `name` is an identifier giving the function its name. * `params` is a vector containing identifiers that hold the parameter names. -* `body` is a single expression (use `do` for multiple statements) that forms - the function body. +* `body` is one or more expressions that form the function body. This form will create a new function called `name` with as many parameters as `params` has identifiers. `params` can be empty, but must otherwise contain only diff --git a/pkg/builtin/rudifunc/functions.go b/pkg/builtin/rudifunc/functions.go index af5e8b7..f1bf440 100644 --- a/pkg/builtin/rudifunc/functions.go +++ b/pkg/builtin/rudifunc/functions.go @@ -19,7 +19,7 @@ var ( // funcFunction should never be called without the bang modifier, as without it, the created function // just instantly vanishes into thin air. -func funcFunction(ctx types.Context, name ast.Expression, namingVector ast.Expression, body ast.Expression) (any, error) { +func funcFunction(ctx types.Context, name ast.Expression, namingVector ast.Expression, body ...ast.Expression) (any, error) { nameIdent, ok := name.(ast.Identifier) if !ok { return nil, fmt.Errorf("first argument must be an identifier that specifies the function name, but got %T instead", name) @@ -60,7 +60,7 @@ func funcBangHandler(ctx types.Context, originalArgs []ast.Expression, value any type rudispaceFunc struct { name string params []string - body ast.Expression + body []ast.Expression } var _ types.Function = rudispaceFunc{} @@ -84,7 +84,21 @@ func (f rudispaceFunc) Evaluate(ctx types.Context, args []ast.Expression) (any, funcArgs[paramName] = arg } - _, result, err := ctx.Runtime().EvalExpression(ctx.WithVariables(funcArgs), f.body) + // user-defined functions form a sub-program and all statements share the same context + funcCtx := ctx.WithVariables(funcArgs) + runtime := ctx.Runtime() + + var ( + result any + err error + ) + + for _, expr := range f.body { + funcCtx, result, err = runtime.EvalExpression(funcCtx, expr) + if err != nil { + return nil, err + } + } return result, err }