I was just working on some early work on the STG->IL compiler I mentioned last week. Perhaps my brain is just shifting a little too much into functional mode, but I designed my entire AST to avoid any fields for almost all nodes. Things that would ordinarily call for fields are just pairs (or higher order combinations of pairs, such as triples and lists). I don't know if this is good, bad, or whatever, but it's certainly interesting.
For example, given the nonterminals:
prog : binds
binds : var1 = lf1; ...; varn = lfn (where n >= 1)
lf : varsa \u varsb -> expr
Well, prog just derives from binds. Binds is simply a list of pairs of vars and lfs (lambda forms). Vars are just symbols (which themselves are just strings, but alas I can't derive from string in the Framework since it's sealed, so here I do need a field unless I choose to just use List<char>), and lambda forms are just quintuples of other stuff. All of these use a marker interface IAstNode to indicate that they're an AST node rather than cluttering up their inheritence hierarchy.
For example:
interface IAstNode {}
class Program : IAstNode, BindingList {}
class BindingList : IAstNode, List<Binding> {}
class Binding : Pair<Variable, LambdaForm> {}
class Quintuple<A,B,C,D> : Pair<A,Pair<B,Pair<C,D>>>
class LambdaForm : Quintuple<VariableList, UpdateFlag, VariableList, Expression> {}
...and so on...
Why do I find this so much more beautiful than the dirty, messy, more verbose field-based approach? I mean, everybody always talks about the is-a thing when preaching OOP... But if a program is-a list of bindings, and a binding is-a blah why not reflect that in the type relationships?
Anyhow, this isn't thought through too much. But it certainly seems clearer. For example, we do a lot of work in the framework to make strings seem like lists of characters... Why fake it? E.g. class String : List<char> { /*string-specific functions*/ }. Surely string is conceptually closer to having a list of characters being its direct supertype than plain old vanilla object. (Although on second thought the realities of performance and such probably limit the extent to which we could do this.) I guess it's the old aggregation versus inheritence argument. Blegh.
But it's really nice that I can program like this if I wish. Being able to compose powerful abstractions through nothing but inheritence and polymorphism is a great feature of and a testament to the CTS.
BTW: How did we all survive w/out generics pre-2.0?