Over the weekend ran the full up tests, with upgrade as well as regression tests, on Mac OSX Lion with the following lispen:
ccl clisp sbcl cmucl lispworks allegro_64 allegromodern_64 allegro8_64 allegromodern8_64 allegro_64_s allegromodern_64_s allegro8_64_s allegromodern8_64_s ecl abcl
All tests completed successfully.
I would like to do the following:
1. Add warnings as appropriate wrt changes in OPERATION hierarchy for code that extends OPERATION classes.
2. Add warnings for people using ABCL or ECL + bundle-op on Mac OS.
...and then do a release.
Cheers, r
I would like to do the following:
- Add warnings as appropriate wrt changes in OPERATION hierarchy for
code that extends OPERATION classes.
Not sure what you mean by "add warnings": issue an official declaration warning people that code may break? Using the MOP to detect code that uses defclass with operation as a superclass and issue warnings? I mean, all known existing code has been fixed by now, or never tested.
- Add warnings for people using ABCL or ECL + bundle-op on Mac OS.
The ABCL and ECL bugs... well, I'm not sure these configurations ever worked, so it's no regression. But yes, it should be easy to detect such a situation and issue a warning of a cerror might help the user diagnose why his code is failing, e.g. in the failing perform method, before the failure.
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org "Market Failure" has nothing to do with either Market or Failure, only with whiny bullies demanding that others bend to their preferences, based on the absurd belief that violence is a magic wand that yields coordination for free.
Faré wrote:
I would like to do the following:
- Add warnings as appropriate wrt changes in OPERATION hierarchy for
code that extends OPERATION classes.
Not sure what you mean by "add warnings": issue an official declaration warning people that code may break? Using the MOP to detect code that uses defclass with operation as a superclass and issue warnings? I mean, all known existing code has been fixed by now, or never tested.
Find cases where people define new OPERATION subclasses and issue a warning about the changes.
The problem is that the current change in implementation breaks unknown code in ways that quietly cause it to fail to do what the programmer expects without any visible notification of the change.
Programmers who do not follow ASDF-DEVEL carefully will be completely blindsided by this change.
Indeed, *I* was blindsided by this change, and I do read ASDF-DEVEL.
I need to look into a means to identify these problematic cases. Ugh. MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
- Add warnings for people using ABCL or ECL + bundle-op on Mac OS.
The ABCL and ECL bugs... well, I'm not sure these configurations ever worked, so it's no regression. But yes, it should be easy to detect such a situation and issue a warning of a cerror might help the user diagnose why his code is failing, e.g. in the failing perform method, before the failure.
I believe so, and while waiting for a fix, we can allow programmers to live dangerously by pushing something like :asdf-try-broken-bundle-op onto *FEATURES*.
This should be easy to add.
cheers, r
On 12 Jan 2014, at 19:57, Robert P. Goldman rpgoldman@sift.info wrote:
Faré wrote:
I would like to do the following:
- Add warnings as appropriate wrt changes in OPERATION hierarchy for
code that extends OPERATION classes.
Not sure what you mean by "add warnings": issue an official declaration warning people that code may break? Using the MOP to detect code that uses defclass with operation as a superclass and issue warnings? I mean, all known existing code has been fixed by now, or never tested.
Find cases where people define new OPERATION subclasses and issue a warning about the changes.
The problem is that the current change in implementation breaks unknown code in ways that quietly cause it to fail to do what the programmer expects without any visible notification of the change.
Programmers who do not follow ASDF-DEVEL carefully will be completely blindsided by this change.
Indeed, *I* was blindsided by this change, and I do read ASDF-DEVEL.
I need to look into a means to identify these problematic cases. Ugh. MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
I wouldn’t recommend fiddling with ensure-class or ensure-class-using-class. It’s too hard to specialize methods correctly on those, and their portability across CL implementations is not very good.
If you want to implement checks on subclasses, it’s better to do this in initialize-instance / reinitialize-instance on metaclasses, but this requires that the subclasses also use those metaclasses, which may break other assumptions.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
The interface a class exposes to its subclasses is an interface like any other, and yes, interface changes may have negative consequences. That’s correct for any kind of interfaces. It can be better to create new interfaces and leave the existing ones untouched if backwards compatibility is a major concern.
Just my 0.02€…
Pascal
-- Pascal Costanza The views expressed in this email are my own, and not those of my employer.
Pascal Costanza wrote:
I need to look into a means to identify these problematic cases. Ugh.
MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
I wouldn’t recommend fiddling with ensure-class or ensure-class-using-class. It’s too hard to specialize methods correctly on those, and their portability across CL implementations is not very good.
If you want to implement checks on subclasses, it’s better to do this in initialize-instance / reinitialize-instance on metaclasses, but this requires that the subclasses also use those metaclasses, which may break other assumptions.
Sorry if this is a foolish question, but the class at issue here is OPERATION, whose metaclass is STANDARD-CLASS.
So am I correct in thinking I would need something like
(defmethod INITIALIZE-INSTANCE :BEFORE ((class standard-class) &key direct-superclasses) (when (member (find-class 'asdf:operation) direct-superclasses) ...handle my troublesome case...)))
where the primary portability challenge would be to make sure I have the right STANDARD-CLASS for the various implementations? [Hoping to cargo-cult that out of Closer-MOP.]
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
The interface a class exposes to its subclasses is an interface like any other, and yes, interface changes may have negative consequences. That’s correct for any kind of interfaces. It can be better to create new interfaces and leave the existing ones untouched if backwards compatibility is a major concern.
I don't believe that it's true that a class exposes an interface like any other.
If my library exposes an API in the form of a function call, then I may easily catch any invocation of that function call and, e.g., provide a deprecation warning, etc.
But I believe that your email indicates that we *cannot* portably identify cases where a user is extending a class that the library exposes.
Am I wrong about this? If so, please let me know! If not, I believe it shows that CLOS-based programs must be more cautious when exposing classes for extension, then when exposing other code artifacts.
Alas, now the cat is out of the bag, and we must figure out the best way to handle this specific case. Let me say again that I welcome all suggestions and guidance (like yours, above) in doing this.
Best, Robert
[Sorry for the delay, but I haven’t seen this email before.]
On 13 Jan 2014, at 17:25, Robert P. Goldman rpgoldman@sift.info wrote:
Pascal Costanza wrote:
I need to look into a means to identify these problematic cases. Ugh.
MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
I wouldn’t recommend fiddling with ensure-class or ensure-class-using-class. It’s too hard to specialize methods correctly on those, and their portability across CL implementations is not very good.
If you want to implement checks on subclasses, it’s better to do this in initialize-instance / reinitialize-instance on metaclasses, but this requires that the subclasses also use those metaclasses, which may break other assumptions.
Sorry if this is a foolish question, but the class at issue here is OPERATION, whose metaclass is STANDARD-CLASS.
So am I correct in thinking I would need something like
(defmethod INITIALIZE-INSTANCE :BEFORE ((class standard-class) &key direct-superclasses) (when (member (find-class 'asdf:operation) direct-superclasses) ...handle my troublesome case...)))
where the primary portability challenge would be to make sure I have the right STANDARD-CLASS for the various implementations? [Hoping to cargo-cult that out of Closer-MOP.]
You are not allowed to define a method for a pre-defined generic function in the MOP on a pre-defined metaclass like this. At least one specializer must be one of your own metaclasses. (See “restrictions on portable programs” in the “Concepts” section of the CLOS MOP specification. It’s _very_ important to stick to these restrictions.)
So you’re only chance is to define an operation-class metaclass, and make sure that operation is an instance of operation-class (or similar).
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
The interface a class exposes to its subclasses is an interface like any other, and yes, interface changes may have negative consequences. That’s correct for any kind of interfaces. It can be better to create new interfaces and leave the existing ones untouched if backwards compatibility is a major concern.
I don't believe that it's true that a class exposes an interface like any other.
If my library exposes an API in the form of a function call, then I may easily catch any invocation of that function call and, e.g., provide a deprecation warning, etc.
Classes are essentially call-back interfaces. You would have similar issues if you would expose a higher-order function that starts to call the function parameters differently, and with different arguments. Especially, calling function parameters with different keyword arguments is very similar.
But I believe that your email indicates that we *cannot* portably identify cases where a user is extending a class that the library exposes.
Am I wrong about this? If so, please let me know! If not, I believe it shows that CLOS-based programs must be more cautious when exposing classes for extension, then when exposing other code artifacts.
Yes, you have to be more cautious with CLOS. In fact, this applies to OOP in general, not only CLOS.
Pascal
-- Pascal Costanza The views expressed in this email are my own, and not those of my employer.
Pascal Costanza wrote:
If you want to implement checks on subclasses, it’s better to do this in initialize-instance / reinitialize-instance on metaclasses, but this requires that the subclasses also use those metaclasses, which may break other assumptions.
Sorry if this is a foolish question, but the class at issue here is OPERATION, whose metaclass is STANDARD-CLASS.
So am I correct in thinking I would need something like
(defmethod INITIALIZE-INSTANCE :BEFORE ((class standard-class) &key direct-superclasses) (when (member (find-class 'asdf:operation) direct-superclasses) ...handle my troublesome case...)))
where the primary portability challenge would be to make sure I have the right STANDARD-CLASS for the various implementations? [Hoping to cargo-cult that out of Closer-MOP.]
You are not allowed to define a method for a pre-defined generic function in the MOP on a pre-defined metaclass like this. At least one specializer must be one of your own metaclasses. (See “restrictions on portable programs” in the “Concepts” section of the CLOS MOP specification. It’s _very_ important to stick to these restrictions.)
So your only chance is to define an operation-class metaclass, and make sure that operation is an instance of operation-class (or similar).
This is quite a crippling limitation, then. Adding a metaclass would add another quite substantial amount of compatibility code to ASDF to permit us to use the MOP (even in a relatively limited way) on all of the supported implementations.
Can you think of any other way we might detect the creation of subclasses of OPERATION?
Thank you,
R
So your only chance is to define an operation-class metaclass, and make sure that operation is an instance of operation-class (or similar).
This is quite a crippling limitation, then. Adding a metaclass would add another quite substantial amount of compatibility code to ASDF to permit us to use the MOP (even in a relatively limited way) on all of the supported implementations.
Can you think of any other way we might detect the creation of subclasses of OPERATION?
It would be nice if ASDF could do without pulling in closer-mop or substantial parts thereof.
Adding a metaclass might also in and of itself be an incompatibility, which might require all clients to be updated to use the metaclass, in addition to requiring a punt during hot upgrade (i.e. dropping existing state on the ground). I'm not sure it is worth the cost at this point.
As suggested previously, a :before or :after method on make-instance for operation objects or such should be enough for this particular case, although this defers the warning until the operation is used, rather than when it is defined.
The big problem, however, in this case, is still how do you distinguish between a "good" class that shouldn't trigger a warning, and a "bad" class that should.
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org May you live all the days of your life. — Jonathan Swift
On 17 Jan 2014, at 19:06, Faré fahree@gmail.com wrote:
So your only chance is to define an operation-class metaclass, and make sure that operation is an instance of operation-class (or similar).
This is quite a crippling limitation, then. Adding a metaclass would add another quite substantial amount of compatibility code to ASDF to permit us to use the MOP (even in a relatively limited way) on all of the supported implementations.
Can you think of any other way we might detect the creation of subclasses of OPERATION?
It would be nice if ASDF could do without pulling in closer-mop or substantial parts thereof.
I agree, especially since there are still CL implementations that closer-mop doesn’t cover.
Adding a metaclass might also in and of itself be an incompatibility, which might require all clients to be updated to use the metaclass, in addition to requiring a punt during hot upgrade (i.e. dropping existing state on the ground). I'm not sure it is worth the cost at this point.
You can define a metaclass in such a way that subclasses of standard-class are still compatible with it.
As suggested previously, a :before or :after method on make-instance for operation objects or such should be enough for this particular case, although this defers the warning until the operation is used, rather than when it is defined.
I agree that this is the best solution.
The big problem, however, in this case, is still how do you distinguish between a "good" class that shouldn't trigger a warning, and a "bad" class that should.
By deprecating the use of operation as a superclass, and providing a new ‘new-operation class (or some such) that user code has to use instead. (new-operation could be a subclass of operation…)
Pascal
-- Pascal Costanza
The big problem, however, in this case, is still how do you distinguish between a "good" class that shouldn't trigger a warning, and a "bad" class that should.
By deprecating the use of operation as a superclass, and providing a new ‘new-operation class (or some such) that user code has to use instead. (new-operation could be a subclass of operation…)
I'm not sure this helps at all: * For code going forward, that could be a signalling mechanism — but going forward, whichever way we do it is easy, since people who today are hacking for ASDF2 and not ASDF3 are responsible for their own problems, really. * For existing code, it doesn't help, since that code already exists, and the problem is precisely to distinguish between the good code and the bad code if any is left. * As a general versioning mechanism, I don't think that introducing operation-v3, operation-v4, operation-v5 is a good way to distinguish between good and bad code as we evolve the hierarchy, especially since actual code will likely already inherit from one of the subclasses, in which case it automatically gets the new superclass, which then can't be used as a reliable discriminating criterion.
The thing is, CLOS simply doesn't have any standardized mechanism to deal with code versioning, though it does provide the low-level hooks to deal with data upgrade, and you can probably build your versioning system on top of it. I'd love it if there were a mechanism, and if ASDF could use it, and/or be a platform to discover it. But in the meantime, we shouldn't expect ASDF to do more than is possible. ASDF is always doing so much more than most software in terms of static and dynamic version compatibility and upgrade!
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org "To alcohol! The cause of... and solution to all of life's problems!" — Homer Simpson, quoted by H. Duray about Addiction to Government
On 17 Jan 2014, at 18:42, Robert P. Goldman rpgoldman@sift.info wrote:
Pascal Costanza wrote:
If you want to implement checks on subclasses, it’s better to do this in initialize-instance / reinitialize-instance on metaclasses, but this requires that the subclasses also use those metaclasses, which may break other assumptions.
Sorry if this is a foolish question, but the class at issue here is OPERATION, whose metaclass is STANDARD-CLASS.
So am I correct in thinking I would need something like
(defmethod INITIALIZE-INSTANCE :BEFORE ((class standard-class) &key direct-superclasses) (when (member (find-class 'asdf:operation) direct-superclasses) ...handle my troublesome case...)))
where the primary portability challenge would be to make sure I have the right STANDARD-CLASS for the various implementations? [Hoping to cargo-cult that out of Closer-MOP.]
You are not allowed to define a method for a pre-defined generic function in the MOP on a pre-defined metaclass like this. At least one specializer must be one of your own metaclasses. (See “restrictions on portable programs” in the “Concepts” section of the CLOS MOP specification. It’s _very_ important to stick to these restrictions.)
So your only chance is to define an operation-class metaclass, and make sure that operation is an instance of operation-class (or similar).
This is quite a crippling limitation, then. Adding a metaclass would add another quite substantial amount of compatibility code to ASDF to permit us to use the MOP (even in a relatively limited way) on all of the supported implementations.
Can you think of any other way we might detect the creation of subclasses of OPERATION?
No, I don’t think there is any. There are good reasons for this limitation, including allowing CLOS implementations to be free of any restrictions when dealing with predefined metaclasses which enables good performance, and also ensuring that different metaclasses don’t step on each other’s toes.
Pascal
-- Pascal Costanza The views expressed in this email are my own, and not those of my employer.
On Sun, Jan 12, 2014 at 1:57 PM, Robert P. Goldman rpgoldman@sift.info wrote:
Faré wrote:
- Add warnings as appropriate wrt changes in OPERATION hierarchy for
code that extends OPERATION classes.
Not sure what you mean by "add warnings": issue an official declaration warning people that code may break? Using the MOP to detect code that uses defclass with operation as a superclass and issue warnings? I mean, all known existing code has been fixed by now, or never tested.
Find cases where people define new OPERATION subclasses and issue a warning about the changes.
How do you detect whether the code is made to work for ASDF2 or for ASDF3?
The ASDF2 model was inconsistent, with contradictory assumptions about OPERATION (1), and ASDF3 fixes as many or more systems than it breaks. I don't think it's possible to detect that something wrong is going on, without being a nuisance for the vast majority of system that work (some of them after being fixed).
[(1) (a) OPERATION is the root of the operation class hierarchy (b) in ASDF2, all operations are propagated downwards and sideway (big mistake) Some systems define operations that really do not want not to be propagated either downward or sideway, and work around that as they can (if they can); ASDF3 fixes them. Some systems define operations rely on operations being propagated either downward or sideway, and ASDF3 breaks them unless the definition has #+asdf3 downward-operation and/or #+asdf3 sideway-operation.]
I considered having a completely separate incompatible hierarchy, but that would instead have broken *everything*, instead of breaking a fraction and fixing a larger fraction of code. That wouldn't have been much of an ASDF3, and I might as well have stuck to XCVB.
The problem is that the current change in implementation breaks unknown code in ways that quietly cause it to fail to do what the programmer expects without any visible notification of the change.
Any change in implementation will do that: if people start relying on bugs, instead of reporting them and getting them fixed, then of course their code will break when the bugs are fixed
With ASDF, no one really understood the bug (or the structure of ASDF in general), and back in the days of ASDF1, there was no incentive in figuring things out anyway, since there was no good process for getting things fixed. Many people were using kludges
Programmers who do not follow ASDF-DEVEL carefully will be completely blindsided by this change.
Indeed, *I* was blindsided by this change, and I do read ASDF-DEVEL.
That's my fault. I should have shouted about it in ALL CAPS, MANY TIMES OVER. Instead, I wrongly assumed that chasing the offenders in Quicklisp and getting them fixed would be enough.
I need to look into a means to identify these problematic cases. Ugh. MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
What if the previous hierarchy was plain buggy?
Every bug fix *is* an incompatibility, and yes, sometimes it is necessary to fix changes in the class hierarchy, and getting clients fixed can be a pain. Hopefully, this part of the hierarchy is stable now. There are other potential incompatible changes that I annotated ASDF4 in the source code, that I didn't undertake, because they would have been a lot of work to fix all clients, without sufficient functional gain. A more motivated maintainer than I would have provided a transition plan, so gotten all clients could be fixed in a backward compatible way, before the old code is changed incompatibly.
That's another reason for fixing bugs early rather than late, and fixing as many as possible at once when you introduce incompatibility, because the price is proportional to the number of times that things break, not to how much breaks each time. Maybe I should have been more, not less, aggressive in introducing incompatibilities.
- Add warnings for people using ABCL or ECL + bundle-op on Mac OS.
The ABCL and ECL bugs... well, I'm not sure these configurations ever worked, so it's no regression. But yes, it should be easy to detect such a situation and issue a warning of a cerror might help the user diagnose why his code is failing, e.g. in the failing perform method, before the failure.
I believe so, and while waiting for a fix, we can allow programmers to live dangerously by pushing something like :asdf-try-broken-bundle-op onto *FEATURES*.
I recommend against a feature, which requires recompilation, and is thus useless when using an implementation-provided ASDF, as well as introduces hard-to-control environmental dependencies. Instead, I much recommend using CERROR, which can be controled programmatically at runtime with RESTART-CASE.
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org There are just two rules of governance in a free society: Mind your own business. Keep your hands to yourself. — P. J. O'Rourke
Faré wrote:
I need to look into a means to identify these problematic cases. Ugh.
MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
What if the previous hierarchy was plain buggy?
Then going forward we have to introduce a NEW class to replace the old class.
Yes, that breaks code, but it only breaks code VISIBLY. Redefinition of existing classes breaks code INVISIBLY.
If we had a new name, say OPERATION-CLASS, then everyone would see that their existing code needed modification. Yes, a nuisance.
But I can speak to the fact that code that quietly goes away and does something unexpected or, worse, quietly does not do something expected, is very difficult to debug.
It's particularly bad in ASDF, because people often don't think to look into it (it's like looking for a bug in "make") and because even if you do think to look into it, you may be getting it from your implementation, which may make it difficult to find.
This will be a maintenance principle going forward: no changes to user-extendible classes without renaming.
This suggests a corollary: in order to prevent horrible *internal* update issues, we should split apart user-extensible classes from internal classes.
E.g., it would make sense to have something like INTERNAL-OPERATION and OPERATION be distinct going forward. That naming policy is obviously not tenable for ASDF now -- OPERATION is used everywhere and must remain -- but adding USER-OPERATION might be a plausible step.
That allows us to incompatibly modify the guts of the program while providing a stable, or visibly-broken, API to the user.
Cheers, r
What if the previous hierarchy was plain buggy?
Then going forward we have to introduce a NEW class to replace the old class.
Yes, that breaks code, but it only breaks code VISIBLY. Redefinition of existing classes breaks code INVISIBLY.
If we had a new name, say OPERATION-CLASS, then everyone would see that their existing code needed modification. Yes, a nuisance.
I've tried to adhere to this guideline in the past, but sometimes it's just not possible: too many people rely on the old name, for several semantic effects, some of which are preserved and some of which aren't. Why break all those that are preserved, or even enhanced, when only a few of them are broken, what more, that were already broken in corner cases?
In the case of OPERATION, many systems were defining methods on OPERATION. The idea that OPERATION was a base class was deeply rooted in the code. Many systems defined methods on TEST-OP, for which the sideway propagation was an undesired bug, not an actual feature: when I test a system of mine, I do NOT want to test every third party library that I depend on, some of which have broken tests that I don't care about and don't care to fix. If I want propagation, it's easy enough to write a MONOLITHIC-TEST-OP or such that will propagate sideway and maybe even downward. As I described in my previous audit, there were many operation classes being defined for which sideway and downward propagation was a bug. It was a notable bug for the bundle operations. It was even a bug for LOAD-OP itself, in corner cases — the root bug that started the whole refactoring.
Once again, sometimes the right thing to do IS to keep the same name, and break some client code that was subtly broken.
But I can speak to the fact that code that quietly goes away and does something unexpected or, worse, quietly does not do something expected, is very difficult to debug.
Indeed, the problem here is not that the breakage was invisible; the problem is that, upon seeing the breakage, the author (you in this case) had trouble tracing down the breakage to its root cause.
You're right that maybe we could have at the end of each file a check for new subclasses of OPERATION, and a warning if an unknown one was found. It is probably more portable to use the MOP to walk all defined subclasses than to intercept their definition as it happens.
It's particularly bad in ASDF, because people often don't think to look into it (it's like looking for a bug in "make") and because even if you do think to look into it, you may be getting it from your implementation, which may make it difficult to find.
Yes, it's always shaking when the foundations you rely upon crumble under your feet. But that's precisely why it's important to fix the structural misdesign early rather than late. The challenge is to do it in non-disruptive ways; I admit I have only been so good at that.
This will be a maintenance principle going forward: no changes to user-extendible classes without renaming.
I fear this is a good guideline, but not a strict principle.
This suggests a corollary: in order to prevent horrible *internal* update issues, we should split apart user-extensible classes from internal classes.
E.g., it would make sense to have something like INTERNAL-OPERATION and OPERATION be distinct going forward. That naming policy is obviously not tenable for ASDF now -- OPERATION is used everywhere and must remain -- but adding USER-OPERATION might be a plausible step.
That allows us to incompatibly modify the guts of the program while providing a stable, or visibly-broken, API to the user.
I'm not convinced that adding a class makes things more stable rather than less. If anything the name should be more like a versions ASDF3-OPERATION than USER-OPERATION. And what of people who want to define methods on ALL operations? Or do you want to document two distinct classes, the base of all the hierarchy OPERATION, versus the base for user-extensibility, with plenty of additional semantics, that would be ASDF3-OPERATION? But what if people precisely want no additional semantics? Worse, what if a third party wants to hook in additional semantics to some (or all) operations? I think it makes sense to define new classes when you add new semantics, and not put all the functionality in the base class; but I don't think it makes sense to create gratuitous new classes just because you believe their might be a need for an entry point.
But once again, I believe it's too late to do anything about OPERATION. So let's consider a potential future cleanup one might want to do. Let's pick an "easy" one: the case of MODULE and SYSTEM. When rewriting ASDF, I initially wanted to make a system NOT be a MODULE, but to introduce separate classes PARENT-COMPONENT and CHILD-COMPONENT, such that a MODULE would be both a PARENT- and a CHILD- whereas a SYSTEM would be a PARENT- but not a CHILD-. This wasn't compatible with many systems that define methods on MODULE for the specific purpose of their being run on systems: indeed, that was the recommended backward-compatible way in the manual(!) to override the file type of Lisp files in a system, as opposed to the new way of using (and maybe defining) a subclass of CL-SOURCE-FILE with a different :type. If you want to clean that up, you'll have to make sure no one write methods on MODULE and expect them to work on SYSTEM. That will require changing a few systems in Quicklisp and providing a transition period; but eventually, you can make that cleanup. And for that cleanup, it would be counter-productive to rename the whole class hierarchy and make everything incompatible, when 99% of the code is compatible, and the offending code is arguably buggy (be it at the suggestion of the buggy manual).
Class hierarchy surgery is hard. But "just create a new hierarchy" is sometimes self-defeating. If the goal is a better system at the price of incompatibility, consider XCVB. If the goal is to slowly steer the CL community towards having a saner build system, all the while providing a smooth upgrade path via backward compatibility with recent-enough versions, then software surgery is the rule.
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org Tradition is the matter of which civilization is made. Anyone who rejects tradition per se should be left naked in a desert island. Innovation is the matter with which civilization is built. Anyone who rejects innovation per se should be left naked in a desert island.
Faré wrote:
But I can speak to the fact that code that quietly goes away and does something unexpected or, worse, quietly does not do something expected, is very difficult to debug.
Indeed, the problem here is not that the breakage was invisible; the problem is that, upon seeing the breakage, the author (you in this case) had trouble tracing down the breakage to its root cause.
Let me clarify, the breakage WAS close to invisible.
The code that came through the allegedly successful compilation simply didn't function.
There was no error message that I could track back, because some code that should have been translated -- not COMPILED, but TRANSLATED, so that when the code wasn't translated, ASDF simply went away and reloaded the obsolete versions of the code.
There was NO ASDF breakage.
The only way to figure out what went wrong, well downstream, was to know that one had to look at the output of TRAVERSE to see that an operation we expected wasn't happening.
The breakage was WAY downstream, when odd things happened as a result of loading old versions of code.
So that's as close as you get to "invisible" without walking into the territory of "not a bug at all"!
You're right that maybe we could have at the end of each file a check for new subclasses of OPERATION, and a warning if an unknown one was >
found.
It is probably more portable to use the MOP to walk all defined subclasses than to intercept their definition as it happens.
That might be nice, but when would we run this check? After loading each new system? That seems a lot harder than running a check when a new OPERATION subclass is instantiated (as Pascal's suggestion).
[I am quite concerned about this because I am 95% sure that we have other systems like this out there that are just waiting to bite me. We have lots of systems that interact with outside software in ways that require ASDF to initiate, e.g., some make operations, something to build some Java code, starting up a server, etc.]
Cheers, r
That might be nice, but when would we run this check? After loading each new system? That seems a lot harder than running a check when a new OPERATION subclass is instantiated (as Pascal's suggestion).
Actually check at instantiation time is all the better for operation since we do our best to memorize them, so the warning would happen only once rather than constantly.
Remains the problem of making a difference between "good" and "bad" operation classes. This I have no idea how to do, short of maintaining a white list for current and past systems, and some new declaration for future systems. And even then, for old systems you might want to see if there's version information that distinguishes working versions from broken ones. So far I've tried to avoid whitelists that require constant maintenance.
[I am quite concerned about this because I am 95% sure that we have other systems like this out there that are just waiting to bite me. We have lots of systems that interact with outside software in ways that require ASDF to initiate, e.g., some make operations, something to build some Java code, starting up a server, etc.]
I would just require the handful of asdf operation defining hackers who haven't upgraded yet to do an audit asap, which they need do anyway. And I would bet against anyone being in need of an audit who isn't reading us. But then again, you're the one to make bets now.
On Mon, 2014-01-13 at 10:05 -0600, Robert P. Goldman wrote:
Faré wrote:
I need to look into a means to identify these problematic cases. Ugh.
MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
What if the previous hierarchy was plain buggy?
Then going forward we have to introduce a NEW class to replace the old class.
I hope you're only referring to new changes, not the ones that already happened.
Stelian Ionescu wrote:
On Mon, 2014-01-13 at 10:05 -0600, Robert P. Goldman wrote:
Faré wrote:
I need to look into a means to identify these problematic cases. Ugh.
MOP not standardized. Ugh ugh ugh ugh. I'll see if I can pilfer code from Closer-MOP to render ENSURE-CLASS accessible.
Is this the moral of this story: code that wishes to be backwards compatible and portable can NEVER change the class hierarchy above an API-visible class, because there is no *portable* way to detect when someone's code will be affected.
What if the previous hierarchy was plain buggy?
Then going forward we have to introduce a NEW class to replace the old class.
I hope you're only referring to new changes, not the ones that already happened.
Yes, as I said to Pascal, this particular cat is already out of the bag.
As ASDF has substantially stabilized, though, we need to be more delicate about breaking backward compatibility going forward.
Cheers, r
Faré wrote:
- Add warnings for people using ABCL or ECL + bundle-op on Mac OS.
>
The ABCL and ECL bugs... well, I'm not sure these configurations ever worked, so it's no regression. But yes, it should be easy to detect such a situation and issue a warning of a cerror might help the user diagnose why his code is failing, e.g. in the failing perform method, before the failure.
I believe so, and while waiting for a fix, we can allow programmers to live dangerously by pushing something like :asdf-try-broken-bundle-op onto *FEATURES*.
I recommend against a feature, which requires recompilation, and is thus useless when using an implementation-provided ASDF, as well as introduces hard-to-control environmental dependencies. Instead, I much recommend using CERROR, which can be controled programmatically at runtime with RESTART-CASE.
I don't see why features necessarily require recompilation. Although a feature DOES enable read-time checking, there is no *obligation* to check at read-time. We can easily do something like
(defmethod PERFORM :BEFORE ((op bundle-op) c) #+darwin ;; or whatever is right to detect Mac OSX (unless (member :asdf-try-broken-bundle-op *features*) (cerror "Try running with bundle-op." "ASDF's BUNDLE-OP is believed to be broken on this implementation on Mac OSX. If you wish to try anyway, push :asdf-try-broken-bundle-op on *FEATURES*.")))
...and then purge the method definition when these platforms are fixed.
I suggest using features because the CL package system makes the Emacs-lisp style of using SETQ and DEFVAR untenable.
Cheers, r
Faré wrote:
- Add warnings as appropriate wrt changes in OPERATION hierarchy for
> code that extends OPERATION classes. >
Not sure what you mean by "add warnings": issue an official declaration warning people that code may break? Using the MOP to detect code that uses defclass with operation as a superclass and issue warnings? I mean, all known existing code has been fixed by now, or never tested.
Find cases where people define new OPERATION subclasses and issue a warning about the changes.
How do you detect whether the code is made to work for ASDF2 or for ASDF3?
The best we can do is issue a warning whenever code defines a subclass of OPERATION, and allow the user to indicate that s/he understands and wishes to proceed.
This is a very nasty case, since the user's agreement is of necessity file-scoped.
The best thing I can think of is to have a wrapper around ASDF system loading that will establish a binding for a variable that the user can rebind local to a particular file.
That will allow the user to modify individual ASDF system definitions to indicate that this is ok.
That's not a very good solution, actually, since the OPERATION subclass might be defined outside an ASD file, in a normal .lisp file. So perhaps wrapping COMPILE-OP and LOAD-OP is also necessary.
I'd like to get this right before going forward, but I see only a choice of unpleasant alternatives.
If anyone has a better one, I'm open to suggestions.
Unfortunately, to take your point about ASDF needing to let the person who knows provide the knowledge, the only person who knows whether an OPERATION redefinition is good is the system definer, and NOT the poor programmer who loads a library.
So I'm inclined to do what's generally bad practice and explicitly provide an out for all of the quicklisp systems that you have checked. Only, oh dear, most people are not providing :VERSION attributes, which means that we cannot distinguish patched from unpatched libraries....
Do you know: can we rely on :VERSION information to identify Quicklisp libraries that you have patched? If not, could we make that happen?
Best, r
On Sun, Jan 12, 2014 at 10:50 PM, Robert P. Goldman rpgoldman@sift.info wrote:
The best we can do is issue a warning whenever code defines a subclass of OPERATION, and allow the user to indicate that s/he understands and wishes to proceed.
I agree in principle, and I believe that would have been a great thing for me to do last year. But I failed to do it, and I fear it's too late now: I strongly suspect the remaining population of people who wrote subclasses of ASDF operations yet didn't get their code fixed is now empty.
That said, though I believe it's too late to do anything about the change in semantics of OPERATION, I think it's a good idea to provide a mechanism that will allow graceful upgrade in the future.
This is a very nasty case, since the user's agreement is of necessity file-scoped.
We could make it defsystem-scoped, by having defsystem specify an :asdf-version "3.1" or some such. Of course, since that will break any older version of ASDF, this will need be #+asdf3.1 :asdf-version #+asdf3.1 "3.1" until 3.1 becomes universal, which will take a year or two. But if you don't add this option NOW, then you'll be in trouble a year or two from now when you need it.
Now of course, since an OPERATION class is not directly linked to a system, that mightn't help in this case.
The best thing I can think of is to have a wrapper around ASDF system loading that will establish a binding for a variable that the user can rebind local to a particular file.
That will allow the user to modify individual ASDF system definitions to indicate that this is ok.
That sounds particularly messy to me. I admit that the fewer special variables, the happier I am. Especially variables that modify the semantics, and that get superdupermessy if something recursively loads a .asd file, or worse does it without going through load-asd.
That's not a very good solution, actually, since the OPERATION subclass might be defined outside an ASD file, in a normal .lisp file. So perhaps wrapping COMPILE-OP and LOAD-OP is also necessary.
It's even worse, because OPERATION subclasses are often defined in a good old Lisp file, called asdf-support.lisp or some such. A flag around the loading of asd files won't help. Or now, you're going to have to do checks around every file.
Frankly, I believe this has a huge cost for negligible benefit after the fact. As for how to deal with future incompatible changes, it's a more interesting question, and I have no great answer, but that flag idea sounds like an especially bad solution.
I'd like to get this right before going forward, but I see only a choice of unpleasant alternatives.
If anyone has a better one, I'm open to suggestions.
I don't see any alternative that can fix the past. Making things future-proof is more interesting. Back in the ASDF2 days, and then again with ASDF3 and ASDF3.1, I had to define and refine various hooks for hot upgrade of ASDF, after finding the hard way where they were lacking. Other hooks may have to be defined for systems and for extensions to specify what version of ASDF they rely upon. Since these hooks don't exist yet, I believe it's erroneous to act as if they used to exist. But it's still time to define them.
Unfortunately, to take your point about ASDF needing to let the person who knows provide the knowledge, the only person who knows whether an OPERATION redefinition is good is the system definer, and NOT the poor programmer who loads a library.
Yes. And unless you come up with something extremely clever, I don't see any way to distinguish between libraries designed for ASDF2 and libraries designed for ASDF3 through programmatic means in a way that doesn't retrospectively break plenty of packages.
So I'm inclined to do what's generally bad practice and explicitly provide an out for all of the quicklisp systems that you have checked. Only, oh dear, most people are not providing :VERSION attributes, which means that we cannot distinguish patched from unpatched libraries....
My recommendation is to provide and all out for all existing systems, and not worry about it for the past anymore. I'll bet you $50 that no one is going to complain next year about a broken OPERATION that isn't already in quicklisp (and thus hopefully already fixed), and wasn't written or maintained by one of the people who's subscribed to this list.
Do you know: can we rely on :VERSION information to identify Quicklisp libraries that you have patched? If not, could we make that happen?
I'm not sure what you mean. Not every library has a usable :VERSION. Quicklisp probably maintains some date information, but according to my recent audit, everything in Quicklisp seems to be fine already, so it's just a matter of telling people "update to the latest version of quicklisp and/or your library".
With all due respect, I think you are being a little unfair to the the victims in this case.
Well, the entire community was dysfunctional with respect to ASDF. Those who extended a buggy ASDF that nobody understood (and they couldn't possibly, because it didn't make sense) have their small share of responsibility in the mess, and are paying the price now. Considering how much effort it was for me to fix ASDF, that was probably a wise choice of theirs; but the price of living with the bugs is still rightfully theirs to pay.
In the past, if you assumed that subclassing OPERATION would give you the same dependency behavior as LOAD-OP, then you weren't far wrong.
But now you get no dependency propagation, and that's almost always wrong.
I disagree. You could rely on the same dependency behavior as LOAD-OP, but that behavior was wrong for LOAD-OP, and usually wronger for whichever operation you were implementing, too. I haven't seen a single operation for which the ASDF1 algorithm for propagating dependencies is correct, and I'm convinced such beast can't exist.
There are a few cases where you want sideway propagation (lib-op); a few cases where you might want downward propagation (docstring-op ?); but sideway and downward without an upward propagation to ensure order, I can hardly conceive it: if you care about order, you almost certainly need an upward preparation operation, and if you don't you probably don't need sideway.
I am not saying there was a better solution, but I don't think it's fair to say that people who subclassed OPERATION were relying on bugs. There WERE bugs in the behavior they relied upon, but that's not the same as relying on bugs....
But this is a quibble. I'm not saying there was a better solution; I'm just saying that fixing these bugs is not without costs, and it's not fair to blame the people who had to live with buggy libraries.
As the person who paid the price to fix things, I think it's fair to say those who didn't should pay the price of living with bugs; I'm not saying they made the wrong choice; I'm saying paying this price is part of their choice; let them assume the bitter part as well as the sweet part (which was being able to make do cheaply without addressing the deep issue).
—♯ƒ • François-René ÐVB Rideau •Reflection&Cybernethics• http://fare.tunes.org Economic illiteracy often leads one to take for wealth creation or cost reduction what is only a forced displacement of activity, with no primary gain, and a lot of secondary costs and negative side-effects.
Faré wrote:
The problem is that the current change in implementation breaks unknown code in ways that quietly cause it to fail to do what the programmer expects without any visible notification of the change.
Any change in implementation will do that: if people start relying on bugs, instead of reporting them and getting them fixed, then of course their code will break when the bugs are fixed
With ASDF, no one really understood the bug (or the structure of ASDF in general), and back in the days of ASDF1, there was no incentive in figuring things out anyway, since there was no good process for getting things fixed. Many people were using kludges
With all due respect, I think you are being a little unfair to the the victims in this case.
In the past, if you assumed that subclassing OPERATION would give you the same dependency behavior as LOAD-OP, then you weren't far wrong.
But now you get no dependency propagation, and that's almost always wrong.
I am not saying there was a better solution, but I don't think it's fair to say that people who subclassed OPERATION were relying on bugs. There WERE bugs in the behavior they relied upon, but that's not the same as relying on bugs....
But this is a quibble. I'm not saying there was a better solution; I'm just saying that fixing these bugs is not without costs, and it's not fair to blame the people who had to live with buggy libraries.
Cheers, r