chore: tcr analysis

This commit is contained in:
Thomas Rooney
2025-08-18 16:26:04 +01:00
parent d859affaa4
commit 0f79b905f6
15 changed files with 1206 additions and 720 deletions

469
.mise.toml Normal file
View File

@@ -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!"
"""

373
Makefile
View File

@@ -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 .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 # Default implementation (deprecated): use mise task arguments instead
IMPL ?= basic
# Pass-through args to mise: use "make <target> <args...>"
ARGS := $(filter-out $@,$(MAKECMDGOALS))
# Default target # Default target
help: help:
@echo "Available targets:" @echo "This project uses mise for toolchain and tasks."
@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 "" @echo ""
@echo "Available implementations:" @echo "Common commands:"
@ls -1 implementations/ 2>/dev/null || echo " No implementations found" @echo " mise install # Install tools (Go 1.23)"
@echo " mise tasks # List available tasks"
@echo " mise run build <impl> # Build with Go (alias for build-go)"
@echo " mise run build-go-optimized <impl> # Build with Go (optimized)"
@echo " mise run build-tinygo <impl> # Build with TinyGo"
@echo " mise run test <impl> # Test (builds first)"
@echo " mise run clean # Clean artifacts"
@echo " mise run watch <impl> # Watch build (requires fswatch)"
@echo " make build <impl> # Convenience wrapper to mise"
@echo " make test <impl> # Convenience wrapper to mise"
@echo "" @echo ""
@echo "Usage: make build IMPL=<implementation>" @echo "Note: Makefile targets are retained for compatibility but logic has moved to mise."
@echo "Example: make build IMPL=basic"
@echo "Example: make build-optimized IMPL=goja"
# Default build uses Go # Default build uses mise task (accepts positional args: make build <impl>)
build: build-go build:
@mise run build $(ARGS)
# Build the WASM binary with Go # Build the WASM binary with Go
build-go: build-go:
@echo "Building WASM binary with Go ($(IMPL) implementation)..." @mise run build-go $(ARGS)
@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))"
# Build the WASM binary with TinyGo # Build the WASM binary with TinyGo
build-tinygo: build-tinygo:
@echo "Building WASM binary with TinyGo ($(IMPL) implementation)..." @mise run build-tinygo $(ARGS)
@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))"
# Default optimized build uses Go # Default optimized build uses Go (mise wrapper)
build-optimized: build-go-optimized build-optimized:
@mise run build-optimized $(ARGS)
# Build the WASM binary with maximum Go optimization + wasm-opt # Build the WASM binary with maximum Go optimization + wasm-opt
build-go-optimized: build-go-optimized:
@echo "Building WASM binary with maximum Go optimization ($(IMPL) implementation)..." @mise run build-go-optimized $(ARGS)
@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))"
# Build the WASM binary with maximum TinyGo optimization + wasm-opt # Build the WASM binary with maximum TinyGo optimization + wasm-opt
build-tinygo-optimized: build-tinygo-optimized:
@echo "Building WASM binary with maximum TinyGo optimization ($(IMPL) implementation)..." @mise run build-tinygo-optimized $(ARGS)
@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))"
# Build WASM binary with Javy (JavaScript to WASM) using dynamic linking # Build WASM binary with Javy (JavaScript to WASM) using dynamic linking
build-javy: build-javy:
@if [ "$(IMPL)" != "javy" ]; then \ @mise run build-javy $(ARGS)
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))"
# Build WASM binary with Javy optimization (dynamic linking is already optimized) # Build WASM binary with Javy optimization (dynamic linking is already optimized)
build-javy-optimized: build-javy-optimized:
@if [ "$(IMPL)" != "javy" ]; then \ @mise run build-javy-optimized $(ARGS)
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))"
# Build WASM binary with Porffor (AOT JavaScript to WASM) # Build WASM binary with Porffor (AOT JavaScript to WASM)
build-porffor: build-porffor:
@if [ "$(IMPL)" != "porffor" ]; then \ @mise run build-porffor $(ARGS)
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))"
# Build WASM binary with Porffor optimization # Build WASM binary with Porffor optimization
build-porffor-optimized: build-porffor-optimized:
@if [ "$(IMPL)" != "porffor" ]; then \ @mise run build-porffor-optimized $(ARGS)
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))"
# Default test uses Go # Default test uses mise task (accepts positional args: make test <impl>)
test: test-go test:
@mise run test $(ARGS)
# Run tests with Go build (builds first) # Run tests with Go build (mise wrapper)
test-go: build-go test-go:
@echo "Installing Node.js dependencies..." @mise run test-go $(ARGS)
@npm install > /dev/null 2>&1
@echo "Running tests with Go build..."
@npx vitest run
# Run tests with TinyGo build (builds first) # Run tests with TinyGo build (mise wrapper)
test-tinygo: build-tinygo test-tinygo:
@echo "Installing Node.js dependencies..." @mise run test-tinygo $(ARGS)
@npm install > /dev/null 2>&1
@echo "Running tests with TinyGo build..."
@npx vitest run
# Test Javy implementation directly # Test Javy implementation directly
test-javy: test-javy:
@$(MAKE) build-javy IMPL=javy > /dev/null 2>&1 @mise run test-javy $(ARGS)
@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
# Clean build artifacts # Clean build artifacts
clean: clean:
@echo "Cleaning build artifacts..." @mise run clean
@rm -rf assets/
@rm -f main.wasm main.wasm.gz
@rm -rf node_modules/
@echo "✅ Clean complete"
# Build in watch mode with Go (requires fswatch) # Build in watch mode with Go (requires fswatch)
watch: watch:
@if ! command -v fswatch >/dev/null 2>&1; then \ @mise run watch $(ARGS)
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
# Compare binary sizes for all implementations # Compare binary sizes for all implementations
size-comparison: size-comparison:
@echo "=== Binary Size Comparison ===" @mise run 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"
gzipped-sizes: gzipped-sizes:
@echo "<22><> Measuring gzipped sizes of all WASM binaries..." @mise run gzipped-sizes
@node measure-gzipped-sizes.js
# Build WASM binary with QuickJS (Rust + QuickJS JavaScript engine) # Build WASM binary with QuickJS (Rust + QuickJS JavaScript engine)
build-quickjs: build-quickjs:
@if [ "$(IMPL)" != "quickjs" ]; then \ @mise run build-quickjs $(ARGS)
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))"
# Build WASM binary with QuickJS optimization (already optimized with --release) # Build WASM binary with QuickJS optimization (already optimized with --release)
build-quickjs-optimized: build-quickjs-optimized:
@if [ "$(IMPL)" != "quickjs" ]; then \ @mise run build-quickjs-optimized $(ARGS)
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))"
# Test QuickJS implementation directly # Test QuickJS implementation directly
test-quickjs: test-quickjs:
@$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1 @mise run test-quickjs $(ARGS)
@echo "Testing QuickJS implementation..."
@node implementations/quickjs/quickjs-wasi-test.js 2>/dev/null
# Test WASI-compatible implementations with Wasmer # Test WASI-compatible implementations with Wasmer
test-wasmer: test-wasmer:
@echo "🧪 Testing WASI-compatible implementations with Wasmer..." @mise run test-wasmer
@./test-wasmer.sh
# Test individual implementations with Wasmer # Test individual implementations with Wasmer
test-quickjs-wasmer: test-quickjs-wasmer:
@$(MAKE) build-quickjs IMPL=quickjs > /dev/null 2>&1 @mise run test-quickjs-wasmer
@echo "Testing QuickJS with Wasmer..."
@cd implementations/quickjs && ./quickjs-wasmer-test.sh
test-porffor-wasmer: test-porffor-wasmer:
@$(MAKE) build-porffor IMPL=porffor > /dev/null 2>&1 @mise run test-porffor-wasmer
@echo "Testing Porffor with Wasmer..."
@cd implementations/porffor && ./porffor-wasmer-test.sh
measure-all: size-comparison gzipped-sizes measure-all:
@echo "" @mise run measure-all
@echo "✅ Complete size analysis finished!"

View File

@@ -1,53 +1,26 @@
// Copyright 2018 The Go Authors. All rights reserved. // Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // 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 enosys = () => {
const err = new Error("not implemented"); const err = new Error("not implemented");
err.code = "ENOSYS"; err.code = "ENOSYS";
return err; return err;
}; };
if (!global.fs) { if (!globalThis.fs) {
let outputBuf = ""; let outputBuf = "";
global.fs = { globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 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) { writeSync(fd, buf) {
outputBuf += decoder.decode(buf); outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n"); const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) { if (nl != -1) {
console.log(outputBuf.substr(0, nl)); console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substr(nl + 1); outputBuf = outputBuf.substring(nl + 1);
} }
return buf.length; return buf.length;
}, },
@@ -85,8 +58,8 @@
}; };
} }
if (!global.process) { if (!globalThis.process) {
global.process = { globalThis.process = {
getuid() { return -1; }, getuid() { return -1; },
getgid() { return -1; }, getgid() { return -1; },
geteuid() { return -1; }, geteuid() { return -1; },
@@ -100,53 +73,66 @@
} }
} }
if (!global.crypto) { if (!globalThis.path) {
const nodeCrypto = require("node:crypto"); globalThis.path = {
global.crypto = { resolve(...pathSegments) {
getRandomValues(b) { return pathSegments.join("/");
nodeCrypto.randomFillSync(b); }
}, }
};
} }
if (!global.performance) { if (!globalThis.crypto) {
global.performance = { throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
now() {
const [sec, nsec] = process.hrtime();
return sec * 1000 + nsec / 1000000;
},
};
} }
if (!global.TextEncoder) { if (!globalThis.performance) {
global.TextEncoder = require("node:util").TextEncoder; throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
} }
if (!global.TextDecoder) { if (!globalThis.TextEncoder) {
global.TextDecoder = require("node:util").TextDecoder; 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 encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("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() { 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; this._nextCallbackTimeoutID = 1;
const mem = () => { const setInt64 = (addr, v) => {
// The buffer may change when requesting more memory. this.mem.setUint32(addr + 0, v, true);
return new DataView(this._inst.exports.memory.buffer); this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
} }
const unboxValue = (v_ref) => { const setInt32 = (addr, v) => {
reinterpretBuf.setBigInt64(0, v_ref, true); this.mem.setUint32(addr + 0, v, true);
const f = reinterpretBuf.getFloat64(0, 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) { if (f === 0) {
return undefined; return undefined;
} }
@@ -154,77 +140,69 @@
return f; return f;
} }
const id = v_ref & 0xffffffffn; const id = this.mem.getUint32(addr, true);
return this._values[id]; return this._values[id];
} }
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
const loadValue = (addr) => { if (typeof v === "number" && v !== 0) {
let v_ref = mem().getBigUint64(addr, true);
return unboxValue(v_ref);
}
const boxValue = (v) => {
const nanHead = 0x7FF80000n;
if (typeof v === "number") {
if (isNaN(v)) { if (isNaN(v)) {
return nanHead << 32n; this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
} }
if (v === 0) { this.mem.setFloat64(addr, v, true);
return (nanHead << 32n) | 1n; return;
}
reinterpretBuf.setFloat64(0, v, true);
return reinterpretBuf.getBigInt64(0, true);
} }
switch (v) { if (v === undefined) {
case undefined: this.mem.setFloat64(addr, 0, true);
return 0n; return;
case null:
return (nanHead << 32n) | 2n;
case true:
return (nanHead << 32n) | 3n;
case false:
return (nanHead << 32n) | 4n;
} }
let id = this._ids.get(v); let id = this._ids.get(v);
if (id === undefined) { if (id === undefined) {
id = this._idPool.pop(); id = this._idPool.pop();
if (id === undefined) { if (id === undefined) {
id = BigInt(this._values.length); id = this._values.length;
} }
this._values[id] = v; this._values[id] = v;
this._goRefCounts[id] = 0; this._goRefCounts[id] = 0;
this._ids.set(v, id); this._ids.set(v, id);
} }
this._goRefCounts[id]++; this._goRefCounts[id]++;
let typeFlag = 1n; let typeFlag = 0;
switch (typeof v) { switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string": case "string":
typeFlag = 2n; typeFlag = 2;
break; break;
case "symbol": case "symbol":
typeFlag = 3n; typeFlag = 3;
break; break;
case "function": case "function":
typeFlag = 4n; typeFlag = 4;
break; break;
} }
return id | ((nanHead | typeFlag) << 32n); this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
} }
const storeValue = (addr, v) => { const loadSlice = (addr) => {
let v_ref = boxValue(v); const array = getInt64(addr + 0);
mem().setBigUint64(addr, v_ref, true); const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
} }
const loadSlice = (array, len, cap) => { const loadSliceOfValues = (addr) => {
return new Uint8Array(this._inst.exports.memory.buffer, array, len); const array = getInt64(addr + 0);
} const len = getInt64(addr + 8);
const loadSliceOfValues = (array, len, cap) => {
const a = new Array(len); const a = new Array(len);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8); a[i] = loadValue(array + i * 8);
@@ -232,81 +210,109 @@
return a; return a;
} }
const loadString = (ptr, len) => { const loadString = (addr) => {
return decoder.decode(new DataView(this._inst.exports.memory.buffer, ptr, len)); 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(); const timeOrigin = Date.now() - performance.now();
this.importObject = { this.importObject = {
wasi_snapshot_preview1: { _gotest: {
// https://github.com/WebAssembly/WASI/blob/main/phases/snapshot/docs.md#fd_write add: (a, b) => a + b,
fd_write: function(fd, iovs_ptr, iovs_len, nwritten_ptr) { callExport: testCallExport,
let nwritten = 0;
if (fd == 1) {
for (let iovs_i=0; iovs_i<iovs_len;iovs_i++) {
let iov_ptr = iovs_ptr+iovs_i*8; // assuming wasm32
let ptr = mem().getUint32(iov_ptr + 0, true);
let len = mem().getUint32(iov_ptr + 4, true);
nwritten += len;
for (let i=0; i<len; i++) {
let c = mem().getUint8(ptr+i);
if (c == 13) { // CR
// ignore
} else if (c == 10) { // LF
// write line
let line = decoder.decode(new Uint8Array(logLine));
logLine = [];
console.log(line);
} else {
logLine.push(c);
}
}
}
} else {
console.error('invalid file descriptor:', fd);
}
mem().setUint32(nwritten_ptr, nwritten, true);
return 0;
},
fd_close: () => 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;
},
}, },
gojs: { gojs: {
// func ticks() float64 // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
"runtime.ticks": () => { // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
return timeOrigin + performance.now(); // 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) // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.sleepTicks": (timeout) => { "runtime.wasmWrite": (sp) => {
// Do not sleep, only reactivate scheduler after the given timeout. sp >>>= 0;
setTimeout(() => { const fd = getInt64(sp + 8);
if (this.exited) return; const p = getInt64(sp + 16);
try { const n = this.mem.getInt32(sp + 24, true);
this._inst.exports.go_scheduler(); fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
} catch (e) { },
if (e !== wasmExit) throw e;
// 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();
} }
}, timeout); },
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) // func finalizeRef(v ref)
"syscall/js.finalizeRef": (v_ref) => { "syscall/js.finalizeRef": (sp) => {
// Note: TinyGo does not support finalizers so this is only called sp >>>= 0;
// for one specific case, by js.go:jsString. and can/might leak memory. const id = this.mem.getUint32(sp + 8, true);
const id = v_ref & 0xffffffffn;
if (this._goRefCounts?.[id] !== undefined) {
this._goRefCounts[id]--; this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) { if (this._goRefCounts[id] === 0) {
const v = this._values[id]; const v = this._values[id];
@@ -314,205 +320,243 @@
this._ids.delete(v); this._ids.delete(v);
this._idPool.push(id); this._idPool.push(id);
} }
} else {
console.error("syscall/js.finalizeRef: unknown id", id);
}
}, },
// func stringVal(value string) ref // func stringVal(value string) ref
"syscall/js.stringVal": (value_ptr, value_len) => { "syscall/js.stringVal": (sp) => {
value_ptr >>>= 0; sp >>>= 0;
const s = loadString(value_ptr, value_len); storeValue(sp + 24, loadString(sp + 8));
return boxValue(s);
}, },
// func valueGet(v ref, p string) ref // func valueGet(v ref, p string) ref
"syscall/js.valueGet": (v_ref, p_ptr, p_len) => { "syscall/js.valueGet": (sp) => {
let prop = loadString(p_ptr, p_len); sp >>>= 0;
let v = unboxValue(v_ref); const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
let result = Reflect.get(v, prop); sp = this._inst.exports.getsp() >>> 0; // see comment above
return boxValue(result); storeValue(sp + 32, result);
}, },
// func valueSet(v ref, p string, x ref) // func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (v_ref, p_ptr, p_len, x_ref) => { "syscall/js.valueSet": (sp) => {
const v = unboxValue(v_ref); sp >>>= 0;
const p = loadString(p_ptr, p_len); Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
const x = unboxValue(x_ref);
Reflect.set(v, p, x);
}, },
// func valueDelete(v ref, p string) // func valueDelete(v ref, p string)
"syscall/js.valueDelete": (v_ref, p_ptr, p_len) => { "syscall/js.valueDelete": (sp) => {
const v = unboxValue(v_ref); sp >>>= 0;
const p = loadString(p_ptr, p_len); Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
Reflect.deleteProperty(v, p);
}, },
// func valueIndex(v ref, i int) ref // func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (v_ref, i) => { "syscall/js.valueIndex": (sp) => {
return boxValue(Reflect.get(unboxValue(v_ref), i)); sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
}, },
// valueSetIndex(v ref, i int, x ref) // valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (v_ref, i, x_ref) => { "syscall/js.valueSetIndex": (sp) => {
Reflect.set(unboxValue(v_ref), i, unboxValue(x_ref)); sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
}, },
// func valueCall(v ref, m string, args []ref) (ref, bool) // 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) => { "syscall/js.valueCall": (sp) => {
const v = unboxValue(v_ref); sp >>>= 0;
const name = loadString(m_ptr, m_len);
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try { try {
const m = Reflect.get(v, name); const v = loadValue(sp + 8);
storeValue(ret_addr, Reflect.apply(m, v, args)); const m = Reflect.get(v, loadString(sp + 16));
mem().setUint8(ret_addr + 8, 1); 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) { } catch (err) {
storeValue(ret_addr, err); sp = this._inst.exports.getsp() >>> 0; // see comment above
mem().setUint8(ret_addr + 8, 0); storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
} }
}, },
// func valueInvoke(v ref, args []ref) (ref, bool) // 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 { try {
const v = unboxValue(v_ref); const v = loadValue(sp + 8);
const args = loadSliceOfValues(args_ptr, args_len, args_cap); const args = loadSliceOfValues(sp + 16);
storeValue(ret_addr, Reflect.apply(v, undefined, args)); const result = Reflect.apply(v, undefined, args);
mem().setUint8(ret_addr + 8, 1); sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) { } catch (err) {
storeValue(ret_addr, err); sp = this._inst.exports.getsp() >>> 0; // see comment above
mem().setUint8(ret_addr + 8, 0); storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
} }
}, },
// func valueNew(v ref, args []ref) (ref, bool) // func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (ret_addr, v_ref, args_ptr, args_len, args_cap) => { "syscall/js.valueNew": (sp) => {
const v = unboxValue(v_ref); sp >>>= 0;
const args = loadSliceOfValues(args_ptr, args_len, args_cap);
try { try {
storeValue(ret_addr, Reflect.construct(v, args)); const v = loadValue(sp + 8);
mem().setUint8(ret_addr + 8, 1); 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) { } catch (err) {
storeValue(ret_addr, err); sp = this._inst.exports.getsp() >>> 0; // see comment above
mem().setUint8(ret_addr+ 8, 0); storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
} }
}, },
// func valueLength(v ref) int // func valueLength(v ref) int
"syscall/js.valueLength": (v_ref) => { "syscall/js.valueLength": (sp) => {
return unboxValue(v_ref).length; sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
}, },
// valuePrepareString(v ref) (ref, int) // valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (ret_addr, v_ref) => { "syscall/js.valuePrepareString": (sp) => {
const s = String(unboxValue(v_ref)); sp >>>= 0;
const str = encoder.encode(s); const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(ret_addr, str); storeValue(sp + 16, str);
mem().setInt32(ret_addr + 8, str.length, true); setInt64(sp + 24, str.length);
}, },
// valueLoadString(v ref, b []byte) // valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (v_ref, slice_ptr, slice_len, slice_cap) => { "syscall/js.valueLoadString": (sp) => {
const str = unboxValue(v_ref); sp >>>= 0;
loadSlice(slice_ptr, slice_len, slice_cap).set(str); const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
}, },
// func valueInstanceOf(v ref, t ref) bool // func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (v_ref, t_ref) => { "syscall/js.valueInstanceOf": (sp) => {
return unboxValue(v_ref) instanceof unboxValue(t_ref); sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
}, },
// func copyBytesToGo(dst []byte, src ref) (int, bool) // func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (ret_addr, dest_addr, dest_len, dest_cap, src_ref) => { "syscall/js.copyBytesToGo": (sp) => {
let num_bytes_copied_addr = ret_addr; sp >>>= 0;
let returned_status_addr = ret_addr + 4; // Address of returned boolean status variable const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
const dst = loadSlice(dest_addr, dest_len);
const src = unboxValue(src_ref);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status this.mem.setUint8(sp + 48, 0);
return; return;
} }
const toCopy = src.subarray(0, dst.length); const toCopy = src.subarray(0, dst.length);
dst.set(toCopy); dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true); setInt64(sp + 40, toCopy.length);
mem().setUint8(returned_status_addr, 1); // Return "ok" status this.mem.setUint8(sp + 48, 1);
}, },
// copyBytesToJS(dst ref, src []byte) (int, bool) // func copyBytesToJS(dst ref, src []byte) (int, bool)
// Originally copied from upstream Go project, then modified: "syscall/js.copyBytesToJS": (sp) => {
// https://github.com/golang/go/blob/3f995c3f3b43033013013e6c7ccc93a9b1411ca9/misc/wasm/wasm_exec.js#L404-L416 sp >>>= 0;
"syscall/js.copyBytesToJS": (ret_addr, dst_ref, src_addr, src_len, src_cap) => { const dst = loadValue(sp + 8);
let num_bytes_copied_addr = ret_addr; const src = loadSlice(sp + 16);
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);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
mem().setUint8(returned_status_addr, 0); // Return "not ok" status this.mem.setUint8(sp + 48, 0);
return; return;
} }
const toCopy = src.subarray(0, dst.length); const toCopy = src.subarray(0, dst.length);
dst.set(toCopy); dst.set(toCopy);
mem().setUint32(num_bytes_copied_addr, toCopy.length, true); setInt64(sp + 40, toCopy.length);
mem().setUint8(returned_status_addr, 1); // Return "ok" status 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) { async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance; 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 this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN, NaN,
0, 0,
null, null,
true, true,
false, false,
global, globalThis,
this, this,
]; ];
this._goRefCounts = []; // number of references that Go has to a JS value, indexed by reference id 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 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._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited this.exited = false; // whether the Go program has exited
this.exitCode = 0;
if (this._inst.exports._start) { // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let exitPromise = new Promise((resolve, reject) => { let offset = 4096;
this._resolveExitPromise = resolve;
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;
};
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;
}); });
// Run program, but catch the wasmExit exception that's thrown // The linker guarantees global data starts from at least wasmMinDataAddr.
// to return back here. // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
try { const wasmMinDataAddr = 4096 + 8192;
this._inst.exports._start(); if (offset >= wasmMinDataAddr) {
} catch (e) { throw new Error("total length of command line and environment variables exceeds limit");
if (e !== wasmExit) throw e;
} }
await exitPromise; this._inst.exports.run(argc, argv);
return this.exitCode; if (this.exited) {
} else { this._resolveExitPromise();
this._inst.exports._initialize();
} }
await this._exitPromise;
} }
_resume() { _resume() {
if (this.exited) { if (this.exited) {
throw new Error("Go program has already exited"); throw new Error("Go program has already exited");
} }
try {
this._inst.exports.resume(); this._inst.exports.resume();
} catch (e) {
if (e !== wasmExit) throw e;
}
if (this.exited) { if (this.exited) {
this._resolveExitPromise(); 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);
});
}
})(); })();

View File

@@ -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();
}

View File

@@ -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"
}
}

View File

@@ -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<HealthResponse>(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<JSON.Value>(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, "");
}

View File

@@ -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"
}
}

View File

@@ -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);

View File

@@ -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) { func transformData(args []js.Value) (string, error) {
js.Global().Get("console").Call("log", "🔄 transformData called with", len(args), "arguments") js.Global().Get("console").Call("log", "🔄 transformData called with", len(args), "arguments")
if len(args) < 2 { if len(args) < 1 {
return "", fmt.Errorf("expected two arguments: JSON string and JavaScript code") return "", fmt.Errorf("expected at least one argument (JSON string)")
} }
// Get the input JSON string // Get the input JSON string
inputJSON := args[0].String() inputJSON := args[0].String()
js.Global().Get("console").Call("log", "📥 Input JSON:", inputJSON) js.Global().Get("console").Call("log", "📥 Input JSON:", inputJSON)
// Get the JavaScript transformation code // Use a default transformation if no JavaScript code is provided
transformJS := args[1].String() var transformJS string
if len(args) >= 2 {
transformJS = args[1].String()
js.Global().Get("console").Call("log", "📜 JavaScript code length:", len(transformJS), "characters") 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 // Parse the input JSON
var inputData interface{} var inputData interface{}
@@ -118,7 +133,7 @@ func main() {
// Add a simple health check function // Add a simple health check function
js.Global().Set("healthCheck", promisify(func(args []js.Value) (string, error) { js.Global().Set("healthCheck", promisify(func(args []js.Value) (string, error) {
js.Global().Get("console").Call("log", "💓 Health check called") 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:") js.Global().Get("console").Call("log", "✅ Functions exposed to JavaScript:")

View File

@@ -3,9 +3,9 @@ name = "quickjs-transform"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[[bin]] [lib]
crate-type = ["cdylib"]
name = "quickjs_transform" name = "quickjs_transform"
path = "src/main.rs"
[dependencies] [dependencies]
rquickjs = { version = "0.6", default-features = false, features = ["bindgen"] } rquickjs = { version = "0.6", default-features = false, features = ["bindgen"] }

View File

@@ -29,8 +29,9 @@ const importObject = {
const wasmModule = await WebAssembly.instantiate(wasmBytes, importObject); const wasmModule = await WebAssembly.instantiate(wasmBytes, importObject);
const wasmInstance = wasmModule.instance; const wasmInstance = wasmModule.instance;
// Initialize WASI - now that we have a _start function, we can call start // Initialize WASI even though we don't have _start
wasi.start(wasmInstance); // This is needed for memory operations to work properly
wasi.initialize(wasmInstance);
// Helper functions to work with C strings // Helper functions to work with C strings
function allocateString(wasmInstance, str) { function allocateString(wasmInstance, str) {

View File

@@ -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<String, String> {
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<Value, _> = 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

View File

@@ -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<String, String> {
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<Value, _> = 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<String> = 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),
}
}

View File

@@ -23,6 +23,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"glob": "^11.0.3", "glob": "^11.0.3",
"javy": "^0.1.2",
"porffor": "^0.60.6",
"vitest": "^3.1.2" "vitest": "^3.1.2"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -13,24 +13,25 @@ export function callGoFunction(fn: any, ...args: any[]): Promise<string> {
// Check if current build is using Javy // Check if current build is using Javy
async function isJavyBuild(): Promise<boolean> { async function isJavyBuild(): Promise<boolean> {
try { try {
// Check if javy-adapter.js exists in the current implementation // Check if the Javy dynamic WASM exists in the expected location
const javyAdapterPath = path.join( // This is only present when Javy build was run
const javyDynamicPath = path.join(
__dirname, __dirname,
"implementations/javy/javy-adapter.js" "implementations/javy/transform_dynamic.wasm"
); );
await access(javyAdapterPath); 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);
// 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; return true;
}
} catch { } catch {
// If Javy artifacts don't exist, this is not a Javy build
return false; return false;
} }
} }