This would require an unbounded number of possible extensions (because there is always something else we could add to Expr, like Sub or Paren, or whatever else). Hmm, unbounded number of possiible Expr, why does that sound familiar?
This sounds like a job for traits!
pub trait Expr {}
struct Var(String);
struct Int(i64);
struct Add(Rc<dyn Expr>, Rc<dyn Expr>);
struct Mul(Rc<dyn Expr>, Rc<dyn Expr>);
impl Expr for Var {}
impl Expr for Int {}
impl Expr for Add {}
impl Expr for Mul {}
// later
struct Pi;
impl Expr for Pi {}
This was not mentioned as a requirement in the original question, but now we lose the ability to do match / de-structuring, and possibly other useful things.
Yes, but you also want an unbounded number of variants. You can't have both pattern matching and an unknown number of variants.
It is an unbounded number of variants because we don't know how many possible extensions we are going to have. Also, you can get many of the uses of pattern matching by just using polymorphism. Just make whatever behaviors that need to be customized based on the type a method.
This retains pattern matching but, of course, the original type has to plan to be extended.
This overall problem is the topic of a paper from the Haskell world called Trees that Grow. That paper also provides solutions for adding fields after the fact (using, you guessed it, another type parameter). It is not the most readable paper, unfortunately, particularly if you don't speak Haskell.