package runtime import ( "context" "fmt" "os" "strings" "github.com/evanw/esbuild/pkg/api" "github.com/fastschema/qjs" "reichard.io/poiesis/internal/functions" _ "reichard.io/poiesis/internal/stdlib" ) type Runtime struct { ctx *qjs.Context opts qjs.Option funcs map[string]functions.Function typeDecls map[string]string } func New(ctx context.Context, opts ...RuntimeOption) (*Runtime, error) { // 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 } r.ctx = rt.Context() // Populate Globals if err := r.populateGlobals(); err != nil { return nil, err } return r, nil } func (r *Runtime) populateGlobals() error { // Initialize Maps r.funcs = make(map[string]functions.Function) r.typeDecls = make(map[string]string) // Load Requested Functions allFuncs := functions.GetRegisteredFunctions() for name, fn := range allFuncs { r.funcs[name] = fn if err := r.addFunctionTypes(fn); err != nil { return fmt.Errorf("failed to add types for function %s: %w", name, err) } } // Register Functions with QuickJS for name, fn := range r.funcs { 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) }) } } return nil } // addFunctionTypes adds types from a function to the runtime's type declarations. // Returns an error if there's a type conflict (same name, different definition). func (r *Runtime) addFunctionTypes(fn functions.Function) error { for name, def := range fn.Types() { if existing, ok := r.typeDecls[name]; ok && existing != def { return fmt.Errorf("type conflict: %s has conflicting definitions (existing: %s, new: %s)", name, existing, def) } r.typeDecls[name] = def } return nil } // GetTypeDeclarations returns all TypeScript type declarations for this runtime. // Includes both type definitions and function declarations. func (r *Runtime) GetTypeDeclarations() string { var decls []string // Add Type Definitions for _, def := range r.typeDecls { decls = append(decls, def) } // Add Function Declarations for _, fn := range r.funcs { decls = append(decls, fn.Definition()) } return strings.Join(decls, "\n\n") } func (r *Runtime) RunFile(filePath string) error { tsFileContent, err := os.ReadFile(filePath) if err != nil { return fmt.Errorf("error reading file: %w", err) } return r.RunCode(string(tsFileContent)) } func (r *Runtime) RunCode(tsCode string) error { transformedCode, err := r.transformCode(tsCode) if err != nil { return err } result, err := r.ctx.Eval("code.ts", qjs.Code(string(transformedCode))) if result != nil { result.Free() } return err } func (r *Runtime) transformCode(tsCode string) ([]byte, error) { result := api.Transform(tsCode, api.TransformOptions{ Loader: api.LoaderTS, Target: api.ES2022, Format: api.FormatIIFE, Sourcemap: api.SourceMapNone, TreeShaking: api.TreeShakingFalse, }) if len(result.Errors) > 0 { var b strings.Builder for i, e := range result.Errors { if i > 0 { b.WriteString(", ") } b.WriteString(e.Text) } return nil, fmt.Errorf("transpilation failed: %s", b.String()) } return result.Code, nil } func callFunc(this *qjs.This, fn functions.Function) (*qjs.Value, error) { qjsArgs := this.Args() fnArgs := fn.Arguments() var allArgs []any for i := range min(len(fnArgs), len(qjsArgs)) { rVal, err := qjs.JsArgToGo(qjsArgs[i], fnArgs[i]) if err != nil { return nil, fmt.Errorf("argument conversion failed: %w", err) } allArgs = append(allArgs, rVal.Interface()) } result, err := fn.Call(this.Context(), allArgs) if err != nil { return nil, err } val, err := qjs.ToJsValue(this.Context(), result) if err != nil { return nil, err } return val, nil }