diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..cfa93df --- /dev/null +++ b/.mise.toml @@ -0,0 +1,469 @@ +[tools] +go = "1.24" +"rust" = { version = "1.83.0" } + +# ============================================================================ +# Individual Implementation Build Tasks +# ============================================================================ + +# Basic implementation - Go +[tasks."build:basic:go"] +description = "Build basic implementation with Go" +env.GOOS = "js" +env.GOARCH = "wasm" +run = """ +set -euo pipefail +echo "Building basic implementation with Go..." +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +go build -trimpath -o main.wasm implementations/basic/main.go +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Basic Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# Basic implementation - TinyGo +[tasks."build:basic:tinygo"] +description = "Build basic implementation with TinyGo" +run = """ +set -euo pipefail +echo "Building basic implementation with TinyGo..." +if ! command -v tinygo >/dev/null 2>&1; then + echo "❌ Error: TinyGo is not installed" + exit 1 +fi +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +tinygo build -target wasm -o main.wasm implementations/basic/main.go +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Basic TinyGo build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# Goja implementation - Go only (doesn't work with TinyGo) +[tasks."build:goja:go"] +description = "Build goja implementation with Go" +env.GOOS = "js" +env.GOARCH = "wasm" +run = """ +set -euo pipefail +echo "Building goja implementation with Go..." +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +go build -trimpath -o main.wasm implementations/goja/main.go +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Goja Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# Javy implementation +[tasks."build:javy"] +description = "Build Javy implementation (JavaScript to WASM)" +run = """ +set -euo pipefail +echo "Building Javy implementation..." +if ! command -v javy >/dev/null 2>&1; then + echo "❌ Error: Javy CLI is not installed or not on PATH" + echo "Please install the Javy CLI from https://github.com/bytecodealliance/javy" + exit 1 +fi +mkdir -p assets/wasm +cd implementations/javy +javy emit-plugin -o plugin.wasm +javy build -C dynamic -C plugin=plugin.wasm -o transform_dynamic.wasm transform.js +cd ../.. +gzip -9 -c implementations/javy/transform_dynamic.wasm > assets/wasm/lib.wasm.gz +echo "✅ Javy build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# Porffor implementation +[tasks."build:porffor"] +description = "Build Porffor implementation (AOT JavaScript to WASM)" +run = """ +set -euo pipefail +echo "Building Porffor implementation..." +mkdir -p assets/wasm +cd implementations/porffor +npx porf wasm transform.js transform.wasm +cd ../.. +gzip -9 -c implementations/porffor/transform.wasm > assets/wasm/lib.wasm.gz +echo "✅ Porffor build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# QuickJS implementation +[tasks."build:quickjs"] +description = "Build QuickJS implementation (Rust + QuickJS)" +run = """ +set -euo pipefail +echo "Building QuickJS implementation..." +if ! command -v cargo >/dev/null 2>&1; then + echo "❌ Error: Rust/Cargo is not installed" + exit 1 +fi +mkdir -p assets/wasm +cd implementations/quickjs +cargo build --target wasm32-wasip1 --release +cd ../.. +gzip -9 -c implementations/quickjs/target/wasm32-wasip1/release/quickjs_transform.wasm > assets/wasm/lib.wasm.gz +echo "✅ QuickJS build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# AssemblyScript implementation +[tasks."build:assemblyscript"] +description = "Build AssemblyScript implementation with json-as" +run = """ +set -euo pipefail +echo "Building AssemblyScript implementation..." +if ! command -v npm >/dev/null 2>&1; then + echo "❌ Error: npm is not installed" + exit 1 +fi +mkdir -p assets/wasm +cd implementations/assemblyscript +npm install --legacy-peer-deps > /dev/null 2>&1 +npm run asbuild > /dev/null 2>&1 +cd ../.. +gzip -9 -c implementations/assemblyscript/build/release.wasm > assets/wasm/lib.wasm.gz +echo "✅ AssemblyScript build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# AssemblyScript optimized implementation +[tasks."build:assemblyscript:optimized"] +description = "Build optimized AssemblyScript implementation" +run = """ +set -euo pipefail +echo "Building optimized AssemblyScript implementation..." +mkdir -p assets/wasm +cd implementations/assemblyscript +npm install --legacy-peer-deps > /dev/null 2>&1 +npm run asbuild > /dev/null 2>&1 +if command -v wasm-opt >/dev/null 2>&1; then + wasm-opt -Oz --enable-bulk-memory --strip-debug --strip-dwarf --strip-producers --converge build/release.wasm -o build/release_opt.wasm + mv build/release_opt.wasm build/release.wasm +fi +cd ../.. +gzip -9 -c implementations/assemblyscript/build/release.wasm > assets/wasm/lib.wasm.gz +echo "✅ AssemblyScript optimized build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + + +# ============================================================================ +# Optimized Build Tasks +# ============================================================================ + +[tasks."build:basic:go:optimized"] +description = "Build basic implementation with optimized Go" +env.GOOS = "js" +env.GOARCH = "wasm" +env.CGO_ENABLED = "0" +run = """ +set -euo pipefail +echo "Building basic implementation with optimized Go..." +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +go build -ldflags="-s -w -buildid=" -gcflags="-l=4 -B -C" -trimpath -o main.wasm implementations/basic/main.go +if command -v wasm-opt >/dev/null 2>&1; then + wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main.wasm -o main_opt.wasm + mv main_opt.wasm main.wasm +fi +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Basic optimized Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +[tasks."build:basic:tinygo:optimized"] +description = "Build basic implementation with optimized TinyGo" +run = """ +set -euo pipefail +echo "Building basic implementation with optimized TinyGo..." +if ! command -v tinygo >/dev/null 2>&1; then + echo "❌ Error: TinyGo is not installed" + exit 1 +fi +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +tinygo build -target wasm -no-debug -gc=leaking -opt=z -size=full -panic=trap -o main.wasm implementations/basic/main.go +if command -v wasm-opt >/dev/null 2>&1; then + wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --converge --all-features main.wasm -o main_opt.wasm + mv main_opt.wasm main.wasm +fi +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Basic optimized TinyGo build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +[tasks."build:goja:go:optimized"] +description = "Build goja implementation with optimized Go" +env.GOOS = "js" +env.GOARCH = "wasm" +env.CGO_ENABLED = "0" +run = """ +set -euo pipefail +echo "Building goja implementation with optimized Go..." +mkdir -p assets/wasm +go mod tidy > /dev/null 2>&1 +go build -ldflags="-s -w -buildid=" -gcflags="-l=4 -B -C" -trimpath -o main.wasm implementations/goja/main.go +if command -v wasm-opt >/dev/null 2>&1; then + wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main.wasm -o main_opt.wasm + mv main_opt.wasm main.wasm +fi +gzip -9 -c main.wasm > assets/wasm/lib.wasm.gz +rm main.wasm +cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js +echo "✅ Goja optimized Go build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + + +[tasks."build:porffor:optimized"] +description = "Build Porffor implementation with optimization" +run = """ +set -euo pipefail +echo "Building optimized Porffor implementation..." +mkdir -p assets/wasm +cd implementations/porffor +npx porf wasm -O3 transform.js transform.wasm +cd ../.. +gzip -9 -c implementations/porffor/transform.wasm > assets/wasm/lib.wasm.gz +echo "✅ Porffor optimized build complete: $(du -h assets/wasm/lib.wasm.gz | cut -f1)" +""" + +# ============================================================================ +# Individual Test Tasks (with dependencies on builds) +# ============================================================================ + +[tasks."test:basic:go"] +description = "Test basic implementation with Go build" +depends = ["build:basic:go"] +run = """ +set -euo pipefail +echo "Testing basic Go implementation..." +npm install > /dev/null 2>&1 +npx vitest run --reporter=verbose +""" + +[tasks."test:basic:tinygo"] +description = "Test basic implementation with TinyGo build" +depends = ["build:basic:tinygo"] +run = """ +set -euo pipefail +echo "Testing basic TinyGo implementation..." +npm install > /dev/null 2>&1 +npx vitest run --reporter=verbose +""" + +[tasks."test:goja:go"] +description = "Test goja implementation with Go build" +depends = ["build:goja:go"] +run = """ +set -euo pipefail +echo "Testing goja Go implementation..." +npm install > /dev/null 2>&1 +npx vitest run --reporter=verbose +""" + + +[tasks."test:javy"] +description = "Test Javy implementation" +depends = ["build:javy"] +run = """ +set -euo pipefail +echo "Testing Javy implementation..." +node -e "import('./implementations/javy/javy-adapter.js').then(async (javy) => { + console.log('🚀 Javy adapter loaded'); + try { + const health = await javy.healthCheck(); + console.log('💓 Health check:', JSON.parse(health).status); + const result = await javy.transformData('{\"name\":\"test\",\"value\":42}'); + const parsed = JSON.parse(result); + console.log('🔄 Transform test:', parsed.engine === 'javy' ? 'PASS' : 'FAIL'); + console.log('✅ Javy tests passed!'); + } catch (error) { + console.error('❌ Javy test failed:', error.message); + process.exit(1); + } +}).catch(console.error);" +""" + +[tasks."test:porffor"] +description = "Test Porffor implementation" +depends = ["build:porffor"] +run = """ +set -euo pipefail +echo "Testing Porffor implementation..." +if command -v wasmer >/dev/null 2>&1; then + cd implementations/porffor + if [ -f porffor-wasmer-test.sh ]; then + ./porffor-wasmer-test.sh + else + echo "⚠️ Porffor test script not found" + fi +else + echo "⚠️ Wasmer not installed, skipping Porffor WASI test" +fi +""" + +[tasks."test:quickjs"] +description = "Test QuickJS implementation" +depends = ["build:quickjs"] +run = """ +set -euo pipefail +echo "Testing QuickJS implementation..." +if [ -f implementations/quickjs/quickjs-wasi-test.js ]; then + node implementations/quickjs/quickjs-wasi-test.js +elif command -v wasmer >/dev/null 2>&1; then + cd implementations/quickjs + if [ -f quickjs-wasmer-test.sh ]; then + ./quickjs-wasmer-test.sh + else + echo "⚠️ QuickJS test script not found" + fi +else + echo "⚠️ No QuickJS test available" +fi +""" + +[tasks."test:assemblyscript"] +description = "Test AssemblyScript implementation" +depends = ["build:assemblyscript"] +run = """ +set -euo pipefail +echo "Testing AssemblyScript implementation..." +cd implementations/assemblyscript +node test.js +""" + +# ============================================================================ +# Master Test Task - depends on all individual tests +# ============================================================================ + +[tasks.test] +description = "Run all working implementation tests" +depends = [ + "test:basic:go", + "test:basic:tinygo", + "test:goja:go", + "test:porffor", + "test:quickjs", + "test:assemblyscript" +] +run = """ +echo "✅ All working tests completed successfully!" +echo "" +echo "Note: Skipped implementations:" +echo " - javy (CLI not available via npm package)" +""" + +# ============================================================================ +# Build All Task - builds all implementations +# ============================================================================ + +[tasks."build:all"] +description = "Build all implementations" +depends = [ + "build:basic:go", + "build:basic:tinygo", + "build:goja:go", + "build:javy", + "build:porffor", + "build:quickjs", + "build:assemblyscript" +] +run = """ +echo "✅ All builds completed successfully!" +""" + +[tasks."build:all:optimized"] +description = "Build all implementations with optimization" +depends = [ + "build:basic:go:optimized", + "build:basic:tinygo:optimized", + "build:goja:go:optimized", + "build:porffor:optimized", + "build:quickjs", + "build:assemblyscript:optimized" +] +run = """ +echo "✅ All optimized builds completed successfully!" +""" + +# ============================================================================ +# Utility Tasks +# ============================================================================ + +[tasks.clean] +description = "Clean build artifacts and node_modules" +run = """ +set -euo pipefail +echo "Cleaning build artifacts..." +rm -rf assets/ +rm -f main.wasm main.wasm.gz +rm -rf node_modules/ +rm -f implementations/javy/*.wasm +rm -f implementations/porffor/*.wasm +rm -rf implementations/quickjs/target/ +echo "✅ Clean complete" +""" + +[tasks."size:compare"] +description = "Compare sizes of all implementations" +run = """ +set -euo pipefail +echo "Building and comparing all implementations..." +echo "" +echo "| Implementation | Compiler | Size (gzipped) |" +echo "|----------------|----------|----------------|" + +# Basic - Go +mise run build:basic:go > /dev/null 2>&1 +printf "| Basic | Go | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Basic - Go Optimized +mise run build:basic:go:optimized > /dev/null 2>&1 +printf "| Basic | Go (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Basic - TinyGo +mise run build:basic:tinygo > /dev/null 2>&1 +printf "| Basic | TinyGo | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Basic - TinyGo Optimized +mise run build:basic:tinygo:optimized > /dev/null 2>&1 +printf "| Basic | TinyGo (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Goja - Go +mise run build:goja:go > /dev/null 2>&1 +printf "| Goja | Go | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Goja - Go Optimized +mise run build:goja:go:optimized > /dev/null 2>&1 +printf "| Goja | Go (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# Javy +if command -v javy >/dev/null 2>&1; then + mise run build:javy > /dev/null 2>&1 + printf "| Javy | Dynamic | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" +fi + +# Porffor +mise run build:porffor > /dev/null 2>&1 +printf "| Porffor | AOT | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +mise run build:porffor:optimized > /dev/null 2>&1 +printf "| Porffor | AOT (opt) | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# QuickJS +mise run build:quickjs > /dev/null 2>&1 +printf "| QuickJS | Rust | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +# AssemblyScript +mise run build:assemblyscript > /dev/null 2>&1 +printf "| AssemblyScript | AssemblyScript | %s |\n" "$(du -h assets/wasm/lib.wasm.gz | cut -f1)" + +echo "" +echo "✅ Size comparison complete!" +""" + diff --git a/Makefile b/Makefile index ed2c919..9be692d 100644 --- a/Makefile +++ b/Makefile @@ -1,387 +1,120 @@ .PHONY: build build-go build-tinygo build-optimized build-go-optimized build-tinygo-optimized test test-go test-tinygo clean watch help size-comparison -# Default implementation -IMPL ?= basic +# Default implementation (deprecated): use mise task arguments instead + +# Pass-through args to mise: use "make " +ARGS := $(filter-out $@,$(MAKECMDGOALS)) # Default target help: - @echo "Available targets:" - @echo " build - Build the WASM binary with Go (default)" - @echo " build-go - Build the WASM binary with Go" - @echo " build-tinygo - Build the WASM binary with TinyGo (smaller size)" - @echo " build-optimized - Build with maximum Go optimization + wasm-opt" - @echo " build-go-optimized - Build with maximum Go optimization + wasm-opt" - @echo " build-tinygo-optimized - Build with maximum TinyGo optimization + wasm-opt" - @echo " build-javy - Build with Javy (JavaScript-to-WASM)" - @echo " build-porffor - Build with Porffor (AOT JavaScript)" - @echo " build-quickjs - Build with QuickJS (Rust + JavaScript engine)" - @echo " test - Run tests with Go build (default)" - @echo " test-go - Run tests with Go build" - @echo " test-tinygo - Run tests with TinyGo build" - @echo " test-wasmer - Test WASI-compatible implementations with Wasmer" - @echo " test-quickjs-wasmer - Test QuickJS implementation with Wasmer" - @echo " test-porffor-wasmer - Test Porffor implementation with Wasmer" - @echo " clean - Clean build artifacts" - @echo " watch - Build in watch mode with Go" - @echo " size-comparison - Compare binary sizes for all implementations" - @echo " help - Show this help message" + @echo "This project uses mise for toolchain and tasks." @echo "" - @echo "Available implementations:" - @ls -1 implementations/ 2>/dev/null || echo " No implementations found" + @echo "Common commands:" + @echo " mise install # Install tools (Go 1.23)" + @echo " mise tasks # List available tasks" + @echo " mise run build # Build with Go (alias for build-go)" + @echo " mise run build-go-optimized # Build with Go (optimized)" + @echo " mise run build-tinygo # Build with TinyGo" + @echo " mise run test # Test (builds first)" + @echo " mise run clean # Clean artifacts" + @echo " mise run watch # Watch build (requires fswatch)" + @echo " make build # Convenience wrapper to mise" + @echo " make test # Convenience wrapper to mise" @echo "" - @echo "Usage: make build IMPL=" - @echo "Example: make build IMPL=basic" - @echo "Example: make build-optimized IMPL=goja" + @echo "Note: Makefile targets are retained for compatibility but logic has moved to mise." -# Default build uses Go -build: build-go +# Default build uses mise task (accepts positional args: make build ) +build: + @mise run build $(ARGS) # Build the WASM binary with Go build-go: - @echo "Building WASM binary with Go ($(IMPL) implementation)..." - @if [ ! -f "implementations/$(IMPL)/main.go" ]; then \ - echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \ - exit 1; \ - fi - @mkdir -p assets/wasm - @echo "Installing Go dependencies..." - @go mod tidy > /dev/null 2>&1 - @echo "Compiling to WASM with Go..." - @GOOS=js GOARCH=wasm go build -trimpath -o main.wasm implementations/$(IMPL)/main.go - @echo "Compressing WASM binary..." - @gzip -9 -c main.wasm > main.wasm.gz - @rm main.wasm - @mv main.wasm.gz assets/wasm/lib.wasm.gz - @cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js - @echo "✅ Go build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-go $(ARGS) # Build the WASM binary with TinyGo build-tinygo: - @echo "Building WASM binary with TinyGo ($(IMPL) implementation)..." - @if [ ! -f "implementations/$(IMPL)/main.go" ]; then \ - echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \ - exit 1; \ - fi - @mkdir -p assets/wasm - @echo "Installing Go dependencies..." - @go mod tidy > /dev/null 2>&1 - @echo "Compiling to WASM with TinyGo..." - @if ! command -v tinygo >/dev/null 2>&1; then \ - echo "❌ Error: TinyGo is not installed. Please install it from https://tinygo.org/getting-started/install/"; \ - exit 1; \ - fi - @tinygo build -target wasm -o main.wasm implementations/$(IMPL)/main.go - @echo "Compressing WASM binary..." - @gzip -9 -c main.wasm > main.wasm.gz - @rm main.wasm - @mv main.wasm.gz assets/wasm/lib.wasm.gz - @cp "$$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js - @echo "✅ TinyGo build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-tinygo $(ARGS) -# Default optimized build uses Go -build-optimized: build-go-optimized +# Default optimized build uses Go (mise wrapper) +build-optimized: + @mise run build-optimized $(ARGS) # Build the WASM binary with maximum Go optimization + wasm-opt build-go-optimized: - @echo "Building WASM binary with maximum Go optimization ($(IMPL) implementation)..." - @if [ ! -f "implementations/$(IMPL)/main.go" ]; then \ - echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \ - exit 1; \ - fi - @mkdir -p assets/wasm - @echo "Installing Go dependencies..." - @go mod tidy > /dev/null 2>&1 - @echo "Compiling to WASM with maximum Go optimization..." - @CGO_ENABLED=0 GOOS=js GOARCH=wasm go build \ - -ldflags="-s -w -buildid=" \ - -gcflags="-l=4 -B -C" \ - -trimpath \ - -o main_raw.wasm implementations/$(IMPL)/main.go - @echo "Raw size: $$(du -h main_raw.wasm | cut -f1)" - @echo "Optimizing with wasm-opt..." - @if command -v wasm-opt >/dev/null 2>&1; then \ - wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --converge main_raw.wasm -o main.wasm; \ - echo "Optimized size: $$(du -h main.wasm | cut -f1)"; \ - else \ - echo "⚠️ wasm-opt not found, skipping post-build optimization"; \ - mv main_raw.wasm main.wasm; \ - fi - @echo "Compressing WASM binary..." - @gzip -9 -c main.wasm > main.wasm.gz - @rm main.wasm main_raw.wasm 2>/dev/null || true - @mv main.wasm.gz assets/wasm/lib.wasm.gz - @cp "$$(go env GOROOT)/lib/wasm/wasm_exec.js" assets/wasm/wasm_exec.js - @echo "✅ Optimized Go build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-go-optimized $(ARGS) # Build the WASM binary with maximum TinyGo optimization + wasm-opt build-tinygo-optimized: - @echo "Building WASM binary with maximum TinyGo optimization ($(IMPL) implementation)..." - @if [ ! -f "implementations/$(IMPL)/main.go" ]; then \ - echo "❌ Error: Implementation '$(IMPL)' not found in implementations/$(IMPL)/main.go"; \ - exit 1; \ - fi - @mkdir -p assets/wasm - @echo "Installing Go dependencies..." - @go mod tidy > /dev/null 2>&1 - @echo "Compiling to WASM with maximum TinyGo optimization..." - @if ! command -v tinygo >/dev/null 2>&1; then \ - echo "❌ Error: TinyGo is not installed. Please install it from https://tinygo.org/getting-started/install/"; \ - exit 1; \ - fi - @tinygo build \ - -o main_raw.wasm \ - -target wasm \ - -no-debug \ - -gc=leaking \ - -opt=z \ - -size=full \ - -panic=trap \ - implementations/$(IMPL)/main.go - @echo "Raw size: $$(du -h main_raw.wasm | cut -f1)" - @echo "Optimizing with wasm-opt..." - @if command -v wasm-opt >/dev/null 2>&1; then \ - wasm-opt -Oz --enable-bulk-memory --enable-sign-ext --enable-mutable-globals --converge --all-features main_raw.wasm -o main.wasm; \ - echo "Optimized size: $$(du -h main.wasm | cut -f1)"; \ - else \ - echo "⚠️ wasm-opt not found, skipping post-build optimization"; \ - mv main_raw.wasm main.wasm; \ - fi - @echo "Compressing WASM binary..." - @gzip -9 -c main.wasm > main.wasm.gz - @rm main.wasm main_raw.wasm 2>/dev/null || true - @mv main.wasm.gz assets/wasm/lib.wasm.gz - @cp "$$(tinygo env TINYGOROOT)/targets/wasm_exec.js" assets/wasm/wasm_exec.js - @echo "✅ Optimized TinyGo build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-tinygo-optimized $(ARGS) # Build WASM binary with Javy (JavaScript to WASM) using dynamic linking build-javy: - @if [ "$(IMPL)" != "javy" ]; then \ - echo "❌ Error: Javy build only supports IMPL=javy"; \ - exit 1; \ - fi - @echo "Building WASM binary with Javy ($(IMPL) implementation)..." - @mkdir -p assets/wasm - @echo "Creating Javy plugin..." - @if ! command -v javy >/dev/null 2>&1; then \ - echo "❌ Error: Javy is not installed. Please install it from https://github.com/bytecodealliance/javy"; \ - exit 1; \ - fi - @cd implementations/$(IMPL) && javy emit-plugin -o plugin.wasm - @echo "Compiling JavaScript to WASM with dynamic linking..." - @cd implementations/$(IMPL) && javy build -C dynamic -C plugin=plugin.wasm -o transform_dynamic.wasm transform.js - @echo "Plugin size: $$(du -h implementations/$(IMPL)/plugin.wasm | cut -f1)" - @echo "Dynamic module size: $$(du -h implementations/$(IMPL)/transform_dynamic.wasm | cut -f1)" - @echo "Total size: $$(du -ch implementations/$(IMPL)/plugin.wasm implementations/$(IMPL)/transform_dynamic.wasm | tail -1 | cut -f1)" - @echo "Compressing dynamic module..." - @gzip -c implementations/$(IMPL)/transform_dynamic.wasm > assets/wasm/lib.wasm.gz - @echo "✅ Javy build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-javy $(ARGS) # Build WASM binary with Javy optimization (dynamic linking is already optimized) build-javy-optimized: - @if [ "$(IMPL)" != "javy" ]; then \ - echo "❌ Error: Javy build only supports IMPL=javy"; \ - exit 1; \ - fi - @echo "Building WASM binary with optimized Javy ($(IMPL) implementation)..." - @echo "Note: Javy dynamic linking is already highly optimized (4KB modules)" - @echo "Note: wasm-opt corrupts Javy plugins, so using standard dynamic build" - @$(MAKE) build-javy IMPL=$(IMPL) - @echo "✅ Optimized Javy build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-javy-optimized $(ARGS) # Build WASM binary with Porffor (AOT JavaScript to WASM) build-porffor: - @if [ "$(IMPL)" != "porffor" ]; then \ - echo "❌ Error: Porffor build only supports IMPL=porffor"; \ - exit 1; \ - fi - @echo "Building WASM binary with Porffor ($(IMPL) implementation)..." - @mkdir -p assets/wasm - @echo "Compiling JavaScript to WASM with Porffor AOT compiler..." - @if ! command -v porf >/dev/null 2>&1; then \ - echo "❌ Error: Porffor is not installed. Please install it with: npm install -g porffor@latest"; \ - exit 1; \ - fi - @cd implementations/$(IMPL) && porf wasm transform.js transform.wasm - @echo "Raw size: $$(du -h implementations/$(IMPL)/transform.wasm | cut -f1)" - @echo "Compressing WASM binary..." - @gzip -c implementations/$(IMPL)/transform.wasm > assets/wasm/lib.wasm.gz - @echo "✅ Porffor build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-porffor $(ARGS) # Build WASM binary with Porffor optimization build-porffor-optimized: - @if [ "$(IMPL)" != "porffor" ]; then \ - echo "❌ Error: Porffor build only supports IMPL=porffor"; \ - exit 1; \ - fi - @echo "Building WASM binary with optimized Porffor ($(IMPL) implementation)..." - @mkdir -p assets/wasm - @echo "Compiling JavaScript to WASM with Porffor AOT compiler (max optimization)..." - @if ! command -v porf >/dev/null 2>&1; then \ - echo "❌ Error: Porffor is not installed. Please install it with: npm install -g porffor@latest"; \ - exit 1; \ - fi - @cd implementations/$(IMPL) && porf wasm -O3 transform.js transform_opt.wasm - @echo "Optimized size: $$(du -h implementations/$(IMPL)/transform_opt.wasm | cut -f1)" - @echo "Compressing WASM binary..." - @gzip -c implementations/$(IMPL)/transform_opt.wasm > assets/wasm/lib.wasm.gz - @echo "✅ Optimized Porffor build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-porffor-optimized $(ARGS) -# Default test uses Go -test: test-go +# Default test uses mise task (accepts positional args: make test ) +test: + @mise run test $(ARGS) -# Run tests with Go build (builds first) -test-go: build-go - @echo "Installing Node.js dependencies..." - @npm install > /dev/null 2>&1 - @echo "Running tests with Go build..." - @npx vitest run +# Run tests with Go build (mise wrapper) +test-go: + @mise run test-go $(ARGS) -# Run tests with TinyGo build (builds first) -test-tinygo: build-tinygo - @echo "Installing Node.js dependencies..." - @npm install > /dev/null 2>&1 - @echo "Running tests with TinyGo build..." - @npx vitest run +# Run tests with TinyGo build (mise wrapper) +test-tinygo: + @mise run test-tinygo $(ARGS) # Test Javy implementation directly test-javy: - @$(MAKE) build-javy IMPL=javy > /dev/null 2>&1 - @echo "Testing Javy implementation..." - @node -e "import('./implementations/javy/javy-adapter.js').then(async (javy) => { \ - console.log('🚀 Javy adapter loaded'); \ - try { \ - const health = await javy.healthCheck(); \ - console.log('💓 Health check:', JSON.parse(health).status); \ - const result = await javy.transformData('{\"name\":\"test\",\"value\":42}'); \ - const parsed = JSON.parse(result); \ - console.log('🔄 Transform test:', parsed.engine === 'javy' ? 'PASS' : 'FAIL'); \ - console.log('✅ All Javy tests passed!'); \ - } catch (error) { \ - console.error('❌ Javy test failed:', error.message); \ - process.exit(1); \ - } \ - }).catch(console.error);" 2>/dev/null + @mise run test-javy $(ARGS) # Clean build artifacts clean: - @echo "Cleaning build artifacts..." - @rm -rf assets/ - @rm -f main.wasm main.wasm.gz - @rm -rf node_modules/ - @echo "✅ Clean complete" + @mise run clean # Build in watch mode with Go (requires fswatch) watch: - @if ! command -v fswatch >/dev/null 2>&1; then \ - echo "❌ Error: fswatch is not installed. Install with: brew install fswatch (macOS) or apt-get install fswatch (Linux)"; \ - exit 1; \ - fi - @echo "🚀 Starting watch mode with Go ($(IMPL)). Press Ctrl+C to stop." - @$(MAKE) build-go IMPL=$(IMPL) - @echo "👀 Watching for changes to *.go files..." - @fswatch -r -e ".*" -i "\\.go$$" implementations/$(IMPL)/ | while read file; do \ - echo "🔄 Change detected in $$file"; \ - $(MAKE) build-go IMPL=$(IMPL); \ - done + @mise run watch $(ARGS) # Compare binary sizes for all implementations size-comparison: - @echo "=== Binary Size Comparison ===" - @echo "| Implementation | Go (KB) | Go Opt (KB) | TinyGo (KB) | TinyGo Opt (KB) | Javy (KB) | Javy Opt (KB) | Best Reduction |" - @echo "|---------------|---------|-------------|-------------|-----------------|-----------|---------------|----------------|" - @for impl in $$(ls implementations/); do \ - if [ -f "implementations/$$impl/main.go" ]; then \ - echo -n "| $$impl | "; \ - $(MAKE) build-go IMPL=$$impl > /dev/null 2>&1; \ - go_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$go_size | "; \ - $(MAKE) build-go-optimized IMPL=$$impl > /dev/null 2>&1; \ - go_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$go_opt_size | "; \ - if [ "$$impl" = "goja" ]; then \ - echo -n "N/A* | N/A* | N/A | N/A | "; \ - reduction=$$(echo "scale=1; ($$go_size - $$go_opt_size) * 100 / $$go_size" | bc -l 2>/dev/null || echo "N/A"); \ - echo "$$reduction% |"; \ - else \ - $(MAKE) build-tinygo IMPL=$$impl > /dev/null 2>&1; \ - tinygo_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$tinygo_size | "; \ - $(MAKE) build-tinygo-optimized IMPL=$$impl > /dev/null 2>&1; \ - tinygo_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$tinygo_opt_size | N/A | N/A | "; \ - reduction=$$(echo "scale=1; ($$go_size - $$tinygo_opt_size) * 100 / $$go_size" | bc -l 2>/dev/null || echo "N/A"); \ - echo "$$reduction% |"; \ - fi \ - elif [ -f "implementations/$$impl/transform.js" ]; then \ - echo -n "| $$impl | N/A | N/A | N/A | N/A | "; \ - $(MAKE) build-javy IMPL=$$impl > /dev/null 2>&1; \ - javy_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$javy_size | "; \ - $(MAKE) build-javy-optimized IMPL=$$impl > /dev/null 2>&1; \ - javy_opt_size=$$(du -k assets/wasm/lib.wasm.gz | cut -f1); \ - echo -n "$$javy_opt_size | "; \ - reduction=$$(echo "scale=1; ($$javy_size - $$javy_opt_size) * 100 / $$javy_size" | bc -l 2>/dev/null || echo "N/A"); \ - echo "$$reduction% |"; \ - fi \ - done - @echo "" - @echo "*Goja implementation doesn't compile with TinyGo due to dependency complexity" + @mise run size-comparison gzipped-sizes: - @echo "�� Measuring gzipped sizes of all WASM binaries..." - @node measure-gzipped-sizes.js + @mise run gzipped-sizes # Build WASM binary with QuickJS (Rust + QuickJS JavaScript engine) build-quickjs: - @if [ "$(IMPL)" != "quickjs" ]; then \ - echo "❌ Error: QuickJS build only supports IMPL=quickjs"; \ - exit 1; \ - fi - @echo "Building WASM binary with QuickJS ($(IMPL) implementation)..." - @mkdir -p assets/wasm - @echo "Compiling Rust + QuickJS to WASM with WASI target..." - @if ! command -v cargo >/dev/null 2>&1; then \ - echo "❌ Error: Rust/Cargo is not installed. Please install it from https://rustup.rs/"; \ - exit 1; \ - fi - @cd implementations/$(IMPL) && cargo build --target wasm32-wasip1 --release - @echo "Raw size: $$(du -h implementations/$(IMPL)/target/wasm32-wasip1/release/quickjs_transform.wasm | cut -f1)" - @echo "Compressing WASM binary..." - @gzip -c implementations/$(IMPL)/target/wasm32-wasip1/release/quickjs_transform.wasm > assets/wasm/lib.wasm.gz - @echo "✅ QuickJS build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-quickjs $(ARGS) # Build WASM binary with QuickJS optimization (already optimized with --release) build-quickjs-optimized: - @if [ "$(IMPL)" != "quickjs" ]; then \ - echo "❌ Error: QuickJS build only supports IMPL=quickjs"; \ - exit 1; \ - fi - @echo "Building WASM binary with optimized QuickJS ($(IMPL) implementation)..." - @echo "Note: QuickJS Rust build is already optimized with --release profile" - @$(MAKE) build-quickjs IMPL=$(IMPL) - @echo "✅ Optimized QuickJS build complete ($(IMPL)): assets/wasm/lib.wasm.gz ($$(du -h assets/wasm/lib.wasm.gz | cut -f1))" + @mise run build-quickjs-optimized $(ARGS) # Test QuickJS implementation directly test-quickjs: - @$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1 - @echo "Testing QuickJS implementation..." - @node implementations/quickjs/quickjs-wasi-test.js 2>/dev/null + @mise run test-quickjs $(ARGS) # Test WASI-compatible implementations with Wasmer test-wasmer: - @echo "🧪 Testing WASI-compatible implementations with Wasmer..." - @./test-wasmer.sh + @mise run test-wasmer # Test individual implementations with Wasmer test-quickjs-wasmer: - @$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1 - @echo "Testing QuickJS with Wasmer..." - @cd implementations/quickjs && ./quickjs-wasmer-test.sh + @mise run test-quickjs-wasmer test-porffor-wasmer: - @$(MAKE) build-porffor IMPL=porffor > /dev/null 2>&1 - @echo "Testing Porffor with Wasmer..." - @cd implementations/porffor && ./porffor-wasmer-test.sh + @mise run test-porffor-wasmer -measure-all: size-comparison gzipped-sizes - @echo "" - @echo "✅ Complete size analysis finished!" +measure-all: + @mise run measure-all diff --git a/assets/wasm/wasm_exec.js b/assets/wasm/wasm_exec.js index 53ea75f..d71af9e 100644 --- a/assets/wasm/wasm_exec.js +++ b/assets/wasm/wasm_exec.js @@ -1,53 +1,26 @@ // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// -// This file has been modified for use by the TinyGo compiler. + +"use strict"; (() => { - // Map multiple JavaScript environments to a single common API, - // preferring web standards over Node.js API. - // - // Environments considered: - // - Browsers - // - Node.js - // - Electron - // - Parcel - - if (typeof global !== "undefined") { - // global already exists - } else if (typeof window !== "undefined") { - window.global = window; - } else if (typeof self !== "undefined") { - self.global = self; - } else { - throw new Error("cannot export Go (neither global, window nor self is defined)"); - } - - if (!global.require && typeof require !== "undefined") { - global.require = require; - } - - if (!global.fs && global.require) { - global.fs = require("node:fs"); - } - const enosys = () => { const err = new Error("not implemented"); err.code = "ENOSYS"; return err; }; - if (!global.fs) { + if (!globalThis.fs) { let outputBuf = ""; - global.fs = { - constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused + globalThis.fs = { + constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused writeSync(fd, buf) { outputBuf += decoder.decode(buf); const nl = outputBuf.lastIndexOf("\n"); if (nl != -1) { - console.log(outputBuf.substr(0, nl)); - outputBuf = outputBuf.substr(nl + 1); + console.log(outputBuf.substring(0, nl)); + outputBuf = outputBuf.substring(nl + 1); } return buf.length; }, @@ -85,8 +58,8 @@ }; } - if (!global.process) { - global.process = { + if (!globalThis.process) { + globalThis.process = { getuid() { return -1; }, getgid() { return -1; }, geteuid() { return -1; }, @@ -100,53 +73,66 @@ } } - if (!global.crypto) { - const nodeCrypto = require("node:crypto"); - global.crypto = { - getRandomValues(b) { - nodeCrypto.randomFillSync(b); - }, - }; + if (!globalThis.path) { + globalThis.path = { + resolve(...pathSegments) { + return pathSegments.join("/"); + } + } } - if (!global.performance) { - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; + if (!globalThis.crypto) { + throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)"); } - if (!global.TextEncoder) { - global.TextEncoder = require("node:util").TextEncoder; + if (!globalThis.performance) { + throw new Error("globalThis.performance is not available, polyfill required (performance.now only)"); } - if (!global.TextDecoder) { - global.TextDecoder = require("node:util").TextDecoder; + if (!globalThis.TextEncoder) { + throw new Error("globalThis.TextEncoder is not available, polyfill required"); } - // End of polyfills for common API. + if (!globalThis.TextDecoder) { + throw new Error("globalThis.TextDecoder is not available, polyfill required"); + } const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); - let reinterpretBuf = new DataView(new ArrayBuffer(8)); - var logLine = []; - const wasmExit = {}; // thrown to exit via proc_exit (not an error) - global.Go = class { + globalThis.Go = class { constructor() { - this._callbackTimeouts = new Map(); + this.argv = ["js"]; + this.env = {}; + this.exit = (code) => { + if (code !== 0) { + console.warn("exit code:", code); + } + }; + this._exitPromise = new Promise((resolve) => { + this._resolveExitPromise = resolve; + }); + this._pendingEvent = null; + this._scheduledTimeouts = new Map(); this._nextCallbackTimeoutID = 1; - const mem = () => { - // The buffer may change when requesting more memory. - return new DataView(this._inst.exports.memory.buffer); + const setInt64 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); } - const unboxValue = (v_ref) => { - reinterpretBuf.setBigInt64(0, v_ref, true); - const f = reinterpretBuf.getFloat64(0, true); + const setInt32 = (addr, v) => { + this.mem.setUint32(addr + 0, v, true); + } + + const getInt64 = (addr) => { + const low = this.mem.getUint32(addr + 0, true); + const high = this.mem.getInt32(addr + 4, true); + return low + high * 4294967296; + } + + const loadValue = (addr) => { + const f = this.mem.getFloat64(addr, true); if (f === 0) { return undefined; } @@ -154,77 +140,69 @@ return f; } - const id = v_ref & 0xffffffffn; + const id = this.mem.getUint32(addr, true); return this._values[id]; } + const storeValue = (addr, v) => { + const nanHead = 0x7FF80000; - const loadValue = (addr) => { - let v_ref = mem().getBigUint64(addr, true); - return unboxValue(v_ref); - } - - const boxValue = (v) => { - const nanHead = 0x7FF80000n; - - if (typeof v === "number") { + if (typeof v === "number" && v !== 0) { if (isNaN(v)) { - return nanHead << 32n; + this.mem.setUint32(addr + 4, nanHead, true); + this.mem.setUint32(addr, 0, true); + return; } - if (v === 0) { - return (nanHead << 32n) | 1n; - } - reinterpretBuf.setFloat64(0, v, true); - return reinterpretBuf.getBigInt64(0, true); + this.mem.setFloat64(addr, v, true); + return; } - switch (v) { - case undefined: - return 0n; - case null: - return (nanHead << 32n) | 2n; - case true: - return (nanHead << 32n) | 3n; - case false: - return (nanHead << 32n) | 4n; + if (v === undefined) { + this.mem.setFloat64(addr, 0, true); + return; } let id = this._ids.get(v); if (id === undefined) { id = this._idPool.pop(); if (id === undefined) { - id = BigInt(this._values.length); + id = this._values.length; } this._values[id] = v; this._goRefCounts[id] = 0; this._ids.set(v, id); } this._goRefCounts[id]++; - let typeFlag = 1n; + let typeFlag = 0; switch (typeof v) { + case "object": + if (v !== null) { + typeFlag = 1; + } + break; case "string": - typeFlag = 2n; + typeFlag = 2; break; case "symbol": - typeFlag = 3n; + typeFlag = 3; break; case "function": - typeFlag = 4n; + typeFlag = 4; break; } - return id | ((nanHead | typeFlag) << 32n); + this.mem.setUint32(addr + 4, nanHead | typeFlag, true); + this.mem.setUint32(addr, id, true); } - const storeValue = (addr, v) => { - let v_ref = boxValue(v); - mem().setBigUint64(addr, v_ref, true); + const loadSlice = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); + return new Uint8Array(this._inst.exports.mem.buffer, array, len); } - const loadSlice = (array, len, cap) => { - return new Uint8Array(this._inst.exports.memory.buffer, array, len); - } - - const loadSliceOfValues = (array, len, cap) => { + const loadSliceOfValues = (addr) => { + const array = getInt64(addr + 0); + const len = getInt64(addr + 8); const a = new Array(len); for (let i = 0; i < len; i++) { a[i] = loadValue(array + i * 8); @@ -232,287 +210,353 @@ return a; } - const loadString = (ptr, len) => { - return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); + const loadString = (addr) => { + const saddr = getInt64(addr + 0); + const len = getInt64(addr + 8); + return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); + } + + const testCallExport = (a, b) => { + this._inst.exports.testExport0(); + return this._inst.exports.testExport(a, b); } const timeOrigin = Date.now() - performance.now(); this.importObject = { - wasi_snapshot_preview1: { - // https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write - fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { - let nwritten = 0; - if (fd == 1) { - for (let iovs_i=0; iovs_i 0, // dummy - fd_fdstat_get: () => 0, // dummy - fd_seek: () => 0, // dummy - proc_exit: (code) => { - this.exited = true; - this.exitCode = code; - this._resolveExitPromise(); - throw wasmExit; - }, - random_get: (bufPtr, bufLen) => { - crypto.getRandomValues(loadSlice(bufPtr, bufLen)); - return 0; - }, + _gotest: { + add: (a, b) => a + b, + callExport: testCallExport, }, gojs: { - // func ticks() float64 - "runtime.ticks": () => { - return timeOrigin + performance.now(); + // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) + // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported + // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). + // This changes the SP, thus we have to update the SP used by the imported function. + + // func wasmExit(code int32) + "runtime.wasmExit": (sp) => { + sp >>>= 0; + const code = this.mem.getInt32(sp + 8, true); + this.exited = true; + delete this._inst; + delete this._values; + delete this._goRefCounts; + delete this._ids; + delete this._idPool; + this.exit(code); }, - // func sleepTicks(timeout float64) - "runtime.sleepTicks": (timeout) => { - // Do not sleep, only reactivate scheduler after the given timeout. - setTimeout(() => { - if (this.exited) return; - try { - this._inst.exports.go_scheduler(); - } catch (e) { - if (e !== wasmExit) throw e; - } - }, timeout); + // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) + "runtime.wasmWrite": (sp) => { + sp >>>= 0; + const fd = getInt64(sp + 8); + const p = getInt64(sp + 16); + const n = this.mem.getInt32(sp + 24, true); + fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); + }, + + // func resetMemoryDataView() + "runtime.resetMemoryDataView": (sp) => { + sp >>>= 0; + this.mem = new DataView(this._inst.exports.mem.buffer); + }, + + // func nanotime1() int64 + "runtime.nanotime1": (sp) => { + sp >>>= 0; + setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); + }, + + // func walltime() (sec int64, nsec int32) + "runtime.walltime": (sp) => { + sp >>>= 0; + const msec = (new Date).getTime(); + setInt64(sp + 8, msec / 1000); + this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); + }, + + // func scheduleTimeoutEvent(delay int64) int32 + "runtime.scheduleTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this._nextCallbackTimeoutID; + this._nextCallbackTimeoutID++; + this._scheduledTimeouts.set(id, setTimeout( + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, + getInt64(sp + 8), + )); + this.mem.setInt32(sp + 16, id, true); + }, + + // func clearTimeoutEvent(id int32) + "runtime.clearTimeoutEvent": (sp) => { + sp >>>= 0; + const id = this.mem.getInt32(sp + 8, true); + clearTimeout(this._scheduledTimeouts.get(id)); + this._scheduledTimeouts.delete(id); + }, + + // func getRandomData(r []byte) + "runtime.getRandomData": (sp) => { + sp >>>= 0; + crypto.getRandomValues(loadSlice(sp + 8)); }, // func finalizeRef(v ref) - "syscall/js.finalizeRef": (v_ref) => { - // Note: TinyGo does not support finalizers so this is only called - // for one specific case, by js.go:jsString. and can/might leak memory. - const id = v_ref & 0xffffffffn; - if (this._goRefCounts?.[id] !== undefined) { - this._goRefCounts[id]--; - if (this._goRefCounts[id] === 0) { - const v = this._values[id]; - this._values[id] = null; - this._ids.delete(v); - this._idPool.push(id); - } - } else { - console.error("syscall/js.finalizeRef: unknown id", id); + "syscall/js.finalizeRef": (sp) => { + sp >>>= 0; + const id = this.mem.getUint32(sp + 8, true); + this._goRefCounts[id]--; + if (this._goRefCounts[id] === 0) { + const v = this._values[id]; + this._values[id] = null; + this._ids.delete(v); + this._idPool.push(id); } }, // func stringVal(value string) ref - "syscall/js.stringVal": (value_ptr, value_len) => { - value_ptr >>>= 0; - const s = loadString(value_ptr, value_len); - return boxValue(s); + "syscall/js.stringVal": (sp) => { + sp >>>= 0; + storeValue(sp + 24, loadString(sp + 8)); }, // func valueGet(v ref, p string) ref - "syscall/js.valueGet": (v_ref, p_ptr, p_len) => { - let prop = loadString(p_ptr, p_len); - let v = unboxValue(v_ref); - let result = Reflect.get(v, prop); - return boxValue(result); + "syscall/js.valueGet": (sp) => { + sp >>>= 0; + const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 32, result); }, // func valueSet(v ref, p string, x ref) - "syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { - const v = unboxValue(v_ref); - const p = loadString(p_ptr, p_len); - const x = unboxValue(x_ref); - Reflect.set(v, p, x); + "syscall/js.valueSet": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); }, // func valueDelete(v ref, p string) - "syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { - const v = unboxValue(v_ref); - const p = loadString(p_ptr, p_len); - Reflect.deleteProperty(v, p); + "syscall/js.valueDelete": (sp) => { + sp >>>= 0; + Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); }, // func valueIndex(v ref, i int) ref - "syscall/js.valueIndex": (v_ref, i) => { - return boxValue(Reflect.get(unboxValue(v_ref), i)); + "syscall/js.valueIndex": (sp) => { + sp >>>= 0; + storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); }, // valueSetIndex(v ref, i int, x ref) - "syscall/js.valueSetIndex": (v_ref, i, x_ref) => { - Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); + "syscall/js.valueSetIndex": (sp) => { + sp >>>= 0; + Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); }, // func valueCall(v ref, m string, args []ref) (ref, bool) - "syscall/js.valueCall": (ret_addr, v_ref, m_ptr, m_len, args_ptr, args_len, args_cap) => { - const v = unboxValue(v_ref); - const name = loadString(m_ptr, m_len); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); + "syscall/js.valueCall": (sp) => { + sp >>>= 0; try { - const m = Reflect.get(v, name); - storeValue(ret_addr, Reflect.apply(m, v, args)); - mem().setUint8(ret_addr + 8, 1); + const v = loadValue(sp + 8); + const m = Reflect.get(v, loadString(sp + 16)); + const args = loadSliceOfValues(sp + 32); + const result = Reflect.apply(m, v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, result); + this.mem.setUint8(sp + 64, 1); } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 56, err); + this.mem.setUint8(sp + 64, 0); } }, // func valueInvoke(v ref, args []ref) (ref, bool) - "syscall/js.valueInvoke": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { + "syscall/js.valueInvoke": (sp) => { + sp >>>= 0; try { - const v = unboxValue(v_ref); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); - storeValue(ret_addr, Reflect.apply(v, undefined, args)); - mem().setUint8(ret_addr + 8, 1); + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.apply(v, undefined, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr + 8, 0); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); } }, // func valueNew(v ref, args []ref) (ref, bool) - "syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { - const v = unboxValue(v_ref); - const args = loadSliceOfValues(args_ptr, args_len, args_cap); + "syscall/js.valueNew": (sp) => { + sp >>>= 0; try { - storeValue(ret_addr, Reflect.construct(v, args)); - mem().setUint8(ret_addr + 8, 1); + const v = loadValue(sp + 8); + const args = loadSliceOfValues(sp + 16); + const result = Reflect.construct(v, args); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, result); + this.mem.setUint8(sp + 48, 1); } catch (err) { - storeValue(ret_addr, err); - mem().setUint8(ret_addr+ 8, 0); + sp = this._inst.exports.getsp() >>> 0; // see comment above + storeValue(sp + 40, err); + this.mem.setUint8(sp + 48, 0); } }, // func valueLength(v ref) int - "syscall/js.valueLength": (v_ref) => { - return unboxValue(v_ref).length; + "syscall/js.valueLength": (sp) => { + sp >>>= 0; + setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); }, // valuePrepareString(v ref) (ref, int) - "syscall/js.valuePrepareString": (ret_addr, v_ref) => { - const s = String(unboxValue(v_ref)); - const str = encoder.encode(s); - storeValue(ret_addr, str); - mem().setInt32(ret_addr + 8, str.length, true); + "syscall/js.valuePrepareString": (sp) => { + sp >>>= 0; + const str = encoder.encode(String(loadValue(sp + 8))); + storeValue(sp + 16, str); + setInt64(sp + 24, str.length); }, // valueLoadString(v ref, b []byte) - "syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { - const str = unboxValue(v_ref); - loadSlice(slice_ptr, slice_len, slice_cap).set(str); + "syscall/js.valueLoadString": (sp) => { + sp >>>= 0; + const str = loadValue(sp + 8); + loadSlice(sp + 16).set(str); }, // func valueInstanceOf(v ref, t ref) bool - "syscall/js.valueInstanceOf": (v_ref, t_ref) => { - return unboxValue(v_ref) instanceof unboxValue(t_ref); + "syscall/js.valueInstanceOf": (sp) => { + sp >>>= 0; + this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); }, // func copyBytesToGo(dst []byte, src ref) (int, bool) - "syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = loadSlice(dest_addr, dest_len); - const src = unboxValue(src_ref); + "syscall/js.copyBytesToGo": (sp) => { + sp >>>= 0; + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status + this.mem.setUint8(sp + 48, 0); return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); - mem().setUint32(num_bytes_copied_addr, toCopy.length, true); - mem().setUint8(returned_status_addr, 1); // Return "ok" status + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); }, - // copyBytesToJS(dst ref, src []byte) (int, bool) - // Originally copied from upstream Go project, then modified: - // https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 - "syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { - let num_bytes_copied_addr = ret_addr; - let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable - - const dst = unboxValue(dst_ref); - const src = loadSlice(src_addr, src_len); + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + sp >>>= 0; + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { - mem().setUint8(returned_status_addr, 0); // Return "not ok" status + this.mem.setUint8(sp + 48, 0); return; } const toCopy = src.subarray(0, dst.length); dst.set(toCopy); - mem().setUint32(num_bytes_copied_addr, toCopy.length, true); - mem().setUint8(returned_status_addr, 1); // Return "ok" status + setInt64(sp + 40, toCopy.length); + this.mem.setUint8(sp + 48, 1); + }, + + "debug": (value) => { + console.log(value); }, } }; - - // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. - // For compatibility, we use both as long as Go 1.20 is supported. - this.importObject.env = this.importObject.gojs; } async run(instance) { + if (!(instance instanceof WebAssembly.Instance)) { + throw new Error("Go.run: WebAssembly.Instance expected"); + } this._inst = instance; + this.mem = new DataView(this._inst.exports.mem.buffer); this._values = [ // JS values that Go currently has references to, indexed by reference id NaN, 0, null, true, false, - global, + globalThis, this, ]; - this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map(); // mapping from JS values to reference ids - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - this.exitCode = 0; + this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id + this._ids = new Map([ // mapping from JS values to reference ids + [0, 1], + [null, 2], + [true, 3], + [false, 4], + [globalThis, 5], + [this, 6], + ]); + this._idPool = []; // unused ids that have been garbage collected + this.exited = false; // whether the Go program has exited - if (this._inst.exports._start) { - let exitPromise = new Promise((resolve, reject) => { - this._resolveExitPromise = resolve; - }); + // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. + let offset = 4096; - // Run program, but catch the wasmExit exception that's thrown - // to return back here. - try { - this._inst.exports._start(); - } catch (e) { - if (e !== wasmExit) throw e; + const strPtr = (str) => { + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); } + return ptr; + }; - await exitPromise; - return this.exitCode; - } else { - this._inst.exports._initialize(); + const argc = this.argv.length; + + const argvPtrs = []; + this.argv.forEach((arg) => { + argvPtrs.push(strPtr(arg)); + }); + argvPtrs.push(0); + + const keys = Object.keys(this.env).sort(); + keys.forEach((key) => { + argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); + }); + argvPtrs.push(0); + + const argv = offset; + argvPtrs.forEach((ptr) => { + this.mem.setUint32(offset, ptr, true); + this.mem.setUint32(offset + 4, 0, true); + offset += 8; + }); + + // The linker guarantees global data starts from at least wasmMinDataAddr. + // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. + const wasmMinDataAddr = 4096 + 8192; + if (offset >= wasmMinDataAddr) { + throw new Error("total length of command line and environment variables exceeds limit"); } + + this._inst.exports.run(argc, argv); + if (this.exited) { + this._resolveExitPromise(); + } + await this._exitPromise; } _resume() { if (this.exited) { throw new Error("Go program has already exited"); } - try { - this._inst.exports.resume(); - } catch (e) { - if (e !== wasmExit) throw e; - } + this._inst.exports.resume(); if (this.exited) { this._resolveExitPromise(); } @@ -528,26 +572,4 @@ }; } } - - if ( - global.require && - global.require.main === module && - global.process && - global.process.versions && - !global.process.versions.electron - ) { - if (process.argv.length != 3) { - console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); - process.exit(1); - } - - const go = new Go(); - WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then(async (result) => { - let exitCode = await go.run(result.instance); - process.exit(exitCode); - }).catch((err) => { - console.error(err); - process.exit(1); - }); - } })(); diff --git a/implementations/assemblyscript/adapter.js b/implementations/assemblyscript/adapter.js new file mode 100644 index 0000000..6d46141 --- /dev/null +++ b/implementations/assemblyscript/adapter.js @@ -0,0 +1,76 @@ +import { instantiate } from "@assemblyscript/loader"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +// Load and instantiate the WASM module +export async function loadWasm() { + const wasmPath = path.join(__dirname, "build", "release.wasm"); + const wasmBuffer = await fs.readFile(wasmPath); + + const { exports } = await instantiate(wasmBuffer, { + env: { + "console.log": (msgPtr) => { + console.log(exports.__getString(msgPtr)); + } + } + }); + + const { + memory, + __newString, + __getString, + transformDataWithTimestamp, + healthCheck + } = exports; + + return { + // Promise-based API for compatibility with tests + async transformData(jsonString) { + try { + // Generate ISO timestamp in JavaScript + const isoTimestamp = new Date().toISOString(); + + // Marshal strings to/from WASM + const jsonPtr = __newString(jsonString); + const timestampPtr = __newString(isoTimestamp); + const resultPtr = transformDataWithTimestamp(jsonPtr, timestampPtr); + const result = __getString(resultPtr); + + return result; + } catch (error) { + // Match Go behavior on parse errors + throw new Error(`failed to parse input JSON: ${error.message}`); + } + }, + + async healthCheck() { + try { + const resultPtr = healthCheck(); + const result = __getString(resultPtr); + return result; + } catch (error) { + throw new Error(`health check failed: ${error.message}`); + } + } + }; +} + +// Export individual functions for testing +let wasmInstance = null; + +export async function transformData(jsonString) { + if (!wasmInstance) { + wasmInstance = await loadWasm(); + } + return wasmInstance.transformData(jsonString); +} + +export async function healthCheck() { + if (!wasmInstance) { + wasmInstance = await loadWasm(); + } + return wasmInstance.healthCheck(); +} \ No newline at end of file diff --git a/implementations/assemblyscript/asconfig.json b/implementations/assemblyscript/asconfig.json new file mode 100644 index 0000000..2a50385 --- /dev/null +++ b/implementations/assemblyscript/asconfig.json @@ -0,0 +1,17 @@ +{ + "targets": { + "release": { + "outFile": "build/release.wasm", + "optimizeLevel": 3, + "shrinkLevel": 2, + "converge": true, + "noAssert": true, + "runtime": "stub", + "transform": ["json-as/transform"] + } + }, + "options": { + "exportRuntime": false, + "bindings": "raw" + } +} \ No newline at end of file diff --git a/implementations/assemblyscript/assembly/index.ts b/implementations/assemblyscript/assembly/index.ts new file mode 100644 index 0000000..200b342 --- /dev/null +++ b/implementations/assemblyscript/assembly/index.ts @@ -0,0 +1,42 @@ +import { JSON } from "json-as/assembly"; + +// Define data structures with @json decorator +@json +class HealthResponse { + status!: string; + message!: string; +} + +// Health check function +export function healthCheck(): string { + const response = new HealthResponse(); + response.status = "healthy"; + response.message = "AssemblyScript WASM module is running"; + return JSON.stringify(response); +} + +// Transform data function with timestamp passed from JS +export function transformDataWithTimestamp(jsonStr: string, isoTimestamp: string): string { + // Parse arbitrary JSON into a JSON.Value + let original: JSON.Value; + original = JSON.parse(jsonStr); + + // Create wrapper object + const wrapper = new JSON.Obj(); + + // Populate fields + wrapper.set("original", original); + wrapper.set("transformed", true); + wrapper.set("timestamp", isoTimestamp); + wrapper.set("message", "Data has been processed by AssemblyScript WASM"); + + // Stringify the complete object + return wrapper.toString(); +} + +// Export with the standard name for compatibility +export function transformData(jsonStr: string): string { + // For backward compatibility, but this shouldn't be used directly + // The JS adapter will use transformDataWithTimestamp + return transformDataWithTimestamp(jsonStr, ""); +} \ No newline at end of file diff --git a/implementations/assemblyscript/package.json b/implementations/assemblyscript/package.json new file mode 100644 index 0000000..f926515 --- /dev/null +++ b/implementations/assemblyscript/package.json @@ -0,0 +1,20 @@ +{ + "name": "assemblyscript-json-transform", + "version": "1.0.0", + "description": "AssemblyScript JSON transformation using json-as", + "main": "index.js", + "type": "module", + "scripts": { + "asbuild:release": "asc assembly/index.ts --target release --transform json-as/transform", + "asbuild": "npm run asbuild:release", + "build": "npm run asbuild && wasm-opt -Oz --strip-debug --strip-dwarf --strip-producers -o build/module.opt.wasm build/release.wasm", + "test": "node test.js" + }, + "devDependencies": { + "@assemblyscript/loader": "^0.28.4", + "assemblyscript": "^0.28.4" + }, + "dependencies": { + "json-as": "^1.1.21" + } +} diff --git a/implementations/assemblyscript/test.js b/implementations/assemblyscript/test.js new file mode 100644 index 0000000..51a3358 --- /dev/null +++ b/implementations/assemblyscript/test.js @@ -0,0 +1,59 @@ +import { transformData, healthCheck } from './adapter.js'; + +async function runTests() { + console.log('🧪 Testing AssemblyScript implementation...\n'); + + try { + // Test 1: Health check + console.log('Test 1: Health check'); + const health = await healthCheck(); + const healthObj = JSON.parse(health); + console.log('✅ Health:', healthObj); + if (healthObj.status !== 'healthy') { + throw new Error('Health check failed'); + } + + // Test 2: Simple object transformation + console.log('\nTest 2: Simple object'); + const simple = await transformData('{"name":"test","value":42}'); + const simpleObj = JSON.parse(simple); + console.log('✅ Simple:', simpleObj); + if (simpleObj.original.name !== 'test' || simpleObj.original.value !== 42) { + throw new Error('Simple object test failed'); + } + + // Test 3: Array transformation + console.log('\nTest 3: Array'); + const array = await transformData('[1,2,3,"test"]'); + const arrayObj = JSON.parse(array); + console.log('✅ Array:', arrayObj); + if (!Array.isArray(arrayObj.original) || arrayObj.original.length !== 4) { + throw new Error('Array test failed'); + } + + // Test 4: Nested object + console.log('\nTest 4: Nested object'); + const nested = await transformData('{"user":{"id":1,"name":"John"},"meta":{"version":"1.0"}}'); + const nestedObj = JSON.parse(nested); + console.log('✅ Nested:', nestedObj); + if (nestedObj.original.user.name !== 'John') { + throw new Error('Nested object test failed'); + } + + // Test 5: Invalid JSON + console.log('\nTest 5: Invalid JSON'); + try { + await transformData('invalid json {'); + throw new Error('Should have thrown on invalid JSON'); + } catch (error) { + console.log('✅ Invalid JSON correctly rejected:', error.message); + } + + console.log('\n✅ All tests passed!'); + } catch (error) { + console.error('\n❌ Test failed:', error); + process.exit(1); + } +} + +runTests().catch(console.error); \ No newline at end of file diff --git a/implementations/goja/main.go b/implementations/goja/main.go index 6ba3611..58f94f3 100644 --- a/implementations/goja/main.go +++ b/implementations/goja/main.go @@ -48,21 +48,36 @@ func promisify(fn func([]js.Value) (string, error)) js.Func { }) } -// transformData takes a JSON string and JavaScript code, processes it using Goja JavaScript engine, and returns a JSON string +// transformData takes a JSON string, processes it using Goja JavaScript engine with a default transform, and returns a JSON string func transformData(args []js.Value) (string, error) { js.Global().Get("console").Call("log", "🔄 transformData called with", len(args), "arguments") - if len(args) < 2 { - return "", fmt.Errorf("expected two arguments: JSON string and JavaScript code") + if len(args) < 1 { + return "", fmt.Errorf("expected at least one argument (JSON string)") } // Get the input JSON string inputJSON := args[0].String() js.Global().Get("console").Call("log", "📥 Input JSON:", inputJSON) - // Get the JavaScript transformation code - transformJS := args[1].String() - js.Global().Get("console").Call("log", "📜 JavaScript code length:", len(transformJS), "characters") + // Use a default transformation if no JavaScript code is provided + var transformJS string + if len(args) >= 2 { + transformJS = args[1].String() + js.Global().Get("console").Call("log", "📜 JavaScript code length:", len(transformJS), "characters") + } else { + // Default transformation that matches the test expectations + transformJS = ` + function transform(data) { + return { + original: data, + transformed: true, + timestamp: new Date().toISOString(), + message: "Data has been processed by Go WASM" + }; + } + ` + } // Parse the input JSON var inputData interface{} @@ -118,7 +133,7 @@ func main() { // Add a simple health check function js.Global().Set("healthCheck", promisify(func(args []js.Value) (string, error) { js.Global().Get("console").Call("log", "💓 Health check called") - return `{"status": "healthy", "message": "Go WASM module with Goja is running"}`, nil + return `{"status": "healthy", "message": "Go WASM module is running"}`, nil })) js.Global().Get("console").Call("log", "✅ Functions exposed to JavaScript:") diff --git a/implementations/quickjs/Cargo.toml b/implementations/quickjs/Cargo.toml index dd8ec8b..72fc29d 100644 --- a/implementations/quickjs/Cargo.toml +++ b/implementations/quickjs/Cargo.toml @@ -3,9 +3,9 @@ name = "quickjs-transform" version = "0.1.0" edition = "2021" -[[bin]] +[lib] +crate-type = ["cdylib"] name = "quickjs_transform" -path = "src/main.rs" [dependencies] rquickjs = { version = "0.6", default-features = false, features = ["bindgen"] } diff --git a/implementations/quickjs/quickjs-wasi-adapter.js b/implementations/quickjs/quickjs-wasi-adapter.js index acff39e..36e0669 100644 --- a/implementations/quickjs/quickjs-wasi-adapter.js +++ b/implementations/quickjs/quickjs-wasi-adapter.js @@ -29,8 +29,9 @@ const importObject = { const wasmModule = await WebAssembly.instantiate(wasmBytes, importObject); const wasmInstance = wasmModule.instance; -// Initialize WASI - now that we have a _start function, we can call start -wasi.start(wasmInstance); +// Initialize WASI even though we don't have _start +// This is needed for memory operations to work properly +wasi.initialize(wasmInstance); // Helper functions to work with C strings function allocateString(wasmInstance, str) { diff --git a/implementations/quickjs/src/lib.rs b/implementations/quickjs/src/lib.rs new file mode 100644 index 0000000..79bfe60 --- /dev/null +++ b/implementations/quickjs/src/lib.rs @@ -0,0 +1,96 @@ +use rquickjs::{Context, Runtime, Value}; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +fn execute_js_internal(js_code: &str, input_data: &str) -> Result { + let rt = Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?; + let ctx = Context::full(&rt).map_err(|e| format!("Failed to create context: {}", e))?; + + ctx.with(|ctx| { + // Set up the input data as a global variable + let setup_code = format!("const inputData = `{}`;", input_data.replace('`', r#"\`"#)); + if let Err(e) = ctx.eval::<(), _>(setup_code.as_bytes()) { + return Err(format!("Failed to set up input data: {:?}", e)); + } + + // Execute the user's JavaScript code + let result: Result = ctx.eval(js_code.as_bytes()); + + match result { + Ok(value) => { + if let Some(s) = value.as_string() { + Ok(s.to_string().unwrap_or_default()) + } else if let Some(n) = value.as_number() { + Ok(n.to_string()) + } else if let Some(b) = value.as_bool() { + Ok(b.to_string()) + } else if value.is_null() { + Ok("null".to_string()) + } else if value.is_undefined() { + Ok("undefined".to_string()) + } else { + Ok(format!("{:?}", value)) + } + } + Err(e) => Err(format!("JavaScript execution error: {:?}", e)), + } + }) +} + +/// transform_data that accepts a null-terminated C string. +/// Returns a newly allocated C string (must be freed with free_string). +#[no_mangle] +pub extern "C" fn transform_data(input_ptr: *const c_char) -> *mut c_char { + if input_ptr.is_null() { + return std::ptr::null_mut(); + } + // Safety: input_ptr must point to a valid C string. + let input = unsafe { CStr::from_ptr(input_ptr) }.to_string_lossy().into_owned(); + // Default transform: JSON parse then stringify (no-op normalize) + let js_code = "JSON.stringify(JSON.parse(inputData))"; + match execute_js_internal(js_code, &input) { + Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()), + Err(_) => std::ptr::null_mut(), + } +} + +/// transform_data that accepts (ptr, len) input. +#[no_mangle] +pub extern "C" fn transform_data_len(ptr: *const u8, len: usize) -> *mut c_char { + if ptr.is_null() { + return std::ptr::null_mut(); + } + // Safety: ptr must point to a valid buffer of length len. + let input = unsafe { std::slice::from_raw_parts(ptr, len) }; + let input = String::from_utf8_lossy(input).into_owned(); + let js_code = "JSON.stringify(JSON.parse(inputData))"; + match execute_js_internal(js_code, &input) { + Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()), + Err(_) => std::ptr::null_mut(), + } +} + +/// Execute arbitrary JS with inputData available as a global. +#[no_mangle] +pub extern "C" fn execute_js(js_ptr: *const c_char, input_ptr: *const c_char) -> *mut c_char { + if js_ptr.is_null() || input_ptr.is_null() { + return std::ptr::null_mut(); + } + let js = unsafe { CStr::from_ptr(js_ptr) }.to_string_lossy().into_owned(); + let input = unsafe { CStr::from_ptr(input_ptr) }.to_string_lossy().into_owned(); + match execute_js_internal(&js, &input) { + Ok(s) => CString::new(s).map(|c| c.into_raw()).unwrap_or(std::ptr::null_mut()), + Err(_) => std::ptr::null_mut(), + } +} + +/// Free a C string previously returned by this module. +#[no_mangle] +pub extern "C" fn free_string(ptr: *mut c_char) { + if !ptr.is_null() { + // Safety: must only be called on pointers returned by our functions. + unsafe { let _ = CString::from_raw(ptr); } + } +} + +// Note: malloc is already provided by the libc runtime, so we don't define our own \ No newline at end of file diff --git a/implementations/quickjs/src/main.rs b/implementations/quickjs/src/main.rs deleted file mode 100644 index 32971fd..0000000 --- a/implementations/quickjs/src/main.rs +++ /dev/null @@ -1,67 +0,0 @@ -use rquickjs::{Runtime, Context, Value}; -use std::env; -use std::io::{self, Read}; - -fn execute_js(js_code: &str, input_data: &str) -> Result { - let rt = Runtime::new().map_err(|e| format!("Failed to create runtime: {}", e))?; - let ctx = Context::full(&rt).map_err(|e| format!("Failed to create context: {}", e))?; - - ctx.with(|ctx| { - // Set up the input data as a global variable - let setup_code = format!("const inputData = `{}`;", input_data.replace('`', r#"\`"#)); - if ctx.eval::<(), _>(setup_code.as_bytes()).is_err() { - return Err("Failed to set up input data".to_string()); - } - - // Execute the user's JavaScript code - let result: Result = ctx.eval(js_code.as_bytes()); - - match result { - Ok(value) => { - if let Some(s) = value.as_string() { - Ok(s.to_string().unwrap_or_default()) - } else if let Some(n) = value.as_number() { - Ok(n.to_string()) - } else if let Some(b) = value.as_bool() { - Ok(b.to_string()) - } else if value.is_null() { - Ok("null".to_string()) - } else if value.is_undefined() { - Ok("undefined".to_string()) - } else { - Ok(format!("{:?}", value)) - } - } - Err(e) => Err(format!("JavaScript execution error: {:?}", e)) - } - }) -} - -fn main() { - let args: Vec = env::args().collect(); - - // Require JavaScript code as argument - if args.len() < 2 { - eprintln!("Usage: wasmer run quickjs.wasm 'JavaScript code' [input_data]"); - eprintln!("Example: echo '{{\"test\": 123}}' | wasmer run quickjs.wasm 'JSON.stringify({{result: JSON.parse(inputData)}})'"); - return; - } - - let js_code = args[1].clone(); - - // Read input data from stdin - let mut input_data = String::new(); - if let Err(e) = io::stdin().read_to_string(&mut input_data) { - eprintln!("Error reading input: {}", e); - return; - } - - // Trim whitespace from input - input_data = input_data.trim().to_string(); - - // Execute JavaScript with the input data - match execute_js(&js_code, &input_data) { - Ok(result) => println!("{}", result), - Err(e) => eprintln!("Error: {}", e), - } -} diff --git a/package.json b/package.json index 2486ed7..94f907e 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "license": "ISC", "dependencies": { "glob": "^11.0.3", + "javy": "^0.1.2", + "porffor": "^0.60.6", "vitest": "^3.1.2" }, "devDependencies": { diff --git a/testHelpers.ts b/testHelpers.ts index 5c42a26..25b9a6f 100644 --- a/testHelpers.ts +++ b/testHelpers.ts @@ -13,24 +13,25 @@ export function callGoFunction(fn: any, ...args: any[]): Promise { // Check if current build is using Javy async function isJavyBuild(): Promise { try { - // Check if javy-adapter.js exists in the current implementation - const javyAdapterPath = path.join( + // Check if the Javy dynamic WASM exists in the expected location + // This is only present when Javy build was run + const javyDynamicPath = path.join( __dirname, - "implementations/javy/javy-adapter.js" + "implementations/javy/transform_dynamic.wasm" ); - await access(javyAdapterPath); - - // Also check if the current symlink points to javy implementation - const mainGoPath = path.join(__dirname, "main.go"); - try { - const mainGoContent = await readFile(mainGoPath, "utf8"); - // This is a simple heuristic - in a real implementation you might check the symlink target - return false; // Go implementations will have main.go - } catch { - // If main.go doesn't exist, might be a Javy build - return true; - } + await access(javyDynamicPath); + + // Additionally check if the current lib.wasm.gz was built from Javy + // by checking if plugin.wasm exists (only created by Javy build) + const javyPluginPath = path.join( + __dirname, + "implementations/javy/plugin.wasm" + ); + await access(javyPluginPath); + + return true; } catch { + // If Javy artifacts don't exist, this is not a Javy build return false; } }