I've been mulling this over for awhile now. Eventually I'm going to have to actually do something...
What would the perfect language look like?
On the one hand, Paul Graham said something profound when he talked about the C model and the Lisp model. Roughly, C and Lisp both have at their core a small clean model. They're polar opposites, and everything that has tried to bridge the gap (C++, Java) has turned into a horrible morass (to greater and lesser extents).
The other really profound thing about software and programming is that it is evolutionary, and everything that succeeds does so to the extent that it is able to evolve. On the smallest scale, the evolution occurs only in our own minds; on the largest scales you can watch the movement of entire ecosystems. Programming is understanding, managing and modeling complexity, and always things of ever greater complexity. Our minds are only capable of so much; when you cease to be able to understand the entire problem at once, you must leverage evolution. This is why distributed SCMs have been so important - because more than any other single development (well, save the internet) their sole purpose is to make evolution more efficient.
What this means is that the perfect language must be infinitely malleable. Now, some would say we already have that in Lisp - and they might be right. But that only helps you when the problem is something a Lisp is suited to - which in practice is quite a lot, Lisp compilers have been worked on for a long time by brilliant people, but it is certainly not everything. The Linux kernel isn't written in C just for fun.
More importantly, the compiler itself is inevitably separated from the language it implements. This is important, because compilers are complicated things! More than that, the higher level your language, the more compiler you have to implement to get there - a useful Lisp compiler is never going to be as simple as a useful C compiler can be, even when both are self hosting. There's simply far, far more machinery required to implement Lisp.
Sure, once you've got your Lisp compiler you can take advantage of your powerful, expressive language to write its own compiler, but that isn't nearly as big a deal as you might think. There is still a vast amount of machinery used to implement the language you are writing in that's most likely hidden away - at best, you might be able to see it, but it's static. From one side of the divide - where you're using the compiler to write new things - this isn't usually such a big deal, if your language is sufficiently expressive you generally aren't missing out on much, functionally. For some problems it does make performance much more of an issue than it could be; in Lisp it is easy and common to implement other languages essentially with macros that turn the language you're implementing into Lisp. But the layering inevitably hurts performance; making it fast tends to add considerable complexity.
What probably suffers more is the compiler. The users of the compiler continually extend the language, but the compiler itself benefits little, if at all. Arguably the problem is not inherent, but so long as the compiler is a large, complex, monolithic entity it's difficult to see how it could be otherwise. What we want is to build the language up in layers, as independent as reasonable, the upper layers able to leverage everything below them to the greatest extent possible; this actually has been fairly well accomplished, but as long as there's a wall between the compiler and the compiled we'll miss out on the greatest benefits.
The way to solve both problems is to find a path between the C model and the Lisp model that doesn't pass through the murky swamps that most everything between them has come. This isn't a matter of coming up with a clean language that bridges the gap - heck, even that much has (arguably) been done.
No, what must be done is to start from C - or rather the C model - and build, piece by piece, until you get to the Lisp model. Not another compiler at all, but implementing functionality in the language until you have Lisp - and every language along the continuum. Nothing special or hidden away in new features, just ordinary features that you use like any other library. The end goal is to be able to start out in a C like language and do the equivalent of #include <lisp.h>.
The world has actually been moving towards this ideal, by inches; witness LLVM and all the many languages that can compile to C. Nobody is anywhere near the end goal, but different people are working at breaking things up and doing some sort of layering, and all because it makes things easier. But as long as the layers are different languages, you've still got big walls isolating essentially arbitrary parts of the stack. You might be able to directly call down into functionality at a lower layer, but it won't be as natural or as useful as making use of library code written in your own language.
But can it be done at all? Usefully and sanely? That really is the $64,000 question, but I'm slowly convincing myself it can be.
While the C model may be small, elegant and powerful, the syntax is much less so - C syntax almost actively works against implementing any sort of higher level semantic constructs. Some would say to just toss it out go with S-expressions, given the goal. Undoubtedly that would work, but syntax is really the easier part - also, I've been married to C for far too long and have not had nearly as much exposure to Lisp as I ought, so I'm quite set in my ways. Also, I think good syntax might help illuminate the problem at hand.
Getting syntax (or lack of it, that's your preference) right really is of supreme importance, because we can't accomplish what we're after without macros; even if we don't start with real Lisp macros, we need something much more powerful than C macros. And the problem with C macros is as much the language they have to work; you can do a great deal with weak macros provided the language cooperates, and getting turing completeness is easy.
But macros at this level are hard (getting them right is hard everywhere!), so let's think a bit about what the language ought to look like first.
One of C's most well known failings is the artificial distinction between expressions and statements. This is just one example of a somewhat larger class of problems; any code must be able to go in any expression, and in C there are many ways in which this is not true. GCC fixes the worst of them with expression statements and nested functions; both (especially the former) are profoundly powerful, but the syntax for the first is also ugly. But since it never should've been needed in the first place, that one's easily avoided.
This extends to defining new types, too. The most important thing all of this gets you is that it makes even weak macros powerful; your macros can't create new syntax (not with just that), but that is the basic requirement for macros than can define constructs with arbitrary semantics. In C, today, you can use expression statements and C99 for loop declarations to write macros that loop over essentially anything, and hide away the entirety of the implementation - though the macros themselves quickly become grotesque. This isn't everything we need - in particular, we need to do a little more to be able to define arbitrarily complex types and data structures - but it really is the key piece of the puzzle.
Probably the next thing to talk about is the way we group code. This is pretty fundamental; all of a language's flow control structures work on a chunk of code, and functions are made out of them. C got it right in that it unified all these cases into the block, but it got it wrong in that the block isn't just another expression.
Once we've got blocks of some sort, we need lambdas; a function doesn't need a name to be useful. Note that the base language has lambdas but not closures - passing an anonymous function to another function is trivial, but returning it - and having things work - will require more machinery, machinery that we aim to be able to write in this base language. Of course, if a function doesn't refer to any variables in its containing scope it wouldn't be a closure and handling the case where a normal function is returned will be a good and useful - probably necessary - thing to do.
One of the big reasons for C's elegance is that there is very little going on behind the scenes that you can't get at. Contrast that to C++ - there is an incredible amount of machinery in the background that you can't see, let alone touch: C++ is a very stupid language, in that it knows very little about itself. In C there's very little for the language to know about itself, but the one thing we're going to need to do the really interesting things is the stack pointer. Given the ability to manipulate the stack we can implement closures, continuations, spaghetti stacks, coroutines - all different sides of the same coin, really. Of course, this means we need to come up with a sane and relatively portable interface for doing so - this will be new territory. But it's crucial if we're to have a chance of making it all the way to Lisp.
We also need some way of associating code with types. This probably needs more exploration - one could conceive of making the symbol table accessible so it's possible to do manually, and then wrap it up in a nice interface - or something like regular C++ member functions might end up being the cleanest way of doing it, it's hard for me to say at this point. Note that I'm not talking about runtime polymorphism; that should be possible to write in the language. Compile time polymorphism is a smaller problem, and may well be the most reasonable model. Or maybe not - the functionality in the base language isn't intended to be good for end users, but rather powerful and useful for building new things out of. There's some related areas that need to be looked at, too.
Building up the language this way introduces some other challenges; type safety will be an interesting topic. Essentially, if the base language has to work with raw memory so that it can do things like manipulate stacks, it probably has to be able to escape the type system, much like C and void pointers. Perhaps there's some way of getting around that that I'm not seeing - raw pointers make for all sorts of difficulties in the compiler, so it's worth staring hard at. And given a way to escape the type system it'll be present all the way to the top, when we've implemented garbage collection and dynamic types. It's certainly not a fatal flaw, but C++ warns about what can lie down that path so it's something that needs to be gotten right. My thoughts are that the mere existence of raw pointers need not harm the high level language provided there's no reason to use them there, but that is a big provided.
Another large and important area is that a crucial feature of Lisp is that the whole language is available all the time - macros are written in the language and run at compile time, at runtime you can eval, etc. This really needs to be built in from the start; doing it sanely probably will take some thought. Rumor has it GCC is getting something vaguely along those lines, so perhaps there'll be something else to look at before too long.
I've yet to say anything about how any of this functionality is sufficient or even useful for building a Lisp or Lisp like language - that'll come next. I know some of the techniques, but undoubtedly more will be required I haven't thought of. A lot of it boils down to making good use of the optimizer, though. I expect most of it will just have to be discovered in the process of writing it.