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
102 changes: 102 additions & 0 deletions compact_conditionals_cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/howlcipher/howlframe/internal/bytecode"
)

// Compact-conditional built-ins (ver_gt, ver_ge, all, any, inline list_get and
// parse_json operands) must be pure: a program using only them needs no
// capability, and they compute the documented results.
func TestCompactConditionalBuiltinsArePureAndCorrect(t *testing.T) {
bin := c1c2Binary(t)
dir := t.TempDir()
src := filepath.Join(dir, "p.howl")
art := filepath.Join(dir, "p.hfbc")
program := `(do
(let (xs (list 1 2 3) kv (str_split "current=1.9.9\nproposed=1.10.0" "\n"))
(do
(print (all x xs (> x 0)) (all x xs (> x 1)) (all x (list) (= x 9)))
(print (any x xs (= x 3)) (any x xs (= x 7)) (any x (list) (= x 9)))
(print (ver_gt "1.10.0" "1.9.9") (ver_gt "1.2.3" "1.2.3") (ver_ge "1.2.3" "1.2.3") (ver_gt "0.9.9" "1.0.0") (ver_ge "2.0.0" "10.0.0") (ver_gt "1.2.10" "1.2.9"))
(print (list_get (str_split "a.b.c" ".") 2) (ver_gt (list_get (str_split (list_get kv 1) "=") 1) (list_get (str_split (list_get kv 0) "=") 1)))
(print (all x xs (any y (list 3 2 1) (= x y)))))))`
if err := os.WriteFile(src, []byte(program), 0600); err != nil {
t.Fatal(err)
}
for _, profile := range []string{"default", "governed"} {
if out, err := exec.Command(bin, "check", "--profile", profile, src).CombinedOutput(); err != nil {
t.Fatalf("check %s: %v %s", profile, err, out)
}
}
if out, err := exec.Command(bin, "build", "--profile", "governed", src, "-o", art).CombinedOutput(); err != nil {
t.Fatalf("build: %v %s", err, out)
}
out, err := exec.Command(bin, "inspect", "--caps", art).CombinedOutput()
if err != nil {
t.Fatalf("inspect: %v %s", err, out)
}
var report bytecode.Report
if err := json.Unmarshal(out, &report); err != nil {
t.Fatalf("%v: %s", err, out)
}
if len(report.Capabilities) != 0 {
t.Fatalf("pure built-ins required capabilities %v", report.Capabilities)
}
run, err := exec.Command(bin, src).CombinedOutput()
if err != nil {
t.Fatalf("run: %v %s", err, run)
}
want := "true false true\ntrue false false\ntrue false true false false true\nc true\ntrue\n"
if string(run) != want {
t.Fatalf("got %q want %q", run, want)
}
}

// Inline parse_json operands are accepted by the governed profile, but the
// operand is still scanned before anything is built or executed. A forbidden
// construct hidden in the operand must fail closed: diagnostic, no artifact,
// and no earlier effect in the same program runs.
func TestGovernedInlineParseJSONFailsClosed(t *testing.T) {
bin := c1c2Binary(t)
dir := t.TempDir()
side := filepath.Join(dir, "side")
src := filepath.Join(dir, "x.howl")
art := filepath.Join(dir, "x.hfbc")
forms := map[string]string{
"exec": `(exec "touch" "` + side + `.exec")`,
"llm_generate": `(llm_generate "x")`,
"spawn": `(spawn (lambda () (print "x")))`,
}
for name, operand := range forms {
t.Run(name, func(t *testing.T) {
program := `(do (write_file "` + side + `" "ran") (let (d (parse_json Any ` + operand + `)) (print d)))`
os.WriteFile(src, []byte(program), 0600)
for _, args := range [][]string{
{"check", "--profile", "governed", src},
{"build", "--profile", "governed", src, "-o", art},
} {
out, err := exec.Command(bin, args...).CombinedOutput()
if err == nil || !strings.Contains(string(out), "PROFILE_FORBIDDEN_CONSTRUCT") || !strings.Contains(string(out), name) {
t.Fatalf("%v accepted or wrong diagnostic: %v %s", args, err, out)
}
for _, p := range []string{art, side, side + ".exec"} {
if _, err := os.Stat(p); !os.IsNotExist(err) {
t.Fatalf("%s exists after rejected %v", p, args)
}
}
}
})
}
// The allowed shape still builds under governed.
os.WriteFile(src, []byte(`(do (let (d (parse_json Any (read_text "in.json"))) (print d)))`), 0600)
if out, err := exec.Command(bin, "build", "--profile", "governed", src, "-o", art).CombinedOutput(); err != nil {
t.Fatalf("inline parse_json rejected: %v %s", err, out)
}
}
83 changes: 83 additions & 0 deletions docs/journals/2026-10-12_compact_conditionals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Compact conditionals (2026-10-12)

## Goal

Close the conditional-task gap from #134. There, HowlFrame's mean on the 22
conditional tasks was 344.55 tokens, against Python's 196.00. The language
additions had to stay pure, and the governed checks had to stay fail-closed.

## What shipped

All four are `ast.Desugar` sugar, expanding into constructs that already lower
(`internal/ast/desugar_compact.go`). No new opcode, capability or effect is added.

- `(ver_gt a b)` and `(ver_ge a b)`: strict numeric x.y.z comparison.
- `(all x list pred)` and `(any x list pred)`.
- `list_get` accepts any expression as its list. Non-symbol operands are bound to a gensym.
- `(parse_json T <expr>)` binds its operand to a gensym. The governed
pre-desugar profile scan (`construct.scanSupported`) now descends into the
operand instead of rejecting the shape. The default construct scan stays strict.

`all`, `any`, `ver_gt` and `ver_ge` are registered as `CompileTimeOnly`, in
sorted position, and are listed in the bytecode drift test as consumed before lowering.

## Guards (Lain)

- **Purity:**
- The unit test checks that expansions use only a closed set of pure heads.
- The CLI test builds a governed program that uses every form. `inspect --caps`
must report no capabilities, and running it must match the expected truth table.
- **Fail-closed ordering:**
- `exec`, `llm_generate` or `spawn` is hidden inside an inline `parse_json`,
preceded by `write_file`.
- Governed `check` and `build` must both fail with `PROFILE_FORBIDDEN_CONSTRUCT`.
- No artifact, side file or exec output may exist afterwards.
- The allowed shape still builds.

## Motoko follow-up

`experiments/token-efficiency-v0/verification_test.go` checks that
`results/verification.json` has 60 × 4 entries, all passing, each with a SHA-256 that matches the source.

## PR-range correction

#134 cited "#124–#130" and its brief cited "#128–#130". In fact, `git log -S` on
`internal/ast/desugar.go` shows the following sources:

| Sugar used by the references | Introduced in |
|---|---|
| root `do` | #117 |
| `read_text`, `lines` | #118 |
| multi-binding `let` | #120 |
| `when` | #124 |

No #128–#130 sugar is used. The report and the #134 journal are corrected.

## Results (cl100k / o200k)

| Measure | Before | After |
|---|---|---|
| HowlFrame median | 124 / 123 | **117 / 116** |
| vs Python (median per-task ratio) | 1.059 / 1.058 | **0.983 / 0.983** |
| vs JS | 1.019 / 0.953 | **0.959 / 0.896** |
| vs Go | 0.496 / 0.492 | **0.445 / 0.446** |
| Conditional-task mean | 344.55 | **186.00** (Python 196.00) |

Python and JS medians are unchanged at 119 / 118 and 122 / 129.

HowlFrame now uses no more tokens than Python on any task: it is smaller on 44
and ties on the 16 write-record tasks. It is smaller than JS on all 60 tasks.

## Caveats

- The features were shaped by this corpus, so a held-out corpus is needed.
- `ver_*` errors at runtime on version strings with fewer than three parts.
It does not return false.
- The gensym counter makes desugared temporary names process-order dependent.
This does not affect semantics.

## Next hot spots

1. KV lookup for `current=`/`proposed=`: 35 tokens.
2. A pure `filter` to replace the for/when/append loop: about 27 tokens against Python's 17.
3. `dict` pair syntax and the escaped JSON literal for the blocked payload.
2 changes: 1 addition & 1 deletion docs/journals/2026-10-12_token_benchmark_rerun.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Goal

Rerun `experiments/token-efficiency-v0` on main `fd1c6225` (includes the
#124–#130 density sugar). Re-author HowlFrame references in idiomatic current
#117–#130 density sugar; the references use root `do` #117, `read_text`/`lines` #118, multi-binding `let` #120 and `when` #124 — corrected 2026-10-12, previously misstated as #124–#130). Re-author HowlFrame references in idiomatic current
syntax, leave Python/JS/Go untouched, and fix the #133 REPORT.md issues.

## What changed
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/construct_coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
| `>` | Yes | None | No | No | - | AUTHORITATIVE |
| `>=` | Yes | None | No | No | - | AUTHORITATIVE |
| `achieve` | Yes | network | No | No | - | AUTHORITATIVE |
| `all` | No | None | Yes | No | - | AUTHORITATIVE |
| `and` | Yes | None | No | No | - | AUTHORITATIVE |
| `any` | No | None | Yes | No | - | AUTHORITATIVE |
| `append` | Yes | None | No | No | - | AUTHORITATIVE |
| `assert` | No | None | Yes | No | - | AUTHORITATIVE |
| `assert_semantic` | No | None | No | Yes | - | AUTHORITATIVE |
Expand Down Expand Up @@ -135,6 +137,8 @@
| `unix_now` | No | None | Yes | No | - | AUTHORITATIVE |
| `unless` | No | None | Yes | No | - | AUTHORITATIVE |
| `use` | No | None | Yes | No | - | AUTHORITATIVE |
| `ver_ge` | No | None | Yes | No | - | AUTHORITATIVE |
| `ver_gt` | No | None | Yes | No | - | AUTHORITATIVE |
| `wasm_app` | Yes | None | No | No | - | AUTHORITATIVE |
| `web_app` | Yes | None | No | No | - | AUTHORITATIVE |
| `when` | No | None | Yes | No | - | AUTHORITATIVE |
Expand Down
Loading
Loading