jerboa-gerbil: converter from jerboa-emacs .ss to gerbil-emacs

ober

86719f019af8d001b5ba7201e3d6894652f4f9de

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7eb9153
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.gerbil/
+*.o
+*.o1
+*.o1.dSYM/
+*~
+manifest.ss
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..be825db
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# jerboa-gerbil
+
+Tooling to port [`jerboa-emacs`](https://git.sr.ht/~lisp) features to
+[`gerbil-emacs`](https://git.cons.io/ober/), written in Gerbil Scheme.
+
+The two editors are forks of one ancestor and share the same Gerbil surface
+syntax, so porting a feature is mostly **renaming import module paths** and
+pointing Jerboa's "chez-powers" stdlib surface at [`jerboa-compat`](../jerboa-compat).
+
+## convert.ss
+
+Rewrites a jerboa-emacs `.ss` source's imports to gerbil-emacs, textually
+(comments/formatting preserved), and warns about residual `:jerboa-*` refs that
+need a human:
+
+```bash
+gxi convert.ss INPUT.ss [-o OUTPUT.ss]      # default: stdout
+```
+
+Rename rules (specific before generic):
+
+| from | to |
+|---|---|
+| `:jerboa-emacs/chez-powers` | `:jerboa-compat/powers` |
+| `:jerboa-emacs/atom` | `:jerboa-compat/atom` |
+| `:jerboa-scintilla/…` | `:gerbil-scintilla/…` |
+| `:jerboa-qt/…` | `:gerbil-qt/…` |
+| `:jerboa-aws/…` | `:gerbil-aws/…` (no Gerbil lib yet — flagged) |
+| `:jerboa-emacs/…` | `:gemacs/…` |
+
+Library use: `(import :jerboa-gerbil/convert)` → `(convert-string text)` returns
+`(values new-text report residual-count)`.
+
+## Testing Gerbil from this session
+
+`gerbil-mcp` can't be registered as an MCP server here (the jerboa-mcp loader
+occupies that slot), so drive it over stdio with the bundled client:
+
+```bash
+cd ~/mine/gerbil-mcp
+node gmcp.mjs list
+node gmcp.mjs gerbil_compile_check '{"file_path":"…","loadpath":["/Users/user/mine"]}'
+node gmcp.mjs gerbil_eval '{"imports":[":jerboa-compat/powers"],"loadpath":["/Users/user/mine"],"expression":"…"}'
+```
+
+It pins `GERBIL_MCP_GXI_PATH=/opt/gerbil/bin/gxi`.
+
+## Status
+
+Foundation complete: `jerboa-compat` (atom, lru-cache, stm, engine, powers) and
+this converter, both built and tested on Gerbil. End-to-end proof in
+`../proof-port`. Remaining: port the ~484 `cmd-*` features that exist in
+jerboa-emacs but not gerbil-emacs (git/forge, org, editing, games, …).
diff --git a/convert.ss b/convert.ss
new file mode 100644
index 0000000..1b697af
--- /dev/null
+++ b/convert.ss
@@ -0,0 +1,80 @@
+;;; -*- Gerbil -*-
+;;; jerboa-gerbil/convert — rewrite jerboa-emacs .ss import paths to gerbil-emacs.
+;;;
+;;; jerboa-emacs and gerbil-emacs share the same Gerbil surface syntax; porting a
+;;; feature is mostly renaming import module paths and pointing Jerboa's
+;;; "chez-powers" stdlib surface at :jerboa-compat. This applies those renames
+;;; textually (preserving comments/formatting) and reports residual :jerboa-*
+;;; references that need a human (e.g. libraries not yet ported to Gerbil).
+;;;
+;;;   gxi convert.ss INPUT.ss [-o OUTPUT.ss]      # default: write to stdout
+;;;
+;;; Library use: (import :jerboa-gerbil/convert) then (convert-string text).
+
+(export convert-string convert-file! +rename-rules+ main)
+
+(import :std/srfi/13 :std/misc/ports :std/format)
+
+;; (name old new) — order matters: specific :jerboa-emacs/* before the generic.
+(def +rename-rules+
+  '(("chez-powers" ":jerboa-emacs/chez-powers" ":jerboa-compat/powers")
+    ("atom"        ":jerboa-emacs/atom"        ":jerboa-compat/atom")
+    ("scintilla"   ":jerboa-scintilla/"        ":gerbil-scintilla/")
+    ("qt"          ":jerboa-qt/"               ":gerbil-qt/")
+    ("aws"         ":jerboa-aws/"              ":gerbil-aws/")
+    ("emacs"       ":jerboa-emacs/"            ":gemacs/")))
+
+;; literal (non-regex) count + replace, with an optional start offset
+(def (count-sub sub s)
+  (let (n (string-length sub))
+    (let loop ((start 0) (c 0))
+      (let (i (string-contains s sub start))
+        (if i (loop (+ i n) (+ c 1)) c)))))
+
+(def (replace-all s old new)
+  (let (ol (string-length old))
+    (let loop ((start 0) (acc '()))
+      (let (i (string-contains s old start))
+        (if i
+          (loop (+ i ol) (cons new (cons (substring s start i) acc)))
+          (string-concatenate (reverse (cons (substring s start (string-length s)) acc))))))))
+
+;; convert-string : text -> (values new-text report residual-count)
+;; report is a list of (rule-name . count); residual-count is the number of
+;; ":jerboa-" tokens still present after the renames (should be 0).
+(def (convert-string text)
+  (let loop ((rules +rename-rules+) (s text) (report '()))
+    (if (null? rules)
+      ;; residual = leftover :jerboa-* refs, excluding our intended :jerboa-compat target
+      (values s (reverse report) (- (count-sub ":jerboa-" s) (count-sub ":jerboa-compat" s)))
+      (let* ((rule (car rules))
+             (name (car rule)) (old (cadr rule)) (new (caddr rule))
+             (c (count-sub old s)))
+        (loop (cdr rules)
+              (if (> c 0) (replace-all s old new) s)
+              (cons (cons name c) report))))))
+
+(def (convert-file! in (out #f))
+  (let-values (((txt report residual) (convert-string (read-file-string in))))
+    (if out
+      (call-with-output-file out (lambda (p) (display txt p)))
+      (display txt))
+    (values report residual)))
+
+(def (main . args)
+  (let loop ((args args) (out #f) (in #f))
+    (cond
+      ((null? args)
+       (unless in (error "usage: gxi convert.ss INPUT.ss [-o OUTPUT.ss]"))
+       (let-values (((report residual) (convert-file! in out)))
+         (let (ep (current-error-port))
+           (fprintf ep "jerboa-gerbil: converted ~a~a\n" in (if out (format " -> ~a" out) ""))
+           (for-each (lambda (kv)
+                       (when (> (cdr kv) 0)
+                         (fprintf ep "  ~a: ~a\n" (car kv) (cdr kv))))
+                     report)
+           (when (> residual 0)
+             (fprintf ep "  WARNING: ~a residual :jerboa-* reference(s) need manual review\n"
+                      residual)))))
+      ((equal? (car args) "-o") (loop (cddr args) (cadr args) in))
+      (else (loop (cdr args) out (car args))))))
diff --git a/gerbil.pkg b/gerbil.pkg
new file mode 100644
index 0000000..3b4900c
--- /dev/null
+++ b/gerbil.pkg
@@ -0,0 +1 @@
+(package: jerboa-gerbil)