> did you (or could you have) develop that lisp interpreter on the purest TDD approach
Yes, that's what I meant by "fully test-first". I was following along with Paul Graham's The Roots of Lisp[1], so I had a convenient set of pre-made test cases. I would start by copying his examples into a test case:
it('exec atom on a symbol is true', () => {
var symbol = Symbols.get_symbol_named('foo')
expect(exec(empty_scope, [natives.atom, [natives.quote, symbol]])).to.equal(true)
})
it('exec atom on a non-empty list is false', () => {
expect(exec(empty_scope, [natives.atom, [natives.quote, ['a']]])).to.equal(false)
})
it('exec atom on an empty list is true', () => {
expect(exec(empty_scope, [natives.atom, [natives.quote, []]])).to.equal(true)
})
And then write my code:
natives.atom = new Native(function (item) {
var i = exec(this, item)
return (i instanceof Array && i.length == 0) || Symbols.is_symbol(i)
})
Once I got through the paper that way, I had a complete Lisp interpreter working, and wrote a few small programs in it with no trouble at all.
> In my experience, problems that require writing the tests first normally require writing all the tests first. If you start solving only a subset of them, you will have to rewrite most of the code once you start looking for the other tests.
This has been my experience as well. As I say, this is the only program I've ever written fully test-first; I think it was only possible because I already knew exactly what the inputs and outputs were going to be (having them well-specified by virtue of being a Lisp) and which ones I needed to implement to be able to get later ones working (with the help of the paper).
Generally I find it's not possible to work this way; my usual approach for solving problems where the code isn't immediately obvious is to write (and generally rewrite) in parallel:
- A set of usage examples and/or documentation, to clarify what exactly I'm trying to do and make sure the interface makes sense.
- The actual implementation (or, in early stages, some pseudocode).
- The tests (if any), to check that what I've written actually works. Often these are the same as the usage examples.
Frequently questions raised while writing one of these will influcence the others, often significantly; often I'll only think of an edge case while I'm writing the code and then have to go back and add it to the tests, or realize that the code could be simplified by changing the way the API works.
[1]: http://www.paulgraham.com/rootsoflisp.html