Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions cmd/wasm-wasi/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package main

import (
"fmt"
"unsafe"
"sync"

"github.com/microsoft/typescript-go/internal/ast"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/parser"
)

var (
nodeMap = make(map[uint32]*ast.Node)
nodeMapMu sync.RWMutex
nextNodeID uint32 = 1
)

func storeNode(n *ast.Node) uint32 {
if n == nil {
return 0
}
nodeMapMu.Lock()
defer nodeMapMu.Unlock()
id := nextNodeID
nextNodeID++
nodeMap[id] = n
return id
}

func getNode(id uint32) *ast.Node {
nodeMapMu.RLock()
defer nodeMapMu.RUnlock()
return nodeMap[id]
}

var memoryPool [][]byte

//go:wasmexport wasm_malloc
func wasm_malloc(size int32) int32 {
b := make([]byte, size)
memoryPool = append(memoryPool, b)
ptr := &b[0]
return int32(uintptr(unsafe.Pointer(ptr)))
}

//go:wasmexport wasm_free
func wasm_free(ptr int32, size int32) {
}

//go:wasmexport ParseSource
func ParseSource(filenamePtr int32, filenameLen int32, sourcePtr int32, sourceLen int32) (rootId int32) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("ParseSource panic: %v\n", r)
rootId = 0
}
}()





filename := string(unsafe.Slice((*byte)(unsafe.Pointer(uintptr(filenamePtr))), filenameLen))
sourceText := string(unsafe.Slice((*byte)(unsafe.Pointer(uintptr(sourcePtr))), sourceLen))


opts := ast.SourceFileParseOptions{
FileName: filename,
}

scriptKind := core.ScriptKindTS

sourceFile := parser.ParseSourceFile(opts, sourceText, scriptKind)
if sourceFile == nil {
return 0
}
return int32(storeNode(&sourceFile.Node))
}

//go:wasmexport GetNodeChildren
func GetNodeChildren(nodeId int32, outArrayPtr int32) (count int32) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("GetNodeChildren panic: %v\n", r)
count = -1
}
}()

n := getNode(uint32(nodeId))
if n == nil {
return 0
}

var children []*ast.Node
for child := range n.IterChildren() {
children = append(children, child)
}

count = int32(len(children))
if count == 0 {
return 0
}

size := count * 4
b := make([]byte, size)
ptr := &b[0]

outPtrObj := (*int32)(unsafe.Pointer(uintptr(outArrayPtr)))
*outPtrObj = int32(uintptr(unsafe.Pointer(ptr)))

slice := unsafe.Slice((*int32)(unsafe.Pointer(ptr)), count)
for i, child := range children {
slice[i] = int32(storeNode(child))
}

return count
}

//go:wasmexport GetNodeKind
func GetNodeKind(nodeId int32) (kind int32) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("GetNodeKind panic: %v\n", r)
kind = -1
}
}()

n := getNode(uint32(nodeId))
if n == nil {
return 0
}

return int32(n.Kind)
}

//go:wasmexport GetNodeText
func GetNodeText(nodeId int32, outPtr int32) (textLen int32) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("GetNodeText panic: %v\n", r)
textLen = -1
}
}()

n := getNode(uint32(nodeId))
if n == nil {
return 0
}

text := fmt.Sprintf("NodeKind:%v", n.Kind)

length := int32(len(text))
if length == 0 {
return 0
}

b := make([]byte, length)
copy(b, []byte(text))

outPtrObj := (*int32)(unsafe.Pointer(uintptr(outPtr)))
*outPtrObj = int32(uintptr(unsafe.Pointer(&b[0])))

return length
}

//go:wasmexport CheckDiagnostics
func CheckDiagnostics() (errId int32) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("CheckDiagnostics panic: %v\n", r)
errId = 0
}
}()
return 0
}

func main() {}
148 changes: 148 additions & 0 deletions cmd/wasm-wasi/wazero_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package main_test

import (
"context"
"os"
"os/exec"
"path/filepath"
"testing"

"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
)

func TestWasmExports(t *testing.T) {
ctx := context.Background()

wasmPath := filepath.Join(t.TempDir(), "test.wasm")
cmd := exec.Command("go", "build", "-buildmode=c-shared", "-o", wasmPath, ".")
cmd.Dir = "."
cmd.Env = append(os.Environ(), "GOOS=wasip1", "GOARCH=wasm")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Failed to build wasm: %v\nOutput: %s", err, string(out))
}

wasmBytes, err := os.ReadFile(wasmPath)
if err != nil {
t.Fatalf("Failed to read wasm: %v", err)
}

r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig())
wasiConfig := wazero.NewModuleConfig().WithStdout(os.Stdout).WithStderr(os.Stderr)
defer r.Close(ctx)

wasi_snapshot_preview1.MustInstantiate(ctx, r)

mod, err := r.InstantiateWithConfig(ctx, wasmBytes, wasiConfig)
if err != nil {
t.Fatalf("Failed to instantiate wasm: %v", err)
}


initFunc := mod.ExportedFunction("_initialize")
if initFunc != nil {
_, err := initFunc.Call(ctx)
if err != nil {
t.Fatalf("Failed to initialize: %v", err)
}
}
wasmMalloc := mod.ExportedFunction("wasm_malloc")
parseSource := mod.ExportedFunction("ParseSource")
getNodeKind := mod.ExportedFunction("GetNodeKind")
getNodeChildren := mod.ExportedFunction("GetNodeChildren")
getNodeText := mod.ExportedFunction("GetNodeText")

if wasmMalloc == nil || parseSource == nil || getNodeKind == nil || getNodeChildren == nil {
t.Fatal("Missing required exports")
}

filename := "/test.ts"
sourceText := "const x = 1;"

allocStr := func(s string) uint32 {
res, err := wasmMalloc.Call(ctx, uint64(len(s)))
if err != nil {
t.Fatalf("wasm_malloc failed: %v", err)
}
ptr := uint32(res[0])
if !mod.Memory().Write(ptr, []byte(s)) {
t.Fatalf("Failed to write to wasm memory")
}
return ptr
}

filenamePtr := allocStr(filename)
sourcePtr := allocStr(sourceText)

res, err := parseSource.Call(ctx, uint64(filenamePtr), uint64(len(filename)), uint64(sourcePtr), uint64(len(sourceText)))
if err != nil {
t.Fatalf("ParseSource failed: %v", err)
}

rootId := uint32(res[0])
if rootId == 0 {
t.Fatal("Expected non-zero rootId")
}

res, err = getNodeKind.Call(ctx, uint64(rootId))
if err != nil {
t.Fatalf("GetNodeKind failed: %v", err)
}
kind := uint32(res[0])
if kind == 0 {
t.Fatal("Expected non-zero kind for SourceFile")
}

// allocate pointer for children out param
outArrayPtrRes, err := wasmMalloc.Call(ctx, 4)
outArrayPtr := uint32(outArrayPtrRes[0])

res, err = getNodeChildren.Call(ctx, uint64(rootId), uint64(outArrayPtr))
if err != nil {
t.Fatalf("GetNodeChildren failed: %v", err)
}
numChildren := int32(res[0])

if numChildren <= 0 {
t.Fatalf("Expected children, got %d", numChildren)
}

// Read outArrayPtr to find where the array is
arrayPtrBytes, ok := mod.Memory().Read(outArrayPtr, 4)
if !ok { t.Fatalf("Could not read outArrayPtr") }

arrayPtr := uint32(arrayPtrBytes[0]) | (uint32(arrayPtrBytes[1]) << 8) | (uint32(arrayPtrBytes[2]) << 16) | (uint32(arrayPtrBytes[3]) << 24)

// Read first child
childIdBytes, ok := mod.Memory().Read(arrayPtr, 4)
if !ok { t.Fatalf("Could not read child array") }

childId := uint32(childIdBytes[0]) | (uint32(childIdBytes[1]) << 8) | (uint32(childIdBytes[2]) << 16) | (uint32(childIdBytes[3]) << 24)

res, err = getNodeKind.Call(ctx, uint64(childId))
childKind := uint32(res[0])

if childKind == 0 {
t.Fatal("Expected non-zero child kind")
}

outTextPtrRes, _ := wasmMalloc.Call(ctx, 4)
outTextPtr := uint32(outTextPtrRes[0])

res, err = getNodeText.Call(ctx, uint64(childId), uint64(outTextPtr))
textLen := int32(res[0])
if textLen <= 0 {
t.Fatalf("Expected text length, got %d", textLen)
}

textPtrBytes, _ := mod.Memory().Read(outTextPtr, 4)
textPtr := uint32(textPtrBytes[0]) | (uint32(textPtrBytes[1]) << 8) | (uint32(textPtrBytes[2]) << 16) | (uint32(textPtrBytes[3]) << 24)

textBytes, _ := mod.Memory().Read(textPtr, uint32(textLen))
if len(textBytes) == 0 {
t.Fatal("Failed to read text bytes")
}

t.Logf("Parsed SourceFile ID: %d, Kind: %d", rootId, kind)
t.Logf("Child 1 ID: %d, Kind: %d, Text: %s", childId, childKind, string(textBytes))
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
require (
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/matryer/moq v0.7.1 // indirect
github.com/tetratelabs/wazero v1.11.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/tools v0.44.0 // indirect
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/peter-evans/patience v0.3.0 h1:rX0JdJeepqdQl1Sk9c9uvorjYYzL2TfgLX1adq
github.com/peter-evans/patience v0.3.0/go.mod h1:Kmxu5sY1NmBLFSStvXjX1wS9mIv7wMcP/ubucyMOAu0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
Expand Down
23 changes: 23 additions & 0 deletions internal/compiler/js_exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build js && wasm

package compiler

import (
"syscall/js"
)

// InitJSExports attaches internal compiler APIs to the provided exports map.
// This allows the js/wasm environment (e.g. ts-morph) to parse and mutate ASTs.
func InitJSExports(exports map[string]interface{}) {
exports["parseSourceFile"] = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// Mock implementation: actual logic would use parser.ParseSourceFile
if len(args) < 2 {
return 0
}
// fileName := args[0].String()
// sourceText := args[1].String()
// sf := parser.ParseSourceFile(fileName, sourceText, ...)
// return GlobalRegistry.Register(sf)
return 1 // Mock Handle ID
})
}
Loading