Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ jobs:
|| { echo "::error::$repo is not on the page"; exit 1; }
done
echo "the page names every repository this org has"
- name: the playground page is built and linked
run: |
test -f public/playground/index.html || { echo "::error::no /playground/ page"; exit 1; }
grep -q 'id=nd-canvas' public/playground/index.html || { echo "::error::the playground page has no canvas"; exit 1; }
grep -qE 'href="?/playground/"?' public/index.html || { echo "::error::the landing does not link to the playground"; exit 1; }
echo "the playground page is built, and the landing links to it"
24 changes: 24 additions & 0 deletions .github/workflows/hugo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,34 @@ jobs:
id: pages
uses: actions/configure-pages@v6

# The playground's two assets are build artifacts, never committed: the
# wasm compiled from playground/ at this commit, and wasm_exec.js taken
# from the SAME toolchain (the two must match). Built before Hugo, which
# reads the wasm's size for the loader's progress bar.
- name: Setup Go
uses: actions/setup-go@v7
with:
go-version: "1.27.1"
cache-dependency-path: playground/go.sum

- name: Build the playground
working-directory: playground
env:
GOWORK: "off"
GOTOOLCHAIN: local
run: |
mkdir -p ../static/playground
GOOS=js GOARCH=wasm go build -trimpath \
-ldflags "-s -w -X main.buildVersion=$(git rev-parse --short HEAD)" \
-o ../static/playground/playground.wasm ./cmd/playground-wasm
cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" ../static/playground/

- name: Build
env:
HUGO_ENVIRONMENT: production
HUGO_ENV: production
# Cache-busts the playground assets: a new deploy is a new URL.
HUGO_PARAMS_WASMSHA: ${{ github.sha }}
run: |
hugo \
--gc \
Expand Down
131 changes: 131 additions & 0 deletions .github/workflows/playground.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# The playground (playground/, a nested Go module): go-ndarray compiled to
# WebAssembly inside a go-widgets canvas app. Three gates:
#
# test gofmt, vet, the native suite under -race at 100% statement
# coverage (cmd/playground-wasm is the js-only shell and is
# excluded), the engine suite run AS WebAssembly under node
# (where GOMAXPROCS is 1 and go-ndarray must start no helper
# goroutine), and the wasm build, whose size is reported.
# ui-guards mvvmlint (no direct widget-state writes) and bricolint (no
# hand-drawn UI), each with a negative control proving it bites.
# browser-proof the BUILT page (Hugo + the wasm) in headless Chrome, light
# and dark forced, 1x and 2x: real clicks and keys, asserted on
# the app's state AND on canvas pixels. A missing browser is a
# failure here, never a skip.
name: playground

on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

env:
GOWORK: "off"
GOTOOLCHAIN: local

jobs:
test:
name: test, coverage, wasm
runs-on: ubuntu-latest
defaults:
run:
working-directory: playground
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.27.1"
cache-dependency-path: playground/go.sum
- uses: actions/setup-node@v7
with:
node-version: "24"
- name: gofmt
run: test -z "$(gofmt -l .)" || { gofmt -l .; exit 1; }
- name: vet (native and js/wasm)
run: go vet ./... && GOOS=js GOARCH=wasm go vet ./...
- name: test (-race)
run: go test -race -count=1 ./...
- name: 100% statement coverage
run: |
go test -count=1 -coverprofile=coverage.out . ./engine ./vm
go tool cover -func=coverage.out | tee coverage.txt | tail -1
bad=$(awk '$NF!="100.0%"' coverage.txt)
[ -z "$bad" ] || { echo "::error::below 100%:"; echo "$bad"; exit 1; }
- name: engine suite as WebAssembly (node)
run: GOOS=js GOARCH=wasm go test -count=1 -v -run 'TestHelperGoroutines|TestOperations|TestErrors|TestLayout' -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" ./engine
- name: wasm build and size
run: |
GOOS=js GOARCH=wasm go build -trimpath -o /tmp/playground.wasm ./cmd/playground-wasm
raw=$(stat -c %s /tmp/playground.wasm); gz=$(gzip -9 -c /tmp/playground.wasm | wc -c)
echo "playground.wasm: $raw bytes, $gz gzipped" | tee -a "$GITHUB_STEP_SUMMARY"

ui-guards:
name: mvvmlint + bricolint
runs-on: ubuntu-latest
defaults:
run:
working-directory: playground
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.27.1"
cache-dependency-path: playground/go.sum
- name: install the guards
run: |
go install github.com/go-widgets/mvvmlint/cmd/mvvmlint@latest
go install github.com/go-widgets/bricolint/cmd/bricolint@v0.1.0
- name: MVVM only (mvvmlint)
run: go vet -vettool="$(go env GOPATH)/bin/mvvmlint" ./...
- name: no hand-drawn UI (bricolint)
run: go vet -vettool="$(go env GOPATH)/bin/bricolint" ./...
- name: negative controls (each guard must bite)
run: BRICOLINT="$(go env GOPATH)/bin/bricolint" MVVMLINT="$(go env GOPATH)/bin/mvvmlint" bash hack/lint-negative-controls.sh

browser-proof:
name: browser proof (headless Chrome)
runs-on: ubuntu-latest
defaults:
run:
working-directory: playground
env:
PLAYGROUND_REQUIRE_BROWSER: "1"
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version: "1.27.1"
cache-dependency-path: playground/go.sum
- uses: actions/setup-node@v7
with:
node-version: "24"
- uses: peaceiris/actions-hugo@v3
with:
hugo-version: "0.167.0"
extended: true
- name: install headless Chrome
id: chrome
uses: browser-actions/setup-chrome@48ad923757ca74d66703209fe939badbdf80f2f4 # v2.2.0
with:
chrome-version: stable
install-dependencies: true
- name: install puppeteer-core
run: npm install --no-save --prefix "${{ runner.temp }}/pptr" puppeteer-core@24.43.1
- name: run the proof
env:
CHROME: ${{ steps.chrome.outputs.chrome-path }}
PLAYGROUND_NODE_PATH: ${{ runner.temp }}/pptr/node_modules
PLAYGROUND_CAPTURE_DIR: ${{ runner.temp }}/captures
run: go test -count=1 -v -run TestBrowserProof .
- name: keep the screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: playground-captures
path: ${{ runner.temp }}/captures
if-no-files-found: ignore
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ public/
resources/
.hugo_build.lock
.DS_Store
# Built by the pages workflow from playground/ (never committed).
static/playground/playground.wasm
static/playground/wasm_exec.js
4 changes: 4 additions & 0 deletions content/playground.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
title: "Playground"
layout: "playground"
---
141 changes: 141 additions & 0 deletions layouts/_default/playground.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Playground — {{ .Site.Title }}</title>
<meta name="description" content="Try go-ndarray in your browser: create arrays, apply views, broadcasting, ufuncs, reductions and linear algebra step by step, and see shapes, strides, values, timings and the equivalent Go and NumPy code. go-ndarray itself, compiled to WebAssembly.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
{{ partial "styles.html" . }}
<style>
/* Playground-only rules, kept off the landing. The page is the landing's
nav bar over ONE go-widgets canvas that fills the rest of the window; the
app inside draws its own widgets, recoloured from the page theme. */
html,body{height:100%}
body.pg{display:flex;flex-direction:column;overflow:hidden}
.pg .nav{position:relative}
.stage{position:relative;flex:1;min-height:0;background:var(--bg)}
#nd-canvas{display:block;width:100%;height:100%;opacity:0;transition:opacity .2s}
.stage.ready #nd-canvas{opacity:1}
#pg-load{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;color:var(--slate);font-size:14px}
.stage.ready #pg-load{display:none}
.pg-track{width:min(420px,72%);height:8px;border-radius:6px;background:var(--line);overflow:hidden}
.pg-fill{height:100%;width:0;background:var(--accent);transition:width .15s ease}
#pg-pct{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-variant-numeric:tabular-nums}
#pg-err{display:none;max-width:680px;margin:0 16px;padding:11px 13px;border-radius:10px;background:#fdecec;color:#9b1c1c;white-space:pre-wrap;font:12.5px/1.45 ui-monospace,Menlo,monospace}
:root[data-theme="dark"] #pg-err{background:#3a1113;color:#f7b4b4}
@media(prefers-color-scheme:dark){:root:not([data-theme]) #pg-err{background:#3a1113;color:#f7b4b4}}
#pg-err.show{display:block}
.pg .nav nav a.here{color:var(--ink)}
</style>
</head>
<body class="lm pg">
<header class="nav"><div class="wrap">
<a class="brand" href="/"><img class="glyph" src="/img/logo.svg" width="32" height="32" alt="go-ndarray logo"> go-ndarray</a>
<nav><a href="/">Home</a><a href="/docs/">Docs</a><a class="here" href="/playground/">Playground</a></nav>
{{ partial "theme-toggle.html" . }}
<a class="btn btn-ghost" href="https://github.com/go-ndarray/ndarray">{{ partial "icons/github.html" . }}GitHub</a>
</div></header>

<div class="stage">
<canvas id="nd-canvas" aria-label="go-ndarray playground: an interactive canvas application"></canvas>
<div id="pg-load">
<div id="pg-msg">Downloading go-ndarray compiled to WebAssembly…</div>
<div class="pg-track"><div class="pg-fill" id="pg-fill"></div></div>
<div id="pg-pct">0%</div>
<div id="pg-err"></div>
</div>
</div>
<noscript><p style="padding:16px">The playground runs go-ndarray compiled to WebAssembly and needs JavaScript to start it.</p></noscript>

{{/* Both files are build artifacts: the pages workflow compiles the wasm from
playground/ and copies wasm_exec.js from the SAME Go toolchain, so the two
always match. Neither is committed. ?v= is the deployed commit, so a new
deploy is a new URL and a cached copy can only be the bytes it names. */}}
{{ $sha := .Site.Params.wasmsha }}
{{ $wasm := "/playground/playground.wasm" }}
{{ $exec := "/playground/wasm_exec.js" }}
{{ with $sha }}{{ $wasm = printf "%s?v=%s" $wasm . }}{{ $exec = printf "%s?v=%s" $exec . }}{{ end }}
{{ $bytes := 0 }}{{ with (os.Stat "static/playground/playground.wasm") }}{{ $bytes = .Size }}{{ end }}
<script>
/* Go's wasm runtime reads globalThis.fs/process/path; an element id of the
same name would shadow them and kill the runtime at init. None here. */
</script>
<script src="{{ $exec }}"></script>
<script>
"use strict";
(function(){
var $=function(id){return document.getElementById(id);};
var wasmURL={{ $wasm }};
// The decompressed size, read at build time: Pages serves the wasm gzipped,
// so Content-Length is the compressed size while the reader below counts
// decompressed bytes. 0 when the wasm was not built (a bare `hugo server`).
var wasmBytes={{ $bytes }};

function isDark(){
var t=document.documentElement.getAttribute("data-theme");
if(t==="dark") return true;
if(t==="light") return false;
return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
}
function syncTheme(){ if(globalThis.ndarraySetTheme) globalThis.ndarraySetTheme(isDark()); }
// The nav bar's toggle (styles.html) writes data-theme; follow it, and the
// OS preference while it is on System.
new MutationObserver(syncTheme).observe(document.documentElement,{attributes:true,attributeFilter:["data-theme"]});
if(window.matchMedia){ try{ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",syncTheme); }catch(e){} }

function progress(f){
var fill=$("pg-fill"), pct=$("pg-pct");
if(f==null){ fill.style.width="100%"; fill.style.opacity=".45"; pct.textContent="…"; return; }
var p=Math.max(0,Math.min(100,Math.round(f*100)));
fill.style.opacity="1"; fill.style.width=p+"%"; pct.textContent=p+"%";
}
function fail(e){
$("pg-msg").textContent="The playground failed to load.";
var err=$("pg-err");
err.textContent=String(e)+(location.protocol==="file:"?"\n(serve the page over HTTP: a page opened from a file cannot fetch the wasm)":"\n(a hard refresh may help if a stale copy is cached)");
err.classList.add("show");
}
function fetchWasm(url){
return fetch(url).then(function(r){
if(!r.ok) throw new Error("HTTP "+r.status+" fetching "+url);
var total=wasmBytes||parseInt(r.headers.get("Content-Length")||"0",10)||0;
if(!r.body||!r.body.getReader) return r.arrayBuffer();
var reader=r.body.getReader(), got=0, chunks=[];
return (function pump(){
return reader.read().then(function(res){
if(res.done){
var buf=new Uint8Array(got), pos=0;
chunks.forEach(function(c){ buf.set(c,pos); pos+=c.length; });
return buf.buffer;
}
chunks.push(res.value); got+=res.value.length;
progress(total?got/total:null);
return pump();
});
})();
});
}
function start(){
if(typeof Go!=="function"){ fail("wasm_exec.js did not load"); return; }
var go=new Go();
fetchWasm(wasmURL).then(function(ab){
progress(1);
$("pg-msg").textContent="Starting…";
return WebAssembly.instantiate(ab,go.importObject);
}).then(function(res){
go.run(res.instance);
var t=setInterval(function(){
if(globalThis.ndarrayPlaygroundReady){
clearInterval(t);
syncTheme();
document.querySelector(".stage").classList.add("ready");
}
},30);
}).catch(fail);
}
if(document.readyState!=="loading") start(); else document.addEventListener("DOMContentLoaded",start);
})();
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion layouts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<body class="lm">
<header class="nav"><div class="wrap">
<a class="brand" href="/"><img class="glyph" src="/img/logo.svg" width="32" height="32" alt="go-ndarray logo"> go-ndarray</a>
<nav><a href="#features">Why</a><a href="#check">Example</a><a href="#api">API</a><a href="#repos">Repositories</a></nav>
<nav><a href="#features">Why</a><a href="#check">Example</a><a href="#api">API</a><a href="#repos">Repositories</a><a href="/playground/">Playground</a></nav>
{{ partial "theme-toggle.html" . }}
<a class="btn btn-ghost" href="https://github.com/go-ndarray">{{ partial "icons/github.html" . }}GitHub</a>
</div></header>
Expand Down
Loading
Loading