From d5caa61c51aa53c478cf9c98d6bf544e40605c02 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 21:50:31 +0000 Subject: [PATCH 1/3] Dogfood case/trim CONVERT; lower/upper/trim/icontains apps Add native to_lower, to_upper, and str_trim via OpConvert across VM, interpreter, HFIR, and Go/JS backends. Ship densified CLIs and blank-densify existing file apps for the real-app token proof. Co-authored-by: William Elias --- apps/contains_cli/contains_cli.howl | 2 +- apps/ends_cli/ends_cli.howl | 2 +- apps/file_transform/file_transform.howl | 2 +- apps/icontains_cli/README.md | 8 + apps/icontains_cli/icontains_cli.howl | 9 + apps/icontains_cli/icontains_cli_test.go | 34 ++++ apps/lower_cli/README.md | 8 + apps/lower_cli/lower_cli.howl | 5 + apps/lower_cli/lower_cli_test.go | 30 ++++ apps/starts_cli/starts_cli.howl | 2 +- apps/template_cli/template_cli.howl | 2 +- apps/trim_cli/README.md | 8 + apps/trim_cli/trim_cli.howl | 5 + apps/trim_cli/trim_cli_test.go | 30 ++++ apps/upper_cli/README.md | 8 + apps/upper_cli/upper_cli.howl | 5 + apps/upper_cli/upper_cli_test.go | 30 ++++ benchmarks/real_apps/icontains_cli/go/main.go | 29 ++++ .../real_apps/icontains_cli/node/main.js | 25 +++ .../real_apps/icontains_cli/python/main.py | 24 +++ benchmarks/real_apps/lower_cli/go/main.go | 20 +++ benchmarks/real_apps/lower_cli/node/main.js | 13 ++ benchmarks/real_apps/lower_cli/python/main.py | 14 ++ benchmarks/real_apps/prove_real_apps.py | 160 ++++++++++++++++++ benchmarks/real_apps/trim_cli/go/main.go | 20 +++ benchmarks/real_apps/trim_cli/node/main.js | 13 ++ benchmarks/real_apps/trim_cli/python/main.py | 14 ++ benchmarks/real_apps/upper_cli/go/main.go | 20 +++ benchmarks/real_apps/upper_cli/node/main.js | 13 ++ benchmarks/real_apps/upper_cli/python/main.py | 14 ++ .../2026-10-10_dogfood_apps_round10.md | 32 ++++ docs/reference/construct_coverage.md | 3 + internal/ast/desugar.go | 2 + internal/backend/gogen/gogen.go | 12 +- internal/backend/javascript/javascript.go | 9 + internal/bytecode/bytecode.go | 2 +- internal/checker/types.go | 5 +- internal/construct/construct.go | 3 + internal/hfir/lowering.go | 2 +- internal/hfir/model_adapter.go | 2 +- internal/vm/vm.go | 19 +++ 41 files changed, 649 insertions(+), 11 deletions(-) create mode 100644 apps/icontains_cli/README.md create mode 100644 apps/icontains_cli/icontains_cli.howl create mode 100644 apps/icontains_cli/icontains_cli_test.go create mode 100644 apps/lower_cli/README.md create mode 100644 apps/lower_cli/lower_cli.howl create mode 100644 apps/lower_cli/lower_cli_test.go create mode 100644 apps/trim_cli/README.md create mode 100644 apps/trim_cli/trim_cli.howl create mode 100644 apps/trim_cli/trim_cli_test.go create mode 100644 apps/upper_cli/README.md create mode 100644 apps/upper_cli/upper_cli.howl create mode 100644 apps/upper_cli/upper_cli_test.go create mode 100644 benchmarks/real_apps/icontains_cli/go/main.go create mode 100644 benchmarks/real_apps/icontains_cli/node/main.js create mode 100644 benchmarks/real_apps/icontains_cli/python/main.py create mode 100644 benchmarks/real_apps/lower_cli/go/main.go create mode 100644 benchmarks/real_apps/lower_cli/node/main.js create mode 100644 benchmarks/real_apps/lower_cli/python/main.py create mode 100644 benchmarks/real_apps/trim_cli/go/main.go create mode 100644 benchmarks/real_apps/trim_cli/node/main.js create mode 100644 benchmarks/real_apps/trim_cli/python/main.py create mode 100644 benchmarks/real_apps/upper_cli/go/main.go create mode 100644 benchmarks/real_apps/upper_cli/node/main.js create mode 100644 benchmarks/real_apps/upper_cli/python/main.py create mode 100644 docs/journals/2026-10-10_dogfood_apps_round10.md diff --git a/apps/contains_cli/contains_cli.howl b/apps/contains_cli/contains_cli.howl index 016d86c5..5ad69a5c 100644 --- a/apps/contains_cli/contains_cli.howl +++ b/apps/contains_cli/contains_cli.howl @@ -1,6 +1,6 @@ (do (let (needle (cli_args 0) path (cli_args 1)) - (if (or (= needle "") (= path "")) + (if (or (blank needle) (blank path)) (do (print "Usage: contains_cli ") (exit 2)) (try_let (s (read_text path)) (catch e (do (print "error=" e) (exit 2))) diff --git a/apps/ends_cli/ends_cli.howl b/apps/ends_cli/ends_cli.howl index e9b8eae5..9f2fc557 100644 --- a/apps/ends_cli/ends_cli.howl +++ b/apps/ends_cli/ends_cli.howl @@ -1,6 +1,6 @@ (do (let (suffix (cli_args 0) path (cli_args 1)) - (if (or (= suffix "") (= path "")) + (if (or (blank suffix) (blank path)) (do (print "Usage: ends_cli ") (exit 2)) (try_let (s (read_text path)) (catch e (do (print "error=" e) (exit 2))) diff --git a/apps/file_transform/file_transform.howl b/apps/file_transform/file_transform.howl index ab98e90e..2d8d77f9 100644 --- a/apps/file_transform/file_transform.howl +++ b/apps/file_transform/file_transform.howl @@ -1,6 +1,6 @@ (do (let (in (cli_args 0) out (cli_args 1)) - (if (or (= in "") (= out "")) + (if (or (blank in) (blank out)) (do (print "Usage: file_transform ") (exit 2)) (try_let (raw (read_file in)) (catch e (do (print "error=" e) (exit 2))) diff --git a/apps/icontains_cli/README.md b/apps/icontains_cli/README.md new file mode 100644 index 00000000..a75b6727 --- /dev/null +++ b/apps/icontains_cli/README.md @@ -0,0 +1,8 @@ +# icontains_cli + +Case-insensitive substring search using `to_lower` + `str_contains`. + +```bash +go run ../../howlframe.go -compile-bc icontains_cli.howl +go run ../../howlframe.go -run-bc -allow-caps filesystem icontains_cli.howl.bc.bin beta sample.txt +``` diff --git a/apps/icontains_cli/icontains_cli.howl b/apps/icontains_cli/icontains_cli.howl new file mode 100644 index 00000000..642df744 --- /dev/null +++ b/apps/icontains_cli/icontains_cli.howl @@ -0,0 +1,9 @@ +(do + (let (needle (cli_args 0) path (cli_args 1)) + (if (or (blank needle) (blank path)) + (do (print "Usage: icontains_cli ") (exit 2)) + (try_let (s (read_text path)) + (catch e (do (print "error=" e) (exit 2))) + (if (str_contains (to_lower s) (to_lower needle)) + (do (print "FOUND") (exit 0)) + (do (print "MISSING") (exit 1))))))) diff --git a/apps/icontains_cli/icontains_cli_test.go b/apps/icontains_cli/icontains_cli_test.go new file mode 100644 index 00000000..546ebb72 --- /dev/null +++ b/apps/icontains_cli/icontains_cli_test.go @@ -0,0 +1,34 @@ +package icontains_cli_test + +import ( + "os" + "os/exec" + "strings" + "testing" +) + +func TestIContainsCLI(t *testing.T) { + cmd := exec.Command("go", "run", "../../howlframe.go", "-compile-bc", "icontains_cli.howl") + cmd.Dir = "." + if err := cmd.Run(); err != nil { + t.Fatalf("compile: %v", err) + } + os.WriteFile("sample.txt", []byte("Alpha Beta Gamma\n"), 0644) + defer os.Remove("sample.txt") + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", "-allow-caps", "filesystem", + "icontains_cli.howl.bc.bin", "beta", "sample.txt") + cmd.Dir = "." + out, err := cmd.CombinedOutput() + if err != nil || !strings.Contains(string(out), "FOUND") { + t.Fatalf("found: %v %s", err, out) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", "-allow-caps", "filesystem", + "icontains_cli.howl.bc.bin", "delta", "sample.txt") + cmd.Dir = "." + out, err = cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "MISSING") { + t.Fatalf("missing: err=%v out=%s", err, out) + } +} diff --git a/apps/lower_cli/README.md b/apps/lower_cli/README.md new file mode 100644 index 00000000..310019df --- /dev/null +++ b/apps/lower_cli/README.md @@ -0,0 +1,8 @@ +# lower_cli + +Lowercase text via native `to_lower` CONVERT. + +```bash +go run ../../howlframe.go -compile-bc lower_cli.howl +go run ../../howlframe.go -run-bc lower_cli.howl.bc.bin "Hello World" +``` diff --git a/apps/lower_cli/lower_cli.howl b/apps/lower_cli/lower_cli.howl new file mode 100644 index 00000000..c557d2ad --- /dev/null +++ b/apps/lower_cli/lower_cli.howl @@ -0,0 +1,5 @@ +(do + (let (s (cli_args 0)) + (if (blank s) + (do (print "Usage: lower_cli ") (exit 2)) + (do (print (to_lower s)) (exit 0))))) diff --git a/apps/lower_cli/lower_cli_test.go b/apps/lower_cli/lower_cli_test.go new file mode 100644 index 00000000..233be6c3 --- /dev/null +++ b/apps/lower_cli/lower_cli_test.go @@ -0,0 +1,30 @@ +package lower_cli_test + +import ( + "os/exec" + "strings" + "testing" +) + +func TestLowerCLI(t *testing.T) { + cmd := exec.Command("go", "run", "../../howlframe.go", "-compile-bc", "lower_cli.howl") + cmd.Dir = "." + if err := cmd.Run(); err != nil { + t.Fatalf("compile: %v", err) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", + "lower_cli.howl.bc.bin", "Hello World") + cmd.Dir = "." + out, err := cmd.CombinedOutput() + if err != nil || !strings.Contains(string(out), "hello world") { + t.Fatalf("lower: %v %s", err, out) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", "lower_cli.howl.bc.bin") + cmd.Dir = "." + out, err = cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "Usage:") { + t.Fatalf("usage: err=%v out=%s", err, out) + } +} diff --git a/apps/starts_cli/starts_cli.howl b/apps/starts_cli/starts_cli.howl index 318e868f..8080428f 100644 --- a/apps/starts_cli/starts_cli.howl +++ b/apps/starts_cli/starts_cli.howl @@ -1,6 +1,6 @@ (do (let (prefix (cli_args 0) path (cli_args 1)) - (if (or (= prefix "") (= path "")) + (if (or (blank prefix) (blank path)) (do (print "Usage: starts_cli ") (exit 2)) (try_let (s (read_text path)) (catch e (do (print "error=" e) (exit 2))) diff --git a/apps/template_cli/template_cli.howl b/apps/template_cli/template_cli.howl index 0678b8e6..69e66d9a 100644 --- a/apps/template_cli/template_cli.howl +++ b/apps/template_cli/template_cli.howl @@ -1,6 +1,6 @@ (do (let (path (cli_args 0) key (cli_args 1) val (cli_args 2)) - (if (or (= path "") (= key "") (= val "")) + (if (or (blank path) (blank key) (blank val)) (do (print "Usage: template_cli ") (exit 2)) (try_let (s (read_text path)) (catch e (do (print "error=" e) (exit 2))) diff --git a/apps/trim_cli/README.md b/apps/trim_cli/README.md new file mode 100644 index 00000000..5b7ff3d6 --- /dev/null +++ b/apps/trim_cli/README.md @@ -0,0 +1,8 @@ +# trim_cli + +Trim surrounding whitespace via native `str_trim` CONVERT. + +```bash +go run ../../howlframe.go -compile-bc trim_cli.howl +go run ../../howlframe.go -run-bc trim_cli.howl.bc.bin " hello " +``` diff --git a/apps/trim_cli/trim_cli.howl b/apps/trim_cli/trim_cli.howl new file mode 100644 index 00000000..9f4799ea --- /dev/null +++ b/apps/trim_cli/trim_cli.howl @@ -0,0 +1,5 @@ +(do + (let (s (cli_args 0)) + (if (blank s) + (do (print "Usage: trim_cli ") (exit 2)) + (do (print (str_trim s)) (exit 0))))) diff --git a/apps/trim_cli/trim_cli_test.go b/apps/trim_cli/trim_cli_test.go new file mode 100644 index 00000000..eeedda28 --- /dev/null +++ b/apps/trim_cli/trim_cli_test.go @@ -0,0 +1,30 @@ +package trim_cli_test + +import ( + "os/exec" + "strings" + "testing" +) + +func TestTrimCLI(t *testing.T) { + cmd := exec.Command("go", "run", "../../howlframe.go", "-compile-bc", "trim_cli.howl") + cmd.Dir = "." + if err := cmd.Run(); err != nil { + t.Fatalf("compile: %v", err) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", + "trim_cli.howl.bc.bin", " hello ") + cmd.Dir = "." + out, err := cmd.CombinedOutput() + if err != nil || strings.TrimSpace(string(out)) != "hello" { + t.Fatalf("trim: %v %s", err, out) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", "trim_cli.howl.bc.bin") + cmd.Dir = "." + out, err = cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "Usage:") { + t.Fatalf("usage: err=%v out=%s", err, out) + } +} diff --git a/apps/upper_cli/README.md b/apps/upper_cli/README.md new file mode 100644 index 00000000..8e946ace --- /dev/null +++ b/apps/upper_cli/README.md @@ -0,0 +1,8 @@ +# upper_cli + +Uppercase text via native `to_upper` CONVERT. + +```bash +go run ../../howlframe.go -compile-bc upper_cli.howl +go run ../../howlframe.go -run-bc upper_cli.howl.bc.bin "Hello World" +``` diff --git a/apps/upper_cli/upper_cli.howl b/apps/upper_cli/upper_cli.howl new file mode 100644 index 00000000..df532485 --- /dev/null +++ b/apps/upper_cli/upper_cli.howl @@ -0,0 +1,5 @@ +(do + (let (s (cli_args 0)) + (if (blank s) + (do (print "Usage: upper_cli ") (exit 2)) + (do (print (to_upper s)) (exit 0))))) diff --git a/apps/upper_cli/upper_cli_test.go b/apps/upper_cli/upper_cli_test.go new file mode 100644 index 00000000..499dbe97 --- /dev/null +++ b/apps/upper_cli/upper_cli_test.go @@ -0,0 +1,30 @@ +package upper_cli_test + +import ( + "os/exec" + "strings" + "testing" +) + +func TestUpperCLI(t *testing.T) { + cmd := exec.Command("go", "run", "../../howlframe.go", "-compile-bc", "upper_cli.howl") + cmd.Dir = "." + if err := cmd.Run(); err != nil { + t.Fatalf("compile: %v", err) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", + "upper_cli.howl.bc.bin", "Hello World") + cmd.Dir = "." + out, err := cmd.CombinedOutput() + if err != nil || !strings.Contains(string(out), "HELLO WORLD") { + t.Fatalf("upper: %v %s", err, out) + } + + cmd = exec.Command("go", "run", "../../howlframe.go", "-run-bc", "upper_cli.howl.bc.bin") + cmd.Dir = "." + out, err = cmd.CombinedOutput() + if err == nil || !strings.Contains(string(out), "Usage:") { + t.Fatalf("usage: err=%v out=%s", err, out) + } +} diff --git a/benchmarks/real_apps/icontains_cli/go/main.go b/benchmarks/real_apps/icontains_cli/go/main.go new file mode 100644 index 00000000..e6b22f4d --- /dev/null +++ b/benchmarks/real_apps/icontains_cli/go/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) < 2 || args[0] == "" || args[1] == "" { + fmt.Println("Usage: icontains_cli ") + return 2 + } + raw, err := os.ReadFile(args[1]) + if err != nil { + fmt.Printf("error=%v\n", err) + return 2 + } + if strings.Contains(strings.ToLower(string(raw)), strings.ToLower(args[0])) { + fmt.Println("FOUND") + return 0 + } + fmt.Println("MISSING") + return 1 +} diff --git a/benchmarks/real_apps/icontains_cli/node/main.js b/benchmarks/real_apps/icontains_cli/node/main.js new file mode 100644 index 00000000..07f83c99 --- /dev/null +++ b/benchmarks/real_apps/icontains_cli/node/main.js @@ -0,0 +1,25 @@ +#!/usr/bin/env node +const fs = require("fs"); + +function main(argv) { + const args = argv.slice(2); + if (args.length < 2 || args[0] === "" || args[1] === "") { + console.log("Usage: icontains_cli "); + return 2; + } + let s; + try { + s = fs.readFileSync(args[1], "utf8"); + } catch (e) { + console.log("error=" + e); + return 2; + } + if (s.toLowerCase().includes(args[0].toLowerCase())) { + console.log("FOUND"); + return 0; + } + console.log("MISSING"); + return 1; +} + +process.exit(main(process.argv)); diff --git a/benchmarks/real_apps/icontains_cli/python/main.py b/benchmarks/real_apps/icontains_cli/python/main.py new file mode 100644 index 00000000..13dc387c --- /dev/null +++ b/benchmarks/real_apps/icontains_cli/python/main.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 +import sys + + +def main(argv: list[str]) -> int: + if len(argv) < 3 or argv[1] == "" or argv[2] == "": + print("Usage: icontains_cli ") + return 2 + needle, path = argv[1], argv[2] + try: + with open(path, encoding="utf-8") as f: + s = f.read() + except OSError as e: + print(f"error={e}") + return 2 + if needle.lower() in s.lower(): + print("FOUND") + return 0 + print("MISSING") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/benchmarks/real_apps/lower_cli/go/main.go b/benchmarks/real_apps/lower_cli/go/main.go new file mode 100644 index 00000000..861a8735 --- /dev/null +++ b/benchmarks/real_apps/lower_cli/go/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) < 1 || args[0] == "" { + fmt.Println("Usage: lower_cli ") + return 2 + } + fmt.Println(strings.ToLower(args[0])) + return 0 +} diff --git a/benchmarks/real_apps/lower_cli/node/main.js b/benchmarks/real_apps/lower_cli/node/main.js new file mode 100644 index 00000000..a3956523 --- /dev/null +++ b/benchmarks/real_apps/lower_cli/node/main.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +function main(argv) { + const args = argv.slice(2); + if (args.length < 1 || args[0] === "") { + console.log("Usage: lower_cli "); + return 2; + } + console.log(args[0].toLowerCase()); + return 0; +} + +process.exit(main(process.argv)); diff --git a/benchmarks/real_apps/lower_cli/python/main.py b/benchmarks/real_apps/lower_cli/python/main.py new file mode 100644 index 00000000..2abb04c4 --- /dev/null +++ b/benchmarks/real_apps/lower_cli/python/main.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + + +def main(argv: list[str]) -> int: + if len(argv) < 2 or argv[1] == "": + print("Usage: lower_cli ") + return 2 + print(argv[1].lower()) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/benchmarks/real_apps/prove_real_apps.py b/benchmarks/real_apps/prove_real_apps.py index 47c7b846..10458e51 100644 --- a/benchmarks/real_apps/prove_real_apps.py +++ b/benchmarks/real_apps/prove_real_apps.py @@ -278,6 +278,42 @@ def count_tokens(text: str) -> int: "howl_caps": "", "test_pkg": "./apps/sleep_cli/", }, + "lower_cli": { + "howlframe": ROOT / "apps/lower_cli/lower_cli.howl", + "python": HERE / "lower_cli/python/main.py", + "go": HERE / "lower_cli/go/main.go", + "node": HERE / "lower_cli/node/main.js", + "kind": "cli_lower", + "howl_caps": "", + "test_pkg": "./apps/lower_cli/", + }, + "upper_cli": { + "howlframe": ROOT / "apps/upper_cli/upper_cli.howl", + "python": HERE / "upper_cli/python/main.py", + "go": HERE / "upper_cli/go/main.go", + "node": HERE / "upper_cli/node/main.js", + "kind": "cli_upper", + "howl_caps": "", + "test_pkg": "./apps/upper_cli/", + }, + "trim_cli": { + "howlframe": ROOT / "apps/trim_cli/trim_cli.howl", + "python": HERE / "trim_cli/python/main.py", + "go": HERE / "trim_cli/go/main.go", + "node": HERE / "trim_cli/node/main.js", + "kind": "cli_trim", + "howl_caps": "", + "test_pkg": "./apps/trim_cli/", + }, + "icontains_cli": { + "howlframe": ROOT / "apps/icontains_cli/icontains_cli.howl", + "python": HERE / "icontains_cli/python/main.py", + "go": HERE / "icontains_cli/go/main.go", + "node": HERE / "icontains_cli/node/main.js", + "kind": "cli_icontains", + "howl_caps": "filesystem", + "test_pkg": "./apps/icontains_cli/", + }, } LANGS = ["howlframe", "python", "go", "node"] @@ -1349,6 +1385,126 @@ def parity_sleep(lang: str, source: Path, howl_bin: str, caps: str) -> None: raise RuntimeError(f"{lang} sleep too brief") +def run_cli_lower(lang: str, source: Path, howl_bin: str, caps: str, text: str, cwd: Path): + if lang == "howlframe": + bc = cwd / "lower.hfbc" + compile_howl(howl_bin, source, bc) + cmd = [howl_bin, "-run-bc", str(bc), text] + if caps: + cmd = [howl_bin, "-run-bc", "-allow-caps", caps, str(bc), text] + return run(cmd, cwd=cwd) + if lang == "python": + return run([sys.executable, str(source), text], cwd=cwd) + if lang == "go": + exe = cwd / "lower_cli" + r = run(["go", "build", "-o", str(exe), str(source)]) + if r.returncode != 0: + raise RuntimeError(r.stderr) + return run([str(exe), text], cwd=cwd) + if lang == "node": + return run(["node", str(source), text], cwd=cwd) + raise ValueError(lang) + + +def parity_lower(lang: str, source: Path, howl_bin: str, caps: str) -> None: + with tempfile.TemporaryDirectory() as td: + r = run_cli_lower(lang, source, howl_bin, caps, "Hello World", Path(td)) + out = (r.stdout + r.stderr).strip() + if r.returncode != 0 or out != "hello world": + raise RuntimeError(f"{lang} lower failed: {r.returncode} {out!r}") + + +def run_cli_upper(lang: str, source: Path, howl_bin: str, caps: str, text: str, cwd: Path): + if lang == "howlframe": + bc = cwd / "upper.hfbc" + compile_howl(howl_bin, source, bc) + cmd = [howl_bin, "-run-bc", str(bc), text] + if caps: + cmd = [howl_bin, "-run-bc", "-allow-caps", caps, str(bc), text] + return run(cmd, cwd=cwd) + if lang == "python": + return run([sys.executable, str(source), text], cwd=cwd) + if lang == "go": + exe = cwd / "upper_cli" + r = run(["go", "build", "-o", str(exe), str(source)]) + if r.returncode != 0: + raise RuntimeError(r.stderr) + return run([str(exe), text], cwd=cwd) + if lang == "node": + return run(["node", str(source), text], cwd=cwd) + raise ValueError(lang) + + +def parity_upper(lang: str, source: Path, howl_bin: str, caps: str) -> None: + with tempfile.TemporaryDirectory() as td: + r = run_cli_upper(lang, source, howl_bin, caps, "Hello World", Path(td)) + out = (r.stdout + r.stderr).strip() + if r.returncode != 0 or out != "HELLO WORLD": + raise RuntimeError(f"{lang} upper failed: {r.returncode} {out!r}") + + +def run_cli_trim(lang: str, source: Path, howl_bin: str, caps: str, text: str, cwd: Path): + if lang == "howlframe": + bc = cwd / "trim.hfbc" + compile_howl(howl_bin, source, bc) + cmd = [howl_bin, "-run-bc", str(bc), text] + if caps: + cmd = [howl_bin, "-run-bc", "-allow-caps", caps, str(bc), text] + return run(cmd, cwd=cwd) + if lang == "python": + return run([sys.executable, str(source), text], cwd=cwd) + if lang == "go": + exe = cwd / "trim_cli" + r = run(["go", "build", "-o", str(exe), str(source)]) + if r.returncode != 0: + raise RuntimeError(r.stderr) + return run([str(exe), text], cwd=cwd) + if lang == "node": + return run(["node", str(source), text], cwd=cwd) + raise ValueError(lang) + + +def parity_trim(lang: str, source: Path, howl_bin: str, caps: str) -> None: + with tempfile.TemporaryDirectory() as td: + r = run_cli_trim(lang, source, howl_bin, caps, " hello ", Path(td)) + out = (r.stdout + r.stderr).strip() + if r.returncode != 0 or out != "hello": + raise RuntimeError(f"{lang} trim failed: {r.returncode} {out!r}") + + +def run_cli_icontains(lang: str, source: Path, howl_bin: str, caps: str, args: list[str], cwd: Path): + if lang == "howlframe": + bc = cwd / "icontains.hfbc" + compile_howl(howl_bin, source, bc) + return run([howl_bin, "-run-bc", "-allow-caps", caps, str(bc), *args], cwd=cwd) + if lang == "python": + return run([sys.executable, str(source), *args], cwd=cwd) + if lang == "go": + exe = cwd / "icontains_cli" + r = run(["go", "build", "-o", str(exe), str(source)]) + if r.returncode != 0: + raise RuntimeError(r.stderr) + return run([str(exe), *args], cwd=cwd) + if lang == "node": + return run(["node", str(source), *args], cwd=cwd) + raise ValueError(lang) + + +def parity_icontains(lang: str, source: Path, howl_bin: str, caps: str) -> None: + with tempfile.TemporaryDirectory() as td: + cwd = Path(td) + sample = cwd / "sample.txt" + sample.write_text("Alpha Beta Gamma\n") + r = run_cli_icontains(lang, source, howl_bin, caps, ["beta", str(sample)], cwd) + out = r.stdout + r.stderr + if r.returncode != 0 or "FOUND" not in out: + raise RuntimeError(f"{lang} icontains found failed: {r.returncode} {out!r}") + r = run_cli_icontains(lang, source, howl_bin, caps, ["delta", str(sample)], cwd) + out = r.stdout + r.stderr + if r.returncode == 0 or "MISSING" not in out: + raise RuntimeError(f"{lang} icontains missing failed: {r.returncode} {out!r}") + + def run_app_tests(pkg: str) -> None: r = run(["go", "test", pkg, "-count=1"], cwd=ROOT) if r.returncode != 0: @@ -1407,6 +1563,10 @@ def main() -> int: "http_echo": parity_echo, "cli_now": parity_now, "cli_sleep": parity_sleep, + "cli_lower": parity_lower, + "cli_upper": parity_upper, + "cli_trim": parity_trim, + "cli_icontains": parity_icontains, }[meta["kind"]] for lang in LANGS: try: diff --git a/benchmarks/real_apps/trim_cli/go/main.go b/benchmarks/real_apps/trim_cli/go/main.go new file mode 100644 index 00000000..d27bb79b --- /dev/null +++ b/benchmarks/real_apps/trim_cli/go/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) < 1 || args[0] == "" { + fmt.Println("Usage: trim_cli ") + return 2 + } + fmt.Println(strings.TrimSpace(args[0])) + return 0 +} diff --git a/benchmarks/real_apps/trim_cli/node/main.js b/benchmarks/real_apps/trim_cli/node/main.js new file mode 100644 index 00000000..64b34e24 --- /dev/null +++ b/benchmarks/real_apps/trim_cli/node/main.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +function main(argv) { + const args = argv.slice(2); + if (args.length < 1 || args[0] === "") { + console.log("Usage: trim_cli "); + return 2; + } + console.log(args[0].trim()); + return 0; +} + +process.exit(main(process.argv)); diff --git a/benchmarks/real_apps/trim_cli/python/main.py b/benchmarks/real_apps/trim_cli/python/main.py new file mode 100644 index 00000000..ea31cffe --- /dev/null +++ b/benchmarks/real_apps/trim_cli/python/main.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + + +def main(argv: list[str]) -> int: + if len(argv) < 2 or argv[1] == "": + print("Usage: trim_cli ") + return 2 + print(argv[1].strip()) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/benchmarks/real_apps/upper_cli/go/main.go b/benchmarks/real_apps/upper_cli/go/main.go new file mode 100644 index 00000000..6b8142a9 --- /dev/null +++ b/benchmarks/real_apps/upper_cli/go/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +func main() { + os.Exit(run(os.Args[1:])) +} + +func run(args []string) int { + if len(args) < 1 || args[0] == "" { + fmt.Println("Usage: upper_cli ") + return 2 + } + fmt.Println(strings.ToUpper(args[0])) + return 0 +} diff --git a/benchmarks/real_apps/upper_cli/node/main.js b/benchmarks/real_apps/upper_cli/node/main.js new file mode 100644 index 00000000..633d3e8d --- /dev/null +++ b/benchmarks/real_apps/upper_cli/node/main.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node + +function main(argv) { + const args = argv.slice(2); + if (args.length < 1 || args[0] === "") { + console.log("Usage: upper_cli "); + return 2; + } + console.log(args[0].toUpperCase()); + return 0; +} + +process.exit(main(process.argv)); diff --git a/benchmarks/real_apps/upper_cli/python/main.py b/benchmarks/real_apps/upper_cli/python/main.py new file mode 100644 index 00000000..9c54c291 --- /dev/null +++ b/benchmarks/real_apps/upper_cli/python/main.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import sys + + +def main(argv: list[str]) -> int: + if len(argv) < 2 or argv[1] == "": + print("Usage: upper_cli ") + return 2 + print(argv[1].upper()) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/docs/journals/2026-10-10_dogfood_apps_round10.md b/docs/journals/2026-10-10_dogfood_apps_round10.md new file mode 100644 index 00000000..cda67ac3 --- /dev/null +++ b/docs/journals/2026-10-10_dogfood_apps_round10.md @@ -0,0 +1,32 @@ +# Dogfood common apps round 10 (2026-10-10) + +## Goal + +Native case/trim CONVERT ops for general string use; ship lower/upper/trim/icontains apps. + +## Blockers found and fixed + +| Gap | Symptom vs other languages | Fix | +|---|---|---| +| No `to_lower`/`to_upper`/`trim` | Case folds need host escapes or awkward splits. | Native CONVERT: `to_lower`, `to_upper`, `str_trim`. | +| Case-insensitive search verbose | Manual lower on both sides repeats. | `icontains_cli` dogfoods `to_lower` + `str_contains`. | + +## Apps shipped / densified + +1. **`apps/lower_cli`** — lowercase argv text +2. **`apps/upper_cli`** — uppercase argv text +3. **`apps/trim_cli`** — trim argv text +4. **`apps/icontains_cli`** — case-insensitive file substring +5. Densified `starts_cli`/`ends_cli`/`contains_cli`/`template_cli`/`file_transform` with `blank` + +## Token proof + +```bash +python3 benchmarks/real_apps/prove_real_apps.py +``` + +## Remaining limits + +- `middleware` still Unsupported +- `match` still Unsupported (prod-flip lock) +- `schema` / SQL migrations still Unsupported diff --git a/docs/reference/construct_coverage.md b/docs/reference/construct_coverage.md index 336a3e62..fa6cd3f3 100644 --- a/docs/reference/construct_coverage.md +++ b/docs/reference/construct_coverage.md @@ -116,6 +116,7 @@ | `str_join` | Yes | None | No | No | - | AUTHORITATIVE | | `str_replace` | No | None | Yes | No | - | AUTHORITATIVE | | `str_split` | Yes | None | No | No | - | AUTHORITATIVE | +| `str_trim` | Yes | None | No | No | - | AUTHORITATIVE | | `struct` | No | None | No | Yes | - | AUTHORITATIVE | | `switch` | No | None | Yes | No | - | AUTHORITATIVE | | `task` | Yes | None | No | No | - | AUTHORITATIVE | @@ -123,7 +124,9 @@ | `time_now` | Yes | None | No | No | - | AUTHORITATIVE | | `to_float` | Yes | None | No | No | - | AUTHORITATIVE | | `to_int` | Yes | None | No | No | - | AUTHORITATIVE | +| `to_lower` | Yes | None | No | No | - | AUTHORITATIVE | | `to_string` | Yes | None | No | No | - | AUTHORITATIVE | +| `to_upper` | Yes | None | No | No | - | AUTHORITATIVE | | `trace` | No | None | No | Yes | - | AUTHORITATIVE | | `try_let` | Yes | None | No | No | - | AUTHORITATIVE | | `type_hint` | No | None | Yes | No | - | AUTHORITATIVE | diff --git a/internal/ast/desugar.go b/internal/ast/desugar.go index 0c1149b6..79369df4 100644 --- a/internal/ast/desugar.go +++ b/internal/ast/desugar.go @@ -329,6 +329,7 @@ var reservedCallHeads = map[string]bool{ "blank": true, "unix_now": true, "dict": true, "exit": true, "for": true, "if": true, "let": true, "list": true, "print": true, "return": true, "set": true, "stderr": true, + "to_lower": true, "to_upper": true, "str_trim": true, "test": true, "try_let": true, "type_hint": true, "type_hints": true, "type_param": true, "while": true, "http_server": true, "web_app": true, "wasm_app": true, "route": true, "lambda": true, "import": true, @@ -1403,6 +1404,7 @@ var exprBodyHeads = map[string]bool{ "call": true, "list": true, "dict": true, "map_get": true, "list_get": true, "list_len": true, "map_keys": true, "to_int": true, "to_float": true, "to_string": true, "bytes_to_string": true, + "to_lower": true, "to_upper": true, "str_trim": true, "env": true, "read_file": true, "read_line": true, "read_text": true, "cli_args": true, "lines": true, "str_split": true, "str_join": true, "str": true, "str_contains": true, "str_replace": true, "starts_with": true, "ends_with": true, "http_body": true, diff --git a/internal/backend/gogen/gogen.go b/internal/backend/gogen/gogen.go index 89ae5c40..b01e9f0c 100644 --- a/internal/backend/gogen/gogen.go +++ b/internal/backend/gogen/gogen.go @@ -309,7 +309,8 @@ func goValueType(node *ast.Node) string { return "string" } return "[]string" - case "list_get", "str_join", "to_string", "html_escape", "attr_escape", "bytes_to_string", "encode_json", "env": + case "list_get", "str_join", "to_string", "html_escape", "attr_escape", "bytes_to_string", "encode_json", "env", + "to_lower", "to_upper", "str_trim": return "string" case "list_len", "to_int", "time_now": return "int" @@ -1637,6 +1638,15 @@ func EmitGoIR(ir *ir.IRNode, reqVar string, depth int) string { case "bytes_to_string": valStr := generateExpression(ir.Kids[0], reqVar, depth+1) return fmt.Sprintf("string(%s)", valStr) + case "to_lower": + valStr := generateExpression(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("strings.ToLower(fmt.Sprint(%s))", valStr) + case "to_upper": + valStr := generateExpression(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("strings.ToUpper(fmt.Sprint(%s))", valStr) + case "str_trim": + valStr := generateExpression(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("strings.TrimSpace(fmt.Sprint(%s))", valStr) case "encode_json": valStr := generateExpression(ir.Kids[0], reqVar, depth+1) return fmt.Sprintf("func() string { b, _ := json.Marshal(%s); return string(b) }()", valStr) diff --git a/internal/backend/javascript/javascript.go b/internal/backend/javascript/javascript.go index 2f13febd..20aa2cf0 100644 --- a/internal/backend/javascript/javascript.go +++ b/internal/backend/javascript/javascript.go @@ -791,6 +791,15 @@ func EmitJSIR(ir *ir.IRNode, reqVar string, depth int) string { case "to_string", "bytes_to_string": valStr := generateJSStatementRaw(ir.Kids[0], reqVar, depth+1) return fmt.Sprintf("String(%s)", valStr) + case "to_lower": + valStr := generateJSStatementRaw(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("String(%s).toLowerCase()", valStr) + case "to_upper": + valStr := generateJSStatementRaw(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("String(%s).toUpperCase()", valStr) + case "str_trim": + valStr := generateJSStatementRaw(ir.Kids[0], reqVar, depth+1) + return fmt.Sprintf("String(%s).trim()", valStr) case "encode_json": valStr := generateJSStatementRaw(ir.Kids[0], reqVar, depth+1) return fmt.Sprintf("JSON.stringify(%s)", valStr) diff --git a/internal/bytecode/bytecode.go b/internal/bytecode/bytecode.go index 69896b95..4dcae48b 100644 --- a/internal/bytecode/bytecode.go +++ b/internal/bytecode/bytecode.go @@ -732,7 +732,7 @@ func (c *BCCompiler) compileNode(node *ast.Node) []BCInstruction { insts = append(insts, BCInstruction{OpString: "CONVERT", Op: OpConvert, StringOperand: "to_int"}) case "time_now": insts = append(insts, BCInstruction{OpString: "TIME_NOW", Op: OpTimeNow}) - case "to_float", "to_string", "bytes_to_string": + case "to_float", "to_string", "bytes_to_string", "to_lower", "to_upper", "str_trim": insts = append(insts, c.compileNode(node.Children[1])...) insts = append(insts, BCInstruction{OpString: "CONVERT", Op: OpConvert, StringOperand: head}) case "encode_json": diff --git a/internal/checker/types.go b/internal/checker/types.go index e0dbca4a..c5e58730 100644 --- a/internal/checker/types.go +++ b/internal/checker/types.go @@ -712,7 +712,7 @@ func (a *Analysis) inferList(node *ast.Node, env typeEnv) ast.TypeInfo { case "to_float", "confidence": a.inferChild(node, 1, env) return ast.Layout(ast.Float) - case "to_string", "bytes_to_string", "encode_json": + case "to_string", "bytes_to_string", "encode_json", "to_lower", "to_upper", "str_trim": a.inferChild(node, 1, env) return ast.Layout(ast.String) case "html_escape", "attr_escape": @@ -1050,7 +1050,8 @@ func isKeyword(name string) bool { "print", "env", "read_file", "write_file", "mkdir", "exec", "call", "return", "do", "while", "for", "match", "default", "set", "list", "dict", "list_get", "map_get", "map_keys", "map_set", "map_delete", "append", - "to_int", "to_float", "to_string", "bytes_to_string", "encode_json", "html_escape", "attr_escape", "str_split", + "to_int", "to_float", "to_string", "bytes_to_string", "to_lower", "to_upper", "str_trim", + "encode_json", "html_escape", "attr_escape", "str_split", "str_join", "regex_match", "confidence", "achieve", "fuzzy_cast", "time_now", "lazy_synthesize", "semantic_match", "neural_circuit", "ephemeral_circuit", "db_connect", "sql_query", "store_open", diff --git a/internal/construct/construct.go b/internal/construct/construct.go index 6b09294d..dacdd07c 100644 --- a/internal/construct/construct.go +++ b/internal/construct/construct.go @@ -191,6 +191,7 @@ var table = []Entry{ {Name: "str_join", Support: Supported, Note: "STR_JOIN"}, {Name: "str_replace", Support: CompileTimeOnly, Note: "density sugar; replace via str_split/str_join by ast.Desugar"}, {Name: "str_split", Support: Supported, Note: "STR_SPLIT"}, + {Name: "str_trim", Support: Supported, Note: "CONVERT; strings.TrimSpace"}, {Name: "struct", Support: Unsupported, Note: "the VM has no struct representation; accepting the declaration would hide the gap"}, {Name: "switch", Support: CompileTimeOnly, Note: "density sugar; (switch expr (pat body...)... (else body...)) → let+cond by ast.Desugar"}, {Name: "task", Support: Supported, Opaque: true, Note: "TASK; reads description child .Value directly; optional body forms execute through SPAWN_AGENT"}, @@ -198,7 +199,9 @@ var table = []Entry{ {Name: "time_now", Support: Supported, Note: "TIME_NOW"}, {Name: "to_float", Support: Supported, Note: "CONVERT"}, {Name: "to_int", Support: Supported, Note: "CONVERT"}, + {Name: "to_lower", Support: Supported, Note: "CONVERT; strings.ToLower"}, {Name: "to_string", Support: Supported, Note: "CONVERT"}, + {Name: "to_upper", Support: Supported, Note: "CONVERT; strings.ToUpper"}, {Name: "trace", Support: Unsupported, Note: "auto-tracing macro expanded by the Go backend; no opcode"}, {Name: "try_let", Support: Supported, Note: "TRY_LET"}, {Name: "type_hint", Support: CompileTimeOnly, Note: "deliberate empty case in compileNode; a pure annotation"}, diff --git a/internal/hfir/lowering.go b/internal/hfir/lowering.go index 77d2503e..f44f8164 100644 --- a/internal/hfir/lowering.go +++ b/internal/hfir/lowering.go @@ -234,7 +234,7 @@ func (ctx *LoweringContext) lowerSemanticList(node *Node, astNode *ast.Node, hea node.Kind = head id, err := addChildren(1, "value") return id, true, err - case "to_int", "to_float", "to_string", "bytes_to_string", "encode_json": + case "to_int", "to_float", "to_string", "bytes_to_string", "encode_json", "to_lower", "to_upper", "str_trim": if len(astNode.Children) != 2 { return "", false, nil } diff --git a/internal/hfir/model_adapter.go b/internal/hfir/model_adapter.go index 6d69757d..0e55fd3a 100644 --- a/internal/hfir/model_adapter.go +++ b/internal/hfir/model_adapter.go @@ -788,7 +788,7 @@ func allowedBinary(value string) bool { func allowedConvert(value string) bool { switch value { - case "to_int", "to_float", "to_string", "bytes_to_string", "encode_json": + case "to_int", "to_float", "to_string", "bytes_to_string", "encode_json", "to_lower", "to_upper", "str_trim": return true } return false diff --git a/internal/vm/vm.go b/internal/vm/vm.go index c6cf97c9..30a67cc7 100644 --- a/internal/vm/vm.go +++ b/internal/vm/vm.go @@ -611,6 +611,19 @@ func (interp *Interpreter) evalList(node *ast.Node, env *InterpEnv) any { return string(b) } return fmt.Sprint(val) + case "to_lower", "to_upper", "str_trim": + if len(node.Children) != 2 { + InterpErr(fmt.Sprintf("%s expects (%s text)", node.Children[0].Value, node.Children[0].Value), node) + } + s := fmt.Sprint(interp.eval(node.Children[1], env)) + switch node.Children[0].Value { + case "to_lower": + return strings.ToLower(s) + case "to_upper": + return strings.ToUpper(s) + default: + return strings.TrimSpace(s) + } case "list_len": if len(node.Children) != 2 { InterpErr("list_len expects (list_len value)", node) @@ -3515,6 +3528,12 @@ func BcConvert(target string, a any) any { panic(NewRuntimeError("TYPE_ERROR", "main", 0, bytecode.OpConvert, "bytes_to_string expected []any, got %T", a)) } return string(bytesFromNumberList(items, 0, bytecode.BCInstruction{Op: bytecode.OpConvert}, "bytes_to_string")) + case "to_lower": + return strings.ToLower(fmt.Sprint(a)) + case "to_upper": + return strings.ToUpper(fmt.Sprint(a)) + case "str_trim": + return strings.TrimSpace(fmt.Sprint(a)) } return a } From 83482c944391483cf0694676ad844078b12c1d51 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 21:51:23 +0000 Subject: [PATCH 2/3] Record round10 prove totals (4114 vs 5572/5894/5370) Co-authored-by: William Elias --- docs/journals/2026-10-10_dogfood_apps_round10.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/journals/2026-10-10_dogfood_apps_round10.md b/docs/journals/2026-10-10_dogfood_apps_round10.md index cda67ac3..b96de1ad 100644 --- a/docs/journals/2026-10-10_dogfood_apps_round10.md +++ b/docs/journals/2026-10-10_dogfood_apps_round10.md @@ -19,7 +19,11 @@ Native case/trim CONVERT ops for general string use; ship lower/upper/trim/icont 4. **`apps/icontains_cli`** — case-insensitive file substring 5. Densified `starts_cli`/`ends_cli`/`contains_cli`/`template_cli`/`file_transform` with `blank` -## Token proof +## Token proof (`cl100k_base`, 30 apps) + +| | HowlFrame | Python | Go | Node | +|---|---|---|---|---| +| **TOTAL** | **4114** | 5572 | 5894 | 5370 | ```bash python3 benchmarks/real_apps/prove_real_apps.py From b51369ee7f5fed660dcacbdf1bed4c25f6ca6514 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 10 Oct 2026 21:55:02 +0000 Subject: [PATCH 3/3] Alias to_lower/to_upper/str_trim as HFIR convert for prod-flip fence New CONVERT heads lower via the existing convert kind; keep them outside the promote-blocker fence like to_string/bytes_to_string. Co-authored-by: William Elias --- docs/reference/lowered_hfir_prod_flip_criteria.md | 2 +- prod_flip_criteria_test.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/reference/lowered_hfir_prod_flip_criteria.md b/docs/reference/lowered_hfir_prod_flip_criteria.md index 2640ce76..528b193d 100644 --- a/docs/reference/lowered_hfir_prod_flip_criteria.md +++ b/docs/reference/lowered_hfir_prod_flip_criteria.md @@ -38,7 +38,7 @@ The experimental artifact and the production artifact match on every program pro * Operand order stays the AST order on the existing opcodes: `EXEC` is command then arguments, with `IntOperand` equal to the argument count; `FETCH` is URL then method; `REGEX_MATCH` is the pattern, then the string. `TIME_NOW` pops 0, pushes 1, and has no operand. `SLEEP` pops 1, pushes 0, and has an empty operand list; the duration is the compiled child before that instruction. `JUMP_IF_FALSE`, `JUMP`, `FOR_INIT`, `FOR_NEXT`, `CALL`, and `RETURN` keep the relative offsets the AST compiler already emits. * The promote-blocker fence below is empty. Each name leaves the fence only after `LowerToBytecode` emits that construct's existing opcode and a parity case matches production `-compile-bc` on grant and on denial when the construct has a capability. -These names already lower under a different HFIR kind, so they are outside the fence: `cli_app` is `program`, `do` is `sequence`, `try_let` is `try`, the arithmetic and comparison heads are `binary`, and `to_int`, `to_float`, `to_string`, `bytes_to_string`, and `encode_json` are `convert`. +These names already lower under a different HFIR kind, so they are outside the fence: `cli_app` is `program`, `do` is `sequence`, `try_let` is `try`, the arithmetic and comparison heads are `binary`, and `to_int`, `to_float`, `to_string`, `bytes_to_string`, `encode_json`, `to_lower`, `to_upper`, and `str_trim` are `convert`. ### Mediated host effects diff --git a/prod_flip_criteria_test.go b/prod_flip_criteria_test.go index 16ac094e..6ebfcc95 100644 --- a/prod_flip_criteria_test.go +++ b/prod_flip_criteria_test.go @@ -328,6 +328,9 @@ var hfirKindAliases = map[string]string{ "to_string": "convert", "bytes_to_string": "convert", "encode_json": "convert", + "to_lower": "convert", + "to_upper": "convert", + "str_trim": "convert", } func promoteBlockers(t *testing.T) []string {