Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ jobs:
cd compiler
cargo test --workspace --quiet

- name: Checker corpus verdict agreement (139/139 + tripwire)
- name: Checker corpus verdict agreement (140/140 + tripwire)
run: |
set -e
cd compiler && cargo build -p hyb --quiet
Expand Down
2 changes: 1 addition & 1 deletion GUARANTEES.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Everything the checker **accepts was proven** sendable; everything it
## Evidence inventory (what backs the ✅ rows)

- **Adversarial corpus** — [tests/checker-corpus/](tests/checker-corpus/):
139 programs, each with an `EXPECTED-VERDICT` header. Programs 01–18 were
140 programs, each with an `EXPECTED-VERDICT` header. Programs 01–18 were
**pre-registered before the checker existed** (the checker was built
against them, not adjusted after); 19–20 were added during integration
and 21+ after the checker landed, still hand-derived from the rule text
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ help:
@echo "make test-quick — sandbox unit tests (grows with the compiler)"
@echo "make ci — what CI runs on push"
@echo "make examples — the ten target programs, byte-exact acceptance"
@echo "make corpus — Tier-0 corpus verdicts (139/139)"
@echo "make corpus — Tier-0 corpus verdicts (140/140)"
@echo "make census — every stated corpus count matches the corpus"
@echo "make guarantees — GUARANTEES.md claims table matches the rules and corpus"
@echo "make differential — the differential corpus loop (accept + reject)"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ skip the cgo runtime.
| What does a Go→Rust call cost? | **2.8 ns** safe stack-switched trampoline vs **46.8 ns** cgo — 16.5× faster, 0 allocs | ✅ [spike 1](spikes/spike1-call-overhead/RESULTS.md) · [G2 bench: fast path 3.37 ns ≤ 5 ns on the live bridge](gates/G2.md) |
| Can consumers skip the toolchain? | Yes — generated Go + prebuilt artifact; a plain `go run` imports a Hybrid package | ✅ [example 09](examples/09-go-imports-hybrid/) |
| Can a scheduler-safe checked language work at all? | Bounded blocking pool, two-tier budgets, race-clean under `-race` | ✅ [spike 4: 100k-cell matrix](spikes/spike4-scheduling/RESULTS.md) |
| Ownership rules without visible lifetimes? | Tier-0 checker: 6 sendable classes, move/freeze, loans, fail-closed boundary | ✅ [checker corpus 139/139](tests/checker-corpus/) + [differential loop 40/40 + 80/80](tools/differential_loop.py) |
| Ownership rules without visible lifetimes? | Tier-0 checker: 6 sendable classes, move/freeze, loans, fail-closed boundary | ✅ [checker corpus 140/140](tests/checker-corpus/) + [differential loop 40/40 + 80/80](tools/differential_loop.py) |
| Scheduling with blocking Rust calls | resolved: pool + Go-scheduler fallback, budgets pinned | ✅ spike 4 |

## Quick start
Expand Down
15 changes: 15 additions & 0 deletions compiler/crates/hy-emit/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,21 @@ impl Emitter {
};
if let Some((base, head, type_args)) = assoc_field_and_ta {
if let Expr::Ident { name: root, .. } = Self::unwrap_parens(base) {
if root == "hy"
&& !self.local_vars.contains("hy")
&& !self.bindings.contains_key("hy")
&& matches!(head, "pipe" | "new_pipe")
{
let translated_ta = type_args
.as_deref()
.map(|ta| self.translate_type_args_suffix(ta))
.unwrap_or_default();
let mut parts = Vec::new();
for a in args {
parts.push(self.emit_call_arg(a)?);
}
return Ok(format!("hy_pipe{}({})", translated_ta, parts.join(", ")));
}
if !self.is_unshadowed_pkg_root(root) && !self.bindings.contains_key(root) {
let full = format!("{}.{}", root, head);
if self.assoc_fns.contains(&full) {
Expand Down
63 changes: 62 additions & 1 deletion compiler/crates/hy-emit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,39 @@ const POP_PRELUDE: &str = concat!(
"}\n",
);

const PIPE_PRELUDE: &str = concat!(
"// ---- Hybrid prelude: SPSC channel capabilities (auto-generated; do not edit) ----\n",
"type hy_send_cap[t any] chan<- t\n",
"\n",
"func (s hy_send_cap[t]) send(v t) {\n",
"\ts <- v\n",
"}\n",
"\n",
"func (s hy_send_cap[t]) close() {\n",
"\tclose(s)\n",
"}\n",
"\n",
"type hy_recv_cap[t any] <-chan t\n",
"\n",
"func (r hy_recv_cap[t]) recv() t {\n",
"\treturn <-r\n",
"}\n",
"\n",
"func (r hy_recv_cap[t]) drain() {\n",
"\tfor range r {\n",
"\t}\n",
"}\n",
"\n",
"func hy_pipe[t any](cap ...int) (hy_send_cap[t], hy_recv_cap[t]) {\n",
"\tc := 8\n",
"\tif len(cap) > 0 {\n",
"\t\tc = cap[0]\n",
"\t}\n",
"\tch := make(chan t, c)\n",
"\treturn hy_send_cap[t](ch), hy_recv_cap[t](ch)\n",
"}\n",
);

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1036,7 +1069,10 @@ impl Emitter {
// An explicit `import "errors"` whose only Hybrid call was
// `errors.wrap(err, msg)` lowers to `fmt.Errorf(...)` and
// leaves no `errors.` reference in the emitted Go body (#657).
// Builtin `hy.pipe` / `hy.send_cap` / `hy.recv_cap` lower to
// the generated prelude without a Go `"hy"` package.
!(root.as_str() == "errors" && !self.body.contains("errors."))
&& !(root.as_str() == "hy" && !self.body.contains("hy."))
})
.collect();
if !active_imports.is_empty() {
Expand Down Expand Up @@ -1069,7 +1105,10 @@ impl Emitter {
|| body.contains(".insert(");
let needs_pop = body.contains("hy_pop(");
let needs_n5_make = body.contains("hy_n5_make(");
if needs_option || needs_set || needs_pop || needs_n5_make {
let needs_pipe = body.contains("hy_pipe[")
|| body.contains("hy_send_cap[")
|| body.contains("hy_recv_cap[");
if needs_option || needs_set || needs_pop || needs_n5_make || needs_pipe {
out.push('\n');
if needs_option {
out.push_str(OPTION_PRELUDE);
Expand All @@ -1087,6 +1126,10 @@ impl Emitter {
out.push_str(N5_MAKE_PRELUDE);
out.push('\n');
}
if needs_pipe {
out.push_str(PIPE_PRELUDE);
out.push('\n');
}
}
out.push('\n');
out.push_str(&self.body);
Expand Down Expand Up @@ -1825,6 +1868,12 @@ impl Emitter {
if name.starts_with("set[") || name == "set" {
return Some(BindingKind::Set);
}
if name.starts_with("hy.recv_cap[") || name == "hy.recv_cap" {
return Some(BindingKind::Chan);
}
if name.starts_with("hy.send_cap[") || name == "hy.send_cap" {
return None;
}
if name.is_empty() || !name.contains('.') {
return None;
}
Expand Down Expand Up @@ -1858,6 +1907,12 @@ impl Emitter {
}

fn type_is_foreign(&self, t: &TypeExpr) -> bool {
if let TypeExpr::Named { name, .. } = self.resolve_type_alias(t) {
let base = name.split('[').next().unwrap_or(name);
if base == "hy.send_cap" || base == "hy.recv_cap" {
return false;
}
}
match t {
TypeExpr::Named { name, .. } => name.contains('.'),
TypeExpr::Pointer { to, .. } => self.type_is_foreign(to),
Expand Down Expand Up @@ -2095,6 +2150,12 @@ impl Emitter {
return format!("{}[{}]", go_head, go_args);
}
}
if name == "hy.send_cap" {
return "hy_send_cap".to_string();
}
if name == "hy.recv_cap" {
return "hy_recv_cap".to_string();
}
if let Some((root, rest)) = name.split_once('.') {
// Rust-owned types map to their binding package's handle type
// (serde_json.value → jsonbinding.Value) — NOT the naive
Expand Down
36 changes: 36 additions & 0 deletions compiler/crates/hy-emit/src/stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1284,7 +1284,40 @@ impl Emitter {
}

/// Binding hints from the defining values (see `BindingKind`).
fn is_hy_pipe_call(e: &Expr) -> bool {
let Expr::Call { callee, .. } = Self::peel_parens(e) else {
return false;
};
let field = match Self::peel_parens(callee) {
Expr::Field { base, name, .. } => {
let head = name.split('[').next().unwrap_or(name);
Some((base.as_ref(), head))
}
Expr::Index { base, .. } => match Self::peel_parens(base) {
Expr::Field { base, name, .. } => Some((base.as_ref(), name.as_str())),
_ => None,
},
_ => None,
};
matches!(
field,
Some((base, head))
if matches!(Self::peel_parens(base), Expr::Ident { name, .. } if name == "hy")
&& matches!(head, "pipe" | "new_pipe")
)
}

fn hint_from_values(&mut self, names: &[String], values: &[Expr]) {
let pipe_rx_hint = if names.len() == 2
&& values.len() == 1
&& names[1] != "_"
&& !self.local_vars.contains("hy")
&& !self.bindings.contains_key("hy")
{
Self::is_hy_pipe_call(&values[0])
} else {
false
};
let single_hint = if names.len() == 1 && values.len() == 1 && names[0] != "_" {
Some(self.binding_kind_of(&values[0]))
} else {
Expand All @@ -1296,6 +1329,9 @@ impl Emitter {
Vec::new()
};
self.shadow_local_names(names);
if pipe_rx_hint {
self.bindings.insert(names[1].clone(), BindingKind::Chan);
}
if let Some(kind) = single_hint {
if kind != BindingKind::Plain {
self.bindings.insert(names[0].clone(), kind);
Expand Down
117 changes: 117 additions & 0 deletions compiler/crates/hy-emit/tests/hy_pipe_capabilities_4464.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use hy_emit::emit_go;
use hy_syntax::parse_file;

fn emit(src: &str) -> String {
let ast = parse_file("test.hy", src).expect("parses");
emit_go(&ast).unwrap_or_else(|e| panic!("emit failed: {}", e.message))
}

fn find_go() -> Option<String> {
if std::process::Command::new("go")
.arg("version")
.output()
.is_ok_and(|o| o.status.success())
{
return Some("go".to_string());
}
let fallback = "/home/max/.local/go/bin/go";
std::path::Path::new(fallback)
.exists()
.then(|| fallback.to_string())
}

#[test]
fn pipe_happy_path_and_explicit_hy_import_emit_prelude_without_hy_import() {
let go = emit(
"import \"hy\"\nfn worker(tx hy.send_cap[int]) {\n tx.send(42)\n tx.close()\n}\nfn main() {\n tx, rx := hy.pipe[int]()\n task worker(move tx)\n v := rx.recv()\n _ = v\n rx.drain()\n}\n",
);
assert!(
!go.contains("\"hy\""),
"must not emit bogus Go import \"hy\":\n{go}"
);
for want in [
"type hy_send_cap[t any] chan<- t",
"type hy_recv_cap[t any] <-chan t",
"func hy_pipe[t any](cap ...int) (hy_send_cap[t], hy_recv_cap[t])",
"func worker(tx hy_send_cap[int])",
"tx.send(42)",
"tx.close()",
"tx, rx := hy_pipe[int]()",
"v := rx.recv()",
"rx.drain()",
] {
assert!(go.contains(want), "missing `{want}` in:\n{go}");
}
for bad in ["tx.Send", "tx.Close", "rx.Recv", "rx.Drain", "hy.pipe"] {
assert!(!go.contains(bad), "unexpected `{bad}` in:\n{go}");
}
}

#[test]
fn new_pipe_for_range_select_closure_and_typed_var_emit_correctly() {
let go = emit(
"import \"fmt\"\nstruct pair {\n tx hy.send_cap[string]\n rx hy.recv_cap[string]\n}\nfn pipeline(rx1 hy.recv_cap[int], tx2 hy.send_cap[int]) {\n v := rx1.recv()\n tx2.send(v)\n rx1.drain()\n tx2.close()\n}\nfn main() {\n tx, rx := hy.new_pipe[int](16)\n tx.send(1)\n tx.close()\n for item in rx {\n fmt.println(item)\n }\n var s hy.send_cap[int]\n var r hy.recv_cap[int]\n s, r = hy.pipe[int]()\n go_fn := fn(sc hy.send_cap[int], rc hy.recv_cap[int]) {\n sc.send(7)\n sc.close()\n select {\n x := <-rc: fmt.println(x)\n }\n for rem in rc {\n fmt.println(rem)\n }\n }\n go_fn(s, r)\n _ = pipeline\n}\n",
);
for want in [
"tx, rx := hy_pipe[int](16)",
"for item := range rx {",
"var s hy_send_cap[int]",
"var r hy_recv_cap[int]",
"s, r = hy_pipe[int]()",
"func(sc hy_send_cap[int], rc hy_recv_cap[int])",
"sc.send(7)",
"sc.close()",
"case x := <-rc:",
"for rem := range rc {",
"rx hy_recv_cap[string]",
"tx hy_send_cap[string]",
] {
assert!(go.contains(want), "missing `{want}` in:\n{go}");
}
assert!(
!go.contains("for _, item := range rx") && !go.contains("for _, rem := range rc"),
"channel cap range must emit 1-var range:\n{go}"
);
assert!(
!go.contains("sc.Send") && !go.contains("sc.Close"),
"closure send_cap methods must stay lowercase:\n{go}"
);
}

#[test]
fn non_pipe_programs_do_not_emit_pipe_prelude() {
let go = emit("import \"fmt\"\nfn main() {\n fmt.println(1)\n}\n");
assert!(
!go.contains("hy_pipe") && !go.contains("hy_send_cap") && !go.contains("hy_recv_cap"),
"unexpected pipe prelude in plain program:\n{go}"
);
}

#[test]
fn pipe_capabilities_build_and_run_cleanly_under_go_race() {
let go = emit(
"import \"fmt\"\nfn producer(sender hy.send_cap[int]) {\n for i in 0..4 {\n sender.send(i)\n }\n sender.close()\n}\nfn relay(upstream hy.recv_cap[int], downstream hy.send_cap[int]) {\n first := upstream.recv()\n downstream.send(first)\n for rest in upstream {\n downstream.send(rest)\n }\n downstream.close()\n}\nfn main() {\n tx1, rx1 := hy.pipe[int]()\n tx2, rx2 := hy.new_pipe[int](8)\n task producer(move tx1)\n task relay(move rx1, move tx2)\n sum := 0\n for v in rx2 {\n sum = sum + v\n }\n tx3, rx3 := hy.pipe[string](2)\n tx3.send(\"ok\")\n tx3.close()\n msg := \"\"\n select {\n s := <-rx3: msg = s\n }\n rx3.drain()\n fmt.println(sum, msg)\n}\n",
);
let Some(go_bin) = find_go() else {
return;
};
let tmp = std::env::temp_dir().join(format!("hy_pipe_caps_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("create temp dir");
std::fs::write(tmp.join("go.mod"), "module pipecaps4464\n\ngo 1.21\n").expect("write go.mod");
std::fs::write(tmp.join("main.go"), &go).expect("write main.go");
let out = std::process::Command::new(&go_bin)
.args(["run", "-race", "."])
.current_dir(&tmp)
.output()
.expect("invoke go run -race");
let _ = std::fs::remove_dir_all(&tmp);
assert!(
out.status.success(),
"go run -race failed on emitted Go:\n{}\nstderr:\n{}",
go,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert_eq!(stdout, "6 ok\n");
}
2 changes: 1 addition & 1 deletion compiler/crates/hy-syntax/src/ast.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! The Hybrid AST — the cross-component contract between `hy-syntax`
//! (produces), `hy-check` (consumes), and the MVP-0 emitter (consumes).
//!
//! Scope: the surface exercised by tests/checker-corpus/*.hy (139 programs),
//! Scope: the surface exercised by tests/checker-corpus/*.hy (140 programs),
//! examples/ (ten target programs), and spikes/spike6-ownership/work/*.hy
//! (ten programs). Anything outside that surface is allowed to be
//! unimplemented by the parser — it must produce a clean `unsupported`
Expand Down
2 changes: 1 addition & 1 deletion compiler/crates/hy-syntax/tests/parse_all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fn must_parse(path: &PathBuf) {
fn checker_corpus_all_parse() {
let files = hy_files_under("tests/checker-corpus");

assert_eq!(files.len(), 139, "expected exactly 139 corpus programs",);
assert_eq!(files.len(), 140, "expected exactly 140 corpus programs",);
// 61-string-escape-fail-closed.hy is the corpus's first PARSE-reject
// fixture (a malformed LX7 string escape must be named by the lexer,
// never silently accepted): it is excluded from the must-parse loop
Expand Down
2 changes: 1 addition & 1 deletion tests/checker-corpus/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# tests/checker-corpus — adversarial programs with pre-registered verdicts

**Current registered corpus: 139 programs — ACCEPT 45 · ACCEPT-MANAGED 2 · REJECT 92 — each with a machine-readable `// EXPECTED-VERDICT:` header and a
**Current registered corpus: 140 programs — ACCEPT 46 · ACCEPT-MANAGED 2 · REJECT 92 — each with a machine-readable `// EXPECTED-VERDICT:` header and a
row in [VERDICTS.md](VERDICTS.md). The 18 originally registered here were
written BEFORE the Phase-1 checker existed; that registration grew to 20
during the 2026-10-12 integration, and every case after it was added
Expand Down
10 changes: 9 additions & 1 deletion tests/checker-corpus/VERDICTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,4 +1111,12 @@ the move freeze reach the send path as well.
| struct-chan-scalars-send-accept | ACCEPT | — | — | channel safe-by-type + immutable scalars; owned `move` over `chan packet`; no use after move |
| struct-chan-shared-map-send-reject | REJECT | A1b | `HYB-E3003` | the map field aliases the sender's `m`; moving the struct over the channel freezes that storage, so `m["after"] = 2` is a write through a live alias |

With this registration the corpus is **139 programs — ACCEPT 45 · ACCEPT-MANAGED 2 · REJECT 92**.
## Additive corpus registration — 2026-11-14: hy.pipe / capability emit path (#4464)

One `ACCEPT` fixture added WITH the emitter lowering for `hy.pipe[T]` / `hy.new_pipe[T]` and `hy.send_cap[T]` / `hy.recv_cap[T]` (#4464): accepted SPSC capability programs now emit compilable, `-race`-clean Go with `type hy_send_cap[t any] chan<- t`, `type hy_recv_cap[t any] <-chan t`, and `func hy_pipe[t any](cap ...int)` in the generated prelude rather than emitting an unresolvable `import "hy"`.

| case | verdict | rule | code | note |
|------|---------|------|------|------|
| hy-pipe-capabilities-accept | ACCEPT | — | — | `hy.pipe` / `hy.new_pipe` + `hy.send_cap` / `hy.recv_cap` + `for v in rx` + `.recv()` / `.drain()` build and print `3 9` |

With this registration the corpus is **140 programs — ACCEPT 46 · ACCEPT-MANAGED 2 · REJECT 92**.
Loading