How the vanilla code generation strategy handles instances, kinds, and properties.


§1. Properties. Early in code-generation, we declare the properties. Generators might want to represent these in all kinds of ways for the sake of efficiency; on Inform 6, for example, some either-or properties of objects may be represented as "attributes". But that's not our concern. We will try to keep the model as simple as possible here.

What we assume is that:

§2. The biggest complication we face is that the linking process has left us, in some cases, with multiple property declarations for what is actually the same property.

For example, this arises when a property is defined in Inform 7 source like so:

A room can be privately-named or publicly-named.
The privately-named property translates into Inter as "privately_named".

...where the property privately_named actually originates in a kit, written in Inform 6 notation like so:

Attribute privately_named;

We now have two property declarations, one in the Standard Rules module, the other in the BasicInformKit module. It's tempting to have the linker delete the Standard Rules one and convert references to it to point them to the kit definition, but this is not a good idea because the kit definition doesn't have the metadata or permissions which the Standard Rules definition has. So we keep both in play, and reconcile them in the code below.

It gets worse: the Standard Rules properties "lighted" and "lit", though different — one applies to rooms, one to things — both translate to the same BasicInformKit property light. At present that's the worst case scenario (i.e., three different properties all coinciding) but we won't assume that.

So what we do is to work through the properties and group them into equivalence classes by their final identifier names. Here, for example, we recognise these two properties as the same because they both want to be called privately_named. By scanning the assimilated properties (i.e. those from kits) first, we ensure that the first one found in each set will be the definitive source of the property. But it will likely be the later members of the set which have the necessary metadata attached.

Of course, in the benign case where there is just one Inform 7-level definition of a property, first_with_name and last_with_name will be the same, and the list will be a singleton.

void VanillaObjects::declare_properties(code_generation *gen) {
    dictionary *first_with_name = Dictionaries::new(1024, FALSE);  of inter_symbol
    dictionary *last_with_name = Dictionaries::new(1024, FALSE);  of inter_symbol
    dictionary *all_with_name = Dictionaries::new(1024, FALSE);  of linked_list of inter_symbol

    inter_symbol *prop_name;
    LOOP_OVER_LINKED_LIST(prop_name, inter_symbol, gen->assimilated_properties)
        Group the properties by name2.1;
    LOOP_OVER_LINKED_LIST(prop_name, inter_symbol, gen->unassimilated_properties)
        Group the properties by name2.1;
    LOOP_OVER_LINKED_LIST(prop_name, inter_symbol, gen->assimilated_properties)
        Declare one property for each name group2.2;
    LOOP_OVER_LINKED_LIST(prop_name, inter_symbol, gen->unassimilated_properties)
        Declare one property for each name group2.2;
}

§2.1. Group the properties by name2.1 =

    text_stream *name = InterSymbol::trans(prop_name);
    if (Dictionaries::find(last_with_name, name) == NULL) {
        text_stream *inner_name = Str::duplicate(name);
        Dictionaries::create(last_with_name, inner_name);
        Dictionaries::write_value(last_with_name, inner_name, (void *) prop_name);
        linked_list *L = NEW_LINKED_LIST(inter_symbol);
        ADD_TO_LINKED_LIST(prop_name, inter_symbol, L);
        Dictionaries::create(all_with_name, inner_name);
        Dictionaries::write_value(all_with_name, inner_name, (void *) L);
    } else {
        Dictionaries::write_value(last_with_name, name, (void *) prop_name);
        linked_list *L = Dictionaries::read_value(all_with_name, name);
        ADD_TO_LINKED_LIST(prop_name, inter_symbol, L);
    }

§2.2. So here's an annoyance. We will need two identifier names for each property. One is the metadata array, while the other will probably be used by the generator to hold the actual storage — that other is called the "inner name".

In the case of our privately_named example, the metadata array will be called something like A_privately_named, and any references to the property in kit code or in Inform 7 source text will compile to this array. The inner name will preserve the original identifier privately_named, and will likely be used by the final generator for where a property value is actually stored. For Inform 6, for example, we will have:

    A_privately_named --> 0     2
                      --> 1     privately_named (an I6 attribute)
                      --> 2     1
                      --> 3     "privately named"
                      --> 4     ... permissions follow

In some ways it would be more convenient to use these names the other way around: to call the array itself privately_named and have the inner identifier be something like I_privately_named. But this fails on Inform 6 in exasperating ways because of the built-in name property, whose name cannot be declared or altered.

Declare one property for each name group2.2 =

    text_stream *name = InterSymbol::trans(prop_name);
    text_stream *inner_name = NULL;
    if (Dictionaries::find(first_with_name, name) == NULL) {
        LOGIF(PROPERTY_ALLOCATION, "! NEW name=%S   sname=%S   eor=%d   assim=%d\n",
            name, InterSymbol::identifier(prop_name),
            VanillaObjects::is_either_or_property(prop_name),
            SymbolAnnotation::get_b(prop_name, ASSIMILATED_IANN));
        inner_name = Str::duplicate(name);
        Dictionaries::create(first_with_name, inner_name);
        Dictionaries::write_value(first_with_name, inner_name, (void *) prop_name);
        SymbolAnnotation::set_t(gen->from, InterSymbol::package(prop_name),
            prop_name, INNER_PROPERTY_NAME_IANN, inner_name);
        Set the translation to a new metadata array2.2.1;
    } else {
        LOGIF(PROPERTY_ALLOCATION, "! OLD name=%S   sname=%S   eor=%d   assim=%d\n",
            name, InterSymbol::identifier(prop_name),
            VanillaObjects::is_either_or_property(prop_name),
            SymbolAnnotation::get_b(prop_name, ASSIMILATED_IANN));
        inter_symbol *existing_prop_name =
            (inter_symbol *) Dictionaries::read_value(first_with_name, name);
        inner_name = VanillaObjects::inner_property_name(gen, existing_prop_name);
        InterSymbol::set_translate(prop_name, InterSymbol::trans(existing_prop_name));
        SymbolAnnotation::set_t(gen->from, InterSymbol::package(prop_name),
            prop_name, INNER_PROPERTY_NAME_IANN, inner_name);
    }
    LOGIF(PROPERTY_ALLOCATION, "! Translation %S, inner name %S\n",
        InterSymbol::trans(prop_name), VanillaObjects::inner_property_name(gen, prop_name));

§2.2.1. Note that Generators::declare_property calls the generator to ask it to create the first two entries in the metadata array. Those can be anything the generator wants.

Set the translation to a new metadata array2.2.1 =

    text_stream *array_name = Str::new();
    WRITE_TO(array_name, "A_%S", inner_name);
    InterSymbol::set_translate(prop_name, array_name);

    linked_list *all_forms = (linked_list *) Dictionaries::read_value(all_with_name, name);

    segmentation_pos saved;
    Generators::begin_array(gen, array_name, prop_name, NULL, WORD_ARRAY_FORMAT, -1, &saved);
    Generators::declare_property(gen, prop_name, all_forms);
    Write the either-or flag2.2.1.1;
    Write the property name in double quotes2.2.1.2;
    Write a list of kinds or objects which are permitted to have this property2.2.1.3;
    Generators::mangled_array_entry(gen, I"NULL", WORD_ARRAY_FORMAT);
    Generators::end_array(gen, WORD_ARRAY_FORMAT, -1, &saved);

§2.2.1.1. Write the either-or flag2.2.1.1 =

    if (VanillaObjects::is_either_or_property(prop_name))
        Generators::array_entry(gen, I"1", WORD_ARRAY_FORMAT);
    else
        Generators::array_entry(gen, I"0", WORD_ARRAY_FORMAT);

§2.2.1.2. Note that we extract the printed name from the last property in the set, because that will come from an I7 source text definition.

Write the property name in double quotes2.2.1.2 =

    inter_symbol *last_prop_name =
        (inter_symbol *) Dictionaries::read_value(last_with_name, name);
    text_stream *pname =
        Metadata::optional_textual(InterSymbol::package(last_prop_name), I"^name");
    if (pname == NULL) pname = I"<nameless>";
    TEMPORARY_TEXT(entry)
    CodeGen::select_temporary(gen, entry);
    Generators::compile_literal_text(gen, pname, TRUE);
    CodeGen::deselect_temporary(gen);
    Generators::array_entry(gen, entry, WORD_ARRAY_FORMAT);
    DISCARD_TEXT(entry)

§2.2.1.3. Type-safety at runtime is managed with a hybrid of compile-time and runtime checking. Compile-time checking polices all uses of properties of values other than objects, but it will usually allow any object property of any object to be accessed, because it's not usually possible for the typechecker to know if an object value O is a vehicle, a direction, and so on. For this reason some runtime checking is needed, and to perform that checking, properties need a list of permissions to be stored in memory. This is where.

Note that permissions are accumulated for all of the properties in a given name set. In the case of "lighted" and "lit" and light, therefore, the permissions written will be those for "lighted" (rooms, basically) and then those for "lit" (things); light, the WorldModelKit original, has no permissions — assimilated properties never do have.

Write a list of kinds or objects which are permitted to have this property2.2.1.3 =

    inter_tree *I = gen->from;
    inter_symbol *eprop_name;
    LOOP_OVER_LINKED_LIST(eprop_name, inter_symbol, all_forms) {
        inter_node_list *EVL =
            InterWarehouse::get_node_list(InterTree::warehouse(I),
                PropertyInstruction::permissions_list(eprop_name));
        List any kind of object with an explicit permission2.2.1.3.1;
        List any individual instance with an explicit permission2.2.1.3.2;
        List all top-level kinds if "object" itself has an explicit permission2.2.1.3.3;
    }

§2.2.1.3.1. List any kind of object with an explicit permission2.2.1.3.1 =

    inter_symbol *kind_s;
    LOOP_OVER_LINKED_LIST(kind_s, inter_symbol, gen->kinds_in_declaration_order)
        if (VanillaObjects::is_kind_of_object(gen, kind_s)) {
            inter_tree_node *X;
            LOOP_THROUGH_INTER_NODE_LIST(X, EVL) {
                inter_symbol *owner_s = PermissionInstruction::owner(X);
                if (owner_s == kind_s)
                    Generators::symbol_array_entry(gen, kind_s, WORD_ARRAY_FORMAT);
            }
        }

§2.2.1.3.2. An unusual feature of Inform as a programming language is that individual objects can be given properties, even when other objects of the same kind may lack them. So:

List any individual instance with an explicit permission2.2.1.3.2 =

    inter_symbol *inst_s;
    LOOP_OVER_LINKED_LIST(inst_s, inter_symbol, gen->instances_in_declaration_order)
        if (VanillaObjects::is_kind_of_object(gen, InstanceInstruction::typename(inst_s))) {
            inter_tree_node *X;
            LOOP_THROUGH_INTER_NODE_LIST(X, EVL) {
                inter_symbol *owner_s = PermissionInstruction::owner(X);
                if (owner_s == inst_s)
                    Generators::symbol_array_entry(gen, inst_s, WORD_ARRAY_FORMAT);
            }
        }

§2.2.1.3.3. It's happily a rare occurrence, but "object" itself can have properties — so that every object of any kind has permission to have that. We convey that by giving permission for every top-level kind of object. (There are typically only four of these top-level kinds, and not many properties have these permissions, so it's not as wasteful as it looks.)

List all top-level kinds if "object" itself has an explicit permission2.2.1.3.3 =

    inter_tree_node *X;
    LOOP_THROUGH_INTER_NODE_LIST(X, EVL) {
        inter_symbol *owner_s = PermissionInstruction::owner(X);
        if (owner_s == RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM)) {
            Generators::mangled_array_entry(gen, I"K0_kind", WORD_ARRAY_FORMAT);
            inter_symbol *kind_s;
            LOOP_OVER_LINKED_LIST(kind_s, inter_symbol, gen->kinds_in_declaration_order) {
                if (TypenameInstruction::super(kind_s) == RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM)) {
                    Generators::symbol_array_entry(gen, kind_s, WORD_ARRAY_FORMAT);
                }
            }
        }
    }

§3. Generators can then access the inner name (if they want it) thus:

text_stream *VanillaObjects::inner_property_name(code_generation *gen, inter_symbol *prop_name) {
    text_stream *inner_name = SymbolAnnotation::get_t(prop_name,
        gen->from, INNER_PROPERTY_NAME_IANN);
    if (inner_name == NULL) inner_name = I"<nameless>";
    return inner_name;
}

§4. Instances, kinds and values of properties. Round two is to make declarations of our kinds and instances. Again, we want to make as few assumptions as possible about the eventual runtime representation of these ideas, but that will not be no assumptions.

In particular, whereas generators can stash object properties more or less in any way they like, they are required to stash properties of non-object values in small arrays called "sticks", which mimic table columns. This is imposed by the language itself, which allows properties of values to be defined by tables of values.

So while it would be attractive here to make no distinction between objects and other property-owners, we cannot do so.

void VanillaObjects::declare_kinds_and_instances(code_generation *gen) {
    Declare kinds of value4.1;
    Declare kinds of object4.2;
    Declare instances4.3;
}

§4.1. We start then with kinds which have properties but are not kinds of objects. We want to ensure that no property is assigned more than once (for the same kind), so we use "marks" on those already done.

Declare kinds of value4.1 =

    int unique_kovp_id = 0;
    inter_symbol *kind_s;
    LOOP_OVER_LINKED_LIST(kind_s, inter_symbol, gen->kinds_in_declaration_order) {
        if (VanillaObjects::value_kind_with_properties(gen, kind_s)) {
            segmentation_pos saved;
            Generators::declare_kind(gen, kind_s, &saved);
            inter_symbol *prop_name;
            LOOP_OVER_LINKED_LIST(prop_name, inter_symbol, gen->unassimilated_properties)
                CodeGen::unmark(prop_name);
            Declare properties which every instance of this kind of value can have4.1.1;
            Declare properties which only some instances of this kind of value can have4.1.2;
            Generators::end_kind(gen, kind_s, saved);
        }
    }

§4.1.1. Declare properties which every instance of this kind of value can have4.1.1 =

    inter_node_list *FL = TypenameInstruction::permissions_list(kind_s);
    Work through this node list of permissions4.1.1.1;

§4.1.2. Declare properties which only some instances of this kind of value can have4.1.2 =

    inter_symbol *inst_s;
    LOOP_OVER_LINKED_LIST(inst_s, inter_symbol, gen->instances_in_declaration_order) {
        if (TypenameInstruction::is_a(InstanceInstruction::typename(inst_s), kind_s)) {
            inter_node_list *FL = InstanceInstruction::permissions_list(inst_s);
            Work through this node list of permissions4.1.1.1;
        }
    }

§4.1.1.1. Work through this node list of permissions4.1.1.1 =

    inter_tree_node *X;
    LOOP_THROUGH_INTER_NODE_LIST(X, FL) {
        inter_symbol *prop_name = PermissionInstruction::property(X);
        if (prop_name == NULL) internal_error("no property");
        if (CodeGen::marked(prop_name) == FALSE) {
            CodeGen::mark(prop_name);
            Assign the property values for this property4.1.1.1.1;
        }
    }

§4.1.1.1.1. In the case where a kind of value has been created by table, as in this example:

    Planet is a kind of value. The planets are defined by the Table of Outer Planets.

    Table of Outer Planets
    planet      semimajor axis
    Jupiter     5 AU
    Saturn      10 AU
    Uranus      19 AU
    Neptune     30 AU
    Pluto       39 AU

the property "semimajor axis" is already stored in a table column. That becomes our stick array. But in other cases, where the instances have not been created by table, no sticks exist and we must compile them.

Assign the property values for this property4.1.1.1.1 =

    text_stream *ident = NULL;
    inter_symbol *store = PermissionInstruction::storage(X);
    if (store) {
        ident = InterSymbol::trans(store);
    } else {
        ident = Str::new();
        WRITE_TO(ident, "KOVP_%d", unique_kovp_id++);
        Compile a stick of property values and put its address here4.1.1.1.1.1;
    }
    Generators::assign_properties(gen, kind_s, prop_name, ident);

§4.1.1.1.1.1. These little arrays are sticks of property values, and they are laid out as if they were column arrays in a Table data structure. This means they must be TABLE_ARRAY_FORMAT arrays (which wastes one word of memory) and must have blanked-out table column header words at the front (which wastes a further COL_HSIZE words). But the cost is a simple overhead, not rising with the number of instances, and is worth it for simplicity and speed.

Compile a stick of property values and put its address here4.1.1.1.1.1 =

    segmentation_pos saved;
    Generators::begin_array(gen, ident, NULL, NULL, TABLE_ARRAY_FORMAT, -1, &saved);
    Generators::array_entry(gen, I"0", TABLE_ARRAY_FORMAT);
    Generators::array_entry(gen, I"0", TABLE_ARRAY_FORMAT);
    inter_symbol *inst_s;
    LOOP_OVER_LINKED_LIST(inst_s, inter_symbol, gen->instances_in_declaration_order) {
        if (TypenameInstruction::is_a(InstanceInstruction::typename(inst_s), kind_s)) {
            int found = 0;
            inter_node_list *PVL = InstanceInstruction::properties_list(inst_s);
            Work through this node list of values4.1.1.1.1.1.1;
            PVL = TypenameInstruction::properties_list(kind_s);
            Work through this node list of values4.1.1.1.1.1.1;
            if (found == 0) Generators::array_entry(gen, I"0", TABLE_ARRAY_FORMAT);
        }
    }
    Generators::end_array(gen, TABLE_ARRAY_FORMAT, -1, &saved);

§4.1.1.1.1.1.1. Work through this node list of values4.1.1.1.1.1.1 =

    inter_tree_node *Y;
    LOOP_THROUGH_INTER_NODE_LIST(Y, PVL) {
        inter_symbol *p_name = PropertyValueInstruction::property(Y);
        if ((p_name == prop_name) && (found == 0)) {
            found = 1;
            inter_pair pair = PropertyValueInstruction::value(Y);
            TEMPORARY_TEXT(val)
            CodeGen::select_temporary(gen, val);
            CodeGen::pair(gen, Y, pair);
            CodeGen::deselect_temporary(gen);
            Generators::array_entry(gen, val, TABLE_ARRAY_FORMAT);
            DISCARD_TEXT(val)
        }
    }

§4.2. So now for the objects. First we declare each kind of object, first calling Generators::declare_kind, then Generators::assign_property for each property value, and then Generators::end_kind.

Declare kinds of object4.2 =

    inter_symbol *kind_s;
    LOOP_OVER_LINKED_LIST(kind_s, inter_symbol, gen->kinds_in_declaration_order) {
        if ((kind_s == RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM)) ||
            (VanillaObjects::is_kind_of_object(gen, kind_s))) {
            segmentation_pos saved;
            Generators::declare_kind(gen, kind_s, &saved);
            VanillaObjects::append(gen, kind_s);
            inter_node_list *FL = TypenameInstruction::properties_list(kind_s);
            Declare the properties of this kind or instance4.2.1;
            Generators::end_kind(gen, kind_s, saved);
        }
    }

§4.3. And then the instances. As with kinds, we call Generators::declare_instance, then give some property values, then call Generators::end_instance.

With instances of values, note that we have no property assignment to do: that was all taken care of with the sticks of property values already declared.

Declare instances4.3 =

    inter_symbol *inst_s;
    LOOP_OVER_LINKED_LIST(inst_s, inter_symbol, gen->instances_in_declaration_order) {
        inter_symbol *inst_kind = InstanceInstruction::typename(inst_s);
        int N = -1;
        inter_symbol *object_kind = RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM);
        if ((object_kind == NULL) || (TypenameInstruction::is_a(inst_kind, object_kind) == FALSE))
            N = (int) InterValuePairs::to_number(
                InstanceInstruction::enumerated_value(inst_s));
        segmentation_pos saved;
        Generators::declare_instance(gen, inst_s, inst_kind, N, &saved);
        if (TypenameInstruction::is_a(inst_kind, RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM))) {
            VanillaObjects::append(gen, inst_s);
            inter_node_list *FL = InstanceInstruction::properties_list(inst_s);
            Declare the properties of this kind or instance4.2.1;
        }
        Generators::end_instance(gen, inst_s, inst_kind, saved);
    }

§4.2.1. The following, then, is used either for properties of a kind of object, or properties of an instance of object, and issues a stream of Generators::assign_property function calls.

Declare the properties of this kind or instance4.2.1 =

    inter_tree_node *X;
    LOOP_THROUGH_INTER_NODE_LIST(X, FL)
        Generators::assign_property(gen,
            PropertyValueInstruction::property(X),
            PropertyValueInstruction::value(X),
            X);

§5. That just leaves the following horrible function, which is called for each kind or instance of object, and passes raw splat matter down into the declaration which the generator is making.

This is a bad idea, because it presupposes which generator is being used, or at any rate what the syntax will be. It arises from source text like this:

Include (-
    with before [; Go: return 1; ],
-) when defining a rideable vehicle.

...which should probably not be allowed. The splat is the text between (- and -) here, and as can be seen, it's in Inform 6 syntax, which would be bad news for, say, the C generator.

void VanillaObjects::append(code_generation *gen, inter_symbol *symb) {
    text_stream *OUT = CodeGen::current(gen);
    inter_tree *I = gen->from;
    text_stream *S = SymbolAnnotation::get_t(symb, I, APPEND_IANN);
    if (Str::len(S) > 0) Vanilla::splat_matter(OUT, I, S);
}

§6. Utility functions. Returns the weak ID of a kind, which is a small integer known at compile time.

int VanillaObjects::weak_id(inter_symbol *kind_s) {
    inter_package *pack = InterPackage::container(kind_s->definition);
    inter_symbol *weak_s = Metadata::optional_symbol(pack, I"^weak_id");
    int alt_N = -1;
    if (weak_s) alt_N = InterSymbol::evaluate_to_int(weak_s);
    if (alt_N >= 0) return alt_N;
    return 0;
}

§7. TRUE for something like "thing" or "room", but FALSE for "object" itself.

int VanillaObjects::is_kind_of_object(code_generation *gen, inter_symbol *kind_s) {
    inter_symbol *object_kind = RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM);
    if (object_kind == NULL) return FALSE;
    if (kind_s == object_kind) return FALSE;
    if (InterTypes::is_unchecked(InterTypes::from_type_name(kind_s))) return FALSE;
    if (TypenameInstruction::is_a(kind_s, object_kind)) return TRUE;
    return FALSE;
}

§8. TRUE for a kind which can have properties but is not any sort of object.

int VanillaObjects::value_kind_with_properties(code_generation *gen, inter_symbol *kind_s) {
    if (VanillaObjects::is_kind_of_object(gen, kind_s)) return FALSE;
    if (kind_s == RunningPipelines::get_symbol(gen->from_step, object_kind_RPSYM)) return FALSE;
    if (InterTypes::is_unchecked(InterTypes::from_type_name(kind_s))) return FALSE;
    inter_node_list *FL = TypenameInstruction::permissions_list(kind_s);
    if (InterNodeList::empty(FL) == FALSE) return TRUE;
    inter_symbol *inst_s;
    LOOP_OVER_LINKED_LIST(inst_s, inter_symbol, gen->instances_in_declaration_order) {
        if (TypenameInstruction::is_a(InstanceInstruction::typename(inst_s), kind_s)) {
            inter_node_list *FL = InstanceInstruction::permissions_list(inst_s);
            if (InterNodeList::empty(FL) == FALSE) return TRUE;
        }
    }
    return FALSE;
}

§9. TRUE for a property which might be held by one or more instances which are not objects.

int VanillaObjects::is_property_of_values(code_generation *gen, inter_symbol *prop_s) {
    inter_tree *I = gen->from;
    inter_node_list *PL =
        InterWarehouse::get_node_list(
            InterTree::warehouse(I),
            PropertyInstruction::permissions_list(prop_s));
    if (PL == NULL) internal_error("no permissions list");
    inter_tree_node *X;
    LOOP_THROUGH_INTER_NODE_LIST(X, PL) {
        inter_symbol *owner_s = PermissionInstruction::owner(X);
        if (owner_s == NULL) internal_error("bad owner");
        inter_symbol *owner_kind_s = NULL;
        inter_tree_node *D = InterSymbol::definition(owner_s);
        if (Inode::is(D, INSTANCE_IST)) {
            owner_kind_s = InstanceInstruction::typename(owner_s);
        } else {
            owner_kind_s = owner_s;
        }
        if (VanillaObjects::is_kind_of_object(gen, owner_kind_s) == FALSE)
            return TRUE;
    }
    return FALSE;
}

§10. TRUE for an either-or property.

int VanillaObjects::is_either_or_property(inter_symbol *prop_s) {
    inter_type type = InterTypes::of_symbol(prop_s);
    if (InterTypes::constructor_code(type) == INT2_ITCONC) return TRUE;
    return FALSE;
}

§11. The spatial depth is a piece of metadata attached to an instance which gives its depth in the containment tree. This is meaningful only in interactive fiction projects. For example, a map in a crate in an Attic room will have a spatial depth of 2: the crate will have depth 1, the room depth 0.

int VanillaObjects::spatial_depth(inter_symbol *inst_s) {
    inter_package *pack = InterSymbol::package(inst_s);
    return (int) Metadata::read_optional_numeric(pack, I"^spatial_depth");
}