1
0
Fork 0
mirror of https://github.com/ganelson/inform.git synced 2024-07-01 06:24:58 +03:00
inform7/inter/bytecode-module/Chapter 2/Transmigration.w
2022-03-11 11:15:11 +00:00

458 lines
21 KiB
OpenEdge ABL

[Transmigration::] Transmigration.
The act of moving a package from one Inter tree to another.
@ The design of Inter has all been leading up to this: how to move a body of
material in one tree, the |migrant| package, into a different tree entirely,
where it will become part of the |destination| package. Transmigration is
how Inter merges material compiled at different times together. For example,
when a Basic Inform project is compiled:
(*) //inform7// compiles source text into tree 1 of Inter code.
(*) //inter// loads a precompiled copy of BasicInformKit as tree 2.
(*) The package |/main/BasicInformKit| in tree 2, really its entire substantive
content, is transmigrated to |/main| in tree 1; it has become |/main/BasicInformKit|
in tree 1. Tree 2 is left almost empty, and is discarded.
(*) //inter// loads a precompiled copy of BasicInformKitExtras as tree 3.
(*) A similar transmigration moves its content to become |/main/BasicInformKitExtras|
in tree 1, and the remains of tree 3 are discarded.
@ Transmigration is a move, not a copy. The destination tree simply makes a node
link to the subtree making up |migrant|: so, in principle, this is a fast process,
but the devil is in the detail. The |migrant| matter may be full of references
to other resources in the origin tree, and those have to be made good. Read
//The Wiring// before tackling the algorithms below.
Because the operation is a move and not a copy, the origin tree will probably
be left with a gaping hole in it: its symbols may be wired to resources which
are no longer there. It may be that the origin tree is going to be discarded
anyway, so that this doesn't matter. If not, setting |tidy_origin| here will
spend some time and effort making it valid again.
Note that the |/main| and |/main/connectors| packages of a tree cannot be
transmigrated, and nor can the root package |/|. Anything else is fair game
to be a |migrant| here.
=
typedef struct transmigration_details {
inter_package *migrant; /* package which is to move between trees */
inter_tree *origin_tree; /* tree it moves from */
inter_tree *destination_tree; /* tree it moves to */
inter_package *origin; /* original parent package of the migrant */
inter_package *destination; /* eventual parent package of the migrant */
inter_bookmark deletion_point; /* where the migrant's head node starts */
inter_bookmark insertion_point; /* where the migrant's head node ends */
inter_bookmark primitives_point; /* where primitives are declared in destination tree */
inter_bookmark ptypes_point; /* where package types are declared in destination tree */
} transmigration_details;
@ =
void Transmigration::move(inter_package *migrant, inter_package *destination,
int tidy_origin) {
inter_package *P = migrant; while (P) P = InterPackage::parent(P); /* make names exist */
transmigration_details det;
@<Initialise the transmigration details@>;
@<Mark the insertion and deletion points with comments@>;
@<Move the head node of the migrant to its new home@>;
@<Correct cross-references between the migrant and the rest of the origin tree@>;
@<Transfer any sockets wired to the migrant@>;
}
@ Both trees will have, at the root level, declarations of the Inter primitives
and package types which they use. (See //building: Large-Scale Structure//.)
Now, they will probably both declare exactly the same set of each: but just in
case the |migrant| package uses primitives or package types not declared at
the root of the |destination_tree|, we will need to declare those as extras,
and we make the bookmarks |primitives_point| and |ptypes_point| to mark where
such extra declarations would go.
|deletion_point| and |insertion_point|, more straightforwardly, mark the
position of the |migrant|'s head node in the origin tree before transmigration
and in the destination tree afterwards.
@<Initialise the transmigration details@> =
det.migrant = migrant;
det.origin = InterPackage::parent(migrant);
det.destination = destination;
det.origin_tree = InterPackage::tree(migrant);
det.destination_tree = InterPackage::tree(destination);
det.deletion_point = InterBookmark::after_this_node(migrant->package_head);
det.insertion_point = InterBookmark::at_end_of_this_package(destination);
inter_tree_node *prims = NULL;
LOOP_THROUGH_INTER_CHILDREN(F, destination->package_head->tree->root_node)
if (F->W.instruction[ID_IFLD] == PRIMITIVE_IST)
prims = F;
if (prims == NULL) internal_error("destination has no primitives");
det.primitives_point = InterBookmark::after_this_node(prims);
inter_tree_node *ptypes = NULL;
LOOP_THROUGH_INTER_CHILDREN(F, destination->package_head->tree->root_node)
if (F->W.instruction[ID_IFLD] == PACKAGETYPE_IST)
ptypes = F;
if (ptypes == NULL) internal_error("destination has no package types");
det.ptypes_point = InterBookmark::after_this_node(ptypes);
@<Mark the insertion and deletion points with comments@> =
Transmigration::comment(&det.deletion_point, InterPackage::baseline(migrant),
I"Transmigration removed", InterPackage::name(migrant));
Transmigration::comment(&det.insertion_point, InterPackage::baseline(destination) + 1,
I"Transmigration inserted", InterPackage::name(migrant));
@ =
void Transmigration::comment(inter_bookmark *IBM, int level, text_stream *action,
text_stream *content) {
TEMPORARY_TEXT(C)
WRITE_TO(C, "%S %S here", action, content);
CommentInstruction::new(IBM, C, NULL, (inter_ti) level);
DISCARD_TEXT(C)
}
@ This is the only point anywhere in the Inform tool chain where a node is moved
between packages. What makes it tricky is that the symbol giving the name of the
|migrant| package, which was in the original parent package's symbols table,
must be deleted. A new symbol with the same name must be created in the symbols
table for the destination package, and the bytecode for the |package| instruction
at the node must be altered to conform with this. But when we are done, all is
consistent again.
@<Move the head node of the migrant to its new home@> =
text_stream *migrant_name = Str::duplicate(InterPackage::name(migrant));
inter_symbol *OS = InterSymbolsTable::symbol_from_name_not_following(
InterPackage::parent(migrant)->package_scope, migrant_name);
if (OS) InterSymbolsTable::remove_symbol(OS);
NodePlacement::move_to_moving_bookmark(migrant->package_head, &det.insertion_point);
migrant->package_head->tree = det.destination_tree;
migrant->package_head->package = destination;
inter_symbol *NS = InterSymbolsTable::symbol_from_name_creating(
destination->package_scope, migrant_name);
PackageInstruction::set_name_symbol(migrant, NS);
if (InterPackage::container(migrant->package_head) != destination)
internal_error("transmigration did not take the migrant to the right place");
inter_symbol *S = PackageInstruction::name_symbol(migrant);
if (NS != S) internal_error("transmigration of head node and symbol failed");
@ That was the easy part. The migrant package is now inside the destination tree.
Unfortunately:
(*) |migrant| may contain symbols |S ~~> O| wired to symbols |O| still in the origin
tree, because they lay outside |migrant|. This means the destination tree is now
incorrect.
(*) The origin tree may contain symbols |O ~~> S| wired to symbols |S| in the
migrant, which are therefore not in the origin tree any more. This means the origin
tree is now incorrect.
@<Correct cross-references between the migrant and the rest of the origin tree@> =
InterTree::traverse(det.destination_tree, Transmigration::correct_migrant, &det, migrant, 0);
if (tidy_origin)
InterTree::traverse(det.origin_tree, Transmigration::correct_origin, &det, NULL, 0);
@ A further issue is that the original tree may have offered sockets for some
of the definitions in |migrant|. For example, |migrant| might contain some
useful function, which the origin tree was offering for linking. We want to
make an equivalent socket in the destination tree for the same function.
This is important for two reasons: firstly, because after transmigration, the
caller will need to use those sockets (see //Wiring::connect_plugs_to_sockets//),
and secondly, because this may be only one of a series of transmigrations of
Inter kits. All those kits need to see each others' sockets. So we cannot assume
that having transmigrated BasicInformKit (say), we no longer need its resources
to be socketed.
@<Transfer any sockets wired to the migrant@> =
inter_package *origin_connectors =
LargeScale::connectors_package_if_it_exists(det.origin_tree);
if (origin_connectors) {
inter_symbols_table *T = InterPackage::scope(origin_connectors);
LOOP_OVER_SYMBOLS_TABLE(S, T) {
if (InterSymbol::is_socket(S)) {
inter_symbol *target = Wiring::cable_end(S);
if (InterSymbol::defined_inside(target, migrant))
@<S is a socket wired to a definition in the migrant@>;
}
}
}
@ The difficult case here is where the destination already has a socket of
the same name. This would happen, for instance, if you transmigrated two kits
in turn, and both of them provided a function called |OverlyEagerFn()|: the
first time, a socket would be made in the destination tree; but the second
time we would find that a socket already existed.
This is arguably just a linking error and we should halt. In fact we let it
slide, and allow the destination's original socket to remain as it was. We do
this because the issues involved in linking property/attribute declarations in
WorldModelKit with their Inform 7 counterparts in the main tree are just too
awkward to confront here. (Essentially, this is a situation where the same
declaration is made twice, once in each tree, an issue to confront later. They
will end up meaning the same thing, though, so it's fine to keep using the
existing socket here.)
@<S is a socket wired to a definition in the migrant@> =
LOGIF(INTER_CONNECTORS, "Origin offers socket $3 ~~> $3 in migrant\n", S, target);
inter_symbol *equivalent = Wiring::find_socket(det.destination_tree, InterSymbol::identifier(S));
if (equivalent) {
inter_symbol *e_target = Wiring::cable_end(equivalent);
if (InterSymbol::is_defined(e_target) == FALSE) {
LOGIF(INTER_CONNECTORS, "Co-opted undefined socket $3 ~~> $3\n",
equivalent, e_target);
Wiring::wire_to(equivalent, target);
Wiring::wire_to(e_target, target);
} else {
LOGIF(INTER_CONNECTORS,
"There is already a socket %S ~~> $3\n"
"We use this rather than continue with %S ~~> $3\n",
InterSymbol::identifier(S), e_target, InterSymbol::identifier(S), target);
}
} else {
Wiring::socket(det.destination_tree, InterSymbol::identifier(S), S);
}
@ Okay, so now for the first cross-referencing fix. The following function traverses
every node inside the |migrant| tree. The first thing to do is to correct |P->tree|,
which records, for every node, the tree to which it belongs: this is why the traverse
needs to visit every node inside |migrant| (including its own head node).
But then there are only two cases of interest: primitive invocations, and package
head nodes.
=
void Transmigration::correct_migrant(inter_tree *I, inter_tree_node *P, void *state) {
transmigration_details *det = (transmigration_details *) state;
P->tree = I;
inter_symbol *primitive = InvInstruction::read_primitive(det->origin_tree, P);
if (primitive)
@<Transfer from a primitive in the origin tree to one in the destination@>;
if (P->W.instruction[ID_IFLD] == PACKAGE_IST)
@<This is the headnode of a subpackage of migrant@>;
}
@ Primitive invocations matter because, say, |inv !printnumber| in the migrant
will contain a reference to the origin tree's definition of |!printnumber|; this
must be converted to a reference to the destination's definition of the same thing.
Note that we expect to perform this operation frequently -- there may be, say,
10,000 primitive invocations in the migrant, but always of the same 50 or so
primitives round and around -- so we cache the results.
@<Transfer from a primitive in the origin tree to one in the destination@> =
inter_symbol *equivalent_primitive = Transmigration::known_equivalent(primitive);
if (equivalent_primitive == NULL) {
equivalent_primitive = InterSymbolsTable::symbol_from_name(
InterTree::global_scope(det->destination_tree), InterSymbol::identifier(primitive));
if (equivalent_primitive == NULL) @<Duplicate this primitive@>;
Transmigration::learn_equivalent(primitive, equivalent_primitive);
}
if (equivalent_primitive)
InvInstruction::write_primitive(det->destination_tree, P, equivalent_primitive);
@ In the worst-case scenario, the destination might not even have a declaration
of |!printnumber|. (Actually this is unlikely in practice, because we tend to make
the same set of primitive declarations in every tree.) In this case, we write a
new declaration in the root package of the destination, duplicating the one in
the root package of the origin.
@<Duplicate this primitive@> =
equivalent_primitive = InterSymbolsTable::symbol_from_name_creating(
InterTree::global_scope(det->destination_tree), InterSymbol::identifier(primitive));
inter_tree_node *D = Inode::new_with_1_data_field(&(det->primitives_point), PRIMITIVE_IST,
InterSymbolsTable::id_from_symbol(det->destination_tree, NULL, equivalent_primitive),
NULL, 0);
inter_tree_node *old_D = primitive->definition;
for (int i=SIGNATURE_PRIM_IFLD; i<old_D->W.extent; i++) {
Inode::extend_instruction_by(D, 1);
D->W.instruction[i] = old_D->W.instruction[i];
}
inter_error_message *E = VerifyingInter::instruction(
InterBookmark::package(&(det->primitives_point)), D);
if (E) {
InterErrors::issue(E);
equivalent_primitive = NULL;
} else {
NodePlacement::move_to_moving_bookmark(D, &(det->primitives_point));
}
@<This is the headnode of a subpackage of migrant@> =
inter_package *pack = PackageInstruction::at_this_head(P);
if (InterPackage::is_a_linkage_package(pack))
internal_error("tried to transmigrate /main, /main/connectors or /");
@<Correct the reference to this package type@>;
@<Correct outbound wiring from the package's symbols table@>;
@ Package types present the exact same issue as primitive invocations: the types
are given in terms of declarations in the origin tree, and have to be transferred
to matching declarations in the destination.
@<Correct the reference to this package type@> =
inter_symbol *original_ptype = PackageInstruction::get_type_of(det->origin_tree, P);
inter_symbol *equivalent_ptype = Transmigration::known_equivalent(original_ptype);
if (equivalent_ptype == NULL) {
equivalent_ptype = InterSymbolsTable::symbol_from_name(
InterTree::global_scope(det->destination_tree), InterSymbol::identifier(original_ptype));
if (equivalent_ptype == NULL) @<Duplicate this package type@>;
Transmigration::learn_equivalent(original_ptype, equivalent_ptype);
}
PackageInstruction::set_type(det->destination_tree, P, equivalent_ptype);
@<Duplicate this package type@> =
equivalent_ptype = InterSymbolsTable::symbol_from_name_creating(
InterTree::global_scope(det->destination_tree), InterSymbol::identifier(original_ptype));
inter_tree_node *D = Inode::new_with_1_data_field(&(det->ptypes_point), PACKAGETYPE_IST,
InterSymbolsTable::id_from_symbol(det->destination_tree, NULL, equivalent_ptype), NULL, 0);
inter_error_message *E = VerifyingInter::instruction(
InterBookmark::package(&(det->ptypes_point)), D);
if (E) {
InterErrors::issue(E);
equivalent_ptype = NULL;
} else {
NodePlacement::move_to_moving_bookmark(D, &(det->ptypes_point));
}
@ Here |S| is some miscellaneous symbol in our subpackage of |migrant| -- it
can't be either a plug or a socket, since the connectors never migrate -- and
there are three bad possibilities:
@<Correct outbound wiring from the package's symbols table@> =
inter_symbols_table *T = InterPackage::scope(pack);
if (T == NULL) internal_error("package with no symbols");
LOOP_OVER_SYMBOLS_TABLE(S, T) {
if (Wiring::is_wired(S)) {
inter_symbol *target = Wiring::cable_end(S);
if (SymbolAnnotation::get_b(target, ARCHITECTURAL_IANN))
@<S is wired to an architectural symbol in the origin tree@>
else if (InterSymbol::is_plug(target))
@<S is wired to a loose plug in the origin tree@>
else if (InterSymbol::defined_inside(S, det->migrant) == FALSE)
@<S is wired to a miscellaneous symbol still in the origin tree@>
}
}
@ For example, |S| is wired to |WORDSIZE| ih the origin tree, which is (let
us say) a constant equal to 4. We wire it instead to |WORDSIZE| in the destination
tree, which will also be equal to 4 because we only ever transmigrate between
trees with the same Inter architecture.
@<S is wired to an architectural symbol in the origin tree@> =
inter_symbol *equivalent = Transmigration::known_equivalent(target);
if (equivalent == NULL) {
equivalent = LargeScale::find_architectural_symbol(det->destination_tree,
InterSymbol::identifier(target));
Transmigration::learn_equivalent(target, equivalent);
}
Wiring::wire_to(S, equivalent);
@ Here |S| is wired to a plug, and it must be a loose plug because |target| is
the cable-end from |S|: if that cable ends in a plug, clearly the plug is not
wired to a socket. That means it is wired to a name, |target ~~> "some_name"|.
We wire |S| instead to a plug seeking |some_name| in the destination tree;
note that this may result in a new plug being made there, or may re-use an
existing one already looking for (or indeed already having found) |"some_name"|.
@<S is wired to a loose plug in the origin tree@> =
inter_symbol *equivalent = Transmigration::known_equivalent(target);
if (equivalent == NULL) {
text_stream *N = Wiring::wired_to_name(target);
equivalent = Wiring::plug(det->destination_tree, N);
Transmigration::learn_equivalent(target, equivalent);
}
Wiring::wire_to(S, equivalent);
@ Finally |S| may be wired to some ordinary symbol defined in the origin tree
but outside of |migrant|. Well, that resource is now outside of the destination
tree: and this is exactly what plugs in the destination tree are for.
@<S is wired to a miscellaneous symbol still in the origin tree@> =
inter_symbol *equivalent = Transmigration::known_equivalent(target);
if (equivalent == NULL) {
equivalent = Wiring::plug(det->destination_tree, InterSymbol::identifier(target));
Transmigration::learn_equivalent(target, equivalent);
}
Wiring::wire_to(S, equivalent);
@ Now time for the second sort of correction: references from the origin tree
into the migrant. If we care about those, then we traverse so that the following
visits every node in the origin tree. Note that at this point the head node
of |migrant| has been removed from the origin tree -- so this visitor can never
visit anything inside |migrant|.
Note that we do not correct references from the origin tree's |/main/connectors|
package, i.e., plugs and sockets wired to something in |migrant|; we handle
those separately (see above).
=
void Transmigration::correct_origin(inter_tree *I, inter_tree_node *P, void *state) {
transmigration_details *det = (transmigration_details *) state;
if (P->W.instruction[ID_IFLD] == PACKAGE_IST) {
inter_package *pack = PackageInstruction::at_this_head(P);
if (InterPackage::is_a_linkage_package(pack) == FALSE) {
inter_symbols_table *T = InterPackage::scope(pack);
LOOP_OVER_SYMBOLS_TABLE(S, T)
if (Wiring::is_wired(S)) {
inter_symbol *target = Wiring::cable_end(S);
if (InterSymbol::defined_inside(target, det->migrant))
@<S is wired to a symbol in the migrant@>;
}
}
}
}
@ This is now symmetrical to the case above. |S| is wired to what is now a
resource in a different tree, so it needs to be wired to a plug instead.
@<S is wired to a symbol in the migrant@> =
inter_symbol *equivalent = Transmigration::known_equivalent(target);
if (equivalent == NULL) {
equivalent = Wiring::plug(det->origin_tree, InterSymbol::identifier(S));
Transmigration::learn_equivalent(target, equivalent);
}
Wiring::wire_to(S, equivalent);
@ That just leaves the cache. The idea is that for each different act of
transmigration, we want to cache the symbol conversions made. The following
is fast, but a little wasteful of memory, since it involves storing two fields
in every //inter_symbol//:
=
typedef struct transmigration_data {
int valid_on_which_transmigration;
struct inter_symbol *cached_equivalent;
} transmigration_data;
@ =
transmigration_data Transmigration::new_transmigration_data(inter_symbol *S) {
transmigration_data td;
td.valid_on_which_transmigration = 0;
td.cached_equivalent = NULL;
return td;
}
@ The scheme is that each different act of transmigration has its own unique
ID, counting upwards from 1.
=
int current_transmigration_count = 1;
void Transmigration::begin_cache_session(void) {
++current_transmigration_count;
}
@ This count is used only to see if the |cached_equivalent| symbol was set
during the current transmigration (rather than some previous one). Using this
count is quicker, since it saves the time needed to walk through all existing
symbols resetting the |cached_equivalent| fields to |NULL|.
=
void Transmigration::learn_equivalent(inter_symbol *S, inter_symbol *V) {
S->transmigration.cached_equivalent = V;
S->transmigration.valid_on_which_transmigration = current_transmigration_count;
}
inter_symbol *Transmigration::known_equivalent(inter_symbol *S) {
if (S->transmigration.valid_on_which_transmigration == current_transmigration_count)
return S->transmigration.cached_equivalent;
return NULL;
}