Write a compiler. It gives you great insights into parsing/tokenizing, building syntax trees, implementing syntax and Symantec checks and code generation. Obviously, you would have to pick a toy language (typically a subset of a known programming language) and your generated code will be inefficient. Still, you would learn a lot.
I'd add writing compiler optimizations as well. Common subexpression elimination, dead code-elimination, copy propagation, register allocation to name a few.
The old chestnut, but valid: Learn, and get comfortable in, a functional language. The typical case being Lisp (often in conjunction with a readthrough of SICP), but if you have the stomach for it, Haskell (or languages of its lineage- ML, perhaps OCaml or Scala) offers the same functional "enlightenment", and the mysteries of strong type systems besides.
Rich Hickey's talks are top-notch and a great introduction to the pillars of functional programming.
One thing notably absent from those talks (and Clojure itself) is the power of a good type system. I think Elm is a good introduction to that, as well as a solid FP language with good docs and a helpful compiler, and putting the "transformation over stream of inputs" at the forefront of your program design. Looking into other FRP talks (specifically I think Netflix has one or more good ones) is a good idea as well.
Parse natural language (e.g. English) toward understanding it and responding. You will learn a great deal about ambiguity, the tradeoff between elegance/verbosity and the basics of information (sufficiency, necessity, ambiguity and excess). You'll also better appreciate the language itself, its basic patterns and the value and limits of grammar.
This will reveal the inevitability of verbal interfaces in computing devices (esp. mobile) and the difficulties therein.
It's then worth extending the effort another step, to introduce yourself to the statistical and numerical techniques used by natural language processing pros to keep up with the explosion in language in modern media: personal, social, and mass.
Write a program with another peogrammer. Do it as many times as possible with as many people. Software is built in the mind. Understand other minds and you will understand software better.
Ask why and understand the reasons for a feature so well that when you explain it back to the business user requesting it, they nod, say "exactly" and their eyes sparkle.
Then change how the feature works, or eliminate it, or address the business reasons using a completely different implementation and sell it to the same person in a way that has them enthusiastically agree and sign off on it.
Supporting and extending other peoples code, however old the code is. Greenfield projects with new libraries and frameworks get all the limelight, yet you will learn a lot more by developing and supporting existing software in an organisation.
1. Use your application. (Not the same as testing.)
2. Adopt an apprentice programmer, teach them your seat, pass on the seat. Leave the seat.
3. Program something for fun that is your own idea.
4. Program something for someone else that is their idea.
5. Sort the books/library.
6. No books/library? Build books/library.
7. Re-write the code from scratch, for the hell of it.
8. Take over someone elses' project, finish it to the end users satisfaction.
9. Give your project to someone else, in a state that they can do #8 easily.
10. Build a list of positive observations over a period of X, where X is how long you think it might be fun to do so.
The accomplishment of a number of these actions have, in my opinion, resulted in some really great programmers and some really great software.