Skip to content

Commit 729bf54

Browse files
authored
Merge pull request #38 from aallan/c6d-state-host-imports
Add State<T> host imports for WASM codegen (C6d)
2 parents ff3b216 + 8d48fac commit 729bf54

8 files changed

Lines changed: 390 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
66

77
## [Unreleased]
88

9+
## [0.0.13] - 2026-02-24
10+
11+
### Added
12+
- **State\<T\> WASM host imports** (C6d): compile `get`/`put` operations for `State<T>` effects as WASM host imports
13+
- `State<Int>`, `State<Nat>`, `State<Bool>`, `State<Float64>` compile to typed host import pairs
14+
- `get(())``call $vera.state_get_{T}` (returns typed value); `put(x)``call $vera.state_put_{T}` (consumes typed value)
15+
- Host runtime maintains mutable state cells per type, initialized to zero
16+
- `execute()` accepts optional `initial_state` parameter and returns final `state` in `ExecuteResult`
17+
- Mixed effects supported: `effects(<State<Int>, IO>)` compiles correctly
18+
- `effect_ops` dict mechanism in `WasmContext` redirects bare `get`/`put` calls to host imports
19+
- `_is_void_expr` recognizes `put()` as void (no `drop` emitted in ExprStmt)
20+
- **Codegen tests**: 15 new tests — get default, put-then-get, increment pattern, example file, Bool/Float64/Nat state, String rejection, mixed effects, WAT imports, multiple types, void semantics, initial state override, pure function purity (585 total, up from 570)
21+
- `examples/increment.vera` now compiles and runs (7 of 14 examples compilable)
22+
923
## [0.0.12] - 2026-02-24
1024

1125
### Added

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ vera parse file.vera # Print the parse tree
1717
vera ast file.vera # Print the typed AST
1818
vera ast --json file.vera # Print the AST as JSON
1919

20-
pytest tests/ -v # Run the test suite (570 tests)
20+
pytest tests/ -v # Run the test suite (585 tests)
2121
mypy vera/ # Type-check the compiler itself
2222

2323
python scripts/check_examples.py # Verify all 14 examples parse + check + verify

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,13 +159,13 @@ Development follows an **interleaved spiral** — each phase adds a complete com
159159
| C3 | v0.0.5 | **Type checker** — decidable type checking, slot resolution, effect tracking | Done |
160160
| C4 | v0.0.8 | **Contract verifier** — Z3 integration, refinement types, counterexamples | Done |
161161
| C5 | v0.0.9 | **WASM codegen** — compile to WebAssembly, `vera compile` / `vera run` | Done |
162-
| C6 | v0.0.10–0.0.23 | **Codegen completeness** — ADTs, match, closures, effects, generics in WASM | **In progress** (C6a done) |
162+
| C6 | v0.0.10–0.0.23 | **Codegen completeness** — ADTs, match, closures, effects, generics in WASM | **In progress** (C6a–C6d done) |
163163
| C7 || **Module system** — cross-file imports, public/private visibility | Planned |
164164
| C8 | v0.1.0 | **End-to-end** — all examples compile and run, spec complete, polish | Planned |
165165

166166
### What's next: C6 — Codegen Completeness (v0.0.10–v0.0.23)
167167

168-
The code generator compiles 6 of 14 examples. C6 extends WASM compilation to all language constructs, working through the dependency graph from simplest to most complex.
168+
The code generator compiles 7 of 14 examples. C6 extends WASM compilation to all language constructs, working through the dependency graph from simplest to most complex.
169169

170170
**Independent tasks (no dependencies on each other):**
171171

@@ -174,7 +174,7 @@ The code generator compiles 6 of 14 examples. C6 extends WASM compilation to all
174174
| ~~C6a~~ | ~~Float64 — `f64` literals, arithmetic, comparisons~~ | ~~#25~~ | ~~Done (v0.0.10)~~ |
175175
| ~~C6b~~ | ~~Callee preconditions — verify `requires()` at call sites~~ | ~~#19~~ | ~~Done (v0.0.11)~~ |
176176
| ~~C6c~~ | ~~Match exhaustiveness — verify all constructors covered~~ | ~~#18~~ | ~~Done (v0.0.12)~~ |
177-
| C6d | State\<T\> operations — get/put as host imports || increment.vera |
177+
| ~~C6d~~ | ~~State\<T\> operations — get/put as host imports~~ || ~~Done (v0.0.13)~~ |
178178

179179
**Allocator and data types (sequential chain):**
180180

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vera"
3-
version = "0.0.12"
3+
version = "0.0.13"
44
description = "Vera: a programming language designed for LLMs, with full contracts, algebraic effects, and typed slot references"
55
readme = "README.md"
66
license = "MIT"

tests/test_codegen.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,3 +1395,211 @@ def test_first_export_used_when_no_main(self) -> None:
13951395
assert "main" not in result.exports
13961396
exec_result = execute(result) # no fn_name specified
13971397
assert exec_result.value == 99
1398+
1399+
1400+
# =====================================================================
1401+
# 6d: State<T> host imports
1402+
# =====================================================================
1403+
1404+
def _run_state(
1405+
source: str,
1406+
fn: str | None = None,
1407+
args: list[int | float] | None = None,
1408+
initial_state: dict[str, int | float] | None = None,
1409+
) -> ExecuteResult:
1410+
"""Compile, execute, and return the full ExecuteResult."""
1411+
result = _compile_ok(source)
1412+
return execute(result, fn_name=fn, args=args, initial_state=initial_state)
1413+
1414+
1415+
class TestStateEffect:
1416+
1417+
def test_state_int_get_default(self) -> None:
1418+
"""get(()) returns 0 by default for State<Int>."""
1419+
source = """\
1420+
fn f(-> @Int)
1421+
requires(true) ensures(true) effects(<State<Int>>)
1422+
{ get(()) }
1423+
"""
1424+
exec_result = _run_state(source, fn="f")
1425+
assert exec_result.value == 0
1426+
1427+
def test_state_int_put_then_get(self) -> None:
1428+
"""put(42) then get(()) returns 42."""
1429+
source = """\
1430+
fn f(-> @Int)
1431+
requires(true) ensures(true) effects(<State<Int>>)
1432+
{
1433+
put(42);
1434+
get(())
1435+
}
1436+
"""
1437+
exec_result = _run_state(source, fn="f")
1438+
assert exec_result.value == 42
1439+
1440+
def test_increment_pattern(self) -> None:
1441+
"""Classic increment: get, add 1, put — state goes from 0 to 1."""
1442+
source = """\
1443+
fn increment(@Unit -> @Unit)
1444+
requires(true) ensures(true) effects(<State<Int>>)
1445+
{
1446+
let @Int = get(());
1447+
put(@Int.0 + 1);
1448+
()
1449+
}
1450+
"""
1451+
exec_result = _run_state(source, fn="increment")
1452+
assert exec_result.value is None # Unit return
1453+
assert exec_result.state["State_Int"] == 1
1454+
1455+
def test_increment_example_file(self) -> None:
1456+
"""examples/increment.vera compiles and executes."""
1457+
from pathlib import Path
1458+
path = Path(__file__).parent.parent / "examples" / "increment.vera"
1459+
source = path.read_text()
1460+
tree = parse_file(str(path))
1461+
program = transform(tree)
1462+
result = compile(program, source=source, file=str(path))
1463+
assert result.ok
1464+
assert "increment" in result.exports
1465+
exec_result = execute(result, fn_name="increment")
1466+
assert exec_result.state["State_Int"] == 1
1467+
1468+
def test_state_bool_get_default(self) -> None:
1469+
"""Bool state defaults to 0 (false)."""
1470+
source = """\
1471+
fn f(-> @Bool)
1472+
requires(true) ensures(true) effects(<State<Bool>>)
1473+
{ get(()) }
1474+
"""
1475+
exec_result = _run_state(source, fn="f")
1476+
assert exec_result.value == 0
1477+
1478+
def test_state_bool_put_get(self) -> None:
1479+
"""put(true) then get(()) returns 1."""
1480+
source = """\
1481+
fn f(-> @Bool)
1482+
requires(true) ensures(true) effects(<State<Bool>>)
1483+
{
1484+
put(true);
1485+
get(())
1486+
}
1487+
"""
1488+
exec_result = _run_state(source, fn="f")
1489+
assert exec_result.value == 1
1490+
1491+
def test_state_float64_get_default(self) -> None:
1492+
"""Float64 state defaults to 0.0."""
1493+
source = """\
1494+
fn f(-> @Float64)
1495+
requires(true) ensures(true) effects(<State<Float64>>)
1496+
{ get(()) }
1497+
"""
1498+
exec_result = _run_state(source, fn="f")
1499+
assert exec_result.value == 0.0
1500+
1501+
def test_state_nat_compiles(self) -> None:
1502+
"""State<Nat> compiles (Nat maps to i64)."""
1503+
source = """\
1504+
fn f(-> @Nat)
1505+
requires(true) ensures(true) effects(<State<Nat>>)
1506+
{ get(()) }
1507+
"""
1508+
exec_result = _run_state(source, fn="f")
1509+
assert exec_result.value == 0
1510+
1511+
def test_state_string_rejected(self) -> None:
1512+
"""State<String> is unsupported — function skipped with warning."""
1513+
source = """\
1514+
fn f(-> @Int)
1515+
requires(true) ensures(true) effects(<State<String>>)
1516+
{ 42 }
1517+
"""
1518+
result = _compile(source)
1519+
warnings = [d for d in result.diagnostics if d.severity == "warning"]
1520+
assert any("unsupported" in w.description.lower() for w in warnings)
1521+
assert "f" not in result.exports
1522+
1523+
def test_state_with_io(self) -> None:
1524+
"""Mixed effects(<State<Int>, IO>) compiles and both work."""
1525+
source = """\
1526+
fn f(@Unit -> @Unit)
1527+
requires(true) ensures(true) effects(<State<Int>, IO>)
1528+
{
1529+
put(42);
1530+
IO.print("done");
1531+
()
1532+
}
1533+
"""
1534+
exec_result = _run_state(source, fn="f")
1535+
assert exec_result.state["State_Int"] == 42
1536+
assert exec_result.stdout == "done"
1537+
1538+
def test_state_wat_has_imports(self) -> None:
1539+
"""WAT output contains State import declarations."""
1540+
source = """\
1541+
fn f(-> @Int)
1542+
requires(true) ensures(true) effects(<State<Int>>)
1543+
{ get(()) }
1544+
"""
1545+
result = _compile_ok(source)
1546+
assert 'import "vera" "state_get_Int"' in result.wat
1547+
assert 'import "vera" "state_put_Int"' in result.wat
1548+
1549+
def test_multiple_state_types(self) -> None:
1550+
"""Multiple State types emit all imports."""
1551+
source = """\
1552+
fn f(@Int -> @Unit)
1553+
requires(true) ensures(true) effects(<State<Int>, State<Bool>>)
1554+
{
1555+
put(@Int.0);
1556+
()
1557+
}
1558+
"""
1559+
result = _compile_ok(source)
1560+
assert 'import "vera" "state_get_Int"' in result.wat
1561+
assert 'import "vera" "state_put_Int"' in result.wat
1562+
assert 'import "vera" "state_get_Bool"' in result.wat
1563+
assert 'import "vera" "state_put_Bool"' in result.wat
1564+
assert len(result.state_types) == 2
1565+
1566+
def test_put_void_no_drop(self) -> None:
1567+
"""put(x) in ExprStmt does not emit a drop instruction."""
1568+
source = """\
1569+
fn f(@Unit -> @Unit)
1570+
requires(true) ensures(true) effects(<State<Int>>)
1571+
{
1572+
put(42);
1573+
()
1574+
}
1575+
"""
1576+
result = _compile_ok(source)
1577+
# The function body should NOT contain 'drop' after the put call
1578+
fn_start = result.wat.index("(func $f")
1579+
fn_body = result.wat[fn_start:]
1580+
# put call should be present, drop should not follow it
1581+
assert "call $vera.state_put_Int" in fn_body
1582+
assert "drop" not in fn_body
1583+
1584+
def test_state_initial_value(self) -> None:
1585+
"""Initial state override: get(()) returns the initial value."""
1586+
source = """\
1587+
fn f(-> @Int)
1588+
requires(true) ensures(true) effects(<State<Int>>)
1589+
{ get(()) }
1590+
"""
1591+
exec_result = _run_state(
1592+
source, fn="f", initial_state={"State_Int": 10}
1593+
)
1594+
assert exec_result.value == 10
1595+
1596+
def test_pure_no_state_imports(self) -> None:
1597+
"""Pure functions don't produce State imports."""
1598+
source = """\
1599+
fn f(-> @Int)
1600+
requires(true) ensures(true) effects(pure)
1601+
{ 42 }
1602+
"""
1603+
result = _compile_ok(source)
1604+
assert "state_get" not in result.wat
1605+
assert "state_put" not in result.wat

vera/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Vera: a programming language designed for LLMs."""
22

3-
__version__ = "0.0.12"
3+
__version__ = "0.0.13"

0 commit comments

Comments
 (0)