doc: update grammar spec and AGENTS.md for v2 design decisions
- policyDecl: replace verbose on{hook,table,priority} block with
compact `hook <Hook> [priority <P>]` syntax; table is inferred
from hook, priority defaults to canonical value for that hook
- Add portforwardDecl and masqueradeDecl top-level declarations
- Add implicit injection rules for stateful/loopback/ndp to
compiler behaviour section (MVP; importable builtins deferred)
- Remove nat_prerouting / nat_postrouting from canonical policy
example (replaced by portforward/masquerade declarations)
- Update reserved keywords: add portforward, masquerade, hook (was
already reserved), priority (was already reserved); remove table
as a reserved word since it no longer appears in policyDecl
- AGENTS.md: update architecture notes, reserved-words rule, and
boundaries to reflect new declarations and compiler synthesis
This commit is contained in:
105
AGENTS.md
105
AGENTS.md
@@ -10,9 +10,9 @@ Stack: GHC 9.10.3, Cabal, Parsec 3.x, Aeson 2.x, Tasty/HUnit for tests.
|
||||
```bash
|
||||
cabal build # build everything
|
||||
cabal test # run all test suites
|
||||
cabal run fwlc -- check examples/router.fwl # parse + type-check a source file
|
||||
cabal run fwlc -- compile examples/router.fwl # emit nftables JSON to stdout
|
||||
cabal run fwlc -- pretty examples/router.fwl # pretty-print the parsed AST
|
||||
cabal run fwlc -- check examples/simple-router.fwl # parse + type-check a source file
|
||||
cabal run fwlc -- compile examples/simple-router.fwl # emit nftables JSON to stdout
|
||||
cabal run fwlc -- pretty examples/simple-router.fwl # pretty-print the parsed AST
|
||||
```
|
||||
|
||||
Run tests before marking any task complete. The test suite is `cabal test`.
|
||||
@@ -25,22 +25,24 @@ Run tests before marking any task complete. The test suite is `cabal test`.
|
||||
fwl/
|
||||
├── AGENTS.md
|
||||
├── doc/
|
||||
│ ├── proposal.md ← initial design document and exploration
|
||||
│ ├── fwl_grammar.md ← authoritative grammar reference; keep in sync with Parser.hs
|
||||
│ ├── proposal.md <- initial design document and exploration
|
||||
│ ├── fwl_grammar.md <- authoritative grammar reference; keep in sync with Parser.hs
|
||||
│ └── ref/
|
||||
│ ├── ruleset.nft ← example nftables ruleset
|
||||
│ └── ruleset.json ← the same example nftables ruleset in json format
|
||||
│ ├── ruleset.nft <- example nftables ruleset
|
||||
│ └── ruleset.json <- the same example nftables ruleset in json format
|
||||
├── examples/
|
||||
│ └── router.fwl ← canonical example; must parse and compile cleanly
|
||||
│ ├── simple-router.fwl <- canonical simple example; must parse and compile cleanly
|
||||
│ ├── simple-router.nft <- compiled nftables text output of simple-router.fwl
|
||||
│ └── router.fwl <- full router example with WireGuard detection
|
||||
├── src/FWL/
|
||||
│ ├── AST.hs ← all data types; source of truth for the AST
|
||||
│ ├── Lexer.hs ← Parsec TokenParser, reservedNames, reservedOpNames
|
||||
│ ├── Parser.hs ← top-level parser, all sub-parsers
|
||||
│ ├── Pretty.hs ← AST → FWL source (round-trip printer)
|
||||
│ ├── TypeCheck.hs ← effect row checker, exhaustiveness, CIDR intervals
|
||||
│ ├── Interpret.hs ← evaluator + effect dispatch
|
||||
│ ├── Compile.hs ← AST → nftables JSON (Aeson Value)
|
||||
│ └── Util.hs ← shared helpers
|
||||
│ ├── AST.hs <- all data types; source of truth for the AST
|
||||
│ ├── Lexer.hs <- Parsec TokenParser, reservedNames, reservedOpNames
|
||||
│ ├── Parser.hs <- top-level parser, all sub-parsers
|
||||
│ ├── Pretty.hs <- AST -> FWL source (round-trip printer)
|
||||
│ ├── TypeCheck.hs <- effect row checker, exhaustiveness, CIDR intervals
|
||||
│ ├── Interpret.hs <- evaluator + effect dispatch
|
||||
│ ├── Compile.hs <- AST -> nftables JSON (Aeson Value)
|
||||
│ └── Util.hs <- shared helpers
|
||||
└── test/
|
||||
├── Main.hs
|
||||
├── ParserTests.hs
|
||||
@@ -48,7 +50,7 @@ fwl/
|
||||
└── CompileTests.hs
|
||||
```
|
||||
|
||||
The grammar document at `docs/grammar.md` must stay in sync with `Parser.hs` and `Lexer.hs`.
|
||||
The grammar document at `doc/fwl_grammar.md` must stay in sync with `Parser.hs` and `Lexer.hs`.
|
||||
When changing the parser, update the grammar doc in the same commit.
|
||||
|
||||
---
|
||||
@@ -59,15 +61,29 @@ The pipeline is strictly linear with no back-edges:
|
||||
|
||||
```
|
||||
source text
|
||||
→ Lexer (Text.Parsec.Token)
|
||||
→ Parser → [Decl] (AST.hs)
|
||||
→ TypeCheck → TypedDecl
|
||||
→ Compile → Aeson Value (nftables JSON)
|
||||
-> Lexer (Text.Parsec.Token)
|
||||
-> Parser -> [Decl] (AST.hs)
|
||||
-> TypeCheck -> TypedDecl
|
||||
-> Compile -> Aeson Value (nftables JSON)
|
||||
```
|
||||
|
||||
The interpreter (`Interpret.hs`) runs the policy against a mock packet environment
|
||||
and is separate from the compiler. It uses the same typed AST.
|
||||
|
||||
### Compiler Synthesis
|
||||
|
||||
These constructs are synthesised by `Compile.hs` and do not appear directly in the
|
||||
nftables output as user-written rules:
|
||||
|
||||
| FWL construct | Synthesised nftables output |
|
||||
|----------------------|-----------------------------|
|
||||
| `portforward` decl | A named `map`, a `nat hook prerouting priority dstnat` chain with `fib daddr type local` guard and `dnat ip to ... map` rewrite, and a `ct status dnat accept` rule injected into every `Forward` chain in the file. |
|
||||
| `masquerade` decl | A `nat hook postrouting priority srcnat` chain with `ip saddr @set masquerade` rule. |
|
||||
| Filter hook policy | `ct state { established, related } accept` (stateful), `iifname "lo" accept` (loopback), and `meta nfproto ipv6 ip6 nexthdr ipv6-icmp ip6 saddr fe80::/10 accept` (NDP) are prepended automatically to every `Input`, `Forward`, and `Output` chain before user-written rules. |
|
||||
|
||||
These injections are intentional and documented in `doc/fwl_grammar.md`. Do not remove
|
||||
them without updating the grammar document and all affected tests.
|
||||
|
||||
---
|
||||
|
||||
## Reserved Words Rule
|
||||
@@ -84,6 +100,42 @@ and expression positions without causing parse errors.
|
||||
If you add a new keyword: add it to both `reservedNames` in `Lexer.hs`
|
||||
AND use `reserved "word"` in `Parser.hs`. Never add a word to only one place.
|
||||
|
||||
**Current reserved keywords:**
|
||||
```
|
||||
config interface zone import from
|
||||
let in pattern flow rule policy on
|
||||
case of if then else do perform
|
||||
within as dynamic cidr4 cidr6
|
||||
hook priority
|
||||
portforward masquerade
|
||||
WAN LAN WireGuard
|
||||
Input Forward Output Prerouting Postrouting
|
||||
Filter NAT Mangle DstNat SrcNat
|
||||
Raw ConnTrack
|
||||
true false
|
||||
```
|
||||
|
||||
> `table` is **not** a reserved keyword — it was removed when `policyDecl` switched
|
||||
> from the verbose `on { hook = ..., table = ..., priority = ... }` syntax to the
|
||||
> compact `hook <Hook> [priority <P>]` form.
|
||||
|
||||
---
|
||||
|
||||
## Policy Hook Syntax
|
||||
|
||||
The `on { hook = ..., table = ..., priority = ... }` block is gone.
|
||||
Policies now use:
|
||||
|
||||
```fwl
|
||||
policy name : Frame hook Input = { ... };
|
||||
-- or with a non-default priority:
|
||||
policy name : Frame hook Prerouting priority Mangle = { ... };
|
||||
```
|
||||
|
||||
The table is inferred from the hook; the priority defaults to the canonical
|
||||
value for that hook. See `doc/fwl_grammar.md` → *Policy Declaration* for the
|
||||
full hook-to-table-to-priority mapping.
|
||||
|
||||
---
|
||||
|
||||
## IP Address Representation
|
||||
@@ -132,6 +184,8 @@ never a string. Do not use the old `priorityStr` function (deleted).
|
||||
not `literal` — the base `literal` parser does not handle `:N` syntax.
|
||||
- `Frame` and `FlowPattern` are NOT in `reservedNames`; they appear as type
|
||||
names and must be accepted by `identifier`.
|
||||
- `portforward` and `masquerade` are in `reservedNames`; their parsers
|
||||
(`portforwardDeclP`, `masqueradeDeclP`) must use `reserved` for these words.
|
||||
|
||||
---
|
||||
|
||||
@@ -143,6 +197,8 @@ never a string. Do not use the old `priorityStr` function (deleted).
|
||||
`LIPv4 (a,b,c,d)` tuple constructors.
|
||||
- Priority assertions use `Priority n` directly, e.g. `Priority 0`, `Priority (-100)`.
|
||||
- All parse tests must compile and pass before any PR is merged.
|
||||
- `CompileTests.hs` must include tests for `portforward` and `masquerade` synthesis
|
||||
(synthesised chain names, injected `ct status dnat accept`, injected stateful/loopback/ndp rules).
|
||||
|
||||
---
|
||||
|
||||
@@ -151,16 +207,19 @@ never a string. Do not use the old `priorityStr` function (deleted).
|
||||
### ✅ Safe to do without asking
|
||||
- Read any file, list directories
|
||||
- Run `cabal build`, `cabal test`, `cabal run fwlc`
|
||||
- Edit `src/`, `test/`, `examples/`, `docs/`
|
||||
- Edit `src/`, `test/`, `examples/`, `doc/`
|
||||
- Add new test cases to existing test files
|
||||
|
||||
### ⚠️ Ask first
|
||||
- Add or remove Cabal dependencies (`fwl.cabal`)
|
||||
- Rename or delete source modules
|
||||
- Change the nftables JSON schema emitted by `Compile.hs`
|
||||
- Modify `examples/router.fwl` in ways that change its semantics
|
||||
- Modify `examples/simple-router.fwl` or `examples/router.fwl` in ways that change their semantics
|
||||
- Add new compiler-injected rules (stateful, loopback, ndp, or new ones)
|
||||
|
||||
### 🚫 Never
|
||||
- Add semantic value names (`Allow`, `Drop`, `Log`, etc.) to `reservedNames`
|
||||
- Add `table` to `reservedNames` — it is not a keyword in the current grammar
|
||||
- Break the `cabal test` suite
|
||||
- Emit nftables `"prio"` as a string — it must always be an integer
|
||||
- Remove the implicit stateful/loopback/ndp injections from filter-hook chain compilation without updating the grammar doc and all tests
|
||||
|
||||
Reference in New Issue
Block a user