While back I wrote a Forth interpreter. (2.4kloc of portable C, passes nearly all the ANS Forth test suite.) I recommend this to anybody interested in programming languages; you don't have to finish it, but once you get the core interpreter working (in mine, the inner loop is two lines of C!) there's this aha moment and you finally get what the fuss is about, and you can feel your brain expanding.

But as a programming language to work in? Well...

- Forth has no (well, very little) runtime error checking. If your stack underflows, and you're lucky, it'll halt and tell you. But get anything else wrong, and it won't notice, and it'll just silently and invisibly poison its internal state and then crash later in a way that's really hard to debug.

- The language is strongly typed. It just doesn't have any type checking, and if you get the types wrong, it'll silently and invisibly poison its internal state and the crash later etc. You have to remember what type of object you push onto the stack, because you need to use the right kind of word to consume the object again. Does this word push a float? You need to know, because you need to use FDROP instead of DROP to consume it.

- The main culprit here is the compiler, which passes internal state around is by pushing objects onto the compiler stack. e.g. the BEGIN word, which starts a loop, pushes a 'dest' object onto the compiler stack. You have to consume this with UNTIL, WHILE or AGAIN. If you use the wrong word, or exit the definition without consuming it, it'll silently and invisibly poison its internal state etc. So you're now mentally juggling three different stacks: the data stack, the return stack, and the compiler stack, and if you make a mistake anywhere you're going to have a bad day.

- There's a lot of trivia to remember, which you need to remember. EXIT is allowed inside a BEGIN..AGAIN loop. But it's not allowed inside a DO..LOOP loop, because DO..LOOP loops are permitted to store a state object on the return stack. I will retrieve the index of the innermost DO..LOOP loop, and J will retrieve the index of the next DO..LOOP loop... but not if you've pushed something onto the return stack yourself inside the outer loop. If you don't get it right, it will silently and invisibly etc.

- Forth's only really intended to work on bare metal, and this shows. e.g. data space is assumed to increase from HERE up to the top of memory, and there's no facility for disjoint data spaces. The way you define data is to take a pointer to HERE and then just write stuff. All that stuff is defined to be contiguous. There's no scope for, e.g., running out of heap and allocating another block from the operating system. Oh, and try not to run out of memory, because it will etc.

- Stack juggling gets old very, very quickly. (Although some Forths, not mine, support local variables.)

Where Forth really excels is as an assembler substitute, where you're will to focus down and concentrate on every byte of source and you're going to put up with a complete absence of error checking because you need the performance. It's certainly great there. Being able to fit a complete interactive environment into a tiny, tiny space is astonishingly useful for debugging. But I'd hate to have to write anything of any great complexity in it.

I have heard of type-checked Forths; here's one, although I notice it takes a lot of liberties with the language (like not really having a return stack): http://www.arestlessmind.org/2009/02/03/intro.html

But I've never used one. I should, one day; I'm willing to believe that it's a much more pleasant experience than raw Forth. In particular, an embedded Forth with strict type checking but no runtime overhead sounds amazing...

(Aside: the Jupiter Ace was a Z80-based home computer from the 1980s, about the same grade as the ZX81. It came with Forth. Javascript emulator! http://jupiler.retrolandia.net/ It's got games! In Forth! Load one, do SHIFT+SPACE to exit, then 'vlist' will dump the dictionary and 'list word' will decode a word.)