From 012d86433ea7db17da0522e6d20fe68cb471732f Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 15 Apr 2026 17:38:17 +1000 Subject: [PATCH 01/10] prepare for local 'Publishing to Docker Hub' --- doc/docker.md | 2 +- packages/runtime/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/docker.md b/doc/docker.md index d2d9850c..c5f3dbf4 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -129,7 +129,7 @@ export VERSION=$(cat packages/runtime/package.json | jq -r '.version') # # or if already created # docker buildx use ohmjs-builder -# generate a person access token at https://app.docker.com/accounts/millergarym/settings/personal-access-tokens +# generate a person access token at https://app.docker.com/accounts//settings/personal-access-tokens # assuming DHPAT contains your PAT echo $DHPAT | docker login -u --password-stdin cd docker diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 60296b47..0bfedfde 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "ohm-js", - "version": "18.0.0-beta.10", + "version": "18.0.0-beta.13", "description": "Ohm runtime — CST nodes, Grammar, and MatchResult", "keywords": ["parser", "parsing", "ohm", "ohm-js", "runtime"], "homepage": "https://github.com/ohmjs/ohm#readme", From 85f0f7c496590306d90c61f11a7ca582e0c107e6 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 15 Apr 2026 20:23:13 +1000 Subject: [PATCH 02/10] moved and refactor golang code --- .github/workflows/go-test.yml | 26 ++++++++ go.work | 5 ++ go.work.sum | 4 ++ golang/runtime/.gitignore | 1 + .../test/go => golang/runtime}/README.md | 0 .../test/go => golang/runtime}/cst.go | 6 -- golang/runtime/generate.sh | 11 ++++ golang/runtime/go.mod | 7 ++ golang/runtime/go.sum | 4 ++ .../test/go => golang/runtime}/grammar.go | 66 ++----------------- .../go => golang/runtime}/grammar_test.go | 21 ++---- packages/compiler/package.json | 2 +- packages/compiler/test/go/go.mod | 5 -- packages/compiler/test/go/go.sum | 2 - 14 files changed, 68 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/go-test.yml create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 golang/runtime/.gitignore rename {packages/compiler/test/go => golang/runtime}/README.md (100%) rename {packages/compiler/test/go => golang/runtime}/cst.go (99%) create mode 100755 golang/runtime/generate.sh create mode 100644 golang/runtime/go.mod create mode 100644 golang/runtime/go.sum rename {packages/compiler/test/go => golang/runtime}/grammar.go (96%) rename {packages/compiler/test/go => golang/runtime}/grammar_test.go (92%) delete mode 100644 packages/compiler/test/go/go.mod delete mode 100644 packages/compiler/test/go/go.sum diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 00000000..24565f53 --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,26 @@ +name: Go Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.work + + - name: Compile grammar to wasm + run: ./golang/runtime/generate.sh + + - name: Run Go tests + working-directory: golang/runtime + run: go test ./... \ No newline at end of file diff --git a/go.work b/go.work new file mode 100644 index 00000000..7079e67a --- /dev/null +++ b/go.work @@ -0,0 +1,5 @@ +go 1.24.2 + +use ( + ./golang/runtime +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 00000000..6cfca8b4 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,4 @@ +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/golang/runtime/.gitignore b/golang/runtime/.gitignore new file mode 100644 index 00000000..32e6b260 --- /dev/null +++ b/golang/runtime/.gitignore @@ -0,0 +1 @@ +es5.wasm diff --git a/packages/compiler/test/go/README.md b/golang/runtime/README.md similarity index 100% rename from packages/compiler/test/go/README.md rename to golang/runtime/README.md diff --git a/packages/compiler/test/go/cst.go b/golang/runtime/cst.go similarity index 99% rename from packages/compiler/test/go/cst.go rename to golang/runtime/cst.go index b3c5da0d..ef293d75 100644 --- a/packages/compiler/test/go/cst.go +++ b/golang/runtime/cst.go @@ -16,7 +16,6 @@ const ( CstNodeTypeList CstNodeType = 2 CstNodeTypeOpt CstNodeType = 3 ) - const ( matchRecordTypeMask = 3 cstNodeHeaderSize = 16 @@ -54,7 +53,6 @@ func isTerminal(raw uint32) bool { } // --- internal field accessors --- - func (n *CstNode) typeAndDetails() int32 { data, ok := n.ctx.memory.Read(n.base+4, 4) if !ok { @@ -83,7 +81,6 @@ func (n *CstNode) count() uint32 { } // --- public API (matches the ohm-js CstNode interface) --- - // Type returns the CstNodeType for this node. func (n *CstNode) Type() CstNodeType { if isTerminal(n.base) { @@ -214,12 +211,10 @@ func (n *CstNode) Children() []*CstNode { return children[:i] } slot := readUint32(data, 0) - // Bit 1 is the HAS_LEADING_SPACES edge flag. hasLeadingSpaces := slot&2 != 0 // Strip the edge flag to get the actual value. raw := slot & ^uint32(2) - // Account for implicit leading spaces. // Only apply if spaces were actually recorded at this position // and the result stays within the parent's span. @@ -229,7 +224,6 @@ func (n *CstNode) Children() []*CstNode { startIdx += spacesLen } } - child := newCstNode(n.ctx, raw, startIdx) children[i] = child startIdx += child.MatchLength() diff --git a/golang/runtime/generate.sh b/golang/runtime/generate.sh new file mode 100755 index 00000000..ff56d935 --- /dev/null +++ b/golang/runtime/generate.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +REPO_ROOT=$(git rev-parse --show-toplevel) + +if [ "$OHM_DOCKER_IMAGE_VERSION" != "" ]; then + VERSION="$OHM_DOCKER_IMAGE_VERSION" +else + VERSION=$(cat "${REPO_ROOT}/packages/runtime/package.json" | jq -r '.version') +fi +echo "Using Ohm Docker image version: ${VERSION}" +docker run --rm -v "${REPO_ROOT}:/local" ohmjs/ohm:${VERSION} compile -o golang/runtime/es5.wasm examples/ecmascript/src/es5.ohm \ No newline at end of file diff --git a/golang/runtime/go.mod b/golang/runtime/go.mod new file mode 100644 index 00000000..eaffd370 --- /dev/null +++ b/golang/runtime/go.mod @@ -0,0 +1,7 @@ +module github.com/ohmjs/goohm + +go 1.24.2 + +require github.com/tetratelabs/wazero v1.11.0 + +require golang.org/x/sys v0.38.0 // indirect diff --git a/golang/runtime/go.sum b/golang/runtime/go.sum new file mode 100644 index 00000000..6cfca8b4 --- /dev/null +++ b/golang/runtime/go.sum @@ -0,0 +1,4 @@ +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= diff --git a/packages/compiler/test/go/grammar.go b/golang/runtime/grammar.go similarity index 96% rename from packages/compiler/test/go/grammar.go rename to golang/runtime/grammar.go index 7b93a814..d4a5be19 100644 --- a/packages/compiler/test/go/grammar.go +++ b/golang/runtime/grammar.go @@ -35,13 +35,11 @@ func (g *Grammar) GetModule() api.Module { // This parallels Grammar.instantiate() in the JS API. func NewGrammar(ctx context.Context, wasmBytes []byte) (*Grammar, error) { config := wazero.NewRuntimeConfig().WithCustomSections(true) - g := &Grammar{ runtime: wazero.NewRuntimeWithConfig(ctx, config), ctx: ctx, ruleIds: make(map[string]int), } - // Create the env module with the abort function _, err := g.runtime.NewHostModuleBuilder("env"). NewFunctionBuilder(). @@ -53,41 +51,26 @@ func NewGrammar(ctx context.Context, wasmBytes []byte) (*Grammar, error) { if err != nil { return nil, fmt.Errorf("failed to create env module: %v", err) } - // Create the ohmRuntime module with required host functions + // Note: referring to a method, without calling it is a function with the receiever curried. _, err = g.runtime.NewHostModuleBuilder("ohmRuntime"). - NewFunctionBuilder(). - WithFunc(func(ctx context.Context, mod api.Module, dest, length uint32) uint32 { - return g.fillInputBuffer(ctx, mod, dest, length) - }). - Export("fillInputBuffer"). - NewFunctionBuilder(). - WithFunc(func(ctx context.Context, mod api.Module, categoryBitmap uint32) uint32 { - return g.matchUnicodeChar(ctx, mod, categoryBitmap) - }). - Export("matchUnicodeChar"). - NewFunctionBuilder(). - WithFunc(func(ctx context.Context, mod api.Module, stringIdx uint32) uint32 { - return g.matchCaseInsensitive(ctx, mod, stringIdx) - }). - Export("matchCaseInsensitive"). + NewFunctionBuilder().WithFunc(g.fillInputBuffer).Export("fillInputBuffer"). + NewFunctionBuilder().WithFunc(g.matchUnicodeChar).Export("matchUnicodeChar"). + NewFunctionBuilder().WithFunc(g.matchCaseInsensitive).Export("matchCaseInsensitive"). Instantiate(g.ctx) if err != nil { return nil, fmt.Errorf("failed to create ohmRuntime module: %v", err) } - // Compile the module to access the custom sections compiledModule, err := g.runtime.CompileModule(g.ctx, wasmBytes) if err != nil { return nil, fmt.Errorf("error compiling module: %v", err) } - // Parse custom sections customSections := compiledModule.CustomSections() if customSections == nil { return nil, fmt.Errorf("no custom sections found in module") } - for _, section := range customSections { switch section.Name() { case "ruleNames": @@ -102,23 +85,19 @@ func NewGrammar(ctx context.Context, wasmBytes []byte) (*Grammar, error) { } } } - if g.ruleNames == nil { return nil, fmt.Errorf("required custom section 'ruleNames' not found") } - // Instantiate the module g.module, err = g.runtime.InstantiateModule(g.ctx, compiledModule, wazero.NewModuleConfig()) if err != nil { return nil, fmt.Errorf("error instantiating module: %v", err) } - // Build the ruleIds map g.ruleIds = make(map[string]int, len(g.ruleNames)) for i, name := range g.ruleNames { g.ruleIds[name] = i } - return g, nil } @@ -128,7 +107,6 @@ func parseLEB128Strings(data []byte) ([]string, error) { if len(data) == 0 { return nil, fmt.Errorf("empty custom section data") } - numUint64, bytesRead := binary.Uvarint(data) if bytesRead <= 0 { return nil, fmt.Errorf("failed to read count: %v", io.ErrUnexpectedEOF) @@ -136,10 +114,8 @@ func parseLEB128Strings(data []byte) ([]string, error) { if numUint64 > uint64(^uint32(0)) { return nil, fmt.Errorf("count exceeds maximum uint32 value") } - num := uint32(numUint64) data = data[bytesRead:] - result := make([]string, num) for i := uint32(0); i < num; i++ { lenUint64, bytesRead := binary.Uvarint(data) @@ -149,18 +125,14 @@ func parseLEB128Strings(data []byte) ([]string, error) { if lenUint64 > uint64(^uint32(0)) { return nil, fmt.Errorf("string length exceeds maximum uint32 value") } - strLen := uint32(lenUint64) data = data[bytesRead:] - if uint64(len(data)) < uint64(strLen) { return nil, fmt.Errorf("buffer too small to read string bytes") } - result[i] = string(data[:strLen]) data = data[strLen:] } - return result, nil } @@ -204,7 +176,6 @@ func (g *Grammar) writeOffset(val uint64) { func (g *Grammar) Match(input string, startRule ...string) (*MatchResult, error) { g.input = input g.inputUTF16 = utf16.Encode([]rune(input)) - // Resolve the rule ID var ruleId uint64 startExpr := "" @@ -216,22 +187,18 @@ func (g *Grammar) Match(input string, startRule ...string) (*MatchResult, error) } ruleId = uint64(id) } - // Snapshot the heap bump pointer before the match. heapWatermark := g.readOffset() - // Call match(inputLength, startRuleId) matchFunc := g.module.ExportedFunction("match") if matchFunc == nil { return nil, fmt.Errorf("match function not exported by module") } - inputLength := uint64(len(g.inputUTF16)) results, err := matchFunc.Call(g.ctx, inputLength, ruleId) if err != nil { return nil, fmt.Errorf("error calling match function: %v", err) } - result := &MatchResult{ grammar: g, input: input, @@ -304,28 +271,23 @@ func (r *MatchResult) cstContext() *cstContext { func (r *MatchResult) GetCstRoot() (*CstNode, error) { g := r.grammar ctx := r.cstContext() - bindingsAtFunc := g.module.ExportedFunction("bindingsAt") if bindingsAtFunc == nil { return nil, fmt.Errorf("bindingsAt function not exported") } - getBindingsLengthFunc := g.module.ExportedFunction("getBindingsLength") if getBindingsLengthFunc == nil { return nil, fmt.Errorf("getBindingsLength function not exported") } - // Get first binding results, err := bindingsAtFunc.Call(g.ctx, 0) if err != nil { return nil, fmt.Errorf("error calling bindingsAt(0): %v", err) } firstNode := newCstNode(ctx, uint32(results[0]), 0) - if firstNode.CtorName() != "$spaces" { return firstNode, nil } - // If first node is $spaces, the actual root is at binding 1 lenResults, err := getBindingsLengthFunc.Call(g.ctx) if err != nil { @@ -334,7 +296,6 @@ func (r *MatchResult) GetCstRoot() (*CstNode, error) { if lenResults[0] <= 1 { return nil, fmt.Errorf("expected more than 1 binding, got %d", lenResults[0]) } - results, err = bindingsAtFunc.Call(g.ctx, 1) if err != nil { return nil, fmt.Errorf("error calling bindingsAt(1): %v", err) @@ -348,23 +309,19 @@ func (r *MatchResult) GetCstRoot() (*CstNode, error) { func (r *MatchResult) GetAllBindings() ([]*CstNode, error) { g := r.grammar ctx := r.cstContext() - bindingsAtFunc := g.module.ExportedFunction("bindingsAt") if bindingsAtFunc == nil { return nil, fmt.Errorf("bindingsAt function not exported") } - getBindingsLengthFunc := g.module.ExportedFunction("getBindingsLength") if getBindingsLengthFunc == nil { return nil, fmt.Errorf("getBindingsLength function not exported") } - lenResults, err := getBindingsLengthFunc.Call(g.ctx) if err != nil { return nil, fmt.Errorf("error calling getBindingsLength: %v", err) } numBindings := int(lenResults[0]) - // Determine leading spaces offset (for syntactic start rules in pos-only mode, // there is no $spaces binding, but the root node starts after leading spaces). startIdx := 0 @@ -374,7 +331,6 @@ func (r *MatchResult) GetAllBindings() ([]*CstNode, error) { startIdx = spacesLen } } - nodes := make([]*CstNode, numBindings) for i := 0; i < numBindings; i++ { results, err := bindingsAtFunc.Call(g.ctx, uint64(i)) @@ -385,7 +341,6 @@ func (r *MatchResult) GetAllBindings() ([]*CstNode, error) { nodes[i] = node startIdx += node.MatchLength() } - return nodes, nil } @@ -418,18 +373,15 @@ func (g *Grammar) fillInputBuffer(ctx context.Context, mod api.Module, dest, len if memory == nil { panic("WebAssembly module has no memory") } - // Write UTF-16LE code units numUnits := uint32(len(g.inputUTF16)) if length < numUnits { numUnits = length } - for i := uint32(0); i < numUnits; i++ { offset := dest + i*2 memory.WriteUint16Le(offset, g.inputUTF16[i]) } - return numUnits } @@ -456,7 +408,6 @@ func (g *Grammar) matchUnicodeChar(ctx context.Context, mod api.Module, category if int(pos) >= len(g.inputUTF16) { return 0 } - // Decode the rune at pos (may be a surrogate pair) codeUnit := g.inputUTF16[pos] var r rune @@ -467,7 +418,6 @@ func (g *Grammar) matchUnicodeChar(ctx context.Context, mod api.Module, category } else { r = rune(codeUnit) } - // Check each category bit for bit := 0; bit < 32; bit++ { if categoryBitmap&(1<= len(g.strings) { return 0 } - str := g.strings[stringIdx] pos := g.readPos() - // Build a regex for case-insensitive match pattern := "(?i)" + regexp.QuoteMeta(str) re := regexp.MustCompile(pattern) - // Convert input from pos onward back to a Go string for matching remaining := g.inputUTF16[pos:] remainingStr := string(utf16.Decode(remaining)) - loc := re.FindStringIndex(remainingStr) if loc == nil || loc[0] != 0 { return 0 } - // Advance pos by the number of UTF-16 code units consumed matched := remainingStr[:loc[1]] matchedUTF16 := utf16.Encode([]rune(matched)) @@ -524,12 +468,10 @@ func (g *Grammar) Close() error { return err } } - if g.runtime != nil { if err := g.runtime.Close(g.ctx); err != nil { return err } } - return nil } diff --git a/packages/compiler/test/go/grammar_test.go b/golang/runtime/grammar_test.go similarity index 92% rename from packages/compiler/test/go/grammar_test.go rename to golang/runtime/grammar_test.go index e7d6ae0a..c39a887b 100644 --- a/packages/compiler/test/go/grammar_test.go +++ b/golang/runtime/grammar_test.go @@ -31,7 +31,6 @@ func unparseAll(nodes []*CstNode) string { } return result.String() } - func unparseNode(node *CstNode, result *strings.Builder) { if node.IsTerminal() { result.WriteString(node.Value()) @@ -52,25 +51,23 @@ func unparseNode(node *CstNode, result *strings.Builder) { } } +//go:generate sh ./generate.sh func BenchmarkES5Match(b *testing.B) { ctx := context.Background() - wasmPath := os.Getenv("OHM_WASM") if wasmPath == "" { - wasmPath = "../../build/es5.wasm" + wasmPath = "./es5.wasm" } wasmBytes, err := os.ReadFile(wasmPath) if err != nil { b.Fatalf("reading wasm file: %v", err) } - g, err := NewGrammar(ctx, wasmBytes) if err != nil { b.Fatalf("instantiating grammar: %v", err) } defer g.Close() - - input, err := os.ReadFile("../data/_underscore-1.8.3.js") + input, err := os.ReadFile("../../packages/compiler/test/data/_underscore-1.8.3.js") if err != nil { b.Fatalf("reading input file: %v", err) } @@ -87,45 +84,37 @@ func BenchmarkES5Match(b *testing.B) { result.Close() } } - func TestES5Match(t *testing.T) { ctx := context.Background() - wasmPath := os.Getenv("OHM_WASM") if wasmPath == "" { - wasmPath = "../../build/es5.wasm" + wasmPath = "./es5.wasm" } wasmBytes, err := os.ReadFile(wasmPath) if err != nil { t.Fatalf("reading wasm file: %v", err) } - g, err := NewGrammar(ctx, wasmBytes) if err != nil { t.Fatalf("instantiating grammar: %v", err) } defer g.Close() - - input, err := os.ReadFile("../data/_html5shiv-3.7.3.js") + input, err := os.ReadFile("../../packages/compiler/test/data/_html5shiv-3.7.3.js") if err != nil { t.Fatalf("reading input file: %v", err) } - result, err := g.Match(string(input)) if err != nil { t.Fatalf("matching: %v", err) } defer result.Close() - if !result.Succeeded() { t.Fatal("match failed") } - nodes, err := result.GetAllBindings() if err != nil { t.Fatalf("getting bindings: %v", err) } - unparsed := unparseAll(nodes) if unparsed != string(input) { t.Errorf("unparsed text does not match input (got %d bytes, want %d bytes)", len(unparsed), len(input)) diff --git a/packages/compiler/package.json b/packages/compiler/package.json index d70fc15a..55cf56d1 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@ohm-js/compiler", - "version": "18.0.0-beta.10", + "version": "18.0.0-beta.13", "description": "Compile Ohm.js grammars to WebAssembly", "main": "dist/index.js", "exports": { diff --git a/packages/compiler/test/go/go.mod b/packages/compiler/test/go/go.mod deleted file mode 100644 index 431e736e..00000000 --- a/packages/compiler/test/go/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/ohmjs/ohm/packages/compiler/test/go - -go 1.20 - -require github.com/tetratelabs/wazero v1.6.0 diff --git a/packages/compiler/test/go/go.sum b/packages/compiler/test/go/go.sum deleted file mode 100644 index a9e284f5..00000000 --- a/packages/compiler/test/go/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/tetratelabs/wazero v1.6.0 h1:z0H1iikCdP8t+q341xqepY4EWvHEw8Es7tlqiVzlP3g= -github.com/tetratelabs/wazero v1.6.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= From 591f87bd6823e9924cc7e7f7395efd4f58588ee3 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 15 Apr 2026 20:39:21 +1000 Subject: [PATCH 03/10] adding golang/examples --- go.work | 1 + golang/examples/.gitignore | 2 ++ golang/examples/generate.go | 3 +++ golang/examples/go.mod | 3 +++ golang/examples/load_and_use.go | 39 +++++++++++++++++++++++++++++++++ golang/examples/my-grammar.ohm | 9 ++++++++ golang/runtime/cst.go | 2 +- golang/runtime/grammar.go | 2 +- golang/runtime/grammar_test.go | 2 +- 9 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 golang/examples/.gitignore create mode 100644 golang/examples/generate.go create mode 100644 golang/examples/go.mod create mode 100644 golang/examples/load_and_use.go create mode 100644 golang/examples/my-grammar.ohm diff --git a/go.work b/go.work index 7079e67a..883754d9 100644 --- a/go.work +++ b/go.work @@ -2,4 +2,5 @@ go 1.24.2 use ( ./golang/runtime + ./golang/examples ) diff --git a/golang/examples/.gitignore b/golang/examples/.gitignore new file mode 100644 index 00000000..b432a803 --- /dev/null +++ b/golang/examples/.gitignore @@ -0,0 +1,2 @@ +my-grammar.wasm +ohmgo-examples \ No newline at end of file diff --git a/golang/examples/generate.go b/golang/examples/generate.go new file mode 100644 index 00000000..9a528eec --- /dev/null +++ b/golang/examples/generate.go @@ -0,0 +1,3 @@ +package main + +//go:generate docker run --rm -v "$PWD:/local" ohmjs/ohm:latest compile my-grammar.ohm diff --git a/golang/examples/go.mod b/golang/examples/go.mod new file mode 100644 index 00000000..9f35cbeb --- /dev/null +++ b/golang/examples/go.mod @@ -0,0 +1,3 @@ +module github.com/ohmjs/ohmgo-examples + +go 1.24.2 diff --git a/golang/examples/load_and_use.go b/golang/examples/load_and_use.go new file mode 100644 index 00000000..7898a435 --- /dev/null +++ b/golang/examples/load_and_use.go @@ -0,0 +1,39 @@ +package main + +import ( + "context" + _ "embed" + "log" + "os" + + goohm "github.com/ohmjs/goohm" +) + +//go:embed my-grammar.wasm +var wasmBytes []byte + +func main() { + // or, if you prefer, read the .wasm file from disk + // wasmBytes, err := os.ReadFile("my-grammar.wasm") + ctx := context.Background() + grmr, err := goohm.NewGrammar(ctx, wasmBytes) + if err != nil { + log.Fatalf("creating grammar: %v", err) + } + defer grmr.Close() + // use the grammar to match some input text + input := "Hello, world!" + if len(os.Args) > 1 && os.Args[1] != "" { + input = os.Args[1] + } + result, err := grmr.Match(input) + if err != nil { + log.Fatalf("matching: %v", err) + } + defer result.Close() + if !result.Succeeded() { + log.Printf("match failed") + os.Exit(1) + } + log.Printf("match succeeded") +} diff --git a/golang/examples/my-grammar.ohm b/golang/examples/my-grammar.ohm new file mode 100644 index 00000000..42637aaf --- /dev/null +++ b/golang/examples/my-grammar.ohm @@ -0,0 +1,9 @@ +MyGrammar { + Start = hello name "!"? + hello = + | "hello," + | "Hello," + | "hello" + | "Hello" + name = letter+ +} diff --git a/golang/runtime/cst.go b/golang/runtime/cst.go index ef293d75..54db0d5f 100644 --- a/golang/runtime/cst.go +++ b/golang/runtime/cst.go @@ -1,4 +1,4 @@ -package main +package goohm import ( "unicode/utf16" diff --git a/golang/runtime/grammar.go b/golang/runtime/grammar.go index d4a5be19..4d0b0e38 100644 --- a/golang/runtime/grammar.go +++ b/golang/runtime/grammar.go @@ -1,4 +1,4 @@ -package main +package goohm import ( "context" diff --git a/golang/runtime/grammar_test.go b/golang/runtime/grammar_test.go index c39a887b..0be7be12 100644 --- a/golang/runtime/grammar_test.go +++ b/golang/runtime/grammar_test.go @@ -1,4 +1,4 @@ -package main +package goohm import ( "context" From cdb0f7b94753e5a6062525ebad84214bc9039c86 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 15 Apr 2026 20:42:15 +1000 Subject: [PATCH 04/10] typo --- golang/runtime/grammar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golang/runtime/grammar.go b/golang/runtime/grammar.go index 4d0b0e38..0a053b90 100644 --- a/golang/runtime/grammar.go +++ b/golang/runtime/grammar.go @@ -52,7 +52,7 @@ func NewGrammar(ctx context.Context, wasmBytes []byte) (*Grammar, error) { return nil, fmt.Errorf("failed to create env module: %v", err) } // Create the ohmRuntime module with required host functions - // Note: referring to a method, without calling it is a function with the receiever curried. + // Note: referring to a method, without calling it is a function with the receiver curried. _, err = g.runtime.NewHostModuleBuilder("ohmRuntime"). NewFunctionBuilder().WithFunc(g.fillInputBuffer).Export("fillInputBuffer"). NewFunctionBuilder().WithFunc(g.matchUnicodeChar).Export("matchUnicodeChar"). From 35ac08c8c4704d985096692c1527323b5ad84056 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 15 Apr 2026 22:02:17 +1000 Subject: [PATCH 05/10] initial commit of golang/cli aka ohmgo --- go.work | 1 + go.work.sum | 12 +++-- golang/cli/.gitignore | 1 + golang/cli/go.mod | 7 +++ golang/cli/main.go | 105 ++++++++++++++++++++++++++++++++++++ golang/examples/generate.go | 3 +- golang/runtime/grammar.go | 8 +++ 7 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 golang/cli/.gitignore create mode 100644 golang/cli/go.mod create mode 100644 golang/cli/main.go diff --git a/go.work b/go.work index 883754d9..ecbda34b 100644 --- a/go.work +++ b/go.work @@ -3,4 +3,5 @@ go 1.24.2 use ( ./golang/runtime ./golang/examples + ./golang/cli ) diff --git a/go.work.sum b/go.work.sum index 6cfca8b4..d2f50830 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,8 @@ -github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= -github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/jpillora/opts v1.2.3 h1:Q0YuOM7y0BlunHJ7laR1TUxkUA7xW8A2rciuZ70xs8g= +github.com/jpillora/opts v1.2.3/go.mod h1:7p7X/vlpKZmtaDFYKs956EujFqA6aCrOkcCaS6UBcR4= +github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3 h1:GqpA1/5oN1NgsxoSA4RH0YWTaqvUlQNeOpHXD/JRbOQ= +github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= diff --git a/golang/cli/.gitignore b/golang/cli/.gitignore new file mode 100644 index 00000000..99c91b83 --- /dev/null +++ b/golang/cli/.gitignore @@ -0,0 +1 @@ +ohmgo diff --git a/golang/cli/go.mod b/golang/cli/go.mod new file mode 100644 index 00000000..d558b70b --- /dev/null +++ b/golang/cli/go.mod @@ -0,0 +1,7 @@ +module github.com/ohmjs/ohmgo + +require ( + github.com/jpillora/opts v1.2.3 +) + +go 1.24.2 diff --git a/golang/cli/main.go b/golang/cli/main.go new file mode 100644 index 00000000..1a686be2 --- /dev/null +++ b/golang/cli/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "fmt" + "os" + "strings" + + "github.com/jpillora/opts" + "github.com/ohmjs/goohm" +) + +var ( + rootCmd = &struct{}{} + builder = opts.New(rootCmd). + Name("ohmgo"). + EmbedGlobalFlagSet(). + Complete() +) + +func init() { + builder.AddCommand(opts.New(&struct{}{}).Name("generate"). + AddCommand(opts.New(NewGenGeneCmd()).Name("generate")), + ) +} + +func main() { + cli, err := builder.ParseArgsError(os.Args) + if err != nil { + fmt.Fprintf(os.Stderr, "%[1]v\n\n", err) + os.Exit(3) + } + err = cli.Run() + if err != nil { + fmt.Fprintf(os.Stderr, "%[2]s\nError: %[1]v\n\n", err, cli.Selected().Help()) + os.Exit(2) + } +} + +type genGeneCmd struct { + Debug bool + GrammarName string + Output string + Format format `help:"Output format. One of: command, go_generate, script."` + SourceFile string `opts:"mode=arg" help:"Path to .ohm grammar file to compile."` +} + +type format string + +var validFormats = []string{"command", "go_generate", "script"} + +func (format) Complete(s string) []string { + return validFormats +} + +func (e *format) Set(s string) error { + for _, v := range validFormats { + if s == v { + *e = format(s) + return nil + } + } + return fmt.Errorf("must be one of: %v", strings.Join(validFormats, ", ")) +} + +func NewGenGeneCmd() *genGeneCmd { + return &genGeneCmd{ + Format: format("command"), + } +} + +func (c *genGeneCmd) Run() error { + var ( + debug = "" + grammar = "" + output = "" + ) + if c.Debug { + debug = "--debug " + } + if c.GrammarName != "" { + grammar = fmt.Sprintf("--grammarName %s ", c.GrammarName) + } + if c.Output != "" { + output = fmt.Sprintf("--output %s ", c.Output) + } + + dockerTag := ((*goohm.Grammar)(nil)).MatchingDockerImageTags()[0] + switch c.Format { + case "command": + fmt.Printf(`To generate a .wasm file for use with this version of the runtime, run: +docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s +`, dockerTag, debug, grammar, output, c.SourceFile) + case "go_generate": + fmt.Printf(`//go:generate docker run --rm -v $PWD:/local ohmjs/ohm:%s compile %s%s%s%s +`, dockerTag, debug, grammar, output, c.SourceFile) + case "script": + fmt.Printf(`#!/bin/sh + +docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s +`, dockerTag, debug, grammar, output, c.SourceFile) + default: + return fmt.Errorf("invalid format: %s", c.Format) + } + return nil +} diff --git a/golang/examples/generate.go b/golang/examples/generate.go index 9a528eec..0e3359fa 100644 --- a/golang/examples/generate.go +++ b/golang/examples/generate.go @@ -1,3 +1,4 @@ package main -//go:generate docker run --rm -v "$PWD:/local" ohmjs/ohm:latest compile my-grammar.ohm +// The docker image tag needs to take into account the version of the goohm runtime library. +//go:generate docker run --rm -v "$PWD:/local" ohmjs/ohm:18.0.0-beta.13 compile my-grammar.ohm diff --git a/golang/runtime/grammar.go b/golang/runtime/grammar.go index 0a053b90..0472794c 100644 --- a/golang/runtime/grammar.go +++ b/golang/runtime/grammar.go @@ -26,6 +26,14 @@ type Grammar struct { resultStack []*MatchResult } +// MatchingDockerImageTags, list of docker image tags which generate wasm parsers which work with this runtime version. +// The head of the list is the recommended tag to use. +func (*Grammar) MatchingDockerImageTags() []string { + return []string{ + "18.0.0-beta.13", + } +} + // GetModule returns the WebAssembly module func (g *Grammar) GetModule() api.Module { return g.module From a48c8c82d82d11d354ab3cdcdc40e4c1d67d2bce Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Thu, 16 Apr 2026 21:31:01 +1000 Subject: [PATCH 06/10] embed compiler version a custom section in wasm, in go check accepted and compiler versions match --- .github/workflows/docker-release.yml | 8 +--- doc/docker.md | 2 +- golang/examples/generate.go | 2 +- golang/runtime/grammar.go | 55 +++++++++++++++++++++------- packages/compiler/package.json | 2 +- packages/compiler/src/Compiler.ts | 2 + packages/compiler/tsconfig.json | 1 + 7 files changed, 49 insertions(+), 23 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index e95328ea..8fc32ba3 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -1,11 +1,5 @@ name: Docker Release -# This workflow was largely generated from the following prompt: -# "only run this on merge to main; clone the repo; set the VERSION environment -# variable from packages/runtime/package.json; use `docker manifest inspect` -# to verify the release doesn't already exist; create a multi-platform builder; -# build and push the image" - on: push: branches: [main] @@ -20,7 +14,7 @@ jobs: - name: Get version id: version run: | - echo "VERSION=$(cat packages/runtime/package.json | jq -r '.version')" >> "$GITHUB_OUTPUT" + echo "VERSION=$(cat packages/compiler/package.json | jq -r '.version')" >> "$GITHUB_OUTPUT" - name: Check if image already exists id: check diff --git a/doc/docker.md b/doc/docker.md index c5f3dbf4..42fda097 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -118,7 +118,7 @@ Build and push a versioned image to Docker Hub using the git tag as the version: # if not set default to ohmjs (ie docker hub using the ohmjs org) export DOCKER_REPO= # if not set defaults to 'development' -export VERSION=$(cat packages/runtime/package.json | jq -r '.version') +export VERSION=$(cat packages/compiler/package.json | jq -r '.version') # or export VERSION=$(git describe --tag --dirty) # # it might be necessary (particularly on osx) to create a new builder diff --git a/golang/examples/generate.go b/golang/examples/generate.go index 0e3359fa..ca15dafe 100644 --- a/golang/examples/generate.go +++ b/golang/examples/generate.go @@ -1,4 +1,4 @@ package main // The docker image tag needs to take into account the version of the goohm runtime library. -//go:generate docker run --rm -v "$PWD:/local" ohmjs/ohm:18.0.0-beta.13 compile my-grammar.ohm +//go:generate docker run --rm -v "$PWD:/local" ohmjs/ohm:18.0.0-beta.14 compile my-grammar.ohm diff --git a/golang/runtime/grammar.go b/golang/runtime/grammar.go index 0472794c..766e7555 100644 --- a/golang/runtime/grammar.go +++ b/golang/runtime/grammar.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "regexp" + "strings" "unicode" "unicode/utf16" @@ -13,25 +14,30 @@ import ( "github.com/tetratelabs/wazero/api" ) +// Needs to manually be kept in sync (updated) as packages/compiler changes. +// The cli uses the head of the list is the recommended tag when generating a generate command (eg docker run ohmjs/ohm:). +var acceptedVersions = []string{ + "18.0.0-beta.14", + "18.0.0-beta.13", +} + // Grammar is a Go implementation of the JavaScript Grammar class from miniohm. type Grammar struct { - runtime wazero.Runtime - module api.Module - input string - inputUTF16 []uint16 // UTF-16 code units of the current input - ctx context.Context - ruleIds map[string]int - ruleNames []string - strings []string // strings table from the custom section - resultStack []*MatchResult + runtime wazero.Runtime + module api.Module + input string + inputUTF16 []uint16 // UTF-16 code units of the current input + ctx context.Context + ruleIds map[string]int + ruleNames []string + strings []string // strings table from the custom section + compilerVersion []string // version string from the custom section + resultStack []*MatchResult } // MatchingDockerImageTags, list of docker image tags which generate wasm parsers which work with this runtime version. -// The head of the list is the recommended tag to use. func (*Grammar) MatchingDockerImageTags() []string { - return []string{ - "18.0.0-beta.13", - } + return acceptedVersions } // GetModule returns the WebAssembly module @@ -91,8 +97,31 @@ func NewGrammar(ctx context.Context, wasmBytes []byte) (*Grammar, error) { if err != nil { return nil, fmt.Errorf("failed to parse strings: %v", err) } + case "version": + g.compilerVersion, err = parseLEB128Strings(section.Data()) + if err != nil { + return nil, fmt.Errorf("failed to parse version: %v", err) + } } } + if g.compilerVersion == nil { + return nil, fmt.Errorf("Using a .wasm file compiled with an incompatible compiler version. Required custom section 'version' not found.") + } + versionMatches := false +outter: + for _, acceptVersion := range g.MatchingDockerImageTags() { + for _, compilerVersion := range g.compilerVersion { + if compilerVersion == acceptVersion { + versionMatches = true + break outter + } + } + } + if !versionMatches { + return nil, fmt.Errorf("Compiler version(s) no match found. Accepted: '%v'. Found: '%v'", + strings.Join(g.MatchingDockerImageTags(), ", "), strings.Join(g.compilerVersion, ", "), + ) + } if g.ruleNames == nil { return nil, fmt.Errorf("required custom section 'ruleNames' not found") } diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 55cf56d1..33a51112 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@ohm-js/compiler", - "version": "18.0.0-beta.13", + "version": "18.0.0-beta.14", "description": "Compile Ohm.js grammars to WebAssembly", "main": "dist/index.js", "exports": { diff --git a/packages/compiler/src/Compiler.ts b/packages/compiler/src/Compiler.ts index 3b6af036..8c911257 100644 --- a/packages/compiler/src/Compiler.ts +++ b/packages/compiler/src/Compiler.ts @@ -3,6 +3,7 @@ import * as pexprs from 'ohm-js-legacy/src/pexprs-build.js'; import {Grammar as ParsedGrammar} from 'ohm-js-legacy/src/Grammar.js'; // import wabt from 'wabt'; +import pkg from '../package.json' with {type: 'json'}; import * as ir from './ir.ts'; import type {Expr} from './ir.ts'; import * as prebuilt from '../build/ohmRuntime.wasm_sections.ts'; @@ -1698,6 +1699,7 @@ export class Compiler { mergeSections(w.SECTION_ID_CODE, adjustedCodesec, codes), w.customsec(this.buildStringTable('ruleNames', ruleNames)), w.customsec(this.buildStringTable('strings', this._strings)), + w.customsec(this.buildStringTable('version', [pkg.version])), w.customsec( w.custom( w.name('syntacticRules'), diff --git a/packages/compiler/tsconfig.json b/packages/compiler/tsconfig.json index 8717b89f..09272f63 100644 --- a/packages/compiler/tsconfig.json +++ b/packages/compiler/tsconfig.json @@ -17,6 +17,7 @@ // [end overrides] }, "include": [ + "package.json", "src/**/*", "test/**/*.ts", "scripts/**/*.ts", From 5b37ce44af51f5896aae0f9f8340cdecbebcd3d8 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Thu, 16 Apr 2026 22:05:05 +1000 Subject: [PATCH 07/10] enabed caching for local docker buildx bake --- .dockerignore | 1 + .gitignore | 3 +++ doc/docker.md | 21 +++++++++++++++------ docker/docker-compose.yml | 6 ++++++ 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/.dockerignore b/.dockerignore index a10e43d6..120a342d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ Dockerfile **/node_modules/** docker-compose.yml docker-compose.dev.yml +docker/.buildx-cache doc/** .dockerignore **/build diff --git a/.gitignore b/.gitignore index d282e231..a9c58d66 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ yarn-error.log # api-extractor temp files temp/ + +# Docker buildx local layer cache +docker/.buildx-cache diff --git a/doc/docker.md b/doc/docker.md index 42fda097..dbff8629 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -110,16 +110,16 @@ The `-v $(pwd):/local` mount makes your current directory available at `/local` The `ohm-dev:latest` images is 1.62 GB and is 97% efficient with only 64 MB potentially wasted space. -### Publishing to Docker Hub +### Publishing to Docker Hub (from development machine) + +**Note: it is perferable to have the gha docker-release.yml do this** +But there can be a chicken and egg situation where it is easier to do this from a development machine. Build and push a versioned image to Docker Hub using the git tag as the version: ```sh # if not set default to ohmjs (ie docker hub using the ohmjs org) export DOCKER_REPO= -# if not set defaults to 'development' -export VERSION=$(cat packages/compiler/package.json | jq -r '.version') -# or export VERSION=$(git describe --tag --dirty) # # it might be necessary (particularly on osx) to create a new builder # # the default builder might not support multi-platform builds @@ -133,10 +133,19 @@ export VERSION=$(cat packages/compiler/package.json | jq -r '.version') # assuming DHPAT contains your PAT echo $DHPAT | docker login -u --password-stdin cd docker -docker buildx bake --allow=fs.read=.. --push +# if not set defaults to 'development' +export VERSION=$(cat ../packages/compiler/package.json | jq -r '.version') +docker buildx use ohmjs-builder +docker buildx bake \ + --set="*.cache-from=type=local,src=.buildx-cache" \ + --set="*.cache-to=type=local,dest=.buildx-cache,mode=max" \ + --allow=fs.read=.. \ + --push ``` -`git describe --tag --dirty` produces a version string based on the nearest git tag, appending commit info and a `-dirty` suffix if there are uncommitted changes. +Builds use a local layer cache stored in `docker/.buildx-cache`. +On subsequent runs this avoids re-downloading base layers and reinstalling dependencies. +Note the `docker/.buildx-cache` directory is over 1GB, and needs to be cleaned up manually. The defaults in `docker-compose.yml` are `DOCKER_REPO=ohmjs` and `VERSION=development`. See [docker-compose.yml](../docker/docker-compose.yml) for details. diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 22aeda81..61a8e650 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -19,6 +19,12 @@ services: platforms: - linux/amd64 - linux/arm64/v8 + # # Not specifying cache here as it would just slow down gha builds. + # # When doing local development, see docker.md for instructions on how to use it local cache. + # cache-from: + # - type=local,src=.buildx-cache + # cache-to: + # - type=local,dest=.buildx-cache,mode=max target: ${TARGET:-dist} volumes: - .:/local From a9ca7122fc80644949f40498718ea77a7a9a8b19 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Fri, 17 Apr 2026 15:03:07 +1000 Subject: [PATCH 08/10] refactored gencmd --- golang/cli/README.md | 36 +++++++++++++++ golang/cli/gencmd/gencmd.go | 79 +++++++++++++++++++++++++++++++++ golang/cli/go.mod | 7 ++- golang/cli/go.sum | 10 +++++ golang/cli/main.go | 87 ++++++------------------------------- 5 files changed, 144 insertions(+), 75 deletions(-) create mode 100644 golang/cli/README.md create mode 100644 golang/cli/gencmd/gencmd.go create mode 100644 golang/cli/go.sum diff --git a/golang/cli/README.md b/golang/cli/README.md new file mode 100644 index 00000000..b5a043dd --- /dev/null +++ b/golang/cli/README.md @@ -0,0 +1,36 @@ +# OhmGo, Cli for Ohm, for Go runtime, in Go + +### Generate Command for Command to Generate wasm from grammar. + +``` bash + + Usage: ohmgo generate command [options] + + Generate a command, in the specified format (default is 'command') to compile a .ohm grammar file + using the ohmjs/ohm docker image. Does not actually compile the file. + + Path to .ohm grammar file to compile. + + Options: + --debug, -d + --docker-tag, -t The version tag of the ohmjs/ohm docker image to use in the generated command. + Defaults to the version of the goohm runtime included in this cli. (default + 18.0.0-beta.14) + --grammar-name, -g + --output, -o + --format, -f Output format. One of: command, go_generate, script. (default command) + --help, -h display help + +``` + + +**Example:** + +Running `ohmgo generate command my-grammar.ohm` produces; + +``` bash + +# To generate a .wasm file for use with this version of the runtime, run: +docker run --rm -v "$PWD":/local ohmjs/ohm:18.0.0-beta.14 compile my-grammar.ohm +``` + diff --git a/golang/cli/gencmd/gencmd.go b/golang/cli/gencmd/gencmd.go new file mode 100644 index 00000000..49a26ad1 --- /dev/null +++ b/golang/cli/gencmd/gencmd.go @@ -0,0 +1,79 @@ +package gencmd + +import ( + "fmt" + "strings" + + "github.com/ohmjs/goohm" +) + +type genCmdCmd struct { + Debug bool + DockerTag string `opts:"short=t" help:"The version tag of the ohmjs/ohm docker image to use in the generated command. Defaults to the version of the goohm runtime included in this cli."` + GrammarName string + Output string + Format format `help:"Output format. One of: command, go_generate, script."` + SourceFile string `opts:"mode=arg" help:"Path to .ohm grammar file to compile."` +} + +type format string + +var validFormats = []string{"command", "go_generate", "script"} + +func (format) Complete(s string) []string { + return validFormats +} + +func (e *format) Set(s string) error { + for _, v := range validFormats { + if s == v { + *e = format(s) + return nil + } + } + return fmt.Errorf("must be one of: %v", strings.Join(validFormats, ", ")) +} + +func NewGenCmdCmd() *genCmdCmd { + return &genCmdCmd{ + Format: format("command"), + DockerTag: ((*goohm.Grammar)(nil)).MatchingDockerImageTags()[0], + } +} + +func (c *genCmdCmd) Run() error { + var ( + debug = "" + grammar = "" + output = "" + ) + if c.Debug { + debug = "--debug " + } + if c.GrammarName != "" { + grammar = fmt.Sprintf("--grammarName %s ", c.GrammarName) + } + if c.Output != "" { + output = fmt.Sprintf("--output %s ", c.Output) + } + + switch c.Format { + case "command": + fmt.Printf(` +# To generate a .wasm file for use with this version of the runtime, run: +docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s +`, c.DockerTag, debug, grammar, output, c.SourceFile) + case "go_generate": + fmt.Printf(` +//go:generate docker run --rm -v $PWD:/local ohmjs/ohm:%s compile %s%s%s%s +`, c.DockerTag, debug, grammar, output, c.SourceFile) + case "script": + fmt.Printf(`#!/bin/sh + +docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s +`, c.DockerTag, debug, grammar, output, c.SourceFile) + default: + return fmt.Errorf("invalid format: %s", c.Format) + } + return nil +} diff --git a/golang/cli/go.mod b/golang/cli/go.mod index d558b70b..82530f2c 100644 --- a/golang/cli/go.mod +++ b/golang/cli/go.mod @@ -1,7 +1,12 @@ module github.com/ohmjs/ohmgo +require github.com/jpillora/opts v1.2.3 + require ( - github.com/jpillora/opts v1.2.3 + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/jpillora/md-tmpl v1.3.0 // indirect + github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3 // indirect ) go 1.24.2 diff --git a/golang/cli/go.sum b/golang/cli/go.sum new file mode 100644 index 00000000..a12d321a --- /dev/null +++ b/golang/cli/go.sum @@ -0,0 +1,10 @@ +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/jpillora/md-tmpl v1.3.0 h1:M9XrHfGD0Jue5arhejVUj4HE74PXuaR1/KC0FJ9olWk= +github.com/jpillora/md-tmpl v1.3.0/go.mod h1:7kH6OzBwoCAbrXV0OiXKAYLdXwVCMAEirNxZ5uUl5pA= +github.com/jpillora/opts v1.2.3 h1:Q0YuOM7y0BlunHJ7laR1TUxkUA7xW8A2rciuZ70xs8g= +github.com/jpillora/opts v1.2.3/go.mod h1:7p7X/vlpKZmtaDFYKs956EujFqA6aCrOkcCaS6UBcR4= +github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3 h1:GqpA1/5oN1NgsxoSA4RH0YWTaqvUlQNeOpHXD/JRbOQ= +github.com/posener/complete v1.2.2-0.20190308074557-af07aa5181b3/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E= diff --git a/golang/cli/main.go b/golang/cli/main.go index 1a686be2..e621c554 100644 --- a/golang/cli/main.go +++ b/golang/cli/main.go @@ -3,10 +3,9 @@ package main import ( "fmt" "os" - "strings" "github.com/jpillora/opts" - "github.com/ohmjs/goohm" + "github.com/ohmjs/ohmgo/gencmd" ) var ( @@ -19,87 +18,27 @@ var ( func init() { builder.AddCommand(opts.New(&struct{}{}).Name("generate"). - AddCommand(opts.New(NewGenGeneCmd()).Name("generate")), + AddCommand(opts.New(gencmd.NewGenCmdCmd()).Name("command"). + Summary("Generate a command, in the specified format (default is 'command') to compile a .ohm grammar file using the ohmjs/ohm docker image. Does not actually compile the file.")), ) } +//go:generate go run github.com/jpillora/md-tmpl -w README.md + func main() { - cli, err := builder.ParseArgsError(os.Args) + var ( + cli opts.ParsedOpts + err error + ) + cli, err = builder.ParseArgsError(os.Args) if err != nil { - fmt.Fprintf(os.Stderr, "%[1]v\n\n", err) - os.Exit(3) + fmt.Fprintf(os.Stderr, "%[1]v", err) + os.Exit(1) } + // cli = builder.ParseArgs(os.Args) err = cli.Run() if err != nil { fmt.Fprintf(os.Stderr, "%[2]s\nError: %[1]v\n\n", err, cli.Selected().Help()) os.Exit(2) } } - -type genGeneCmd struct { - Debug bool - GrammarName string - Output string - Format format `help:"Output format. One of: command, go_generate, script."` - SourceFile string `opts:"mode=arg" help:"Path to .ohm grammar file to compile."` -} - -type format string - -var validFormats = []string{"command", "go_generate", "script"} - -func (format) Complete(s string) []string { - return validFormats -} - -func (e *format) Set(s string) error { - for _, v := range validFormats { - if s == v { - *e = format(s) - return nil - } - } - return fmt.Errorf("must be one of: %v", strings.Join(validFormats, ", ")) -} - -func NewGenGeneCmd() *genGeneCmd { - return &genGeneCmd{ - Format: format("command"), - } -} - -func (c *genGeneCmd) Run() error { - var ( - debug = "" - grammar = "" - output = "" - ) - if c.Debug { - debug = "--debug " - } - if c.GrammarName != "" { - grammar = fmt.Sprintf("--grammarName %s ", c.GrammarName) - } - if c.Output != "" { - output = fmt.Sprintf("--output %s ", c.Output) - } - - dockerTag := ((*goohm.Grammar)(nil)).MatchingDockerImageTags()[0] - switch c.Format { - case "command": - fmt.Printf(`To generate a .wasm file for use with this version of the runtime, run: -docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s -`, dockerTag, debug, grammar, output, c.SourceFile) - case "go_generate": - fmt.Printf(`//go:generate docker run --rm -v $PWD:/local ohmjs/ohm:%s compile %s%s%s%s -`, dockerTag, debug, grammar, output, c.SourceFile) - case "script": - fmt.Printf(`#!/bin/sh - -docker run --rm -v "$PWD":/local ohmjs/ohm:%s compile %s%s%s%s -`, dockerTag, debug, grammar, output, c.SourceFile) - default: - return fmt.Errorf("invalid format: %s", c.Format) - } - return nil -} From 343b041ce46071b7ec0432b05d0e48c58161bfb9 Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Fri, 17 Apr 2026 15:10:17 +1000 Subject: [PATCH 09/10] ml fleshed out readme --- golang/cli/README.md | 128 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/golang/cli/README.md b/golang/cli/README.md index b5a043dd..bb44b820 100644 --- a/golang/cli/README.md +++ b/golang/cli/README.md @@ -1,6 +1,39 @@ -# OhmGo, Cli for Ohm, for Go runtime, in Go +# OhmGo — CLI for Ohm, for Go + +`ohmgo` is a command-line tool that helps you compile `.ohm` grammar files into WebAssembly (`.wasm`) for use with the [`goohm`](https://github.com/ohmjs/goohm) Go runtime. + +It does not compile grammars directly. Instead, it generates the Docker command needed to compile them via the official `ohmjs/ohm` image — letting you review, adapt, or automate the compilation step however you like. + +## Overview + +The typical workflow for using Ohm in a Go project looks like this: + +``` +my-grammar.ohm → (docker compile) → my-grammar.wasm → embedded in Go binary +``` + +`ohmgo` handles the middle step by generating the correct `docker run` invocation, pinned to the version of the `goohm` runtime you're using. + +## Installation + +```bash +go install github.com/ohmjs/ohmgo@latest +``` + +Or build from source: + +```bash +git clone https://github.com/ohmjs/ohmgo +cd ohmgo +go build -o ohmgo . +``` + +## Commands + +### `generate command` + +Generates a Docker command to compile a `.ohm` grammar file into a `.wasm` file compatible with the current `goohm` runtime version. -### Generate Command for Command to Generate wasm from grammar. ``` bash @@ -24,9 +57,16 @@ ``` -**Example:** +The `--docker-tag` default is derived at runtime from the `goohm` package, so it always matches the runtime version bundled with this CLI — preventing version mismatches between the compiler and the runtime. + +## Output Formats + +The `--format` flag controls how the generated command is wrapped. + +### `command` (default) + +A ready-to-run shell snippet with a comment: -Running `ohmgo generate command my-grammar.ohm` produces; ``` bash @@ -34,3 +74,83 @@ Running `ohmgo generate command my-grammar.ohm` produces; docker run --rm -v "$PWD":/local ohmjs/ohm:18.0.0-beta.14 compile my-grammar.ohm ``` + +### `go_generate` + +A `//go:generate` directive for embedding in a Go source file: + + +``` bash + +//go:generate docker run --rm -v $PWD:/local ohmjs/ohm:18.0.0-beta.14 compile my-grammar.ohm +``` + + +Paste this into any `.go` file in your package. Running `go generate ./...` will compile the grammar and produce `my-grammar.wasm` in the same directory. + +### `script` + +A standalone shell script with a shebang: + + +``` bash +#!/bin/sh + +docker run --rm -v "$PWD":/local ohmjs/ohm:18.0.0-beta.14 compile my-grammar.ohm +``` + + +Useful when you want a reusable script checked into your repo: + +```bash +ohmgo generate command --format=script my-grammar.ohm > compile.sh +chmod +x compile.sh +./compile.sh +``` + +## Using the Compiled Grammar in Go + +Once you have a `.wasm` file, load it with `goohm`: + +```go +package main + +import ( + "context" + _ "embed" + "log" + + goohm "github.com/ohmjs/goohm" +) + +//go:embed my-grammar.wasm +var wasmBytes []byte + +func main() { + ctx := context.Background() + grmr, err := goohm.NewGrammar(ctx, wasmBytes) + if err != nil { + log.Fatalf("creating grammar: %v", err) + } + defer grmr.Close() + + result, err := grmr.Match("Hello, world!") + if err != nil { + log.Fatalf("matching: %v", err) + } + defer result.Close() + + if result.Succeeded() { + log.Println("match succeeded") + } +} +``` + +## Shell Completion + +Install or remove zsh, bash or fish completion: + +```bash +ohmgo --install # install zsh, bash or fish completions +ohmgo --uninstall # remove zsh, bash or fish completion +``` \ No newline at end of file From 02cdbdaf8f2af72f4af4827596df24a6bc9ec61f Mon Sep 17 00:00:00 2001 From: Gary Miller Date: Wed, 6 May 2026 11:32:58 +1000 Subject: [PATCH 10/10] fix: go test gha --- golang/runtime/generate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/golang/runtime/generate.sh b/golang/runtime/generate.sh index ff56d935..207b4651 100755 --- a/golang/runtime/generate.sh +++ b/golang/runtime/generate.sh @@ -5,7 +5,7 @@ REPO_ROOT=$(git rev-parse --show-toplevel) if [ "$OHM_DOCKER_IMAGE_VERSION" != "" ]; then VERSION="$OHM_DOCKER_IMAGE_VERSION" else - VERSION=$(cat "${REPO_ROOT}/packages/runtime/package.json" | jq -r '.version') + VERSION=$(cat "${REPO_ROOT}/packages/compiler/package.json" | jq -r '.version') fi echo "Using Ohm Docker image version: ${VERSION}" docker run --rm -v "${REPO_ROOT}:/local" ohmjs/ohm:${VERSION} compile -o golang/runtime/es5.wasm examples/ecmascript/src/es5.ohm \ No newline at end of file