You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(ast): add owned visitor, fold, and printer API for lifetime-free AST
Adds complete owned-AST infrastructure so users of `parse()` can traverse,
transform, and print results without touching the arena or managing lifetimes:
- `php_ast::owned::visitor` — `OwnedVisitor`, `OwnedScopeVisitor`,
`OwnedScopeWalker`, and `walk_owned_*` free functions mirroring the
arena visitor
- `php_ast::owned::fold` — `FoldOwned` trait and `fold_owned_*` free
functions; identity defaults rebuild the tree identically; includes
`fold_closure_use_var` hook that was silently bypassed before
- `php_ast::owned` re-exports all `fold_owned_*` and `walk_owned_*`
symbols so callers need only `use php_ast::owned::*`
- `php_printer` — `pretty_print_owned`, `pretty_print_owned_file`,
`pretty_print_owned_with_config`, `pretty_print_owned_with_comments`,
and `pretty_print_owned_with_comments_and_config`; convert owned→arena
via a short-lived bump arena internally
- `php-parser` `ParserContext` — `reparse_owned` and
`reparse_owned_versioned` return lifetime-free `ParseResult`
- Splits `crates/php-ast/src/owned.rs` into a submodule directory
(`owned/mod.rs`, `owned/visitor.rs`, `owned/fold.rs`)
- README rewrite: `parse()` is now the default entry point; owned
visitor/fold/printer examples precede arena variants; "when to use
arena vs. owned" decision table added
@@ -9,13 +9,11 @@ A fast, fault-tolerant PHP parser written in Rust. Produces a full typed AST wit
9
9
```toml
10
10
[dependencies]
11
11
php-rs-parser = "*"
12
-
php-ast = "*"# AST types and Visitor trait
13
-
14
-
# Needed when using parse_arena(), the Visitor/Fold traits, or the printer
15
-
bumpalo = "*"
12
+
php-ast = "*"# AST types and visitor/fold traits
16
13
17
14
# Optional
18
15
php-printer = "*"# pretty-print AST back to PHP source
16
+
bumpalo = "*"# only needed when using parse_arena() directly
19
17
```
20
18
21
19
## Quick Start
@@ -41,8 +39,11 @@ let pos = result.source_map.offset_to_line_col(6);
41
39
42
40
-**`parse()` / `parse_versioned()`** — main entry points; return a fully-owned `ParseResult` with no lifetime parameters. See [`docs.rs/php-rs-parser`](https://docs.rs/php-rs-parser)
43
41
-**`parse_arena()` / `parse_arena_versioned()`** — arena-allocated variants for LSP servers and hot paths; return `ArenaParseResult<'arena, 'src>`
44
-
-**`ParserContext`** — reusable context for repeated re-parses; wraps the arena API
45
-
-**`Visitor` / `ScopeVisitor`** — AST traversal traits; see [`docs.rs/php-ast`](https://docs.rs/php-ast) for the visitor infrastructure
42
+
-**`ParserContext`** — reusable context for repeated re-parses; `reparse_owned()` returns a lifetime-free `ParseResult`, `reparse()` returns the arena form
43
+
-**`OwnedVisitor` / `OwnedScopeVisitor`** — traverse a `ParseResult` AST with no arena involved; see `php_ast::owned::visitor`
44
+
-**`FoldOwned`** — transform a `ParseResult` AST into a new owned AST; see `php_ast::owned::fold`
45
+
-**`Visitor` / `ScopeVisitor`** — arena-form traversal traits; see [`docs.rs/php-ast`](https://docs.rs/php-ast)
46
+
-**`Fold`** — arena-form transformation trait; reads one arena, writes another
46
47
-**`ParseError` variants** — see [`crates/php-parser/src/diagnostics.rs`](crates/php-parser/src/diagnostics.rs) for all variants and recovery behavior
47
48
-**AST node types** — see [`docs.rs/php-ast/ast`](https://docs.rs/php-ast/latest/php_ast/ast/index.html) for the full set of statement, expression, and declaration nodes
48
49
@@ -104,11 +105,22 @@ assert!(!result.program.stmts.is_empty()); // AST still produced
104
105
105
106
### Re-parsing (LSP / editor use)
106
107
107
-
Use `ParserContext` when parsing the same document repeatedly (e.g. on every keystroke). It reuses the backing arena memory in O(1), avoiding allocator churn. The returned `ArenaParseResult` borrows from the context's arena — drop it before the next re-parse:
108
+
Use `ParserContext` when parsing the same document repeatedly (e.g. on every keystroke). It reuses the backing arena memory in O(1), avoiding allocator churn.
109
+
110
+
`reparse_owned()` returns a fully-owned `ParseResult` — no lifetimes, no `drop` discipline required:
108
111
109
112
```rust
110
113
letmutctx=php_rs_parser::ParserContext::new();
111
114
115
+
leta=ctx.reparse_owned("<?php echo 1;");
116
+
letb=ctx.reparse_owned("<?php echo 2;"); // a can stay alive
117
+
assert!(a.errors.is_empty());
118
+
assert!(b.errors.is_empty());
119
+
```
120
+
121
+
`reparse()` returns an `ArenaParseResult` that borrows from the context arena. The borrow checker prevents calling `reparse` again while that result is alive — drop it first:
122
+
123
+
```rust
112
124
letresult=ctx.reparse("<?php echo 1;");
113
125
assert!(result.errors.is_empty());
114
126
drop(result); // must be dropped before the next reparse
@@ -117,22 +129,73 @@ let result = ctx.reparse("<?php echo 2;");
117
129
assert!(result.errors.is_empty());
118
130
```
119
131
120
-
`reparse_versioned`is also available for targeting a specific PHP version.
132
+
`reparse_versioned`and `reparse_owned_versioned` are also available for targeting a specific PHP version.
121
133
122
-
### Arena API
134
+
### Visitor API (owned)
123
135
124
-
When integrating with the printer or visitor (which take arena-allocated types), use `parse_arena` directly:
136
+
`OwnedVisitor` works directly on a `ParseResult` — no arena, no lifetime parameters. Override only the node types you care about:
Return `ControlFlow::Break(())` to stop traversal early. Return `ControlFlow::Continue(())` without calling `walk_owned_*` to skip a subtree.
161
+
162
+
#### Scope-aware owned traversal
134
163
135
-
Implement `Visitor` to walk the AST depth-first. Override only the node types you care about; the default implementations recurse into children automatically.
164
+
Use `OwnedScopeVisitor` + `OwnedScopeWalker` when you need to know **which namespace, class, or function** you are currently inside:
Return `ControlFlow::Break(())` to stop traversal early. Return `ControlFlow::Continue(())` without calling `walk_*` to skip a subtree.
155
-
156
-
### Scope-aware traversal
157
-
158
-
Use `ScopeVisitor` + `ScopeWalker` when your visitor needs to know **which namespace, class, or function** it is currently inside. Every visit method receives a `Scope` with that context:
217
+
Use `ScopeVisitor` + `ScopeWalker` for scope-aware arena traversal:
`Fold` is the transformation counterpart of `Visitor`. Where `Visitor` reads a tree in place, `Fold` rebuilds it — reading from an input arena and writing into a new output arena. This is the correct design for arena-allocated ASTs: in-place mutation would break the arena lifetime invariant.
258
+
structNegateInts;
196
259
197
-
Implement `Fold` and override only the node types you want to change. All other nodes are rebuilt identically by the default implementations:
// transformed is a new owned::Program with all integers negated
272
+
```
273
+
274
+
### AST transformation — Fold (arena)
275
+
276
+
`Fold` is the arena-form transformation trait. It reads from one arena and writes into a new output arena — the correct design for arena-allocated ASTs where in-place mutation would break lifetime invariants:
198
277
199
278
```rust
200
279
usebumpalo::Bump;
@@ -236,18 +315,29 @@ for param in find_tags(&doc, "param") {
236
315
237
316
### Pretty printer
238
317
318
+
`pretty_print_owned` works directly on a `ParseResult` — no arena needed:
Use `pretty_print_owned_file` to append a trailing newline. Use `pretty_print_owned_with_config` for custom indentation.
327
+
328
+
When using the arena API (e.g. inside an LSP handler that already holds an `ArenaParseResult`), use the arena-form functions directly to avoid an extra conversion:
|**php-lexer**|[](https://crates.io/crates/php-lexer)| Hand-written tokenizer with handling for strings, heredoc/nowdoc, and inline HTML |
|**php-printer**|[](https://crates.io/crates/php-printer)| Pretty printer — converts an AST back to PHP source |
366
+
|**php-printer**|[](https://crates.io/crates/php-printer)| Pretty printer — converts an AST back to PHP source; supports both arena and owned AST |
367
+
368
+
Source flows through `Lexer → Parser → arena-allocated AST nodes`. The lexer is lazy (tokens produced on demand with peeking slots); the parser is Pratt-based recursive descent with panic-mode error recovery. The owned AST (`php_ast::owned`) provides lifetime-free mirrors of every node type, enabling storage and manipulation without arena lifetime constraints.
369
+
370
+
**When to use the arena API vs. the owned API:**
277
371
278
-
Source flows through `Lexer → Parser → arena-allocated AST nodes`. The lexer is lazy (tokens produced on demand with peeking slots); the parser is Pratt-based recursive descent with panic-mode error recovery.
0 commit comments