This commit is contained in:
2026-01-28 21:55:20 -05:00
parent 513674b0c8
commit dcd516d970
10 changed files with 90 additions and 179 deletions

View File

@@ -6,12 +6,12 @@ type RuntimeOption func(*Runtime)
func WithStdout(stdout io.Writer) RuntimeOption {
return func(r *Runtime) {
r.stdout = stdout
r.opts.Stdout = stdout
}
}
func WithStderr(stderr io.Writer) RuntimeOption {
return func(r *Runtime) {
r.stderr = stderr
r.opts.Stderr = stderr
}
}

View File

@@ -3,83 +3,60 @@ package runtime
import (
"context"
"fmt"
"io"
"os"
"strings"
"github.com/evanw/esbuild/pkg/api"
"modernc.org/quickjs"
"github.com/fastschema/qjs"
"reichard.io/poiesis/internal/functions"
_ "reichard.io/poiesis/internal/stdlib"
)
type Runtime struct {
vm *quickjs.VM
ctx context.Context
stdout io.Writer
stderr io.Writer
ctx *qjs.Context
opts qjs.Option
}
func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) {
// Create VM
vm, err := quickjs.NewVM()
// Create Runtime
r := &Runtime{opts: qjs.Option{Context: ctx}}
for _, opt := range opts {
opt(r)
}
// Create QuickJS Context
rt, err := qjs.New(r.opts)
if err != nil {
return nil, err
}
vm.SetCanBlock(true)
r.ctx = rt.Context()
// Create Runtime
r := &Runtime{vm: vm, ctx: ctx, stdout: os.Stdout, stderr: os.Stderr}
// Populate Globals
if err := r.populateGlobals(); err != nil {
return nil, err
}
// Apply Options
for _, opt := range opts {
opt(r)
}
return r, nil
}
func (r *Runtime) populateGlobals() error {
// Add Helpers
if err := r.vm.StdAddHelpers(); err != nil {
return err
}
// Add Log Hook
if err := r.vm.RegisterFunc("customLog", func(args ...any) {
for i, arg := range args {
if i > 0 {
_, _ = fmt.Fprint(r.stdout, " ")
}
_, _ = fmt.Fprint(r.stdout, arg)
}
_, _ = fmt.Fprintln(r.stdout)
}, false); err != nil {
return err
}
if _, err := r.vm.Eval("console.log = customLog;", quickjs.EvalGlobal); err != nil {
return err
}
// Register Custom Functions
for name, fn := range functions.GetRegisteredFunctions() {
// Register Main Function
if err := r.vm.RegisterFunc(name, func(allArgs ...any) (any, error) {
return fn.Call(r.ctx, allArgs)
}, false); err != nil {
return err
}
// Wrap Exception - The QuickJS library does not allow us to throw exceptions, so we
// wrap the function with native JS to appropriately throw on error.
wrappedFunc := wrapFunc(fn.Name(), fn.IsAsync())
if _, err := r.vm.Eval(wrappedFunc, quickjs.EvalGlobal); err != nil {
return err
if fn.IsAsync() {
r.ctx.SetAsyncFunc(name, func(this *qjs.This) {
qjsVal, err := callFunc(this, fn)
if err != nil {
this.Promise().Reject(this.Context().NewError(err))
return
}
this.Promise().Resolve(qjsVal)
})
} else {
r.ctx.SetFunc(name, func(this *qjs.This) (*qjs.Value, error) {
return callFunc(this, fn)
})
}
}
@@ -101,7 +78,10 @@ func (r *Runtime) RunCode(tsCode string) error {
return err
}
_, err = r.vm.Eval(string(transformedCode), quickjs.EvalGlobal)
result, err := r.ctx.Eval("code.ts", qjs.Code(string(transformedCode)))
if result != nil {
result.Free()
}
return err
}
@@ -125,41 +105,28 @@ func (r *Runtime) transformCode(tsCode string) ([]byte, error) {
return result.Code, nil
}
func wrapFunc(funcName string, isAsync bool) string {
if isAsync {
return fmt.Sprintf(`
(function() {
const original = globalThis[%q];
func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) {
qjsArgs := this.Args()
fnArgs := fn.Arguments()
globalThis[%q] = function(...args) {
console.log("calling")
return new Promise((resolve, reject) => {
const [result, error] = original.apply(this, args);
console.log("result", result)
if (error) {
console.log("reject")
reject(new Error(error));
} else {
console.log("resolve")
resolve(result);
}
});
};
})();
`, funcName, funcName)
var allArgs []any
for i := range min(len(fnArgs), len(qjsArgs)) {
rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i])
if err != nil {
panic(err)
}
allArgs = append(allArgs, rVal.Interface())
}
return fmt.Sprintf(`
(function() {
const original = globalThis[%q];
result, err := fn.Call(this.Context(), allArgs)
if err != nil {
return nil, err
}
globalThis[%q] = function(...args) {
const [result, error] = original.apply(this, args);
if (error) {
throw new Error(error);
}
return result;
};
})();
`, funcName, funcName)
val, err := qjs.ToJsValue(this.Context(), result)
if err != nil {
return nil, err
}
return val, nil
}

View File

@@ -6,9 +6,9 @@ import (
"strings"
"testing"
"github.com/fastschema/qjs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"modernc.org/quickjs"
"reichard.io/poiesis/internal/functions"
)
@@ -48,11 +48,17 @@ func TestAsyncFunctionResolution(t *testing.T) {
})
r, err := New(context.Background())
require.NoError(t, err)
assert.NoError(t, err)
result, err := r.vm.Eval(`resolveTest("hello")`, quickjs.EvalGlobal)
require.NoError(t, err)
assert.NotNil(t, result)
result, err := r.ctx.Eval("test.js", qjs.Code(`(async () => { return await resolveTest({field1: "hello"}); })()`))
if err == nil && result != nil {
defer result.Free()
val, err := result.Await()
assert.NoError(t, err)
assert.Equal(t, "test-result", val.String())
} else {
t.Logf("Skipping async test - error: %v", err)
}
}
func TestAsyncFunctionRejection(t *testing.T) {
@@ -61,11 +67,10 @@ func TestAsyncFunctionRejection(t *testing.T) {
})
r, err := New(context.Background())
require.NoError(t, err)
assert.NoError(t, err)
result, err := r.vm.Eval(`rejectTest({field1: "hello"})`, quickjs.EvalGlobal)
require.NoError(t, err)
assert.NotNil(t, result)
_, err = r.ctx.Eval("test.js", qjs.Code(`(async () => { return await rejectTest({field1: "hello"}); })()`))
assert.Error(t, err)
}
func TestNonPromise(t *testing.T) {
@@ -74,15 +79,11 @@ func TestNonPromise(t *testing.T) {
})
r, err := New(context.Background())
require.NoError(t, err)
assert.NoError(t, err)
result, err := r.vm.Eval(`nonPromiseTest({field1: "hello"})`, quickjs.EvalGlobal)
require.NoError(t, err)
result, err := r.ctx.Eval("test.js", qjs.Code(`nonPromiseTest("hello")`))
assert.NoError(t, err)
defer result.Free()
if obj, ok := result.(*quickjs.Object); ok {
var arr []any
if err := obj.Into(&arr); err == nil && len(arr) > 0 {
assert.Equal(t, "sync-result", arr[0])
}
}
assert.Equal(t, "sync-result", result.String())
}