In relation with my post on resurrecting an Ada 83 compiler, is there anybody here who knows how Ada 83 generic units are implemented ?
My view as Ada programmer was that each instantiation of a generic unit generated new code by some template parameter substitution.
Now, studying how Text_io.Integer_io can be compiled from DIANA, I am not convinced of this evidence of code regeneration on each instantiation. Reading carefully the LRM 1815, it seems to me in the contrary that use of attributes and restrictions on formal parameters allow a form of data parametrization of a single compiled generic body with specific descriptors given at instantiation.
It appears also that DIANA does not directly support the idea of expanding the generic with specific formals and compiling a different version at each instantiation.
Ada 83 is a much more subtle language than I believed and this affair of generics is an intellectual challenge.
Chat GPT maintains it is not possible to avoid specific compilation after substitution expansion… and says there is no other solution to generics compilation in Ada 83.
My intuition is that it is false and that another solution is possible. So I’ll try to find one.
But if someone has some idea, it is welcome !
You definitely have to support the illusion of different instantiations. Consider a generic package that declares a package level variable. Each instantiation of that package must have a separate variable created somewhere.
The RM doesn’t go as far as to require a specific implementation. I believe Janus Ada does it very different from GNAT for example (based off of discussions from comp.lang.ada and the ARG issues).
Maybe there is some sort of means to do it as shared code for the operations and types but store the package state differently? I don’t know DIANA at all, so I don’t know.
I’d say shared generics is not something about Ada 83, but an implementation possibility still applicable to modern Ada. See, for example, note on 2.8 Pragmas | Ada Programming Language
For example, a compiler might use Time vs. Space to control whether generic instantiations are implemented with a macro-expansion model, versus a shared-generic-body model.
It confirms my thoughts that some code sharing and parametrization is possible for generics. I’ll go along this path for Text_io.Integer_io, Float_io, Fixed_io and Enumeration_io, because with only one generic formal NUM or ENUM each in a specific category range < >, digits <>, delta <>, or (<>), the code parametrization seems to be subsumed by providing scalar type definition parameters in an instantiation context.
The mental picture I had when tackling this problem about generics is the variable dynamic context of a recursive procedure : the same code executes in different contexts accessed relative to frame pointers ; hence the idea that a generic instantiation could well result from a generic single compiled body using attributes and contextual references, this single body referencing a particular instantiation parametric context which provides type descriptors and constants/variables proper environment.
I’ll think about it this way, as my problem is to have a fasm assembler expression of this idea, and the notion of fasm namespaces should help.
In fact, in Ada, as soon as you succeed in producing a “do nothing” and a “hello world” program, you are confronted with text io handling, that is implementing simple generics, string manipulations and catenation ; all this being not so obvious. (And this is only Ada 83 ! )
There are two ways to instantiate generics: macro expansion, as done by GNAT, and shared code, as done by Janus/Ada (IIRC, DEC Ada implemented both). Shared code is more complex but results in smaller executable sizes; this was an important consideration in the 1980s, but less so now.
Actually, there’s a really big reason that it should be a strongly considered choice now: proving. — If you are able to prove, at the generic-level, rather than the instantiation, you remove all the work for for the prover at that point. / This would be particularly useful in production of tools (including compilers) that need verification/certificaion.
Can you elaborate ? How would choosing how the compiler implements a feature (details we shouldn’t need to be privy of) makes a difference relative to proving. The semantics of your code would not change at all.
Well, there’s two items to consider: first the code itself, which you bring up.
But secondly, there’s also the transformation process. (That is, compilation.)
If you are proving for verification, then with shared generics you can “collect” your Verification Conditions (proofs) as applied to the unit, and they apply to every instantiation exactly because they are shared, and then having a verified compiler-output WRT generics devolves to proving the “base case” of the production of the shared-generic.
With “textual expansion” there is no such collapsing and every instantiation is going to need to be proved independently; granted, you could “bolt on” the collection function with some cache or db.
TL;DR — It’s WRT the process and handling, not [per se] the semantics.
I have trouble grasping those technicities, this “bolting on” seems very much in the spirit of the RM. The generic spec & body have all the information required, so whatever the method used, I would be surprised if the compiler if either the intermediary representation or object code did not include the info “I hail from this generics”.
Eh, I’d argue not; especially given the APSE documents that were also done. (But that’s a bit different than the translator proper.)
I suspect the reason you’re not seeing it is because you’re focusing on the code-proper, not considering the translation-as-a-process as something you can verify. That’s all fine, but there’s a disturbing amount of programmers who refuse to consider code-as-structure-&-data, opting for “plain text”.
Yes, insofar as how to determine valid/invalid instantiations; Ada’s generics were very well designed to allow compiling/instantiation with the generic-implementation being separate/separately-compilable.
Hello to all !
Thank you for helpful comments.
I find shared generic implementation rather clever and appealing, and it seems to me that it was the intent of the language designers at least to allow it (no naked records in generic parameters for example).
But it is in effect a difficult concept and the relation with the stack framing is not clear to me (where are instance variables located and accessed by a common generic code, how the generic actuals are “passed” to the instantiation ; type descriptors or object addresses).
A generic subprogram is probably not so far from a normal subprogram with instantiation fixed added parameters from generic parametrization.
It seems more complicated for a generic package especially when trying to obtain an assembly engine compatible input for fasm. Nonetheless, the generic package implementation is perhaps amenable to some sort of transparent procedure with added parameters for instantiation.
Now it is easier to be a compiler user than a compiler writer !
Says multiple shared libraries instantiate the same generic, where does the generic go if it’s shared? Does it go into it’s own library? Is it linked with one lib, in each lib?
Because OS library-systems are anemic, they don’t “go” anywhere: the thing that is presented out is the subprogram-endpoints as-instantiated. (This is to say that, AFAIK, there’s no way to have a generic-subprogram presented by a shared library.)
This seems more like an architectural question than one of capability. (I read it as “how much do you factor out commonalities into their own libraries?”.)