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
56 changes: 56 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,62 @@ jobs:
revive -config revive.toml -formatter friendly ./... >> $GITHUB_STEP_SUMMARY
printf '```\n\n' >> $GITHUB_STEP_SUMMARY

wasm:
name: Cross-build ${{ matrix.goos }}/${{ matrix.goarch }} with ${{ matrix.go-version }} on ${{ matrix.vm-os }}
runs-on: ${{ matrix.vm-os }}
strategy:
max-parallel: 10
fail-fast: false
matrix:
vm-os: [
ubuntu-22.04,
macos-14,
windows-2022
]
go-version: [
1.19.x,
1.25.x,
]
goos: [
js,
wasip1
]
goarch: [
wasm
]
exclude:
# WASI support starts after Go 1.19; keep the compatibility floor
# on browser js/wasm and verify WASI on the current toolchain.
- go-version: 1.19.x
goos: wasip1
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true
- name: Cross-build package
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
go version
go tool dist list | grep -Fx "${GOOS}/${GOARCH}"
go mod download
go build .
- name: Guard REPL terminal dependencies
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
run: |
if go list -deps . | grep -E '^(go\.starlark\.net/repl|github\.com/chzyer/readline)$'; then
echo "terminal-only REPL dependency leaked into ${GOOS}/${GOARCH}"
exit 1
fi

# cmd/starlet is a separate Go module with its own (older) pins and extra
# third-party dependencies; build it as a non-gating signal so a CLI-side
# breakage is visible without blocking library PRs.
Expand Down
16 changes: 6 additions & 10 deletions run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,16 @@ import (

"github.com/1set/starlet/lib/goidiomatic"
"github.com/1set/starlight/convert"
"go.starlark.net/repl"
"go.starlark.net/starlark"
"go.starlark.net/syntax"
)

// REPL is a Read-Eval-Print-Loop for Starlark.
// It loads the predeclared symbols and modules into the global environment,
func (m *Machine) REPL() {
if err := m.prepareThread(nil); err != nil {
repl.PrintError(err)
return
}
repl.REPLOptions(m.getFileOptions(), m.thread, m.predeclared)
}
// REPL is defined in run_repl.go (terminal targets) / run_repl_stub.go
// (non-terminal targets): the
// interactive REPL pulls go.starlark.net/repl -> chzyer/readline, a terminal
// library that does not compile for browser js/wasm or WASI. Isolating it
// behind a build tag keeps the library core (and every consumer, e.g. a WASM
// playground) free of that terminal dependency.

// RunScript initiates a Machine, executes a script with extra variables, and returns the Machine and the execution result.
func RunScript(content []byte, extras StringAnyMap) (*Machine, StringAnyMap, error) {
Expand Down
19 changes: 19 additions & 0 deletions run_repl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//go:build !js && !wasip1

package starlet

import "go.starlark.net/repl"

// REPL is a Read-Eval-Print-Loop for Starlark. It loads the predeclared
// symbols and modules into the global environment and reads from the terminal.
//
// This lives in a terminal-target file because go.starlark.net/repl pulls in
// chzyer/readline, a terminal library that does not compile for browser
// js/wasm or WASI; see run_repl_stub.go for the no-terminal stub.
func (m *Machine) REPL() {
if err := m.prepareThread(nil); err != nil {
repl.PrintError(err)
return
}
repl.REPLOptions(m.getFileOptions(), m.thread, m.predeclared)
}
12 changes: 12 additions & 0 deletions run_repl_stub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//go:build js || wasip1

package starlet

// REPL is unavailable on no-terminal wasm targets: the upstream
// go.starlark.net/repl pulls in chzyer/readline, which depends on terminal
// primitives that are not available in browser js/wasm or WASI. Drive the
// Machine with Run/RunScript instead. This stub keeps the method present so
// consumer code referencing REPL still compiles for wasm.
func (m *Machine) REPL() {
// no-op on no-terminal wasm targets; use Run/RunScript
}
49 changes: 49 additions & 0 deletions run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"fmt"
"io/fs"
"os"
"os/exec"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -2140,6 +2142,53 @@ func TestMachine_REPL_Error(t *testing.T) {
m.REPL()
}

// TestBuildsForWASM guards the wasm portability of the library core.
// The interactive REPL pulls go.starlark.net/repl -> github.com/chzyer/readline,
// a terminal library that does not compile for no-terminal wasm targets; it is
// therefore isolated behind build tags (run_repl.go for terminal targets,
// run_repl_stub.go for js/wasm and WASI). Re-importing a terminal dependency
// into an always-compiled file would silently break wasm consumers (e.g. a
// browser playground).
func TestBuildsForWASM(t *testing.T) {
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("go toolchain not found; skipping wasm build check")
}

targets := []struct {
goos string
goarch string
}{
{goos: "js", goarch: "wasm"},
{goos: "wasip1", goarch: "wasm"},
}
for _, target := range targets {
target := target
t.Run(target.goos+"/"+target.goarch, func(t *testing.T) {
if !goTargetSupported(t, goBin, target.goos, target.goarch) {
t.Skipf("go toolchain does not support %s/%s", target.goos, target.goarch)
}
cmd := exec.Command(goBin, "build", ".")
cmd.Env = append(os.Environ(), "GOOS="+target.goos, "GOARCH="+target.goarch)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("starlet must compile for GOOS=%s GOARCH=%s — did a terminal "+
"dependency (e.g. chzyer/readline via go.starlark.net/repl) leak into "+
"an always-compiled file?\n%s\n%v", target.goos, target.goarch, out, err)
}
})
}
}

func goTargetSupported(t *testing.T, goBin, goos, goarch string) bool {
t.Helper()
out, err := exec.Command(goBin, "tool", "dist", "list").Output()
if err != nil {
t.Logf("warning: failed to list go targets: %v", err)
return false
}
return strings.Contains(string(out), goos+"/"+goarch+"\n")
}

func Test_Machine_Run_LoadTypedErrors(t *testing.T) {
// an unknown name surfaces as ModuleNotFoundError through the chain
m := starlet.NewDefault()
Expand Down
Loading