Indeed, homoiconicity is a very powerful thing. It doesn't have to be core to the nature of the language, though; as far as I know, any Turing-equivalent language readily admits a metacircular interpreter, and so really a homoiconic language is a language with a compiler in the standard library. As a thought experiment, imagine Lisp without macros. It's not hard; after all, "The Little Schemer" covers metacircular in…
Early on in my Scheme career, I found the tools to create macros a bit confusing and arcane .. but I still knew I wanted macros.
I ended up writing code transformers - a poor man's macro system if you will, taking my "high level" foo.scm through a couple of translation layers that turned the abstractions I wanted into running code. It was literally:
$ scheme expand-foo.scm myprog1.scm
$ scheme expand-bar.scm myprog2.scm
$ scheme myprog2.scm
What made this possible - trivial - was the homoiconicity: simply by (read)ing a program from stdin I had a list of lists that I could pattern match over and make the transformations I wanted. Exactly as my final program did with ordinary data.In some ways, this was a more satisfying approach than using define-syntax / syntax-case, which differ from the rest of Scheme in somewhat uncomfortable ways. That macros could never be first class eventually put me off, but that's another story :).