Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions change_log.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* Dogfood round 2: `apps/notes_cli`, `apps/auth_api`, `apps/grep_cli` built on standalone bytecode; `store_open` accepts a runtime URI expression; AST density sugar `(switch …)` and `(inc x)`. Real-app token harness extended with Python/Go/Node ports and parity. Journal: `docs/journals/2026-10-10_dogfood_apps_round2.md`.

### Fixed
* #159, #155: experimental HFIR bytecode (`-compile-hfir-bc`) now matches the interpreter and governed AST bytecode byte for byte on every frozen benchmark solution (v0, heldout-v0/v1/v2/v3, postcf, postrec: 268 programs; 23 diverged before). Two lowering root causes: (1) `parse_json` evaluated its body symbol onto the operand stack in addition to `PARSE_JSON` reading it by name, so the stray raw string was later popped by `WRITE_FILE` as its path (a false `CAPABILITY_DENIED` inside the grant), by `MAP_GET` as its key (`TYPE_ERROR`), or by `PRINT` (#155 printed the raw text); a content edge that is not the bound symbol now fails closed. (2) `map_get` whose dict is an expression had no HFIR shape and failed to lower; it now emits the AST compiler's exact sequence (`MAP_GET` with an empty variable operand). No opcode, capability or grant change; production `-compile-bc` is unchanged and #90 stays Partial. Every benchmark verifier and `tools/difftest` now run HFIR as a third path, and `TestHFIRBenchmarkSolutionParity` fails CI on any divergence.
* #151: float and mixed int/float `%` and `//` follow Python exactly. `%` is `math.fmod` floored to the divisor's sign (computed exactly by an existing-construct doubling/subtraction loop, so `(% MaxInt64 1e33)` is `9.223372036854776e+18`, `(% 1 0.1)` is `0.09999999999999995`, `(% 4.0 -2.0)` is `-0`), and `//` is CPython's float floor division returned as an int (`(// MaxInt64 1e33)` is `0`). Integral floats that print like ints (`189944.0`) paired with a big int no longer run the int core in float arithmetic. Inf/NaN operands fail with `VALUE_ERROR: % and // need finite operands`, and a `//` result outside int64 with `VALUE_ERROR: // result outside int64`. Mixed int/float comparisons no longer treat the float 2^63 as MinInt64 or round the int through float64. A non-numeric operand now reports `% requires numeric operands (int or float)` at the form instead of `< compares incompatible types`, and template VALUE_ERRORs (destructuring included) carry `at line L, column C`. Instruction cost (-run-bc): a 20k-iteration loop with one `%` and one `//` takes 4.30M instructions (3.17M after #149, 1.55M before #149), so about 460 iterations fit the default 100k limit; `to_base` of MinInt64 in base 2 takes about 16.2k (13.4k after #149); the exact float path loops about twice the binary-exponent gap (worst case `1e308 % 5e-324`, about 53k). Still pure sugar: no opcode or capability; Go/JS still reject with `UNSUPPORTED_ON_TARGET`.
* #149: integer `%`, `//` and `to_base` use exact int64 arithmetic over the whole range with Python floor semantics (previously one float division, wrong above 2^53; `(// (2^53-1) -3)` was already off by one). `(% MaxInt64 10)` is 7, `(// 123456789012345678 1000)` is 123456789012345, `MinInt64 // -1` is the existing `integer overflow` (not `CONVERSION_ERROR`), and `to_base` handles MinInt64. Operands inside (-2^52, 2^52) and float operands keep the old expansion. `let` destructuring of a list of the wrong length fails with `VALUE_ERROR: destructuring expects N elements`. Still pure sugar: no opcode or capability; Go/JS still reject with `UNSUPPORTED_ON_TARGET`.
* HOWL-E2E-UX-001 UX-003: browser JavaScript fetch accepts explicit host grants without reading Node process.env. Missing grants still deny before requests; Node environment authority and exit behavior are preserved. Browser bootstrap errors preserve the original failure.
Expand Down
139 changes: 139 additions & 0 deletions experiments/internal/pathparity/pathparity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Package pathparity replays one HowlFrame program on every execution path the
// benchmark verifiers trust - the AST interpreter (-run), governed AST
// bytecode (build --profile governed, then -run-bc) and the experimental HFIR
// bytecode (-compile-hfir-bc, then -run-bc) - each in a fresh sandbox with the
// same grant, and reports any byte-level divergence (issue #159).
package pathparity

import (
"context"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
)

// Paths lists the compared execution paths in report order.
var Paths = []string{"interpreter", "bytecode", "hfir"}

// Outcome is everything a path is allowed to make observable: its combined
// stdout/stderr, its exit status and the full resulting sandbox tree.
type Outcome struct {
Output string
Exit int
Files map[string]string
}

// Sandbox prepares a fresh, private sandbox under run and returns its path
// together with the -allow-caps grant for it.
type Sandbox func(run string) (dir, caps string, err error)

// Run executes src on path in a sandbox prepared by prepare under work.
func Run(howl, src, path, work string, prepare Sandbox) (Outcome, error) {
if err := os.MkdirAll(work, 0o700); err != nil {
return Outcome{}, err
}
dir, caps, err := prepare(work)
if err != nil {
return Outcome{}, err
}
src, _ = filepath.Abs(src)
art := filepath.Join(work, "prog.hfbc")
command := func(args ...string) (string, int) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, howl, args...)
cmd.Dir = dir
cmd.Env = []string{"PATH=/usr/bin:/bin", "HOME=" + work, "LANG=C", "TZ=UTC"}
out, err := cmd.CombinedOutput()
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
code = -1
}
return strings.ReplaceAll(string(out), work, "$WORK"), code
}
var out string
var code int
switch path {
case "interpreter":
out, code = command("-run", "-allow-caps", caps, src)
case "bytecode":
if o, c := command("build", "--profile", "governed", src, "-o", art); c != 0 {
return Outcome{Output: "build failed: " + o, Exit: c}, nil
}
out, code = command("-run-bc", "-allow-caps", caps, art)
case "hfir":
if o, c := command("-compile-hfir-bc", src, "-o", art); c != 0 {
return Outcome{Output: "hfir compile failed: " + o, Exit: c}, nil
}
out, code = command("-run-bc", "-allow-caps", caps, art)
default:
return Outcome{}, fmt.Errorf("unknown path %q", path)
}
return Outcome{Output: out, Exit: code, Files: snapshot(dir)}, nil
}

// Check runs src on every path and returns "" when all outcomes are
// identical, or a description of the first divergence from the interpreter.
func Check(howl, src, work string, prepare Sandbox) string {
var base Outcome
for i, p := range Paths {
got, err := Run(howl, src, p, filepath.Join(work, p), prepare)
if err != nil {
return p + ": " + err.Error()
}
if i == 0 {
base = got
continue
}
if !reflect.DeepEqual(got, base) {
return fmt.Sprintf("path parity: %s differs from interpreter: %s", p, describe(got, base))
}
}
return ""
}

func describe(got, want Outcome) string {
if got.Exit != want.Exit || got.Output != want.Output {
return fmt.Sprintf("exit %d output %q, interpreter exit %d output %q", got.Exit, got.Output, want.Exit, want.Output)
}
var names []string
for k := range got.Files {
names = append(names, k)
}
for k := range want.Files {
if _, ok := got.Files[k]; !ok {
names = append(names, k)
}
}
sort.Strings(names)
for _, k := range names {
g, gok := got.Files[k]
w, wok := want.Files[k]
if gok != wok || g != w {
return fmt.Sprintf("file %s: %q (present=%v), interpreter %q (present=%v)", k, g, gok, w, wok)
}
}
return "outcomes differ"
}

func snapshot(dir string) map[string]string {
files := map[string]string{}
filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
rel, _ := filepath.Rel(dir, p)
b, _ := os.ReadFile(p)
files[filepath.ToSlash(rel)] = string(b)
return nil
})
return files
}
32 changes: 32 additions & 0 deletions experiments/token-efficiency-heldout-v0/cmd/heldoutverify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"sort"
"strings"
"time"

"github.com/howlcipher/howlframe/experiments/internal/pathparity"
)

type Task struct {
Expand Down Expand Up @@ -75,6 +77,11 @@ func main() {
r.Detail = readErr.Error()
} else {
r.Detail = verify(filepath.Join(work, lang+"-"+t.ID), src, lang, *howl, t)
if r.Detail == "" && lang == "howl" {
// #159: the interpreter, governed bytecode and HFIR bytecode
// must agree byte for byte (output, exit status, file tree).
r.Detail = pathparity.Check(*howl, src, filepath.Join(work, "parity-"+t.ID), heldoutSandbox(t))
}
r.Pass = r.Detail == ""
}
if r.Pass {
Expand Down Expand Up @@ -211,3 +218,28 @@ func must(err error) {
os.Exit(2)
}
}

// heldoutSandbox materializes a task's inputs plus an empty out/ and grants
// exactly the same read and write roots verify uses.
func heldoutSandbox(t Task) pathparity.Sandbox {
return func(run string) (string, string, error) {
dir := filepath.Join(run, "sandbox")
if err := os.MkdirAll(filepath.Join(dir, "out"), 0o755); err != nil {
return "", "", err
}
for p, content := range t.Inputs {
full := filepath.Join(dir, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", "", err
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
return "", "", err
}
}
abs, err := filepath.Abs(dir)
if err != nil {
return "", "", err
}
return abs, "filesystem:read=" + filepath.Join(abs, "in") + ",filesystem:write=" + filepath.Join(abs, "out"), nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"sort"
"strings"
"time"

"github.com/howlcipher/howlframe/experiments/internal/pathparity"
)

type Task struct {
Expand Down Expand Up @@ -75,6 +77,11 @@ func main() {
r.Detail = readErr.Error()
} else {
r.Detail = verify(filepath.Join(work, lang+"-"+t.ID), src, lang, *howl, t)
if r.Detail == "" && lang == "howl" {
// #159: the interpreter, governed bytecode and HFIR bytecode
// must agree byte for byte (output, exit status, file tree).
r.Detail = pathparity.Check(*howl, src, filepath.Join(work, "parity-"+t.ID), heldoutSandbox(t))
}
r.Pass = r.Detail == ""
}
if r.Pass {
Expand Down Expand Up @@ -211,3 +218,28 @@ func must(err error) {
os.Exit(2)
}
}

// heldoutSandbox materializes a task's inputs plus an empty out/ and grants
// exactly the same read and write roots verify uses.
func heldoutSandbox(t Task) pathparity.Sandbox {
return func(run string) (string, string, error) {
dir := filepath.Join(run, "sandbox")
if err := os.MkdirAll(filepath.Join(dir, "out"), 0o755); err != nil {
return "", "", err
}
for p, content := range t.Inputs {
full := filepath.Join(dir, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", "", err
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
return "", "", err
}
}
abs, err := filepath.Abs(dir)
if err != nil {
return "", "", err
}
return abs, "filesystem:read=" + filepath.Join(abs, "in") + ",filesystem:write=" + filepath.Join(abs, "out"), nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"sort"
"strings"
"time"

"github.com/howlcipher/howlframe/experiments/internal/pathparity"
)

type Task struct {
Expand Down Expand Up @@ -80,6 +82,11 @@ func main() {
r.Detail = readErr.Error()
} else {
r.Detail = verify(filepath.Join(work, lang+"-"+t.ID), src, lang, *howl, t)
if r.Detail == "" && lang == "howl" {
// #159: the interpreter, governed bytecode and HFIR bytecode
// must agree byte for byte (output, exit status, file tree).
r.Detail = pathparity.Check(*howl, src, filepath.Join(work, "parity-"+t.ID), heldoutSandbox(t))
}
r.Pass = r.Detail == ""
}
if r.Pass {
Expand Down Expand Up @@ -220,3 +227,28 @@ func must(err error) {
os.Exit(2)
}
}

// heldoutSandbox materializes a task's inputs plus an empty out/ and grants
// exactly the same read and write roots verify uses.
func heldoutSandbox(t Task) pathparity.Sandbox {
return func(run string) (string, string, error) {
dir := filepath.Join(run, "sandbox")
if err := os.MkdirAll(filepath.Join(dir, "out"), 0o755); err != nil {
return "", "", err
}
for p, content := range t.Inputs {
full := filepath.Join(dir, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", "", err
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
return "", "", err
}
}
abs, err := filepath.Abs(dir)
if err != nil {
return "", "", err
}
return abs, "filesystem:read=" + filepath.Join(abs, "in") + ",filesystem:write=" + filepath.Join(abs, "out"), nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"sort"
"strings"
"time"

"github.com/howlcipher/howlframe/experiments/internal/pathparity"
)

type Task struct {
Expand Down Expand Up @@ -81,6 +83,11 @@ func main() {
r.Detail = readErr.Error()
} else {
r.Detail = verify(filepath.Join(work, lang+"-"+t.ID), src, lang, *howl, t)
if r.Detail == "" && lang == "howl" {
// #159: the interpreter, governed bytecode and HFIR bytecode
// must agree byte for byte (output, exit status, file tree).
r.Detail = pathparity.Check(*howl, src, filepath.Join(work, "parity-"+t.ID), heldoutSandbox(t))
}
r.Pass = r.Detail == ""
}
if r.Pass {
Expand Down Expand Up @@ -227,3 +234,28 @@ func must(err error) {
os.Exit(2)
}
}

// heldoutSandbox materializes a task's inputs plus an empty out/ and grants
// exactly the same read and write roots verify uses.
func heldoutSandbox(t Task) pathparity.Sandbox {
return func(run string) (string, string, error) {
dir := filepath.Join(run, "sandbox")
if err := os.MkdirAll(filepath.Join(dir, "out"), 0o755); err != nil {
return "", "", err
}
for p, content := range t.Inputs {
full := filepath.Join(dir, p)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", "", err
}
if err := os.WriteFile(full, []byte(content), 0o644); err != nil {
return "", "", err
}
}
abs, err := filepath.Abs(dir)
if err != nil {
return "", "", err
}
return abs, "filesystem:read=" + filepath.Join(abs, "in") + ",filesystem:write=" + filepath.Join(abs, "out"), nil
}
}
22 changes: 22 additions & 0 deletions experiments/token-efficiency-v0/cmd/tokverify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/howlcipher/howlframe/experiments/falsifiable-v0/harness"
"github.com/howlcipher/howlframe/experiments/internal/pathparity"
)

type Result struct {
Expand Down Expand Up @@ -137,6 +139,13 @@ func main() {
err = readErr
} else {
result.Comparison, result.Diagnostics, err = verify(root, filepath.Join(work, lang+"-"+task.ID), source, lang, *hfexpFlag, *howlFlag, task)
if err == nil && lang == "howl" && result.Comparison.Success {
// #159: interpreter, governed bytecode and HFIR bytecode
// must agree byte for byte in identical sandboxes.
if detail := pathparity.Check(*howlFlag, source, filepath.Join(work, "parity-"+task.ID), v0Sandbox(root, task)); detail != "" {
err = fmt.Errorf("%s", detail)
}
}
}
if err != nil {
result.Diagnostics += " " + err.Error()
Expand All @@ -163,3 +172,16 @@ func main() {
os.Exit(1)
}
}

// v0Sandbox materializes the same reference sandbox the other languages use
// and grants the task's declared capabilities.
func v0Sandbox(root string, task harness.Task) pathparity.Sandbox {
return func(run string) (string, string, error) {
dir := filepath.Join(run, "ref")
if err := os.Mkdir(dir, 0o700); err != nil {
return "", "", err
}
sandbox, _, err := harness.MaterializeReferenceSandbox(filepath.Join(root, "experiments/falsifiable-v0"), dir, task)
return sandbox, strings.Join(task.AllowedCaps, ","), err
}
}
Loading
Loading