diff --git a/cmd/wasm-wasi/main.go b/cmd/wasm-wasi/main.go new file mode 100644 index 00000000000..6e65729b99f --- /dev/null +++ b/cmd/wasm-wasi/main.go @@ -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() {} diff --git a/cmd/wasm-wasi/wazero_test.go b/cmd/wasm-wasi/wazero_test.go new file mode 100644 index 00000000000..339eb927985 --- /dev/null +++ b/cmd/wasm-wasi/wazero_test.go @@ -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)) +} diff --git a/go.mod b/go.mod index e6e9cd8183d..8c6c5ab9c22 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f14eb487af9..e7b42f13991 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/compiler/js_exports.go b/internal/compiler/js_exports.go new file mode 100644 index 00000000000..b05cf57f7a2 --- /dev/null +++ b/internal/compiler/js_exports.go @@ -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 + }) +}