From 33b65908d29da6d2f512d884140aceb25e45f911 Mon Sep 17 00:00:00 2001 From: RT Date: Tue, 10 Feb 2026 01:25:44 -0500 Subject: [PATCH] upgrade risor engine to v2 --- README.md | 12 +-- engines/integration_test.go | 52 ++++++------- engines/risor/adapters/interfaces.go | 4 +- engines/risor/compiler/compiler.go | 2 +- engines/risor/compiler/compiler_test.go | 40 +++++----- engines/risor/compiler/executable.go | 8 +- engines/risor/compiler/executable_test.go | 30 ++++---- .../compiler/internal/compile/compile.go | 48 ++++++------ .../compiler/internal/compile/compile_test.go | 12 +-- engines/risor/evaluator/evaluator.go | 41 +++++++---- engines/risor/evaluator/evaluator_test.go | 13 ++-- engines/risor/evaluator/response.go | 2 +- engines/risor/evaluator/response_test.go | 33 ++++----- engines/risor/internal/converters.go | 21 +++--- engines/risor/internal/converters_test.go | 29 +++++--- engines/risor/new_test.go | 7 +- .../data-prep/risor/testdata/script.risor | 73 +++++++++---------- .../risor/testdata/script.risor | 6 +- examples/simple/risor/testdata/script.risor | 6 +- go.mod | 9 ++- go.sum | 15 +++- platform/evaluator_test.go | 4 +- polyscript_test.go | 16 ++-- readme_test.go | 32 ++++---- 24 files changed, 264 insertions(+), 251 deletions(-) diff --git a/README.md b/README.md index a2a8ce7..8744e2e 100644 --- a/README.md +++ b/README.md @@ -51,15 +51,15 @@ func main() { script := ` // The ctx object from the Go inputData map - name := ctx.get("name") + let name = ctx.get("name") - p := "." - if ctx.get("excited") { + let p = "." + if (ctx.get("excited")) { p = "!" } - - message := "Hello, " + name + p - + + let message = "Hello, " + name + p + // Return a map with our result { "greeting": message, diff --git a/engines/integration_test.go b/engines/integration_test.go index ca437c2..11fd47a 100644 --- a/engines/integration_test.go +++ b/engines/integration_test.go @@ -51,11 +51,11 @@ func TestEngineDataHandlingIntegration(t *testing.T) { // Risor script that accesses data via ctx variable risorScript := ` // Access data through ctx variable -name := ctx["name"] -version := ctx["version"] -debug := ctx["config"]["debug"] -timeout := ctx["config"]["timeout"] -tags := ctx["tags"] +let name = ctx["name"] +let version = ctx["version"] +let debug = ctx["config"]["debug"] +let timeout = ctx["config"]["timeout"] +let tags = ctx["tags"] // Create result { @@ -209,9 +209,9 @@ func TestEngineDataHandlingWithDynamicData(t *testing.T) { t.Run("risor_dynamic_data", func(t *testing.T) { risorScript := ` // Access both static and dynamic data -app_name := ctx["app_name"] -user_id := ctx["user_id"] -action := ctx["action"] +let app_name = ctx["app_name"] +let user_id = ctx["user_id"] +let action = ctx["action"] { "result": "processed", @@ -349,8 +349,8 @@ func TestEngineDataStructureDocumentation(t *testing.T) { t.Run("risor_documentation_example", func(t *testing.T) { script := ` -name := ctx["name"] -debug := ctx["config"]["debug"] +let name = ctx["name"] +let debug = ctx["config"]["debug"] { "name": name, @@ -439,8 +439,8 @@ func TestDataProviderPatterns(t *testing.T) { "risor_context_provider", func(t *testing.T) { script := ` -config := ctx["config"] -user_data := ctx["user_data"] +let config = ctx["config"] +let user_data = ctx["user_data"] { "config": config, @@ -596,9 +596,9 @@ _ = result t.Run("risor_static_provider", func(t *testing.T) { script := ` -app_name := ctx["config"]["app_name"] -version := ctx["config"]["version"] -max_retries := ctx["constants"]["max_retries"] +let app_name = ctx["config"]["app_name"] +let version = ctx["config"]["version"] +let max_retries = ctx["constants"]["max_retries"] { "app_name": app_name, @@ -713,10 +713,10 @@ _ = result t.Run( //nolint:dupl // Each engine test demonstrates different syntax and behavior "risor_composite_provider", func(t *testing.T) { script := ` -app_name := ctx["config"]["app_name"] -version := ctx["config"]["version"] -user_id := ctx["user_id"] -request_id := ctx["request_id"] +let app_name = ctx["config"]["app_name"] +let version = ctx["config"]["version"] +let user_id = ctx["user_id"] +let request_id = ctx["request_id"] { "app_name": app_name, @@ -875,10 +875,10 @@ func TestHttpRequestDataAccess(t *testing.T) { "risor_http_request", func(t *testing.T) { script := ` // HTTP request data as documented in platform/data/README.md -request_method := ctx["request"]["Method"] -url_path := ctx["request"]["URL_Path"] -request_body := ctx["request"]["Body"] -content_type := ctx["request"]["Headers"]["Content-Type"][0] +let request_method = ctx["request"]["Method"] +let url_path = ctx["request"]["URL_Path"] +let request_body = ctx["request"]["Body"] +let content_type = ctx["request"]["Headers"]["Content-Type"][0] { "method": request_method, @@ -1005,9 +1005,9 @@ _ = result t.Run("risor_explicit_keys", func(t *testing.T) { script := ` -request_data := ctx["request"] -user_data := ctx["user"] -config_data := ctx["config"] +let request_data = ctx["request"] +let user_data = ctx["user"] +let config_data = ctx["config"] { "has_request": request_data != nil, diff --git a/engines/risor/adapters/interfaces.go b/engines/risor/adapters/interfaces.go index c93b087..be2649e 100644 --- a/engines/risor/adapters/interfaces.go +++ b/engines/risor/adapters/interfaces.go @@ -1,7 +1,7 @@ package adapters -import risorCompiler "github.com/risor-io/risor/compiler" +import "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" type RisorExecutable struct { - GetRisorByteCode func() *risorCompiler.Code + GetRisorByteCode func() *bytecode.Code } diff --git a/engines/risor/compiler/compiler.go b/engines/risor/compiler/compiler.go index 6bbc9f0..fafead4 100644 --- a/engines/risor/compiler/compiler.go +++ b/engines/risor/compiler/compiler.go @@ -83,7 +83,7 @@ func (c *Compiler) compile(scriptBodyBytes []byte) (*executable, error) { isCommentOnly := true for line := range strings.SplitSeq(trimmedScript, "\n") { if trimmedLine := strings.TrimSpace(line); trimmedLine != "" && - !strings.HasPrefix(trimmedLine, "#") { + !strings.HasPrefix(trimmedLine, "//") { // Found a non-comment line, so we can stop checking lines because there's some real code here! isCommentOnly = false break diff --git a/engines/risor/compiler/compiler_test.go b/engines/risor/compiler/compiler_test.go index 5aa6fc0..c253e95 100644 --- a/engines/risor/compiler/compiler_test.go +++ b/engines/risor/compiler/compiler_test.go @@ -89,23 +89,23 @@ func TestCompiler_Compile(t *testing.T) { }{ { name: "valid script", - script: `print("Hello, World!")`, + script: `"Hello, World!"`, globals: []string{"request"}, }, { name: "with multiple globals", - script: `print(request, response)`, + script: `[request, response]`, globals: []string{"request", "response"}, }, { name: "complex valid script with global override", script: ` request = true -func main() { - if request { - print("Yes") +function main() { + if (request) { + "Yes" } else { - print("No") + "No" } } main() @@ -115,11 +115,11 @@ main() { name: "complex valid script with condition", script: ` -func main() { - if condition { - print("Yes") +function main() { + if (condition) { + "Yes" } else { - print("No") + "No" } } main() @@ -176,8 +176,8 @@ main() err error }{ { - name: "syntax error - missing closing parenthesis", - script: `print("Hello, World!"`, + name: "syntax error - unterminated string", + script: `"Hello, World!`, globals: []string{"request"}, err: ErrValidationFailed, }, @@ -189,13 +189,13 @@ main() }, { name: "undefined global", - script: `print(undefined_global)`, + script: `undefined_global`, globals: []string{"request"}, err: ErrValidationFailed, }, { name: "script using undefined global", - script: `print(undefined)`, + script: `undefined`, globals: []string{"request"}, err: ErrValidationFailed, }, @@ -270,7 +270,7 @@ main() require.NotNil(t, comp, "Expected compiler to be non-nil") // Create a reader that will return an error on close - reader := newMockScriptReaderCloser(`print("Hello, World!")`) + reader := newMockScriptReaderCloser(`"Hello, World!"`) reader.On("Close").Return(errors.New("test error")).Once() execContent, err := comp.Compile(reader) @@ -294,7 +294,7 @@ main() require.NotNil(t, comp, "Expected compiler to be non-nil") // Here we test that we can directly call the compile method with a byteslice - scriptBytes := []byte(`print("Hello, World!")`) + scriptBytes := []byte(`"Hello, World!"`) executable, err := comp.compile(scriptBytes) require.NoError(t, err, "Did not expect an error but got one") require.NotNil(t, executable, "Expected execContent to be non-nil") @@ -343,7 +343,7 @@ func TestCompilerOptions(t *testing.T) { require.NotNil(t, comp) // Test with a script using the globals - script := `print(request, response)` + script := `[request, response]` reader := io.ReadCloser(newMockScriptReaderCloser(script)) if mockReader, ok := reader.(*mockScriptReaderCloser); ok { mockReader.On("Close").Return(nil) @@ -361,7 +361,7 @@ func TestCompilerOptions(t *testing.T) { require.NotNil(t, comp) // Simple script that doesn't require globals - script := `print("Hello")` + script := `"Hello"` reader := io.ReadCloser(newMockScriptReaderCloser(script)) if mockReader, ok := reader.(*mockScriptReaderCloser); ok { mockReader.On("Close").Return(nil) @@ -407,7 +407,7 @@ func TestCompileWithBytecode(t *testing.T) { require.NotNil(t, comp, "Expected compiler to be non-nil") // Here we test that we can directly call the compile method with a byteslice - scriptBytes := []byte(`print("Hello, World!")`) + scriptBytes := []byte(`"Hello, World!"`) executable, err := comp.compile(scriptBytes) require.NoError(t, err, "Did not expect an error but got one") require.NotNil(t, executable, "Expected execContent to be non-nil") @@ -452,7 +452,7 @@ func TestCompileCloseError(t *testing.T) { require.NotNil(t, comp, "Expected compiler to be non-nil") // Create a reader that will return an error on close - reader := newMockScriptReaderCloser(`print("Hello, World!")`) + reader := newMockScriptReaderCloser(`"Hello, World!"`) reader.On("Close").Return(errors.New("test error")).Once() execContent, err := comp.Compile(reader) diff --git a/engines/risor/compiler/executable.go b/engines/risor/compiler/executable.go index b66824e..9b081ec 100644 --- a/engines/risor/compiler/executable.go +++ b/engines/risor/compiler/executable.go @@ -1,16 +1,16 @@ package compiler import ( - risorCompiler "github.com/risor-io/risor/compiler" + "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" machineTypes "github.com/robbyt/go-polyscript/engines/types" ) type executable struct { scriptBodyBytes []byte - ByteCode *risorCompiler.Code + ByteCode *bytecode.Code } -func newExecutable(scriptBodyBytes []byte, byteCode *risorCompiler.Code) *executable { +func newExecutable(scriptBodyBytes []byte, byteCode *bytecode.Code) *executable { if len(scriptBodyBytes) == 0 || byteCode == nil { return nil } @@ -29,7 +29,7 @@ func (e *executable) GetByteCode() any { return e.ByteCode } -func (e *executable) GetRisorByteCode() *risorCompiler.Code { +func (e *executable) GetRisorByteCode() *bytecode.Code { return e.ByteCode } diff --git a/engines/risor/compiler/executable_test.go b/engines/risor/compiler/executable_test.go index ba8ad78..d520ba0 100644 --- a/engines/risor/compiler/executable_test.go +++ b/engines/risor/compiler/executable_test.go @@ -3,7 +3,7 @@ package compiler import ( "testing" - risorCompiler "github.com/risor-io/risor/compiler" + "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" machineTypes "github.com/robbyt/go-polyscript/engines/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,25 +16,25 @@ func TestExecutable(t *testing.T) { // Test creation scenarios t.Run("Creation", func(t *testing.T) { t.Run("valid creation", func(t *testing.T) { - content := "print('Hello, World!')" - bytecode := &risorCompiler.Code{} + content := "'Hello, World!'" + bc := bytecode.NewCode(bytecode.CodeParams{}) - exe := newExecutable([]byte(content), bytecode) + exe := newExecutable([]byte(content), bc) require.NotNil(t, exe) assert.Equal(t, content, exe.GetSource()) - assert.Equal(t, bytecode, exe.GetByteCode()) - assert.Equal(t, bytecode, exe.GetRisorByteCode()) + assert.Equal(t, bc, exe.GetByteCode()) + assert.Equal(t, bc, exe.GetRisorByteCode()) assert.Equal(t, machineTypes.Risor, exe.GetMachineType()) }) t.Run("nil content", func(t *testing.T) { - bytecode := &risorCompiler.Code{} - exe := newExecutable(nil, bytecode) + bc := bytecode.NewCode(bytecode.CodeParams{}) + exe := newExecutable(nil, bc) assert.Nil(t, exe) }) t.Run("nil bytecode", func(t *testing.T) { - content := "print('test')" + content := "'test'" exe := newExecutable([]byte(content), nil) assert.Nil(t, exe) }) @@ -47,9 +47,9 @@ func TestExecutable(t *testing.T) { // Test getters t.Run("Getters", func(t *testing.T) { - content := "print('Hello, World!')" - bytecode := &risorCompiler.Code{} - executable := newExecutable([]byte(content), bytecode) + content := "'Hello, World!'" + bc := bytecode.NewCode(bytecode.CodeParams{}) + executable := newExecutable([]byte(content), bc) require.NotNil(t, executable) t.Run("GetSource", func(t *testing.T) { @@ -59,16 +59,16 @@ func TestExecutable(t *testing.T) { t.Run("GetByteCode", func(t *testing.T) { code := executable.GetByteCode() - assert.Equal(t, bytecode, code) + assert.Equal(t, bc, code) // Test type assertion - _, ok := code.(*risorCompiler.Code) + _, ok := code.(*bytecode.Code) assert.True(t, ok) }) t.Run("GetRisorByteCode", func(t *testing.T) { code := executable.GetRisorByteCode() - assert.Equal(t, bytecode, code) + assert.Equal(t, bc, code) }) t.Run("GetMachineType", func(t *testing.T) { diff --git a/engines/risor/compiler/internal/compile/compile.go b/engines/risor/compiler/internal/compile/compile.go index 2c60948..61d9ee0 100644 --- a/engines/risor/compiler/internal/compile/compile.go +++ b/engines/risor/compiler/internal/compile/compile.go @@ -2,36 +2,30 @@ package compile import ( "context" - "errors" "fmt" + "maps" + "slices" - risorLib "github.com/risor-io/risor" - risorCompiler "github.com/risor-io/risor/compiler" - risorErrors "github.com/risor-io/risor/errz" - risorParser "github.com/risor-io/risor/parser" + risor "github.com/deepnoodle-ai/risor/v2" + "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" + risorCompiler "github.com/deepnoodle-ai/risor/v2/pkg/compiler" + risorParser "github.com/deepnoodle-ai/risor/v2/pkg/parser" ) // Compile parses and compiles the script content into bytecode -func Compile(scriptContent *string, options ...risorCompiler.Option) (*risorCompiler.Code, error) { +func Compile(scriptContent *string, cfg *risorCompiler.Config) (*bytecode.Code, error) { if scriptContent == nil { return nil, ErrContentNil } - ast, err := risorParser.Parse(context.Background(), *scriptContent) + ast, err := risorParser.Parse(context.Background(), *scriptContent, nil) if err != nil { - // Create a better-looking error output when there's a syntax error - errMsg := err.Error() - var friendlyErr risorErrors.FriendlyError - if errors.As(err, &friendlyErr) { - errMsg = friendlyErr.FriendlyErrorMessage() - } - return nil, fmt.Errorf("%w: %s", ErrCompileFailed, errMsg) + return nil, fmt.Errorf("%w: %s", ErrCompileFailed, err.Error()) } - // Compile the AST to bytecode - bc, err := risorCompiler.Compile(ast, options...) + bc, err := risorCompiler.Compile(ast, cfg) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: %s", ErrCompileFailed, err.Error()) } return bc, nil @@ -41,14 +35,20 @@ func Compile(scriptContent *string, options ...risorCompiler.Option) (*risorComp // which are needed when parsing a script that will eventually have globals injected at eval time. // For example, if a script uses a request or response object, it needs to be compiled with those // global names, even though they won't be available until eval time. -func CompileWithGlobals(scriptContent *string, globals []string) (*risorCompiler.Code, error) { - // Retrieve default global names, and append the custom globals - cfg := risorLib.NewConfig() - globalNames := append(cfg.GlobalNames(), globals...) +func CompileWithGlobals(scriptContent *string, globals []string) (*bytecode.Code, error) { + // Start with the standard builtins env and add custom globals + env := risor.Builtins() + for _, g := range globals { + if _, exists := env[g]; !exists { + env[g] = nil + } + } + + globalNames := slices.Sorted(maps.Keys(env)) - options := []risorCompiler.Option{ - risorCompiler.WithGlobalNames(globalNames), + cfg := &risorCompiler.Config{ + GlobalNames: globalNames, } - return Compile(scriptContent, options...) + return Compile(scriptContent, cfg) } diff --git a/engines/risor/compiler/internal/compile/compile_test.go b/engines/risor/compiler/internal/compile/compile_test.go index 352d7d3..c1debba 100644 --- a/engines/risor/compiler/internal/compile/compile_test.go +++ b/engines/risor/compiler/internal/compile/compile_test.go @@ -10,7 +10,7 @@ import ( func TestCompileSuccess(t *testing.T) { scriptContent := `true` - code, err := Compile(&scriptContent) + code, err := Compile(&scriptContent, nil) require.NoError(t, err) require.NotNil(t, code) } @@ -18,10 +18,10 @@ func TestCompileSuccess(t *testing.T) { // TestCompileSyntaxError tests the compilation failure due to syntax errors func TestCompileSyntaxError(t *testing.T) { scriptContent := ` - print("Hello, World! + "Hello, World! ` - code, err := Compile(&scriptContent) + code, err := Compile(&scriptContent, nil) require.Error(t, err) require.Nil(t, code) require.ErrorIs(t, err, ErrCompileFailed) @@ -30,7 +30,7 @@ func TestCompileSyntaxError(t *testing.T) { // TestCompileWithGlobals tests the compilation with custom global names func TestCompileWithGlobals(t *testing.T) { scriptContent := ` - print(request) + request ` globals := []string{"request"} @@ -41,7 +41,7 @@ func TestCompileWithGlobals(t *testing.T) { // TestCompileNilContent tests the handling of nil script content func TestCompileNilContent(t *testing.T) { - code, err := Compile(nil) + code, err := Compile(nil, nil) require.Error(t, err) require.Nil(t, code) require.ErrorIs(t, err, ErrContentNil) @@ -59,7 +59,7 @@ func TestCompileWithGlobalsNilContent(t *testing.T) { // TestCompileWithGlobalsSyntaxError tests the compilation failure due to syntax errors with globals func TestCompileWithGlobalsSyntaxError(t *testing.T) { scriptContent := ` - print(request + [request ` globals := []string{"request"} diff --git a/engines/risor/evaluator/evaluator.go b/engines/risor/evaluator/evaluator.go index 976c156..753bc40 100644 --- a/engines/risor/evaluator/evaluator.go +++ b/engines/risor/evaluator/evaluator.go @@ -6,8 +6,9 @@ import ( "log/slog" "time" - risorLib "github.com/risor-io/risor" - risorCompiler "github.com/risor-io/risor/compiler" + risor "github.com/deepnoodle-ai/risor/v2" + "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" + risorObject "github.com/deepnoodle-ai/risor/v2/pkg/object" "github.com/robbyt/go-polyscript/engines/risor/internal" "github.com/robbyt/go-polyscript/internal/helpers" "github.com/robbyt/go-polyscript/platform" @@ -16,6 +17,10 @@ import ( "github.com/robbyt/go-polyscript/platform/script" ) +// typeRegistry is initialized once at package level to avoid a data race in +// risor v2's DefaultRegistry() lazy initialization. +var typeRegistry = risorObject.DefaultRegistry() + // Evaluator is an abstraction layer for evaluating bytecode on the Risor engine type Evaluator struct { // ctxKey is the variable name used to access input data inside the engine (ctx) @@ -72,20 +77,26 @@ func (be *Evaluator) loadInputData(ctx context.Context) (map[string]any, error) return inputData, nil } -// exec pulls the latest bytecode, and runs it with some input from options +// exec runs the bytecode with the provided environment map func (be *Evaluator) exec( ctx context.Context, - bytecode *risorCompiler.Code, - options ...risorLib.Option, + bc *bytecode.Code, + env map[string]any, ) (*execResult, error) { startTime := time.Now() - result, err := risorLib.EvalCode(ctx, bytecode, options...) + result, err := risor.Run(ctx, bc, risor.WithEnv(env), risor.WithRawResult(), risor.WithTypeRegistry(typeRegistry)) execTime := time.Since(startTime) if err != nil { return nil, fmt.Errorf("risor execution error: %w", err) } - return newEvalResult(be.logHandler, result, execTime, ""), nil + + obj, ok := result.(risorObject.Object) + if !ok { + return nil, fmt.Errorf("unexpected result type from risor: %T", result) + } + + return newEvalResult(be.logHandler, obj, execTime, ""), nil } // Eval evaluates the loaded bytecode and uses the provided EvalData to pass data in to the Risor engine execution @@ -100,8 +111,8 @@ func (be *Evaluator) Eval(ctx context.Context) (platform.EvaluatorResponse, erro } // Get the bytecode from the executable unit - bytecode := be.execUnit.GetContent().GetByteCode() - if bytecode == nil { + rawBytecode := be.execUnit.GetContent().GetByteCode() + if rawBytecode == nil { return nil, fmt.Errorf("bytecode is nil") } @@ -112,11 +123,11 @@ func (be *Evaluator) Eval(ctx context.Context) (platform.EvaluatorResponse, erro } logger = logger.With("exeID", exeID) - // 1. Type assert the bytecode into *risorCompiler.Code - risorByteCode, ok := bytecode.(*risorCompiler.Code) + // 1. Type assert the bytecode into *bytecode.Code + risorByteCode, ok := rawBytecode.(*bytecode.Code) if !ok { return nil, fmt.Errorf( - "unable to type assert bytecode into *risorCompiler.Code for ID: %s", + "unable to type assert bytecode into *bytecode.Code for ID: %s", exeID, ) } @@ -127,11 +138,11 @@ func (be *Evaluator) Eval(ctx context.Context) (platform.EvaluatorResponse, erro return nil, fmt.Errorf("failed to get input data: %w", err) } - // 3. Convert to Risor engine format - runtimeData := internal.ConvertToRisorOptions(be.ctxKey, rawInputData) + // 3. Build the Risor environment with builtins and input data + runtimeEnv := internal.BuildRisorEnv(be.ctxKey, rawInputData) // 4. Execute the program - result, err := be.exec(ctx, risorByteCode, runtimeData...) + result, err := be.exec(ctx, risorByteCode, runtimeEnv) if err != nil { return nil, fmt.Errorf("exec error: %w", err) } diff --git a/engines/risor/evaluator/evaluator_test.go b/engines/risor/evaluator/evaluator_test.go index debc7b3..259e629 100644 --- a/engines/risor/evaluator/evaluator_test.go +++ b/engines/risor/evaluator/evaluator_test.go @@ -10,7 +10,7 @@ import ( "strings" "testing" - risorCompiler "github.com/risor-io/risor/compiler" + "github.com/deepnoodle-ai/risor/v2/pkg/bytecode" "github.com/robbyt/go-polyscript/engines/risor/compiler" "github.com/robbyt/go-polyscript/engines/types" "github.com/robbyt/go-polyscript/internal/helpers" @@ -116,19 +116,18 @@ func TestEvaluator_Evaluate(t *testing.T) { // Define a test script that handles HTTP requests testScript := ` - func handle(request) { - if request == nil { + let handle = function(request) { + if (request == nil) { return error("request is nil") } - if request["Method"] == "POST" { + if (request["Method"] == "POST") { return "post" } - if request["URL_Path"] == "/hello" { + if (request["URL_Path"] == "/hello") { return true } return false } - print(ctx) handle(ctx["request"]) ` @@ -257,7 +256,7 @@ func TestEvaluator_Evaluate(t *testing.T) { return &script.ExecutableUnit{ ID: "", Content: &MockContent{ - Content: &risorCompiler.Code{}, + Content: bytecode.NewCode(bytecode.CodeParams{}), }, } }, diff --git a/engines/risor/evaluator/response.go b/engines/risor/evaluator/response.go index 424da53..5c1503b 100644 --- a/engines/risor/evaluator/response.go +++ b/engines/risor/evaluator/response.go @@ -6,7 +6,7 @@ import ( "os" "time" - risorObject "github.com/risor-io/risor/object" + risorObject "github.com/deepnoodle-ai/risor/v2/pkg/object" "github.com/robbyt/go-polyscript/platform/data" ) diff --git a/engines/risor/evaluator/response_test.go b/engines/risor/evaluator/response_test.go index ea0bfcc..a81997c 100644 --- a/engines/risor/evaluator/response_test.go +++ b/engines/risor/evaluator/response_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - rObj "github.com/risor-io/risor/object" - "github.com/risor-io/risor/op" + rObj "github.com/deepnoodle-ai/risor/v2/pkg/object" + "github.com/deepnoodle-ai/risor/v2/pkg/op" "github.com/robbyt/go-polyscript/platform" "github.com/robbyt/go-polyscript/platform/data" "github.com/stretchr/testify/assert" @@ -35,24 +35,22 @@ func (m *RisorObjectMock) Interface() any { return args.Get(0) } -func (m *RisorObjectMock) Hash() (uint32, error) { - args := m.Called() - return args.Get(0).(uint32), args.Error(1) -} - func (m *RisorObjectMock) String() string { args := m.Called() return args.String(0) } -func (m *RisorObjectMock) Cost() int { - args := m.Called() - return args.Int(0) +func (m *RisorObjectMock) Equals(other rObj.Object) bool { + args := m.Called(other) + return args.Bool(0) } -func (m *RisorObjectMock) Equals(other rObj.Object) rObj.Object { - args := m.Called(other) - return args.Get(0).(rObj.Object) +func (m *RisorObjectMock) Attrs() []rObj.AttrSpec { + args := m.Called() + if v := args.Get(0); v != nil { + return v.([]rObj.AttrSpec) + } + return nil } func (m *RisorObjectMock) GetAttr(name string) (rObj.Object, bool) { @@ -70,14 +68,9 @@ func (m *RisorObjectMock) IsTruthy() bool { return args.Bool(0) } -func (m *RisorObjectMock) RunOperation(opType op.BinaryOpType, right rObj.Object) rObj.Object { +func (m *RisorObjectMock) RunOperation(opType op.BinaryOpType, right rObj.Object) (rObj.Object, error) { args := m.Called(opType, right) - return args.Get(0).(rObj.Object) -} - -func (m *RisorObjectMock) Compare(other rObj.Object) (int, error) { - args := m.Called(other) - return args.Int(0), args.Error(1) + return args.Get(0).(rObj.Object), args.Error(1) } // TestResponseMethods tests all the methods of the EvaluatorResponse interface diff --git a/engines/risor/internal/converters.go b/engines/risor/internal/converters.go index 8732f15..3b41c8a 100644 --- a/engines/risor/internal/converters.go +++ b/engines/risor/internal/converters.go @@ -1,22 +1,21 @@ package internal import ( - risorLib "github.com/risor-io/risor" + risor "github.com/deepnoodle-ai/risor/v2" ) -// ConvertToRisorOptions converts a Go map into Risor engine options object. -// The input data will be wrapped in a single "ctx" object passed to the engine. +// BuildRisorEnv builds the full Risor environment map with standard builtins and input data. +// The input data is made available under the given ctxKey (typically "ctx"). // -// For example, if the inputData is {"foo": "bar", "baz": 123}, the output will be: +// For example, if the inputData is {"foo": "bar", "baz": 123}, the output will be a map +// containing all standard Risor builtins plus: // -// []risorLib.Option{ -// risorLib.WithGlobal("ctx", map[string]any{ +// "ctx": map[string]any{ // "foo": "bar", // "baz": 123, -// }), // } -func ConvertToRisorOptions(ctxKey string, inputData map[string]any) []risorLib.Option { - return []risorLib.Option{ - risorLib.WithGlobal(ctxKey, inputData), - } +func BuildRisorEnv(ctxKey string, inputData map[string]any) map[string]any { + env := risor.Builtins() + env[ctxKey] = inputData + return env } diff --git a/engines/risor/internal/converters_test.go b/engines/risor/internal/converters_test.go index 89db70d..8b48229 100644 --- a/engines/risor/internal/converters_test.go +++ b/engines/risor/internal/converters_test.go @@ -4,19 +4,30 @@ import ( "testing" "github.com/robbyt/go-polyscript/platform/constants" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// TestConvertToRisorOptions tests the convertToRisorOptions method -func TestConvertToRisorOptions(t *testing.T) { +func TestBuildRisorEnv(t *testing.T) { t.Parallel() - // Test with empty data - options := ConvertToRisorOptions(constants.Ctx, map[string]any{}) - require.Len(t, options, 1) + t.Run("empty data includes builtins", func(t *testing.T) { + env := BuildRisorEnv(constants.Ctx, map[string]any{}) + require.NotNil(t, env) + // Should contain the ctx key + _, hasCtx := env[constants.Ctx] + assert.True(t, hasCtx, "env should contain the ctx key") + // Should contain standard builtins like len, type, etc. + _, hasLen := env["len"] + assert.True(t, hasLen, "env should contain standard builtins") + }) - // Test with actual data - testData := map[string]any{"foo": "bar"} - options = ConvertToRisorOptions(constants.Ctx, testData) - require.Len(t, options, 1) + t.Run("includes input data under ctx key", func(t *testing.T) { + testData := map[string]any{"foo": "bar"} + env := BuildRisorEnv(constants.Ctx, testData) + require.NotNil(t, env) + ctxData, ok := env[constants.Ctx].(map[string]any) + require.True(t, ok) + assert.Equal(t, "bar", ctxData["foo"]) + }) } diff --git a/engines/risor/new_test.go b/engines/risor/new_test.go index 1f079ac..8f37fe2 100644 --- a/engines/risor/new_test.go +++ b/engines/risor/new_test.go @@ -16,11 +16,8 @@ import ( ) const testRisorScript = ` -// Simple Risor script that uses built-in print function -print("Hello from Risor") - -// Define and call a simple function -func greet(name) { +// Define and call a greeting handler +function greet(name) { return "Hello, " + name } diff --git a/examples/data-prep/risor/testdata/script.risor b/examples/data-prep/risor/testdata/script.risor index 2cbd298..c40e00f 100644 --- a/examples/data-prep/risor/testdata/script.risor +++ b/examples/data-prep/risor/testdata/script.risor @@ -1,42 +1,35 @@ -// Wrap everything in a function for Risor syntax -func process() { - - // Access static config data (set at compile time) - app_version := ctx.get("app_version", "unknown") - environment := ctx.get("environment", "unknown") - config := ctx.get("config", {}) - - // Get name from runtime data (added via AddDataToContext) - name := ctx.get("name", "Default") - - // Get timestamp from runtime data - timestamp := ctx.get("timestamp", "Unknown") - - // Process user data from runtime data - user_data := ctx.get("user_data", {}) - user_role := user_data.get("role", "guest") - user_id := user_data.get("id", "unknown") - - // Access request data if available - request := ctx.get("request", {}) - request_method := request.get("Method", "") - request_path := request.get("URL_Path", "") - - // Construct result dictionary - result := {} - result["greeting"] = "Hello, " + name + "!" - result["timestamp"] = timestamp - result["message"] = "Processed by " + user_role + " at " + timestamp - result["user_id"] = user_id - result["request_info"] = request_method + " " + request_path - result["app_info"] = { - "version": app_version, - "environment": environment, - "features": config.get("feature_flags", {}) - } - - return result +// Access static config data (set at compile time) +let app_version = ctx.get("app_version", "unknown") +let environment = ctx.get("environment", "unknown") +let config = ctx.get("config", {}) + +// Get name from runtime data (added via AddDataToContext) +let name = ctx.get("name", "Default") + +// Get timestamp from runtime data +let timestamp = ctx.get("timestamp", "Unknown") + +// Process user data from runtime data +let user_data = ctx.get("user_data", {}) +let user_role = user_data.get("role", "guest") +let user_id = user_data.get("id", "unknown") + +// Access request data if available +let request = ctx.get("request", {}) +let request_method = request.get("Method", "") +let request_path = request.get("URL_Path", "") + +// Construct result dictionary +let result = {} +result["greeting"] = "Hello, " + name + "!" +result["timestamp"] = timestamp +result["message"] = "Processed by " + user_role + " at " + timestamp +result["user_id"] = user_id +result["request_info"] = request_method + " " + request_path +result["app_info"] = { + "version": app_version, + "environment": environment, + "features": config.get("feature_flags", {}) } -// Call the function and return its result -process() \ No newline at end of file +result diff --git a/examples/multiple-instantiation/risor/testdata/script.risor b/examples/multiple-instantiation/risor/testdata/script.risor index fc56ff7..484360d 100644 --- a/examples/multiple-instantiation/risor/testdata/script.risor +++ b/examples/multiple-instantiation/risor/testdata/script.risor @@ -1,9 +1,9 @@ // Script has access to ctx variable passed from Go -name := ctx["name"] -message := "Hello, " + name + "!" +let name = ctx["name"] +let message = "Hello, " + name + "!" // Return a map with our result { "greeting": message, "length": len(message) -} \ No newline at end of file +} diff --git a/examples/simple/risor/testdata/script.risor b/examples/simple/risor/testdata/script.risor index ba3c094..0f763b9 100644 --- a/examples/simple/risor/testdata/script.risor +++ b/examples/simple/risor/testdata/script.risor @@ -1,9 +1,9 @@ // Script has access to ctx variable passed from Go -name := ctx["name"] -message := "Hello, " + name + "!" +let name = ctx["name"] +let message = "Hello, " + name + "!" // Return a map with our result { "greeting": message, "length": len(message) -} \ No newline at end of file +} diff --git a/go.mod b/go.mod index e2d03f2..7005c89 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/robbyt/go-polyscript go 1.25.7 require ( + github.com/deepnoodle-ai/risor/v2 v2.0.0 github.com/extism/go-sdk v1.7.1 - github.com/risor-io/risor v1.8.1 github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.11.0 go.starlark.net v0.0.0-20260102030733-3fee463870c9 @@ -12,15 +12,18 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deepnoodle-ai/wonton v0.0.25 // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect go.opentelemetry.io/proto/otlp v1.8.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.39.0 // indirect google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fabebcc..3a33766 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deepnoodle-ai/risor/v2 v2.0.0 h1:lHJSmTdLM86Tfv5E7za+hnl5Hh9Tdy38E6lhe3wGN2c= +github.com/deepnoodle-ai/risor/v2 v2.0.0/go.mod h1:XwfyjmojSwk5HQkWsNhrkxu6MqpsXG1XGVNXyQ+c3Zo= +github.com/deepnoodle-ai/wonton v0.0.25 h1:mLhE4ToU1jMIHaTXaaxKoD/BDKtG+Df9jft+578yD2M= +github.com/deepnoodle-ai/wonton v0.0.25/go.mod h1:oyogeHwAHPrVxZ7jtik55Jnj6CwC1jkF+PfHpCRlUGA= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1 h1:idfl8M8rPW93NehFw5H1qqH8yG158t5POr+LX9avbJY= github.com/dylibso/observe-sdk/go v0.0.0-20240828172851-9145d8ad07e1/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= @@ -11,14 +15,17 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 h1:QCtizt3VTaANvnsd8TtD/eonx7JLIVdEKW1//ZNPZ9A= github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/risor-io/risor v1.8.1 h1:FaycOBo56LudgozpU3FkSBxBgzQJs4GJ4lAno/M2CFo= -github.com/risor-io/risor v1.8.1/go.mod h1:OuP9WH8h3dzvK7NDfBTA+k095dyTaQxWi/qPTCx3W0g= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= @@ -33,8 +40,8 @@ go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8 go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= go.starlark.net v0.0.0-20260102030733-3fee463870c9 h1:nV1OyvU+0CYrp5eKfQ3rD03TpFYYhH08z31NK1HmtTk= go.starlark.net v0.0.0-20260102030733-3fee463870c9/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/platform/evaluator_test.go b/platform/evaluator_test.go index 6ba2cb9..5e8544f 100644 --- a/platform/evaluator_test.go +++ b/platform/evaluator_test.go @@ -117,8 +117,8 @@ func TestEvalDataPreparerInterface(t *testing.T) { // The key name may be different in the new implementation scriptData := map[string]any{"greeting": "Hello, World!"} evaluator, err := polyscript.FromRisorStringWithData(` -method := ctx["request"]["Method"] -greeting := ctx["greeting"] // With new implementation, keys are at top level +let method = ctx["request"]["Method"] +let greeting = ctx["greeting"] method + " " + greeting `, scriptData, diff --git a/polyscript_test.go b/polyscript_test.go index c382b54..9d13d0d 100644 --- a/polyscript_test.go +++ b/polyscript_test.go @@ -105,7 +105,7 @@ func TestMachineEvaluators(t *testing.T) { }, { name: "FromRisorString", - content: `print("Hello, World!")`, + content: `"Hello, World!"`, machineType: types.Risor, creator: polyscript.FromRisorString, }, @@ -141,7 +141,7 @@ func TestFromStringLoaders(t *testing.T) { }, { name: "FromRisorString - Valid", - content: `print("Hello, World!")`, + content: `"Hello, World!"`, creator: polyscript.FromRisorString, logHandler: nil, expectError: false, @@ -285,7 +285,7 @@ func TestEvalHelpers(t *testing.T) { t.Run("PrepareAndEval", func(t *testing.T) { // Create a simple Risor evaluator script := ` - name := ctx["name"] + let name = ctx["name"] { "message": "Hello, " + name + "!", "length": len(name) @@ -512,12 +512,12 @@ _ = result` // Test script risorScript := ` // Access static data - version := ctx["app_version"] - timeout := ctx["config"]["timeout"] - + let version = ctx["app_version"] + let timeout = ctx["config"]["timeout"] + // Access dynamic data - name := ctx["name"] - + let name = ctx["name"] + { "message": "Hello, " + name + " (v" + version + ")", "timeout": timeout diff --git a/readme_test.go b/readme_test.go index 5c283c0..a66a070 100644 --- a/readme_test.go +++ b/readme_test.go @@ -18,14 +18,14 @@ func TestReadmeQuickStart(t *testing.T) { script := ` // The ctx object from the Go inputData map - name := ctx.get("name") + let name = ctx.get("name") - p := "." - if ctx.get("excited") { + let p = "." + if (ctx.get("excited")) { p = "!" } - message := "Hello, " + name + p + let message = "Hello, " + name + p // Return a map with our result { @@ -60,15 +60,15 @@ func TestReadmeStaticProvider(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) script := ` - name := ctx.get("name") - excited := ctx.get("excited") + let name = ctx.get("name") + let excited = ctx.get("excited") - p := "." - if excited { + let p = "." + if (excited) { p = "!" } - message := "Hello, " + name + p + let message = "Hello, " + name + p { "greeting": message @@ -94,8 +94,8 @@ func TestReadmeContextProvider(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) script := ` - name := ctx.get("name") - relationship := ctx.get("relationship") + let name = ctx.get("name") + let relationship = ctx.get("relationship") { "name": name, @@ -127,15 +127,15 @@ func TestReadmeCombiningStaticAndDynamic(t *testing.T) { script := ` // Access both static and dynamic data - name := ctx.get("name") - excited := ctx.get("excited") + let name = ctx.get("name") + let excited = ctx.get("excited") - p := "." - if excited { + let p = "." + if (excited) { p = "!" } - message := "Hello, " + name + p + let message = "Hello, " + name + p { "greeting": message