You don't have to box each individual field. You can eg. define a BinaryExpr struct that contains two Expressions directly, and only box that when necessary.
But in general, you can't build arbitrary, dynamic, recursive trees without indirection (and managing ownership properly likely means that this kind of indirection will always involve heap allocation).
The most expensive parts of compilers usually aren't the AST traversal. Reasonably-sized code bases (a couple 1000s or tens of 1000s of lines) can be parsed and traversed in a few dozen milliseconds on modern hardware. The AST won't be the bottleneck. (Just like traversing the DOM is usually not the bottleneck while rendering a web site, even though HTML pages usually have much more DOM nodes in general than AST nodes in a program).
Slowness kicks in when you start performing sophisticated analyses on the code and/or want to optimize it. For example, lexical and syntactic analysis of Rust is a tiny fraction of the compiler's running time. Most of the time is spent in type checking by rustc and then optimizing the emitted IR in LLVM. Oh, and yeah, linking, if that weren't surprising enough.
This halves the number of references, but is there an easy way to significantly reduce it? If there are many expressions of different length, it seems optimal to allocate blocks of size 2^N to balance lookup cost against allocation space (never implementing more than 2x memory required).
Doing pointer arithmetic into a single block is not more expensive than doing pointer arithmetic into individually-allocated nodes. If you want to amortize the cost of allocations, at the expense of having to keep the whole tree owned at all times, then use an arena.
It's probably not worth worrying about at this point in time. Or at all, really.
The best AST design I've come across so far is un-typed syntax trees in the style of rust-analyzer.
It's a bit hard to describe, so I'll link to some references...
Matklad, the original author of rust-analyzer created an amazing tutorial on implementing a production-grade resilient parser. 10/10 would recommend.
Here is a theoretical explanation of how un-typed syntax trees work:
If you are more visually inclined, here is the first stream he made when explaining how rust-analyzer deals with syntax. That, and the next 3 or 4 videos are all focused on different parts of the syntax tree representation, parser, and parser test suite.
In my opinion, anyway. Things I care about are
Simplicity and maintainability of the parser
Resilience and the ability to report multiple syntax errors
Retaining all information from the source code so you can do things like programmatically read comments
Structural sharing or reference-counting for nodes (makes cloning trivial, which lets you avoid contaminating code with lifetime annotations)
Sorry I meant, how can you do it other than the ways we both said? We're both using O(...) the same number of boxes. It would be great to dump a simple formula on the stack and then work with it there rather than referencing the heap a lot.
These answers have all been incredibly helpful, thank you.