Skip to content

Commit a6a7a80

Browse files
stackdumpclaude
andcommitted
Ship-ready: examples, docs, production codegen, smarter testgen
Production readiness: - GenerateProduction() strips test backdoors (unsafeBecomeOwner, etc.) - Generate() keeps test helpers for development/validation Validate improvements: - Fix arc count display (was 0, now shows actual count from schema.Arcs) - Show individual forge test pass/fail counts - Cleaner aligned output format Testgen improvements: - needsStateSetup: detect guards referencing state variables (>= checks) and generate expectRevert tests when prior state can't be auto-setup - inferZeroParams: use amount=1 when guard has "state >= amount" pattern (fixes 0 >= 0 passing when it should fail) - Works for arbitrary .btw schemas, not just known templates Examples: - counter.btw — scalar state, increment/decrement - erc20.btw — fungible token with nested map allowances - nft.btw — non-fungible token with ownership - escrow.btw — multi-step deposit/lock/release workflow - examples/README.md — full .btw DSL syntax reference All 4 examples pass: ./bitwrap -validate examples/*.btw Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d9a34c4 commit a6a7a80

File tree

7 files changed

+382
-67
lines changed

7 files changed

+382
-67
lines changed

cmd/bitwrap/main.go

Lines changed: 60 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -87,48 +87,46 @@ func main() {
8787
func runValidate(path string) int {
8888
src, err := os.ReadFile(path)
8989
if err != nil {
90-
fmt.Fprintf(os.Stderr, "FAIL: read %s: %v\n", path, err)
90+
fmt.Fprintf(os.Stderr, "FAIL read %s: %v\n", path, err)
9191
return 1
9292
}
9393

9494
// Step 1: Parse DSL
95-
fmt.Printf("parse %s\n", path)
95+
fmt.Printf(" parse %s\n", path)
9696
ast, err := dsl.Parse(string(src))
9797
if err != nil {
98-
fmt.Fprintf(os.Stderr, "FAIL: parse: %v\n", err)
98+
fmt.Fprintf(os.Stderr, "FAIL parse: %v\n", err)
9999
return 1
100100
}
101101

102102
// Step 2: Build metamodel schema
103-
fmt.Printf("● build schema: %s\n", ast.Name)
104103
schema, err := dsl.Build(ast)
105104
if err != nil {
106-
fmt.Fprintf(os.Stderr, "FAIL: build: %v\n", err)
105+
fmt.Fprintf(os.Stderr, "FAIL build: %v\n", err)
107106
return 1
108107
}
109-
fmt.Printf(" states: %d, actions: %d, arcs: %d\n",
110-
len(schema.States), len(schema.Actions), len(schema.InputArcs(""))+len(schema.OutputArcs("")))
108+
fmt.Printf(" schema %s (%d states, %d actions, %d arcs)\n",
109+
ast.Name, len(schema.States), len(schema.Actions), len(schema.Arcs))
111110

112111
// Step 3: Generate Solidity
113112
contractName := solidity.ContractName(schema.Name)
114113
contractCode := solidity.Generate(schema)
115114
testCode := solidity.GenerateTests(schema)
116115
genesisCode := solidity.GenerateGenesis(schema.Name, solidity.GenesisConfig{}, solidity.DefaultAddresses())
117-
fmt.Printf("● generate Solidity: %s.sol (%d bytes)\n", contractName, len(contractCode))
118-
fmt.Printf(" tests: %sTest.t.sol (%d bytes)\n", contractName, len(testCode))
119-
fmt.Printf(" genesis: %sGenesis.s.sol (%d bytes)\n", contractName, len(genesisCode))
116+
fmt.Printf(" solidity %s.sol (%d bytes), tests (%d bytes), genesis (%d bytes)\n",
117+
contractName, len(contractCode), len(testCode), len(genesisCode))
120118

121119
// Step 4: Check for forge
122120
if _, err := exec.LookPath("forge"); err != nil {
123-
fmt.Printf("● forge not installed — skipping compilation and deployment\n")
124-
fmt.Printf("PASS (parse + generate only)\n")
121+
fmt.Printf(" skip forge not installed\n")
122+
fmt.Printf("PASS (parse + generate)\n")
125123
return 0
126124
}
127125

128126
// Step 5: Set up temp Foundry project
129127
dir, err := os.MkdirTemp("", "bitwrap-validate-*")
130128
if err != nil {
131-
fmt.Fprintf(os.Stderr, "FAIL: tmpdir: %v\n", err)
129+
fmt.Fprintf(os.Stderr, "FAIL tmpdir: %v\n", err)
132130
return 1
133131
}
134132
defer os.RemoveAll(dir)
@@ -144,33 +142,57 @@ func runValidate(path string) int {
144142
os.WriteFile(filepath.Join(dir, "script", contractName+"Genesis.s.sol"), []byte(genesisCode), 0o644)
145143

146144
// Step 6: Install forge-std
147-
if !runStep(dir, "install forge-std", "git", "init") {
145+
if !runStep(dir, "deps", "git", "init") {
148146
return 1
149147
}
150-
if !runStep(dir, "install forge-std", "forge", "install", "foundry-rs/forge-std") {
148+
if !runStep(dir, "deps", "forge", "install", "foundry-rs/forge-std") {
151149
return 1
152150
}
153151

154152
// Step 7: Compile
155-
if !runStep(dir, "forge build", "forge", "build") {
153+
if !runStep(dir, "compile", "forge", "build") {
156154
return 1
157155
}
158156

159-
// Step 8: Test
160-
if !runStep(dir, "forge test", "forge", "test", "-vv") {
157+
// Step 8: Test — show individual results
158+
fmt.Printf(" test ")
159+
cmd := exec.Command("forge", "test", "-vv")
160+
cmd.Dir = dir
161+
out, testErr := cmd.CombinedOutput()
162+
outStr := string(out)
163+
164+
// Count pass/fail from forge output
165+
passed, failed := 0, 0
166+
for _, line := range strings.Split(outStr, "\n") {
167+
if strings.Contains(line, "[PASS]") {
168+
passed++
169+
} else if strings.Contains(line, "[FAIL") {
170+
failed++
171+
}
172+
}
173+
if testErr != nil {
174+
fmt.Printf("%d passed, %d failed\n", passed, failed)
175+
// Show failing test names
176+
for _, line := range strings.Split(outStr, "\n") {
177+
if strings.Contains(line, "[FAIL") {
178+
fmt.Printf(" %s\n", strings.TrimSpace(line))
179+
}
180+
}
181+
fmt.Fprintf(os.Stderr, "FAIL forge test\n")
161182
return 1
162183
}
184+
fmt.Printf("%d passed\n", passed)
163185

164186
// Step 9: Deploy to anvil
165187
if _, err := exec.LookPath("anvil"); err != nil {
166-
fmt.Printf("● anvil not installed — skipping deployment\n")
188+
fmt.Printf(" skip anvil not installed\n")
167189
fmt.Printf("PASS\n")
168190
return 0
169191
}
170192

171193
listener, err := net.Listen("tcp", "127.0.0.1:0")
172194
if err != nil {
173-
fmt.Fprintf(os.Stderr, "FAIL: find free port: %v\n", err)
195+
fmt.Fprintf(os.Stderr, "FAIL find free port: %v\n", err)
174196
return 1
175197
}
176198
port := listener.Addr().(*net.TCPAddr).Port
@@ -179,15 +201,14 @@ func runValidate(path string) int {
179201
rpcURL := fmt.Sprintf("http://127.0.0.1:%d", port)
180202
anvil := exec.Command("anvil", "--port", fmt.Sprintf("%d", port), "--silent")
181203
if err := anvil.Start(); err != nil {
182-
fmt.Fprintf(os.Stderr, "FAIL: start anvil: %v\n", err)
204+
fmt.Fprintf(os.Stderr, "FAIL start anvil: %v\n", err)
183205
return 1
184206
}
185207
defer func() { anvil.Process.Kill(); anvil.Wait() }()
186208

187-
// Wait for anvil
188209
for i := 0; i < 50; i++ {
189-
cmd := exec.Command("cast", "chain-id", "--rpc-url", rpcURL)
190-
if out, err := cmd.CombinedOutput(); err == nil && strings.TrimSpace(string(out)) == "31337" {
210+
c := exec.Command("cast", "chain-id", "--rpc-url", rpcURL)
211+
if o, e := c.CombinedOutput(); e == nil && strings.TrimSpace(string(o)) == "31337" {
191212
break
192213
}
193214
time.Sleep(100 * time.Millisecond)
@@ -197,18 +218,21 @@ func runValidate(path string) int {
197218
createArgs := []string{"create", fmt.Sprintf("src/%s.sol:%s", contractName, contractName),
198219
"--rpc-url", rpcURL, "--private-key", privKey, "--broadcast"}
199220

200-
cmd := exec.Command("forge", createArgs...)
201-
cmd.Dir = dir
202-
out, err := cmd.CombinedOutput()
203-
if err != nil || !strings.Contains(string(out), "Deployed to:") {
204-
fmt.Fprintf(os.Stderr, "● deploy FAIL\n%s\n", out)
221+
deployCmd := exec.Command("forge", createArgs...)
222+
deployCmd.Dir = dir
223+
deployOut, deployErr := deployCmd.CombinedOutput()
224+
deployStr := string(deployOut)
225+
if deployErr != nil || !strings.Contains(deployStr, "Deployed to:") {
226+
fmt.Fprintf(os.Stderr, "FAIL deploy\n%s\n", deployStr)
205227
return 1
206228
}
207229

208-
// Extract address
209-
for _, line := range strings.Split(string(out), "\n") {
230+
for _, line := range strings.Split(deployStr, "\n") {
210231
if strings.Contains(line, "Deployed to:") {
211-
fmt.Printf("● deploy: %s\n", strings.TrimSpace(line))
232+
parts := strings.Fields(line)
233+
if len(parts) >= 3 {
234+
fmt.Printf(" deploy %s\n", parts[len(parts)-1])
235+
}
212236
}
213237
}
214238

@@ -217,13 +241,15 @@ func runValidate(path string) int {
217241
}
218242

219243
func runStep(dir, label, name string, args ...string) bool {
220-
fmt.Printf("● %s\n", label)
244+
fmt.Printf(" %-8s ", label)
221245
cmd := exec.Command(name, args...)
222246
cmd.Dir = dir
223247
out, err := cmd.CombinedOutput()
224248
if err != nil {
225-
fmt.Fprintf(os.Stderr, "FAIL: %s\n%s\n", label, out)
249+
fmt.Printf("FAIL\n")
250+
fmt.Fprintf(os.Stderr, "%s\n", out)
226251
return false
227252
}
253+
fmt.Printf("ok\n")
228254
return true
229255
}

examples/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# .btw DSL Reference
2+
3+
The `.btw` (bitwrap) DSL describes Petri net models that compile to Solidity smart contracts.
4+
5+
## Quick Start
6+
7+
```bash
8+
# Validate a .btw file (parse → Solidity → compile → test → deploy)
9+
./bitwrap -validate examples/erc20.btw
10+
11+
# Just compile to JSON schema
12+
./bitwrap -compile examples/erc20.btw
13+
```
14+
15+
## Syntax
16+
17+
### Schema Declaration
18+
19+
```
20+
schema MyToken {
21+
version "1.0.0"
22+
domain "custody" # optional
23+
asset "erc20" # optional
24+
25+
# ... registers, events, functions
26+
}
27+
```
28+
29+
### Registers (State Variables)
30+
31+
Registers declare the contract's state. Scalar types become `uint256` storage. Map types become Solidity `mapping(...)` storage.
32+
33+
```
34+
register BALANCES map[address]uint256 observable
35+
register ALLOWANCES map[address]map[address]uint256 observable
36+
register TOTAL_SUPPLY uint256
37+
register NEXT_ID uint256
38+
```
39+
40+
- `observable` makes the variable `public` in Solidity (auto-generates a getter)
41+
- Supported types: `uint256`, `address`, `bool`, `map[keyType]valueType`
42+
- Nested maps: `map[address]map[address]uint256` for allowance-style patterns
43+
44+
### Events
45+
46+
```
47+
event TransferEvent {
48+
from: address indexed
49+
to: address indexed
50+
amount: uint256
51+
}
52+
```
53+
54+
Events map to Solidity `event` declarations. `indexed` fields become indexed event parameters.
55+
56+
### Functions (Actions)
57+
58+
Functions are the transitions in the Petri net. They consume tokens from input places and produce tokens at output places.
59+
60+
```
61+
fn(transfer) {
62+
var from address
63+
var to address
64+
var amount amount
65+
66+
require(BALANCES[from] >= amount && to != address(0))
67+
@event TransferEvent
68+
69+
BALANCES[from] -|amount|> transfer # input arc: consume from BALANCES[from]
70+
transfer -|amount|> BALANCES[to] # output arc: produce at BALANCES[to]
71+
}
72+
```
73+
74+
#### Variables
75+
76+
`var name type` declares a function parameter. The type `amount` is shorthand for `uint256`.
77+
78+
#### Guards
79+
80+
`require(expr)` adds a Solidity `require(...)` statement. The expression can reference registers, variables, and `caller` (maps to `msg.sender`).
81+
82+
#### Event Reference
83+
84+
`@event EventName` links the function to an event. The event is emitted when the function executes.
85+
86+
#### Arcs
87+
88+
Arcs define token flow using the syntax: `SOURCE -|WEIGHT|> TARGET`
89+
90+
- **Input arc** (consume): `REGISTER[key] -|weight|> functionName`
91+
- **Output arc** (produce): `functionName -|weight|> REGISTER[key]`
92+
- **Nested keys**: `ALLOWANCES[owner][spender] -|amount|> approve`
93+
- **Literal weight**: `transfer -|1|> BALANCES[to]` (for NFTs — always moves exactly 1)
94+
95+
When the weight is a variable name (like `amount`), it becomes a function parameter.
96+
When the weight is a number (like `1`), it's a literal.
97+
98+
## Examples
99+
100+
| File | Pattern | Description |
101+
|------|---------|-------------|
102+
| `counter.btw` | Scalar state | Simple increment/decrement counter |
103+
| `erc20.btw` | Fungible token | ERC-20 with transfer, approve, transferFrom, mint, burn |
104+
| `nft.btw` | Non-fungible token | Mint and transfer with ownership tracking |
105+
| `escrow.btw` | Multi-step workflow | Deposit → lock → release escrow pattern |
106+
107+
## What Gets Generated
108+
109+
For each `.btw` file, `bitwrap -validate` generates:
110+
111+
1. **Contract** (`src/Name.sol`) — Solidity contract with state, functions, events, access control
112+
2. **Tests** (`test/NameTest.t.sol`) — Foundry tests for each function and guard
113+
3. **Deploy script** (`script/NameGenesis.s.sol`) — Foundry deployment script
114+
115+
The contract includes:
116+
- `contractOwner` with `onlyOwner` modifier for privileged functions (mint, etc.)
117+
- `transferOwnership` and `renounceOwnership` admin functions
118+
- View functions for exported registers (`balanceOf`, `allowance`, etc.)
119+
- Epoch counter and event sequencing for on-chain ordering
120+
121+
## Limitations
122+
123+
- `caller` in guards maps to `msg.sender` but can't be used as an arc index yet
124+
- No array/batch operations (ERC-1155 batch patterns need manual extension)
125+
- Struct types (VestingSchedule) are supported via templates but not yet in DSL
126+
- Generated tests handle common patterns; complex multi-step workflows may need manual test setup

examples/counter.btw

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
schema Counter {
2+
version "1.0.0"
3+
4+
register COUNT uint256 observable
5+
6+
fn(increment) {
7+
var amount amount
8+
increment -|amount|> COUNT
9+
}
10+
11+
fn(decrement) {
12+
var amount amount
13+
require(COUNT >= amount)
14+
COUNT -|amount|> decrement
15+
}
16+
}

examples/escrow.btw

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
schema Escrow {
2+
version "1.0.0"
3+
domain "payments"
4+
5+
register DEPOSITS map[address]uint256 observable
6+
register LOCKED map[address]uint256
7+
register TOTAL_LOCKED uint256
8+
9+
event DepositEvent {
10+
from: address indexed
11+
amount: uint256
12+
}
13+
14+
event LockEvent {
15+
from: address indexed
16+
amount: uint256
17+
}
18+
19+
event ReleaseEvent {
20+
to: address indexed
21+
amount: uint256
22+
}
23+
24+
fn(deposit) {
25+
var from address
26+
var amount amount
27+
28+
require(amount > 0)
29+
@event DepositEvent
30+
31+
deposit -|amount|> DEPOSITS[from]
32+
}
33+
34+
fn(lock) {
35+
var from address
36+
var amount amount
37+
38+
require(DEPOSITS[from] >= amount)
39+
@event LockEvent
40+
41+
DEPOSITS[from] -|amount|> lock
42+
lock -|amount|> LOCKED[from]
43+
lock -|amount|> TOTAL_LOCKED
44+
}
45+
46+
fn(release) {
47+
var to address
48+
var amount amount
49+
50+
require(LOCKED[to] >= amount)
51+
@event ReleaseEvent
52+
53+
LOCKED[to] -|amount|> release
54+
TOTAL_LOCKED -|amount|> release
55+
release -|amount|> DEPOSITS[to]
56+
}
57+
}

0 commit comments

Comments
 (0)