31 Commits
Author SHA1 Message Date
alu 57296ed303 use gzip to reduce tx bytes to ~600K 2026-09-08 22:01:21 +00:00
alu 7bbd8f6eb3 switch compiler to go instead of tinygo, TODO: increases size significantly 2026-09-02 18:46:17 +00:00
alu ab5ab2b81b fix html issues 2026-09-02 16:09:32 +00:00
alu f53f344fb3 return wasm object directly 2026-07-29 16:13:32 +00:00
alu f8f8636cc3 update dev-init 2026-07-06 20:47:09 +00:00
alu 13b522f6a0 fix missing dist dir make issue 2026-07-06 20:40:48 +00:00
alu 05cd2d79df remove minification of wasm.js 2026-07-06 20:36:42 +00:00
alu bc52a250a4 update go mod 2026-07-06 18:44:32 +00:00
= 830c0a7586 update wfa.js,
reduce clobbering of global namespaces
2025-10-02 21:56:39 +00:00
= c94a7902c0 remove imports,
switch to opt=s,
add debug index.html
2025-09-30 17:41:14 +00:00
alu 8ccb45748d update go mod 2025-09-17 06:13:27 +00:00
alu 63a375995c minor code cleanup, use uint64 for wavefront values 2025-05-07 21:20:13 +00:00
alu 2351faf2d7 cleanup code 2025-05-01 18:04:42 +00:00
alu a446bbd923 fix readme,
update go mod
2025-03-12 00:09:18 +00:00
alu f1407fd045 remove unused PositiveSlice Preallocate 2024-11-12 19:04:02 +00:00
alu ce635cc2b1 remove wavefront component preallocation 2024-11-12 18:53:03 +00:00
alu f5d2528e20 fix comment 2024-11-12 18:47:25 +00:00
alu 6f78825876 optimize build size by avoiding fmt,
pack lo/hi values into Wavefront
2024-11-12 18:44:12 +00:00
alu a878da42a3 update README 2024-11-08 22:49:37 +00:00
alu bd720f06fb minor optimization switching to wavefront pointers 2024-11-08 21:51:17 +00:00
alu 1bbf38aaab fix DecodeCIGAR argument hint 2024-11-07 20:58:17 +00:00
alu cde429cb80 fix issue in WFBacktrace and change format to proper CIGAR,
add test to ensure CIGAR correctness in the case of different traceback results,
add DecodeCIGAR function to exports
2024-11-07 19:01:01 +00:00
alu 3da3ddf10c major optimization by packing wavefront values 2024-11-05 18:28:28 +00:00
alu 8679c51fb0 update go mod,
move Wavefront String method to debug file,
minor optimizations
2024-11-05 05:35:46 +00:00
alu 2c7adbef06 implement missing finalizeRef implementation from go wasm_exec.js 2024-11-01 17:23:04 +00:00
alu 65516fbd50 move wfa.js glue script to root,
add minify wfa.js job to build command
2024-10-29 17:36:25 +00:00
alu a3beca4ed2 various optimizations to compute time,
add more profiling options to make test
2024-10-29 17:03:19 +00:00
alu aed3ac6f8f add import instructions to README 2024-10-24 18:29:50 +00:00
alu 547dffd8ee rewrite in go and compile to wasm 2024-10-24 18:07:10 +00:00
alu bc50e3ee4d bump version to 1.0.0 2024-08-06 17:45:39 +00:00
alu 096d184b4b improve CIGAR return value to include match positions,
fix test
2024-08-06 17:44:51 +00:00
22 changed files with 1642 additions and 443 deletions
-43
View File
@@ -1,43 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-tabs": [
"error",
{
"allowIndentationTabs": true
}
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"brace-style": [
"error",
"stroustrup",
{
"allowSingleLine": false
}
],
"camelcase": 0
}
}
+1 -2
View File
@@ -1,3 +1,2 @@
**/package-lock.json go.sum
**/node_modules
dist/* dist/*
+31
View File
@@ -0,0 +1,31 @@
.PHONY: build clean test dev-init
build: clean
@echo "======================== Building Binary ======================="
mkdir -p dist
cp wfa.js dist/wfa.js
GOOS=js GOARCH=wasm CGO_ENABLED=0 go build -ldflags="-s -w" -trimpath -o dist/wfa.wasm .
gzip dist/wfa.wasm
clean:
@echo "======================== Cleaning Project ======================"
go clean
rm -f dist/* cover.prof cpu.prof mem.prof test.test
test:
@echo "======================== Running Tests ========================="
go test -v -cover -coverpkg=./pkg/ -coverprofile cover.prof -cpuprofile cpu.prof -memprofile mem.prof ./test/
@echo "======================= Coverage Report ========================"
go tool cover -func=cover.prof
@rm -f cover.prof
@echo "==================== CPU Performance Report ===================="
go tool pprof -top cpu.prof
@rm -f cpu.prof
@echo "=================== Memory Performance Report =================="
go tool pprof -top mem.prof
@rm -f mem.prof
@rm -f test.test
dev-init:
go get -t wfa/test
+13
View File
@@ -0,0 +1,13 @@
# Using WFA-JS
Download `wfa.js` and `wfa.wasm`from [releases](https://git.tronnet.net/alu/WFA-JS/releases) to your project. Add to your script:
```
import wfa from "./wfa.js"
await wfa("<path to wasm>")
let result = wfAlign(...)
operations = DecodeCIGAR(result.CIGAR)
// ...
```
Where `<path to wasm>` is the path from the site root ie. `./scripts/wfa.wasm`. This will depend on your project structure.
+15
View File
@@ -0,0 +1,15 @@
module wfa
go 1.27.0
require (
github.com/schollz/progressbar/v3 v3.19.1
golang.org/x/exp v0.0.0-20260611194520-c48552f49976
)
require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
)
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<script src="wfa.js" type="module"></script>
<script type="module">
import wfaInit from "./wfa.js";
wfaInit("dist/wfa.wasm.gz");
window.addEventListener("DOMContentLoaded", () => {
document.querySelector("#submit").addEventListener("click", () => {
a = document.querySelector("#a").value
b = document.querySelector("#b").value
const penalties = {
m: 0,
x: 1,
o: 0,
e: 1
};
const { score, CIGAR } = wfa.wfAlign(a, b, penalties, true);
const alignment = wfa.DecodeCIGAR(CIGAR);
document.querySelector("#result").innerText = `${score}, ${CIGAR}, ${alignment}`;
})
});
</script>
</head>
<body>
<label>A: </label><input type="text" id="a">
<label>B: </label><input type="text" id="b">
<button id="submit" type="button">Submit</button>
<p><span>Result: </span><span id="result"></span></p>
</body>
</html>
+110
View File
@@ -0,0 +1,110 @@
package main
import (
"syscall/js"
wfa "wfa/pkg"
)
func main() {
c := make(chan bool)
js.Global().Get("wfa").Set("wfAlign", js.FuncOf(wfAlign))
js.Global().Get("wfa").Set("DecodeCIGAR", js.FuncOf(DecodeCIGAR))
<-c
}
func wfAlign(this js.Value, args []js.Value) interface{} {
if len(args) != 4 {
resultMap := map[string]interface{}{
"ok": false,
"error": "invalid number of args, requires 4: s1, s2, penalties, doCIGAR",
}
return js.ValueOf(resultMap)
}
if args[0].Type() != js.TypeString {
resultMap := map[string]interface{}{
"ok": false,
"error": "s1 should be a string",
}
return js.ValueOf(resultMap)
}
s1 := args[0].String()
if args[1].Type() != js.TypeString {
resultMap := map[string]interface{}{
"ok": false,
"error": "s2 should be a string",
}
return js.ValueOf(resultMap)
}
s2 := args[1].String()
if args[2].Type() != js.TypeObject {
resultMap := map[string]interface{}{
"ok": false,
"error": "penalties should be a map with key values m, x, o, e",
}
return js.ValueOf(resultMap)
}
if args[2].Get("m").IsUndefined() || args[2].Get("x").IsUndefined() || args[2].Get("o").IsUndefined() || args[2].Get("e").IsUndefined() {
resultMap := map[string]interface{}{
"ok": false,
"error": "penalties should be a map with key values m, x, o, e",
}
return js.ValueOf(resultMap)
}
m := args[2].Get("m").Int()
x := args[2].Get("x").Int()
o := args[2].Get("o").Int()
e := args[2].Get("e").Int()
penalties := wfa.Penalty{
M: m,
X: x,
O: o,
E: e,
}
if args[3].Type() != js.TypeBoolean {
resultMap := map[string]interface{}{
"ok": false,
"error": "doCIGAR should be a boolean",
}
return js.ValueOf(resultMap)
}
doCIGAR := args[3].Bool()
// Call the actual func.
result := wfa.WFAlign(s1, s2, penalties, doCIGAR)
resultMap := map[string]interface{}{
"ok": true,
"score": result.Score,
"CIGAR": result.CIGAR,
"error": "",
}
return js.ValueOf(resultMap)
}
func DecodeCIGAR(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
println("invalid number of args, requires 1: CIGAR")
return nil
}
if args[0].Type() != js.TypeString {
println("CIGAR should be a string")
return nil
}
CIGAR := args[0].String()
decoded := wfa.RunLengthDecode(CIGAR)
return js.ValueOf(decoded)
}
-20
View File
@@ -1,20 +0,0 @@
{
"name": "wfa-js",
"version": "0.0.1",
"description": "Wavefront alignment algorithm in JS",
"main": "tests/test.js",
"type": "module",
"devDependencies": {
"eslint": "^8.43.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"progress": "^2.0.3"
},
"scripts": {
"test": "node tests/test.js",
"lint": "DEBUG=eslint:cli-engine eslint --fix src/*.js tests/*.js",
"minify": "sed -ze 's/\\t//g; s/\\/\\/[[:print:]]*//g;s/\\n//g;' src/wfa.js > dist/wfa.js"
}
}
+36
View File
@@ -0,0 +1,36 @@
package wfa
type PositiveSlice[T any] struct {
data []T
valid []bool
defaultValue T
}
func (a *PositiveSlice[T]) Valid(idx int) bool {
return 0 <= idx && idx < len(a.valid) && a.valid[idx]
}
func (a *PositiveSlice[T]) Get(idx int) T {
if 0 <= idx && idx < len(a.valid) && a.valid[idx] { // idx is in the slice
return a.data[idx]
} else { // idx is out of the slice
return a.defaultValue
}
}
func (a *PositiveSlice[T]) Set(idx int, value T) {
if idx >= len(a.valid) { // idx is outside the slice
// expand data array to 2*idx
newData := make([]T, 2*idx+1)
copy(newData, a.data)
a.data = newData
// expand valid array to 2*idx
newValid := make([]bool, 2*idx+1)
copy(newValid, a.valid)
a.valid = newValid
}
a.data[idx] = value
a.valid[idx] = true
}
+81
View File
@@ -0,0 +1,81 @@
//go:build debug
package wfa
import (
"fmt"
"math"
)
func (w *WavefrontComponent) String(score int) string {
traceback_str := []string{"OI", "EI", "OD", "ED", "SB", "IN", "DL", "EN"}
s := "<"
min_lo := math.MaxInt
max_hi := math.MinInt
for i := 0; i <= score; i++ {
valid := w.W.Valid(i)
lo, hi := UnpackWavefrontLoHi(w.W.Get(i).lohi)
if valid && lo < min_lo {
min_lo = lo
}
if valid && hi > max_hi {
max_hi = hi
}
}
for k := min_lo; k <= max_hi; k++ {
s = s + fmt.Sprintf("%02d", k)
if k < max_hi {
s = s + "|"
}
}
s = s + ">\t<"
for k := min_lo; k <= max_hi; k++ {
s = s + fmt.Sprintf("%02d", k)
if k < max_hi {
s = s + "|"
}
}
s = s + ">\n"
for i := 0; i <= score; i++ {
s = s + "["
lo, hi := UnpackWavefrontLoHi(w.W.Get(i).lohi)
for k := min_lo; k <= max_hi; k++ {
valid, val, _ := UnpackWavefrontValue(w.W.Get(i).Get(k))
if valid {
s = s + fmt.Sprintf("%02d", val)
} else if k < lo || k > hi {
s = s + "--"
} else {
s = s + " "
}
if k < max_hi {
s = s + "|"
}
}
s = s + "]\t["
// print out traceback matrix
for k := min_lo; k <= max_hi; k++ {
valid, _, tb := UnpackWavefrontValue(w.W.Get(i).Get(k))
if valid {
s = s + traceback_str[tb]
} else if k < lo || k > hi {
s = s + "--"
} else {
s = s + " "
}
if k < max_hi {
s = s + "|"
}
}
s = s + "]\n"
}
return s
}
+154
View File
@@ -0,0 +1,154 @@
package wfa
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}
type Result struct {
Score int
CIGAR string
}
type Penalty struct {
M int
X int
O int
E int
}
type Traceback byte
const (
OpenIns Traceback = iota
ExtdIns
OpenDel
ExtdDel
Sub
Ins
Del
End
)
// bitpacked wavefront lo/hi values with 32 bits each
type WavefrontLoHi uint64
func PackWavefrontLoHi(lo int, hi int) WavefrontLoHi {
loBM := int64(int32(lo)) & 0x0000_0000_FFFF_FFFF
hiBM := int64(int64(hi) << 32)
return WavefrontLoHi(hiBM | loBM)
}
func UnpackWavefrontLoHi(lohi WavefrontLoHi) (int, int) {
loBM := int(int32(lohi & 0x0000_0000_FFFF_FFFF))
hiBM := int(int32(lohi & 0xFFFF_FFFF_0000_0000 >> 32))
return loBM, hiBM
}
// bitpacked wavefront values with 1 valid bit, 3 traceback bits, and 60 bits for the diag distance
type WavefrontValue uint64
// PackWavefrontValue: packs a diag value and traceback into a WavefrontValue
func PackWavefrontValue(value uint64, traceback Traceback) WavefrontValue {
validBM := uint64(0x8000_0000_0000_0000)
tracebackBM := uint64(traceback&0x0000_0007) << 60
valueBM := uint64(value) & 0x0FFF_FFFF_FFFF_FFFF
return WavefrontValue(validBM | tracebackBM | valueBM)
}
// UnpackWavefrontValue: opens a WavefrontValue into a valid bool, diag value and traceback
func UnpackWavefrontValue(wfv WavefrontValue) (bool, uint64, Traceback) {
validBM := wfv&0x8000_0000_0000_0000 != 0
tracebackBM := uint8(wfv & 0x7000_0000_0000_0000 >> 60)
valueBM := uint64(wfv & 0x0000_0000_FFFF_FFFF)
return validBM, valueBM, Traceback(tracebackBM)
}
// Wavefront: stores a single wavefront, stores wavefront's lo value and hi is naturally lo + len(data)
type Wavefront struct { // since wavefronts store diag distance, they should never be negative, and traceback data can be stored as uint8
data []WavefrontValue
lohi WavefrontLoHi
}
// NewWavefront: returns a new wavefront with size accomodating lo and hi (inclusive)
func NewWavefront(lo int, hi int) *Wavefront {
a := &Wavefront{}
a.lohi = PackWavefrontLoHi(lo, hi)
size := hi - lo
a.data = make([]WavefrontValue, size+1)
return a
}
// TranslateIndex: utility function for getting the data index given a diagonal
func (a *Wavefront) TranslateIndex(diagonal int) int {
lo := int(int32(a.lohi & 0x0000_0000_FFFF_FFFF))
return diagonal - lo
}
// Get: returns WavefrontValue for given diagonal
func (a *Wavefront) Get(diagonal int) WavefrontValue {
actualIdx := a.TranslateIndex(diagonal)
if 0 <= actualIdx && actualIdx < len(a.data) { // idx is in the slice
return a.data[actualIdx]
} else { // idx is out of the slice
return 0
}
}
// Set: the diagonal to a WavefrontValue
func (a *Wavefront) Set(diagonal int, value WavefrontValue) {
actualIdx := a.TranslateIndex(diagonal)
/* in theory idx is always in bounds because the wavefront is preallocated
if actualIdx < 0 || actualIdx >= len(a.data) {
return
}
*/
a.data[actualIdx] = value
}
// WavefrontComponent: each M/I/D wavefront matrix including the wavefront data, lo and hi
type WavefrontComponent struct {
W *PositiveSlice[*Wavefront] // wavefront diag distance and traceback for each wavefront
}
// NewWavefrontComponent: returns initialized WavefrontComponent
func NewWavefrontComponent() *WavefrontComponent {
// new wavefront component = {
// lo = [0]
// hi = [0]
// W = []
// }
w := &WavefrontComponent{
W: &PositiveSlice[*Wavefront]{
defaultValue: &Wavefront{
data: []WavefrontValue{0},
},
},
}
return w
}
// GetVal: get value for wavefront=score, diag=k => returns ok, value, traceback
func (w *WavefrontComponent) GetVal(score int, k int) (bool, uint64, Traceback) {
return UnpackWavefrontValue(w.W.Get(score).Get(k))
}
// SetVal: set value, traceback for wavefront=score, diag=k
func (w *WavefrontComponent) SetVal(score int, k int, val uint64, tb Traceback) {
w.W.Get(score).Set(k, PackWavefrontValue(val, tb))
}
// GetLoHi: get lo and hi for wavefront=score
func (w *WavefrontComponent) GetLoHi(score int) (bool, int, int) {
lo, hi := UnpackWavefrontLoHi(w.W.Get(score).lohi)
return w.W.Valid(score), lo, hi
}
// SetLoHi: set lo and hi for wavefront=score
func (w *WavefrontComponent) SetLoHi(score int, lo int, hi int) {
b := NewWavefront(lo, hi)
w.W.Set(score, b)
}
+169
View File
@@ -0,0 +1,169 @@
package wfa
const MaxInt = int(^uint(0) >> 1)
const MinInt = -MaxInt - 1
// convert an unsigned into to string
func UIntToString(num uint) string { // num assumed to be positive
str := []rune{}
for num > 0 {
digit := num % 10
str = append(str, rune('0'+digit))
num /= 10
}
for i, j := 0, len(str)-1; i < j; i, j = i+1, j-1 {
str[i], str[j] = str[j], str[i]
}
return string(str)
}
// decode runlength encoded string such as CIGARs
func RunLengthDecode(encoded string) string {
decoded := []rune{}
length := len(encoded)
i := 0
for i < length {
// If the current character is a digit, we need to extract the run length
runLength := 0
for i < length && encoded[i] >= '0' && encoded[i] <= '9' {
runLength = runLength*10 + int(encoded[i]-'0')
i++
}
// The next character will be the character to repeat
if i < length {
char := encoded[i]
for j := 0; j < runLength; j++ {
decoded = append(decoded, rune(char))
}
i++ // Move past the character
}
}
return string(decoded)
}
// given the min index, return the item in values at that index
func SafeMin[T Integer](values []T, idx int) T {
return values[idx]
}
// given the max index, return the item in values at that index
func SafeMax[T Integer](values []T, idx int) T {
return values[idx]
}
// given array of values and corresponding array of valid flags, find the min of value which is valid or return false if there does not exist any
func SafeArgMin[T Integer](valids []bool, values []T) (bool, int) {
hasValid := false
minIndex := 0
minValue := MaxInt
for i := 0; i < len(valids); i++ {
if valids[i] && int(values[i]) < minValue {
hasValid = true
minIndex = i
minValue = int(values[i])
}
}
if hasValid {
return true, minIndex
} else {
return false, 0
}
}
// given array of values and corresponding array of valid flags, find the max of value which is valid or return false if there does not exist any
func SafeArgMax[T Integer](valids []bool, values []T) (bool, int) {
hasValid := false
maxIndex := 0
maxValue := MinInt
for i := range valids {
if valids[i] && int(values[i]) > maxValue {
hasValid = true
maxIndex = i
maxValue = int(values[i])
}
}
return hasValid, maxIndex
}
// set the lext lo and hi bounds for wavefronts M, I, D
func NextLoHi(M *WavefrontComponent, I *WavefrontComponent, D *WavefrontComponent, score int, penalties Penalty) (int, int) {
x := penalties.X
o := penalties.O
e := penalties.E
a_ok, a_lo, a_hi := M.GetLoHi(score - x)
b_ok, b_lo, b_hi := M.GetLoHi(score - o - e)
c_ok, c_lo, c_hi := I.GetLoHi(score - e)
d_ok, d_lo, d_hi := D.GetLoHi(score - e)
ok_lo, idx := SafeArgMin(
[]bool{a_ok, b_ok, c_ok, d_ok},
[]int{a_lo, b_lo, c_lo, d_lo},
)
lo := SafeMin([]int{a_lo, b_lo, c_lo, d_lo}, idx) - 1
ok_hi, idx := SafeArgMax(
[]bool{a_ok, b_ok, c_ok, d_ok},
[]int{a_hi, b_hi, c_hi, d_hi},
)
hi := SafeMax([]int{a_hi, b_hi, c_hi, d_hi}, idx) + 1
if ok_lo && ok_hi {
M.SetLoHi(score, lo, hi)
I.SetLoHi(score, lo, hi)
D.SetLoHi(score, lo, hi)
}
return lo, hi
}
// set the traceback and diag value for the next I wavefront
func NextI(M *WavefrontComponent, I *WavefrontComponent, score int, k int, penalties Penalty) {
o := penalties.O
e := penalties.E
a_ok, a, _ := M.GetVal(score-o-e, k-1)
b_ok, b, _ := I.GetVal(score-e, k-1)
ok, nextITraceback := SafeArgMax([]bool{a_ok, b_ok}, []uint64{a, b})
nextIVal := SafeMax([]uint64{a, b}, nextITraceback) + 1 // important that the +1 is here
if ok {
I.SetVal(score, k, nextIVal, []Traceback{OpenIns, ExtdIns}[nextITraceback])
}
}
// set the traceback and diag value for the next D wavefront
func NextD(M *WavefrontComponent, D *WavefrontComponent, score int, k int, penalties Penalty) {
o := penalties.O
e := penalties.E
a_ok, a, _ := M.GetVal(score-o-e, k+1)
b_ok, b, _ := D.GetVal(score-e, k+1)
ok, nextDTraceback := SafeArgMax([]bool{a_ok, b_ok}, []uint64{a, b})
nextDVal := SafeMax([]uint64{a, b}, nextDTraceback)
if ok {
D.SetVal(score, k, nextDVal, []Traceback{OpenDel, ExtdDel}[nextDTraceback])
}
}
// set the traceback and diag value for the next M wavefront
func NextM(M *WavefrontComponent, I *WavefrontComponent, D *WavefrontComponent, score int, k int, penalties Penalty) {
x := penalties.X
a_ok, a, _ := M.GetVal(score-x, k)
a++ // important to have +1 here
b_ok, b, _ := I.GetVal(score, k)
c_ok, c, _ := D.GetVal(score, k)
ok, nextMTraceback := SafeArgMax([]bool{a_ok, b_ok, c_ok}, []uint64{a, b, c})
nextMVal := SafeMax([]uint64{a, b, c}, nextMTraceback)
if ok {
M.SetVal(score, k, nextMVal, []Traceback{Sub, Ins, Del}[nextMTraceback])
}
}
+195
View File
@@ -0,0 +1,195 @@
package wfa
// WFAlign takes strings s1, s2, penalties, and returns the score and CIGAR if doCIGAR is true
func WFAlign(s1 string, s2 string, penalties Penalty, doCIGAR bool) Result {
n := len(s1)
m := len(s2)
A_k := m - n // diagonal where both sequences end
A_offset := uint64(m) // offset along a_k diagonal corresponding to end
score := 0
M := NewWavefrontComponent()
M.SetLoHi(0, 0, 0)
M.SetVal(0, 0, 0, End)
I := NewWavefrontComponent()
D := NewWavefrontComponent()
for {
WFExtend(M, s1, n, s2, m, score)
ok, val, _ := M.GetVal(score, A_k)
if ok && val >= A_offset { // exit when M_(s,a_k) >= A_offset, ie the wavefront has reached the end
break
}
score = score + 1
WFNext(M, I, D, score, penalties)
}
CIGAR := ""
if doCIGAR { // if doCIGAR, then perform backtrace, otherwise just return the score
CIGAR = WFBacktrace(M, I, D, score, penalties, A_k, A_offset, s1, s2)
}
return Result{
Score: score,
CIGAR: CIGAR,
}
}
func WFExtend(M *WavefrontComponent, s1 string, n int, s2 string, m int, score int) {
_, lo, hi := M.GetLoHi(score)
for k := lo; k <= hi; k++ { // for each diagonal in current wavefront
// v = M[score][k] - k
// h = M[score][k]
ok, uh, tb := M.GetVal(score, k)
// exit early if M_(s,l) is invalid
if !ok {
continue
}
h := int(uh)
v := h - k
// in the paper, we do v++, h++, M_(s,k)++
// however, note that h = M_(s,k) so instead we just do v++, h++ and set M_(s,k) at the end
// this saves a some memory reads and writes
for v < n && h < m && s1[v] == s2[h] { // extend diagonal for the next set of matches
v++
h++
}
M.SetVal(score, k, uint64(h), tb)
}
}
func WFNext(M *WavefrontComponent, I *WavefrontComponent, D *WavefrontComponent, score int, penalties Penalty) {
// get this score's lo, hi
lo, hi := NextLoHi(M, I, D, score, penalties)
for k := lo; k <= hi; k++ { // for each diagonal, extend the matrices for the next wavefronts
NextI(M, I, score, k, penalties)
NextD(M, D, score, k, penalties)
NextM(M, I, D, score, k, penalties)
}
}
func WFBacktrace(M *WavefrontComponent, I *WavefrontComponent, D *WavefrontComponent, score int, penalties Penalty, A_k int, A_offset uint64, s1 string, s2 string) string {
x := penalties.X
o := penalties.O
e := penalties.E
tb_s := score
tb_k := A_k
done := false
_, current_dist, current_traceback := M.GetVal(tb_s, tb_k)
Ops := []rune{'~'}
Counts := []uint{0}
idx := 0
for !done {
switch current_traceback {
case OpenIns:
if Ops[idx] == 'I' {
Counts[idx]++
} else {
Ops = append(Ops, 'I')
Counts = append(Counts, 1)
idx++
}
tb_s = tb_s - o - e
tb_k = tb_k - 1
_, current_dist, current_traceback = M.GetVal(tb_s, tb_k)
case ExtdIns:
if Ops[idx] == 'I' {
Counts[idx]++
} else {
Ops = append(Ops, 'I')
Counts = append(Counts, 1)
idx++
}
tb_s = tb_s - e
tb_k = tb_k - 1
_, current_dist, current_traceback = I.GetVal(tb_s, tb_k)
case OpenDel:
if Ops[idx] == 'D' {
Counts[idx]++
} else {
Ops = append(Ops, 'D')
Counts = append(Counts, 1)
idx++
}
tb_s = tb_s - o - e
tb_k = tb_k + 1
_, current_dist, current_traceback = M.GetVal(tb_s, tb_k)
case ExtdDel:
if Ops[idx] == 'D' {
Counts[idx]++
} else {
Ops = append(Ops, 'D')
Counts = append(Counts, 1)
idx++
}
tb_s = tb_s - e
tb_k = tb_k + 1
_, current_dist, current_traceback = D.GetVal(tb_s, tb_k)
case Sub:
tb_s = tb_s - x
// tb_k = tb_k;
_, next_dist, next_traceback := M.GetVal(tb_s, tb_k)
if int(current_dist-next_dist)-1 > 0 {
Ops = append(Ops, 'M')
Counts = append(Counts, uint(current_dist-next_dist)-1)
idx++
}
if Ops[idx] == 'X' {
Counts[idx]++
} else {
Ops = append(Ops, 'X')
Counts = append(Counts, 1)
idx++
}
current_dist = next_dist
current_traceback = next_traceback
case Ins:
// tb_s = tb_s;
// tb_k = tb_k;
_, next_dist, next_traceback := I.GetVal(tb_s, tb_k)
Ops = append(Ops, 'M')
Counts = append(Counts, uint(current_dist-next_dist))
idx++
current_dist = next_dist
current_traceback = next_traceback
case Del:
// tb_s = tb_s;
// tb_k = tb_k;
_, next_dist, next_traceback := D.GetVal(tb_s, tb_k)
Ops = append(Ops, 'M')
Counts = append(Counts, uint(current_dist-next_dist))
idx++
current_dist = next_dist
current_traceback = next_traceback
case End:
Ops = append(Ops, 'M')
Counts = append(Counts, uint(current_dist))
idx++
done = true
}
}
CIGAR := ""
for i := len(Ops) - 1; i > 0; i-- {
CIGAR += UIntToString(Counts[i])
CIGAR += string(Ops[i])
}
return CIGAR
}
-331
View File
@@ -1,331 +0,0 @@
class WavefrontComponent {
constructor () {
this.lo = [0]; // lo for each wavefront
this.hi = [0]; // hi for each wavefront
this.W = []; // wavefront diag distance for each wavefront
this.A = []; // compact CIGAR for backtrace
}
// get value for wavefront=score, diag=k
getVal (score, k) {
if (this.W[score] !== undefined && this.W[score][k] !== undefined) {
return this.W[score][k];
}
else {
return NaN;
}
}
// set value for wavefront=score, diag=k
setVal (score, k, val) {
if (this.W[score]) {
this.W[score][k] = val;
}
else {
this.W[score] = [];
this.W[score][k] = val;
}
}
// get alignment traceback
getTraceback (score, k) {
if (this.A[score] !== undefined && this.A[score][k] !== undefined) {
return this.A[score][k];
}
else {
return undefined;
}
}
// set alignment traceback
setTraceback (score, k, traceback) {
if (this.A[score]) {
this.A[score][k] = traceback;
}
else {
this.A[score] = [];
this.A[score][k] = traceback;
}
}
// get hi for wavefront=score
getHi (score) {
const hi = this.hi[score];
return isNaN(hi) ? 0 : hi;
}
// set hi for wavefront=score
setHi (score, hi) {
this.hi[score] = hi;
}
// get lo for wavefront=score
getLo (score) {
const lo = this.lo[score];
return isNaN(lo) ? 0 : lo;
}
// set lo for wavefront=score
setLo (score, lo) {
this.lo[score] = lo;
}
// string representation of all wavefronts
toString () {
const traceback_str = ["OI", "EI", "OD", "ED", "SB", "IN", "DL", "EN"];
let s = "<";
let min_lo = Infinity;
let max_hi = -Infinity;
// get the min lo and max hi values across all wavefronts
for (let i = 0; i < this.W.length; i++) {
const lo = this.lo[i];
const hi = this.hi[i];
if (lo < min_lo) {
min_lo = lo;
}
if (hi > max_hi) {
max_hi = hi;
}
}
// print out two headers, one for wavefront and one for traceback
for (let k = min_lo; k <= max_hi; k++) {
s += FormatNumberLength(k, 2);
if (k < max_hi) {
s += "|";
}
}
s += ">\t<";
for (let k = min_lo; k <= max_hi; k++) {
s += FormatNumberLength(k, 2);
if (k < max_hi) {
s += "|";
}
}
s += ">\n";
// for each wavefront
for (let i = 0; i < this.W.length; i++) {
s += "[";
const lo = this.lo[i];
const hi = this.hi[i];
// print out the wavefront matrix
for (let k = min_lo; k <= max_hi; k++) {
if (this.W[i] !== undefined && this.W[i][k] !== undefined && !isNaN(this.W[i][k])) {
s += FormatNumberLength(this.W[i][k], 2);
}
else if (k < lo || k > hi) {
s += "--";
}
else {
s += " ";
}
if (k < max_hi) {
s += "|";
}
}
s += "]\t[";
// print out the traceback matrix
for (let k = min_lo; k <= max_hi; k++) {
if (this.A[i] !== undefined && this.A[i][k] !== undefined) {
s += traceback_str[this.A[i][k].toString()];
}
else if (k < lo || k > hi) {
s += "--";
}
else {
s += " ";
}
if (k < max_hi) {
s += "|";
}
}
s += "]\n";
}
return s;
}
}
const traceback = {
OpenIns: 0,
ExtdIns: 1,
OpenDel: 2,
ExtdDel: 3,
Sub: 4,
Ins: 5,
Del: 6,
End: 7
};
function FormatNumberLength (num, length) {
let r = "" + num;
while (r.length < length) {
r = " " + r;
}
return r;
}
function min (args) {
args.forEach((el, idx, arr) => {
arr[idx] = isNaN(el) ? Infinity : el;
});
const min = Math.min.apply(Math, args);
return min === Infinity ? NaN : min;
}
function max (args) {
args.forEach((el, idx, arr) => {
arr[idx] = isNaN(el) ? -Infinity : el;
});
const max = Math.max.apply(Math, args);
return max === -Infinity ? NaN : max;
}
function argmax (args) {
const val = max(args);
return args.indexOf(val);
}
export default function wfAlign (s1, s2, penalties, doCIGAR = false) {
const n = s1.length;
const m = s2.length;
const A_k = m - n;
const A_offset = m;
let score = 0;
const M = new WavefrontComponent();
M.setVal(0, 0, 0);
M.setHi(0, 0);
M.setLo(0, 0);
M.setTraceback(0, 0, traceback.End);
const I = new WavefrontComponent();
const D = new WavefrontComponent();
while (true) {
wfExtend(M, s1, n, s2, m, score);
if (M.getVal(score, A_k) >= A_offset) {
break;
}
score++;
wfNext(M, I, D, score, penalties);
}
let CIGAR = null;
if (doCIGAR) {
CIGAR = wfBacktrace(M, I, D, score, penalties, A_k, A_offset);
}
return { score, CIGAR };
}
function wfExtend (M, s1, n, s2, m, score) {
const lo = M.getLo(score);
const hi = M.getHi(score);
for (let k = lo; k <= hi; k++) {
let v = M.getVal(score, k) - k;
let h = M.getVal(score, k);
if (isNaN(v) || isNaN(h)) {
continue;
}
while (s1[v] === s2[h]) {
M.setVal(score, k, M.getVal(score, k) + 1);
v++;
h++;
if (v > n || h > m) {
break;
}
}
}
}
function wfNext (M, I, D, score, penalties, do_traceback) {
const x = penalties.x;
const o = penalties.o;
const e = penalties.e;
const lo = min([M.getLo(score - x), M.getLo(score - o - e), I.getLo(score - e), D.getLo(score - e)]) - 1;
const hi = max([M.getHi(score - x), M.getHi(score - o - e), I.getHi(score - e), D.getHi(score - e)]) + 1;
M.setHi(score, hi);
I.setHi(score, hi);
D.setHi(score, hi);
M.setLo(score, lo);
I.setLo(score, lo);
D.setLo(score, lo);
for (let k = lo; k <= hi; k++) {
I.setVal(score, k, max([
M.getVal(score - o - e, k - 1),
I.getVal(score - e, k - 1)
]) + 1);
I.setTraceback(score, k, [traceback.OpenIns, traceback.ExtdIns][argmax([
M.getVal(score - o - e, k - 1),
I.getVal(score - e, k - 1)
])]);
D.setVal(score, k, max([
M.getVal(score - o - e, k + 1),
D.getVal(score - e, k + 1)
]));
D.setTraceback(score, k, [traceback.OpenDel, traceback.ExtdDel][argmax([
M.getVal(score - o - e, k + 1),
D.getVal(score - e, k + 1)
])]);
M.setVal(score, k, max([
M.getVal(score - x, k) + 1,
I.getVal(score, k),
D.getVal(score, k)
]));
M.setTraceback(score, k, [traceback.Sub, traceback.Ins, traceback.Del][argmax([
M.getVal(score - x, k) + 1,
I.getVal(score, k),
D.getVal(score, k)
])]);
}
}
function wfBacktrace (M, I, D, score, penalties, A_k) {
const traceback_CIGAR = ["I", "I", "D", "D", "X", "", "", ""];
const x = penalties.x;
const o = penalties.o;
const e = penalties.e;
let CIGAR_rev = ""; // reversed CIGAR
let tb_s = score; // traceback score
let tb_k = A_k; // traceback diag k
let current_traceback = M.getTraceback(tb_s, tb_k);
let done = false;
while (!done) {
CIGAR_rev += traceback_CIGAR[current_traceback];
switch (current_traceback) {
case traceback.OpenIns:
tb_s = tb_s - o - e;
tb_k = tb_k - 1;
current_traceback = M.getTraceback(tb_s, tb_k);
break;
case traceback.ExtdIns:
tb_s = tb_s - e;
tb_k = tb_k - 1;
current_traceback = I.getTraceback(tb_s, tb_k);
break;
case traceback.OpenDel:
tb_s = tb_s - o - e;
tb_k = tb_k + 1;
current_traceback = M.getTraceback(tb_s, tb_k);
break;
case traceback.ExtdDel:
tb_s = tb_s - e;
tb_k = tb_k + 1;
current_traceback = D.getTraceback(tb_s, tb_k);
break;
case traceback.Sub:
tb_s = tb_s - x;
// tb_k = tb_k;
current_traceback = M.getTraceback(tb_s, tb_k);
break;
case traceback.Ins:
// tb_s = tb_s;
// tb_k = tb_k;
current_traceback = I.getTraceback(tb_s, tb_k);
break;
case traceback.Del:
// tb_s = tb_s;
// tb_k = tb_k;
current_traceback = D.getTraceback(tb_s, tb_k);
break;
case traceback.End:
done = true;
break;
}
}
return Array.from(CIGAR_rev).reverse().join("");
}
View File
+3 -3
View File
@@ -6,7 +6,7 @@
"o": 2, "o": 2,
"e": 1 "e": 1
}, },
"solutions": "./tests/test_affine_p0_sol" "solutions": "test_affine_p0_sol"
}, },
"p1": { "p1": {
"penalties": { "penalties": {
@@ -15,7 +15,7 @@
"o": 1, "o": 1,
"e": 4 "e": 4
}, },
"solutions": "./tests/test_affine_p1_sol" "solutions": "test_affine_p1_sol"
}, },
"p2": { "p2": {
"penalties": { "penalties": {
@@ -24,6 +24,6 @@
"o": 3, "o": 3,
"e": 2 "e": 2
}, },
"solutions": "./tests/test_affine_p2_sol" "solutions": "test_affine_p2_sol"
} }
} }
+196
View File
@@ -0,0 +1,196 @@
package tests
import (
"bufio"
"encoding/json"
"log"
"math/rand/v2"
"os"
"strconv"
"strings"
"testing"
wfa "wfa/pkg"
"github.com/schollz/progressbar/v3"
"golang.org/x/exp/constraints"
)
const testJsonPath = "tests.json"
const testSequences = "sequences"
type TestPenalty struct {
M int `json:"m"`
X int `json:"x"`
O int `json:"o"`
E int `json:"e"`
}
type TestCase struct {
Penalties TestPenalty `json:"penalties"`
Solutions string `json:"solutions"`
}
func randRange[T constraints.Integer](min, max int) T {
return T(rand.IntN(max-min) + min)
}
func TestWavefrontPacking(t *testing.T) {
for range 1000 {
val := randRange[uint64](0, 1000)
tb := wfa.Traceback(randRange[uint64](0, 7))
v := wfa.PackWavefrontValue(val, tb)
valid, gotVal, gotTB := wfa.UnpackWavefrontValue(v)
if !valid || gotVal != val || gotTB != tb {
t.Errorf(`test WavefrontPack/Unpack, val: %d, tb: %d, packedval: %x, gotok: %t, gotval: %d, gottb: %d\n`, val, tb, v, valid, gotVal, gotTB)
}
}
}
func TestLoHiPacking(t *testing.T) {
for range 1000 {
lo := randRange[int](-1000, 1000)
hi := randRange[int](-1000, 1000)
v := wfa.PackWavefrontLoHi(lo, hi)
gotLo, gotHi := wfa.UnpackWavefrontLoHi(v)
if gotLo != lo || gotHi != hi {
t.Errorf(`test WavefrontPack/Unpack, lo: %d, hi: %d, packedval: %x, gotlo: %d, gothi: %d`, lo, hi, v, gotLo, gotHi)
}
}
}
func GetScoreFromCIGAR(CIGAR string, penalties wfa.Penalty) int {
unpackedCIGAR := wfa.RunLengthDecode(CIGAR)
previousOp := '~'
score := 0
for _, Op := range unpackedCIGAR {
if Op == 'M' {
score = score + penalties.M
} else if Op == 'X' {
score = score + penalties.X
} else if (Op == 'I' && previousOp != 'I') || (Op == 'D' && previousOp != 'D') {
score = score + penalties.O + penalties.E
} else if (Op == 'I' && previousOp == 'I') || (Op == 'D' && previousOp == 'D') {
score = score + penalties.E
}
previousOp = Op
}
return score
}
func CheckCIGARCorrectness(s1 string, s2 string, CIGAR string) bool {
unpackedCIGAR := wfa.RunLengthDecode(CIGAR)
i := 0
j := 0
s1Aligned := strings.Builder{}
alignment := strings.Builder{}
s2Aligned := strings.Builder{}
for c := 0; c < len(unpackedCIGAR); c++ {
Op := unpackedCIGAR[c]
if Op == 'M' {
s1Aligned.WriteByte(s1[i])
alignment.WriteRune('|')
s2Aligned.WriteByte(s2[j])
i++
j++
} else if Op == 'X' {
s1Aligned.WriteByte(s1[i])
alignment.WriteRune(' ')
s2Aligned.WriteByte(s2[j])
i++
j++
} else if Op == 'I' {
s1Aligned.WriteRune('-')
alignment.WriteRune(' ')
s2Aligned.WriteByte(s2[j])
j++
} else if Op == 'D' {
s1Aligned.WriteByte(s1[i])
alignment.WriteRune('|')
s2Aligned.WriteRune('-')
i++
}
}
if i == len(s1) && j == len(s2) {
return true
} else {
log.Printf("\n%s\n%s\n%s\n i=%d, j=%d, |s1|=%d, |s2|=%d\n", s1Aligned.String(), alignment.String(), s2Aligned.String(), i, j, len(s1), len(s2))
return false
}
}
func TestWFA(t *testing.T) {
content, _ := os.ReadFile(testJsonPath)
var testMap map[string]TestCase
json.Unmarshal(content, &testMap)
for k, v := range testMap {
testName := k
testPenalties := wfa.Penalty{
M: v.Penalties.M,
X: v.Penalties.X,
O: v.Penalties.O,
E: v.Penalties.E,
}
sequencesFile, _ := os.Open(testSequences)
sequences := bufio.NewScanner(sequencesFile)
solutionsFile, _ := os.Open(v.Solutions)
solutions := bufio.NewScanner(solutionsFile)
bar := progressbar.Default(305, k)
idx := 0
for solutions.Scan() {
solution := solutions.Text()
expectedScore, _ := strconv.Atoi(strings.Split(solution, "\t")[0])
expectedCIGAR := strings.Split(solution, "\t")[1]
sequences.Scan()
s1 := sequences.Text()
s1 = s1[1:]
sequences.Scan()
s2 := sequences.Text()
s2 = s2[1:]
x := wfa.WFAlign(s1, s2, testPenalties, true)
gotScore := x.Score
gotCIGAR := x.CIGAR
if gotScore != -1*expectedScore {
t.Errorf(`test: %s#%d, s1: %s, s2: %s, got: %d, expected: %d`, testName, idx, s1, s2, gotScore, expectedScore)
os.Exit(1)
}
if gotCIGAR != expectedCIGAR {
checkScore := GetScoreFromCIGAR(gotCIGAR, testPenalties)
CIGARCorrectness := CheckCIGARCorrectness(s1, s2, gotCIGAR)
if checkScore != gotScore && checkScore != -1*expectedScore { // nonequivalent alignment
t.Errorf(`test: %s#%d, s1: %s, s2: %s, got: [%s], expected: [%s]`, testName, idx, s1, s2, gotCIGAR, expectedCIGAR)
t.Errorf(`test: %s#%d, recalculated score: %d`, testName, idx, checkScore)
os.Exit(1)
}
if !CIGARCorrectness {
t.Errorf(`test: %s#%d, s1: %s, s2: %s, got: [%s], expected: [%s]`, testName, idx, s1, s2, gotCIGAR, expectedCIGAR)
os.Exit(1)
}
}
idx++
bar.Add(1)
}
}
}
-41
View File
@@ -1,41 +0,0 @@
import wf_align from "../src/wfa.js";
import fs from "fs";
import ProgressBar from "progress";
let data = fs.readFileSync("./tests/tests.json");
data = JSON.parse(data);
const sequences = fs.readFileSync("./tests/sequences").toString().split("\n");
// const total = sequences.length;
const total = 500; // skip the later tests because of memory usage
const timePerChar = [];
for (const test_name of Object.keys(data)) {
const test = data[test_name];
const penalties = test.penalties;
const solutions = fs.readFileSync(test.solutions).toString().split("\n");
const bar = new ProgressBar(":bar :current/:total", { total: total / 2 });
console.log(`test: ${test_name}`);
let correct = 0;
let j = 0;
for (let i = 0; i < total; i += 2) {
const s1 = sequences[i].replace(">");
const s2 = sequences[i + 1].replace("<");
const start = process.hrtime()[1];
const { score } = wf_align(s1, s2, penalties, false);
const elapsed = process.hrtime()[1] - start;
timePerChar.push((elapsed / 1e9) / (s1.length + s2.length));
const solution_score = Number(solutions[j].split("\t")[0]);
if (solution_score === -score) {
correct += 1;
}
j += 1;
bar.tick();
}
console.log(`correct: ${correct}\ntotal: ${total / 2}\n`);
console.log(`average time per character (ms): ${average(timePerChar) * 1000}`);
}
function average (arr) {
const sum = arr.reduce((a, b) => a + b, 0);
return sum / arr.length;
}
+604
View File
@@ -0,0 +1,604 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
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;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();
// wasm setup code
export default function init (path) {
if (typeof global === "undefined") {
var global = globalThis;
}
return new Promise ((res) => {
const go = new Go();
const run = (obj) => {
const wasm = obj.instance;
global.wfa = wasm
go.run(wasm);
res(wasm)
}
if ('instantiateStreaming' in WebAssembly) {
WebAssembly.instantiateStreaming(fetch(path), go.importObject).then(function (obj) {
run(obj)
})
} else {
fetch(path).then(resp =>
resp.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, go.importObject).then(function (obj) {
run(obj)
})
)
}
})
}