Running with Scriggo
This page covers Stage 2 · Scriggo VM (runtime): how the host drives the extension you wrote in Running with Native Go.
Same source, executed by Scriggo at runtime
Section titled “Same source, executed by Scriggo at runtime”The host reads my.extension.go from the extension directory, compiles it with the Scriggo engine, and invokes entry points by name (Latest / Search / Detail / Watch / Mirror / Load). This is the exact same source as Stage 1 — because the extension imports the SDK itself, the host compiles it as-is, with no rewriting or injected imports.
Runtime entry-point mapping
Section titled “Runtime entry-point mapping”| User action | Host call | Returns |
|---|---|---|
| Browse “Recently updated” | Latest(pkg, page) |
[]sdk.ExtensionListItem |
| Search | Search(pkg, kw, page, filter) |
[]sdk.ExtensionListItem |
| Open detail | Detail(pkg, url) |
*sdk.ExtensionDetail |
| Click play | Watch(pkg, url) → Mirror(pkg, url) |
Watch returns a mirror list (or ExtensionAllMirror); Mirror resolves the final playback shape |
| Extension loads | Load() |
optional, runs once on load |
Runtime notes
Section titled “Runtime notes”- Every call recompiles and runs a fresh Scriggo VM: local variables and package-level globals are not preserved across calls. State you must reuse (tokens, cookies, …) goes through
sdk.SaveCache/sdk.GetCache(values arestring). - Only packages the backend already uses are importable: packages you
importmust be part of the miru-core binary and exported inpackages.go(see “Importable Packages” in Detail Usage). Importing a package that is not registered inpackages.gofails compilation. - Scriggo limitations apply: method declarations, interface definitions, some
for rangeforms, etc. are unsupported — see “Scriggo Limitations” in Detail Usage.
Loading the extension in Miru
Section titled “Loading the extension in Miru”Put my.extension.go into Miru’s extension directory (click the import button in the top-right of the client’s Extensions page to locate it quickly), then restart/refresh extensions to run it under Stage 2 (Scriggo). A PR to the extension repository only needs the source file — no index.json.
Test the extension directly under Scriggo
Section titled “Test the extension directly under Scriggo”Besides falling back to Stage 1’s go test, you can compile and invoke the extension with the Scriggo VM itself to validate Stage 2 behavior — this is exactly what miru-core’s runtime does, so it reproduces “compile failure / runtime error” most faithfully.
Approach 1: low-level API (matches TestDualStage)
Section titled “Approach 1: low-level API (matches TestDualStage)”miru-core exposes NewScriggoVM / Compile / Program.Call: read the source, compile, and call by name:
// scriggo_test.go —— in a test module that can import miru-corepackage myextension_test
import ( "fmt" "os" "testing"
golang "github.com/miru-project/miru-core/pkg/extension/golang")
func TestUnderScriggo(t *testing.T) { src, err := os.ReadFile("my.extension.go") // read the extension source if err != nil { t.Fatal(err) }
vm := golang.NewScriggoVM(nil) // create the Scriggo VM prog, err := vm.Compile("my.extension_Search", string(src)) if err != nil { t.Fatalf("compile failed (this is the runtime 'compile extension' error): %v", err) }
// Call the entry by name; arg order matches the host: pkg first, then business args res, err := prog.Program.Call("Search", "my.extension", "naruto", 1, "") if err != nil { t.Fatalf("runtime error: %v", err) }
// Call returns []any; the first element is your return value ([]sdk.ExtensionListItem here) items, _ := res[0].([]interface{}) fmt.Printf("got %d items: %+v\n", len(items), items)}Key points:
vm.Compile(name, src)—nameis arbitrary;srcis the full text of the.gofile.prog.Program.Call("Search", args...)returns([]any, error)— the same Scriggo compile/call path the host’scallExtensionuses, so compile errors surface verbatim (message starts withcompile extension).- The return is
[]any, indexed by the entry’s return position:res[0]is the first return (e.g.[]sdk.ExtensionListItem),res[1]is theerror.
Approach 2: high-level Runtime (mirrors the host load flow)
Section titled “Approach 2: high-level Runtime (mirrors the host load flow)”To also mirror “parse metadata + run Load + call by name” exactly like the host, use NewRuntime:
func TestUnderScriggoRuntime(t *testing.T) { golang.ExtensionDir = "." // extension dir (holds my.extension.go) rt := golang.NewRuntime(golang.NewScriggoVM(nil))
ext, err := golang.ParseExtensionMetadata("my.extension") if err != nil { t.Fatal(err) } if err := rt.LoadExtension(ext); err != nil { // parse metadata + compile + run Load t.Fatalf("load failed: %v", err) }
res, err := rt.Call("Search", "my.extension", "naruto", 1, "") // call by name if err != nil { t.Fatalf("call failed: %v", err) } _ = res}This is closest to how Miru actually runs: LoadExtension first (executes the Load entry and surfaces compile errors early), then rt.Call drives each entry.
Debugging (Scriggo runtime)
Section titled “Debugging (Scriggo runtime)”Stage 2 has no breakpoint debugger; error messages come from the host’s Scriggo compile/run wrapper. Understanding the error format locates problems fast.
Where errors come from
Section titled “Where errors come from”The host (callExtension in endpoint.go) hands the extension source to Scriggo as-is to compile and invoke by name. Errors fall into two classes:
- Compile errors: the source can’t be compiled by Scriggo; the message starts with
compile extension <pkg>: .... Typical causes:- Scriggo-unsupported syntax (method declarations, interface definitions, labeled
continue/break, certainfor rangeforms — see “Scriggo Limitations” in Detail Usage). - Importing a package not registered in
packages.go(see “Importable Packages”). - Hitting a performance hard cap (per-function registers / types / constants exceeded).
- Scriggo-unsupported syntax (method declarations, interface definitions, labeled
- Runtime errors: compilation succeeded but execution failed; the host wraps it with
withStackTraceand returns a stack trace. Read the top of the stack (entry function + line) first, then go down to see which line panicked / returned an error.
Reading errors in the Miru client
Section titled “Reading errors in the Miru client”When an extension fails at runtime, the Miru client shows an error on the relevant action (detail / play / …). Copy the full error text — especially a compile extension ... message or a line-numbered stack — and triage using the two classes above.
Isolate logic bugs by “falling back to Stage 1”
Section titled “Isolate logic bugs by “falling back to Stage 1””At Scriggo runtime you can’t see fmt output, and errors are less detailed than native Go. To locate a logic bug, take the same source back to Stage 1:
cd my.extensiongo test -run TestSearch -v # reproduce with go test: wrong return shape, or network/parse failure?- If Stage 1
go testalso fails: it’s your logic (parsing, field names, types) — fix it under native Go with Delve /fmt. - If Stage 1 passes but Stage 2 errors: it’s almost certainly a Scriggo limitation or un-exported package issue (unsupported syntax / un-registered import / register-or-type cap) — check it against Detail Usage point by point.
Common runtime pitfalls
Section titled “Common runtime pitfalls”- State lost across calls: don’t assume globals / previous call’s locals persist — every call is a fresh Scriggo VM. Use
sdk.SaveCache/sdk.GetCachefor anything reusable. reflect/fmt %Tcan’t see your custom types: Scriggo-defined types show their underlying wrapped name underreflect; don’t rely on it for type checks.nilvs zero value: when returning a pointer (e.g.*sdk.ExtensionDetail) make sure it’s non-nil, or host deserialization will fail.- Oversized single function: split huge constants / types / closures across multiple entry functions to avoid the performance hard caps.