|
| 1 | +# Stateful If/Match Conditions |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +`if` and `match` conditions were `term` (pure F\* expressions). Hoisting only |
| 6 | +descends into `Tv_App` arguments, so stateful operations nested inside F\*-level |
| 7 | +`if`/`match` within a condition were invisible: |
| 8 | + |
| 9 | +``` |
| 10 | +if (if true then !r = 0 else false) { ... } // fails: !r buried in Tv_Match |
| 11 | +``` |
| 12 | + |
| 13 | +## Solution |
| 14 | + |
| 15 | +Change `Tm_If.b` and `Tm_Match.sc` from `term` to `st_term`, following the |
| 16 | +existing `Tm_While.condition` pattern. A token-stream preprocessor makes |
| 17 | +parentheses optional for backward compatibility. |
| 18 | + |
| 19 | +## Files Changed |
| 20 | + |
| 21 | +### Parser & Sugar |
| 22 | + |
| 23 | +| File | Change | |
| 24 | +|------|--------| |
| 25 | +| `pulseparser.mly` — `ifStmt`, `matchStmt` | `IF LPAREN pulseStmt RPAREN`, `MATCH LPAREN pulseStmt RPAREN` | |
| 26 | +| `PulseSyntaxExtension_Parser.ml` — `make_auto_paren_lexer` | Token preprocessor: auto-inserts `LPAREN`/`RPAREN` when omitted. Detects `LBRACE` at depth 0 as end-of-condition. Passes through F\*-level `if/then` and `match/with` unmodified. | |
| 27 | +| `PulseSyntaxExtension.Sugar.fst` — `If.head`, `Match.head` | `A.term` → `stmt` | |
| 28 | +| `PulseSyntaxExtension.Desugar.fst` — `If`/`Match` cases | `desugar_term` → `desugar_stmt` | |
| 29 | +| `PulseSyntaxExtension.SyntaxWrapper.fsti` — `tm_if`, `tm_match` | Parameter type `term` → `st_term` | |
| 30 | +| `PulseSyntaxExtension_SyntaxWrapper.ml` — `tm_if`, `tm_match` | Wrap with `Tm_STApp`/return as needed | |
| 31 | + |
| 32 | +### AST & Naming |
| 33 | + |
| 34 | +| File | Change | |
| 35 | +|------|--------| |
| 36 | +| `Pulse.Syntax.Base.fsti` — `Tm_If.b`, `Tm_Match.sc` | `term` → `st_term` | |
| 37 | +| `Pulse.Syntax.Base.fst` — `eq_st_term'` | `eq_tm` → `eq_st_term` for `b`/`sc` | |
| 38 | +| `Pulse.Syntax.Naming.fsti` — `freevars_st'`, `ln_st'`, `subst_st_term'` | `*_term` → `*_st_term` for `b`/`sc` (follows `Tm_While.condition`) | |
| 39 | +| `Pulse.Syntax.Naming.fst` — `close_open_inverse_st'` | Same pattern | |
| 40 | +| `Pulse.Typing.FV.fst` — `freevars_close_st_term'` | Same pattern | |
| 41 | +| `Pulse.Syntax.Printer.fst` — `print_st_head` | `term_to_string b` → `st_term_to_string b` | |
| 42 | +| `Pulse.ElimGoto.fst` — `Tm_If`/`Tm_Match` cases | `elab_term` → `elab_st_term` for `b`/`sc` | |
| 43 | + |
| 44 | +### Checker |
| 45 | + |
| 46 | +| File | Function | Change | |
| 47 | +|------|----------|--------| |
| 48 | +| `Pulse.Checker.If.fst` | `check_if_term` (new) | Pure path: extracts `term` from `Tm_Return`, uses original `check_equiv_emp` logic | |
| 49 | +| | `check` | Dispatches: `Tm_Return` → `check_if_term`, other → checks `b` via `check g b ...`, composes with `compose_checker_result_t` | |
| 50 | +| `Pulse.Checker.Match.fst` | `check_match_term` (new) | Pure path: `compute_tot_term_type_and_u` on extracted term | |
| 51 | +| | `check` | Dispatches: `Tm_Return` → `check_match_term`, other → checks `sc` via `check g sc ...`, composes | |
| 52 | +| `Pulse.Checker.fst` | `maybe_elaborate_stateful_head` | Restored `Tm_If`/`Tm_Match` cases: extracts F\* term from `Tm_Return`, runs `hoist_stateful_apps`, rebuilds as `st_term` | |
| 53 | + |
| 54 | +### Extraction |
| 55 | + |
| 56 | +| File | Change | |
| 57 | +|------|--------| |
| 58 | +| `Pulse.Extract.Main.fst` — `Tm_If`, `Tm_Match` | Uniform `extract_dv g b` / `extract_dv g sc` — no `Tm_Return` special-case | |
| 59 | +| `Pulse_Extract_CompilerLib.ml` — `mk_if` | Binds monadic condition to fresh variable via `mk_let`, then branches on it | |
| 60 | + |
| 61 | +### Test |
| 62 | + |
| 63 | +| File | Purpose | |
| 64 | +|------|---------| |
| 65 | +| `test/StatefulIfCondition.fst` | Regression: `if !r = 0 { }`, `if (if true { !r = 0 } else { false }) { }`, `match Some 1 { }` | |
| 66 | + |
| 67 | +## Token Preprocessor Logic |
| 68 | + |
| 69 | +`make_auto_paren_lexer` wraps the base lexer. On `IF`/`MATCH`: |
| 70 | + |
| 71 | +1. If next token is `LPAREN` → pass through (already parenthesized) |
| 72 | +2. Otherwise, buffer tokens tracking `()`/`[]` nesting depth: |
| 73 | + - `LBRACE` at depth 0 → insert `LPAREN` before buffered tokens, `RPAREN` after |
| 74 | + - `RETURNS`/`ENSURES` at depth 0 → same (annotation before body) |
| 75 | + - `THEN`/`WITH` at depth 0 → F\*-level syntax, no modification |
0 commit comments