The C standards take the rather unique approach of dividing behavior into 4 categories:
* standard-defined. You can rely on this; if the code you write is limited to this, you're good to go.
* erroneous. The compiler is supposed to give you an error message when you write code like this.
* implementation-defined. A particular compiler can do something reasonable with the code. If you write code depending on implementation-defined behavior, you're ok, but your code is not portable. A compiler that doesn't do something reasonable in an I-D situation isn't required to generate an error, IIRC. On the other hand, whatever it does is supposed to be consistent.
* undefined. There are explicitly no requirements on the compiler. Whatever it feels like doing is fine with the committee. There's no requirement for a compiler warning or error (and by default, there probably won't be one).
The reason for the different categories is twofold:
1. C is a systems language. It's frequently used to do things that depend entirely on the specific hardware the code is running on. The result isn't portable, but in this case, it isn't supposed to be.
2. Performance. The idea is that the compiler can take portable code and produce a program that runs very quickly on the specific hardware. As long as the code doesn't do anything non-standard, the results are guaranteed to be good.
An example of the latter is signed integer overflow. (I can't remember whether it's I-D or undefined, but...) If you do MAXINT + 1, the resulting value can differ on different hardware; it could be the most-negative integer, it could be zero, it could cause a hardware trap. Now, the C standard could specify what the behavior should be, say generating a signal that kills the program with an error. But that would be very expensive on hardware that doesn't trap signed integer overflow---it would require a check after every arithmetic operation. So the standard doesn't say what should happen.
Another example is poking at memory that isn't allocated by your program (on the stack, or the result of malloc, say). You need to be able to express that for systems programming, but a general standard can't say anything reasonable about it.
As a result, C has a lot of unsafe corners where you can unintentionally invoke I-D or undefined behavior. In the first situation, your program will break if you try to port it. In the second, your program might work, most of the time. Or it might toss a runtime error. Or it could generate mangled results.
Or it could have a really nasty and embarrassing security hole.
As a result, C is regarded as a difficult and (unnecessarily?) dangerous language. And most languages that aren't C and aren't aimed at systems programming try very hard to avoid undefined behavior. Even if you manage to avoid security holes, the bugs resulting from mangled results are a royal pain in the ass to track down.
Clojure tries to put as many user faults into "erroneous" as possible but in the OPs complaint it doesn't have the required information at compile time because it is typed dynamically. This is the fundamental problem with dynamic typing that more problems keep ending up in runtime. Behavioral concerns like security etc. are then often addressed by runtime checks.
Clojure avoids those runtime checks everywhere for performance reasons - It is a clear design choice. So your observation seems correct that the "C approach" is used in the dynamic parts of Clojure.
Since we don't have a standard, the official documentation is the contract we should rely on and very soon clojure.spec, which is intended as a "standard as data", is going to be.
Clojure tries to put as many user faults into "erroneous" as possible
I would absolutely disagree with this. Clojure has long had a Garbage-In-Garbage-Out philosophy, which puts it clearly into "undefined" territory. The examples with clojure.set are pretty clearly GIGO in my opinion, but "erroneous" would mean a runtime check and an exception clearly stating which invariant was violated, not returning something totally unexpected.
You could argue that it's under "implementation-defined", where the number of implementations is one, but I don't see how you could consider the set operations accepting and returning non-set things as consistent.
I disagree on both points there, I don't think that an instanceof check is that costly compared to many of the other performance hits we're willing to accept with Clojure for saner behaviour. For example, persistent data structures are slower than mutable ones, but I'd never go back to using mutable ones in most cases. Why is one performance hit acceptable for better behaviour and the other isn't?
I also disagree about macros, too - I spoke at the conj last year about that, and that talk seems to have been at least part of the motivation for developing spec. I hope that things will improve as spec becomes more widely used, but right now the situation for macro error messages is pretty bad.
I had an itch in my mind that some rigor was lacking in his analysis as I was reading the blog post, and this captures it much better than I could have. My model has always been that methods should have well-defined behavior when the input satisfies preconditions, but may or may not error when input does not satisfy preconditions (i.e. the response could simply be undefined, rather than raising an error).
This taxonomy is much more comprehensive, and you've laid it out well. Thanks.
* standard-defined. You can rely on this; if the code you write is limited to this, you're good to go.
* erroneous. The compiler is supposed to give you an error message when you write code like this.
* implementation-defined. A particular compiler can do something reasonable with the code. If you write code depending on implementation-defined behavior, you're ok, but your code is not portable. A compiler that doesn't do something reasonable in an I-D situation isn't required to generate an error, IIRC. On the other hand, whatever it does is supposed to be consistent.
* undefined. There are explicitly no requirements on the compiler. Whatever it feels like doing is fine with the committee. There's no requirement for a compiler warning or error (and by default, there probably won't be one).
The reason for the different categories is twofold:
1. C is a systems language. It's frequently used to do things that depend entirely on the specific hardware the code is running on. The result isn't portable, but in this case, it isn't supposed to be.
2. Performance. The idea is that the compiler can take portable code and produce a program that runs very quickly on the specific hardware. As long as the code doesn't do anything non-standard, the results are guaranteed to be good.
An example of the latter is signed integer overflow. (I can't remember whether it's I-D or undefined, but...) If you do MAXINT + 1, the resulting value can differ on different hardware; it could be the most-negative integer, it could be zero, it could cause a hardware trap. Now, the C standard could specify what the behavior should be, say generating a signal that kills the program with an error. But that would be very expensive on hardware that doesn't trap signed integer overflow---it would require a check after every arithmetic operation. So the standard doesn't say what should happen.
Another example is poking at memory that isn't allocated by your program (on the stack, or the result of malloc, say). You need to be able to express that for systems programming, but a general standard can't say anything reasonable about it.
As a result, C has a lot of unsafe corners where you can unintentionally invoke I-D or undefined behavior. In the first situation, your program will break if you try to port it. In the second, your program might work, most of the time. Or it might toss a runtime error. Or it could generate mangled results.
Or it could have a really nasty and embarrassing security hole.
As a result, C is regarded as a difficult and (unnecessarily?) dangerous language. And most languages that aren't C and aren't aimed at systems programming try very hard to avoid undefined behavior. Even if you manage to avoid security holes, the bugs resulting from mangled results are a royal pain in the ass to track down.