>> Will all 3 steps ever occur at the same time?
> I don't think this is an important question.
But in the absence of the combination of those 3 steps, your check will
make no difference. Are you saying that you don't think "whether my
check makes a difference" is an important question?
It does make a difference. It's important for specifications to be precise and unsurprising, no matter whether a certain case is frequent or not.
> The important point is: Previously, there were invariants such as
> (integerp x) == (eq (type-of x) 'integer) or that prin1 would only
> generate #s(hash-table ...) for actual hash tables. Now these
> invariants are broken.
Every patch we install changes an "invariant" (except for cosmetic
patches, changes to the build system, ...).
So we can't take such an absolute position, if we want to keep
developing Emacs (including fixing bugs): we necessarily have to judge
which invariants are worthy of being preserved and which ones aren't.
True, but such changes need to be considered very carefully, and we need to state explicitly which invariants can be broken without notifying anybody (e.g. introducing new functions in the global namespace), which can be broken by listing them in the NEWS file under "Breaking changes" (such as removing a function), and which invariants cannot be broken.
The change being discussed here is at least in the second category. I would argue it is even in the third category: it breaks invariants around fundamental ELisp types for no good reason.
E.g. if we ever get good support for bignums, we'll likely have to break
your first invariant.
We can't do that. Bignum support would either have to be completely transparent (which would make `eq' more complex), or they should be a completely separate type.
Also we have to distinguish between breaking an invariant and not
enforcing it. I do consider an Elisp code which creates a record of
type `integer` as a bug, but I don't think it's worth this particular
effort to enforce it.
This doesn't work: if it's not an explicit error, then people will start relying on it ("Hyrum's law"). We need to enforce our specifications, otherwise they aren't specifications.
Even with your extra check the above two invariants won't always hold.
I can trivially break the first one with some add-advice on type-of.
Should we add another check to prevent breaking the "invariant" in that
other way? The second can be broken without even using an advice on
`prin1` simply by creating a record of type (make-symbol "hash-table").
Should we also add yet another check to try and avoid this other way to
break your "invariant"?
No. Even though I dislike the advice functionality, would want to ban it for primitive functions, and think that it's dangerously overused, it (or rather the underlying `fset' primitive) is a part of the language, just like reflection in Java. Users should definitely be warned to steer clear from fset and its wrappers except for specialized uses such as mocking, but its existence shouldn't affect other development: Code may always assume that `fset' hasn't been called on any of the named functions it calls. If that assumption is broken, it's the burden of the code that breaks it.
> But there's a discontinuity between "invariant is guaranteed" and
> "invariant is almost always guaranteed": the latter is identical to
> "invariant is not guaranteed at all".
In Elisp, the general rule is that thanks to the dynamic nature of the
language, there are precious few invariants which really always hold.
E.g. as soon as your "invariant" calls a function by name, you're
exposed to breakage via the advice mechanism.
So, in a sense, Elisp is a landmine just like C.
Most languages have escape hatches (e.g. reflection in Java). That doesn't mean we should accept them as a given or introduce more them for no good reason.
> Yes, absolutely. I don't care whether it's rare or hypothetical, it breaks
> an invariant, and invariants must not be broken.
You might like to try Agda or Coq, but Elisp will inevitably disappoint
you in this area.
As mentioned, such invariants can be written conditionally to no `fset' calls happening. If `fset' is used, all invariants are trivially broken.