initial commit

This commit is contained in:
2026-01-27 09:55:09 -05:00
commit ccbe9cd7bf
18 changed files with 2210 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
package runtime
import "io"
type RuntimeOption func(*Runtime)
func WithStdout(stdout io.Writer) RuntimeOption {
return func(r *Runtime) {
// Set Stdout
r.opts.Stdout = stdout
}
}
func WithStderr(stderr io.Writer) RuntimeOption {
return func(r *Runtime) {
// Set Stderr
r.opts.Stderr = stderr
}
}

180
internal/runtime/runtime.go Normal file
View File

@@ -0,0 +1,180 @@
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
}

View File

@@ -0,0 +1,163 @@
package runtime
import (
"bytes"
"context"
"os"
"strings"
"testing"
"github.com/fastschema/qjs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"reichard.io/poiesis/internal/functions"
)
type TestArgs struct {
Field1 string `json:"field1"`
}
func (t TestArgs) Validate() error {
return nil
}
func TestExecuteTypeScript(t *testing.T) {
// Create Buffers
var stdout, stderr bytes.Buffer
// Create Runtime
rt, err := New(context.Background(), WithStderr(&stderr), WithStdout(&stdout))
assert.NoError(t, err, "Expected no error")
// Create TypeScript Code
tsCode := `interface Person {
name: string;
age: number;
email: string;
}
function greet(person: Person): string {
return "Hello, " + person.name + "! You are " + person.age + " years old.";
}
const user: Person = {
name: "Alice",
age: 30,
email: "alice@example.com",
};
console.log(greet(user));
console.log("Email: " + user.email);
function calculateSum(a: number, b: number): number {
return a + b;
}
console.log("Sum of 5 and 10 is: " + calculateSum(5, 10));
`
// Create Temp File
tmpFile, err := os.CreateTemp("", "*.ts")
assert.NoError(t, err, "Failed to create temp file")
t.Cleanup(func() {
_ = os.Remove(tmpFile.Name())
})
// Write Code to File
_, err = tmpFile.WriteString(tsCode)
assert.NoError(t, err, "Failed to write to temp file")
err = tmpFile.Close()
assert.NoError(t, err, "Failed to close temp file")
// Run File
err = rt.RunFile(tmpFile.Name())
// Verify Execution
assert.NoError(t, err, "Expected no error")
assert.Empty(t, stderr.String(), "Expected no error output")
// Verify Output
output := stdout.String()
assert.Contains(t, output, "Hello, Alice!", "Should greet Alice")
assert.Contains(t, output, "You are 30 years old", "Should show age")
assert.Contains(t, output, "Email: alice@example.com", "Should show email")
assert.Contains(t, output, "Sum of 5 and 10 is: 15", "Should calculate sum correctly")
// Verify Line Count
lines := strings.Split(strings.TrimSpace(output), "\n")
assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 output lines")
}
func TestAsyncFunctionResolution(t *testing.T) {
// Register Async Function
functions.RegisterAsyncFunction("resolveTest", func(_ context.Context, args TestArgs) (string, error) {
return "test-result", nil
})
// Create Runtime
r, err := New(context.Background())
require.NoError(t, err)
// Execute Async Function - Must be awaited in async context
result, err := r.ctx.Eval("test.js", qjs.Code(`async function run() { return await resolveTest("hello"); }; run()`))
require.NoError(t, err)
require.NotNil(t, result)
defer result.Free()
// Verify Result
assert.Equal(t, "test-result", result.String())
}
func TestAsyncFunctionRejection(t *testing.T) {
// Register Async Function that Returns Error
functions.RegisterAsyncFunction("rejectTest", func(_ context.Context, args TestArgs) (string, error) {
return "", assert.AnError
})
// Create Runtime
r, err := New(context.Background())
require.NoError(t, err)
// Execute Async Function - Rejected Promises Throw When Awaited
_, err = r.ctx.Eval("test.js", qjs.Code(`async function run() { return await rejectTest("hello"); }; run()`))
assert.Error(t, err)
}
func TestNonPromise(t *testing.T) {
// Register Sync Function
functions.RegisterFunction("nonPromiseTest", func(_ context.Context, args TestArgs) (string, error) {
return "sync-result", nil
})
// Create Runtime
r, err := New(context.Background())
assert.NoError(t, err)
// Execute Sync Function
result, err := r.ctx.Eval("test.js", qjs.Code(`nonPromiseTest("hello")`))
assert.NoError(t, err)
defer result.Free()
// Verify Result
assert.Equal(t, "sync-result", result.String())
}
func TestGetTypeDeclarations(t *testing.T) {
// Register Function
functions.RegisterFunction("testFunc", func(_ context.Context, args TestArgs) (string, error) {
return "result", nil
})
// Create Runtime
r, err := New(context.Background())
require.NoError(t, err)
// Get Type Declarations
decls := r.GetTypeDeclarations()
// Verify Declarations
assert.Contains(t, decls, "interface TestArgs")
assert.Contains(t, decls, "declare function testFunc")
assert.Contains(t, decls, "field1: string")
}