10.2/11.0 Working development version of Dynamic Objects / Dynamic Tables (by Tara McGrew)

@Draconis Thank you for the help getting those pushed to the repository. I thought I should also alert those of you who helpfully chimed in on my initial inquiry, @Zed, @zarf, @vaporware, and @Dannii in particular, in case you’d care to have a look.

I’ve been using the extensions myself (with a 10.2 build of Inform from the end of March) without problem so far in some development I’ve been working on, though there are a couple of issues worth mentioning (also noted in the documentation), in case anyone has thoughts on the subject …

(1) Between the early releases of those extensions and now, “indexed text” as a type is gone, and all text has the ability to be dynamic. Which is great, of course, but for Dynamic Objects, it also means that almost any kind of object we might care to clone now has “block” properties. So the “fix the cloned property …” calls that would previously have been rarely required, are now nearly universally applicable to just about any object we might try to clone.

This is straightforward enough to do, but it would be lovely if there were a way to iterate over the properties of an object and do the fixup automatically from within the extension.

I had been hoping for a straightforward solution for that, and I found a few earlier threads that touched on the idea, but from what I was able to glean, it looks as though there weren’t ever any solutions. Overall, it sounds like the consensus was that, due to the way Inform “bakes in” kind and property information at compile-time, a generalized runtime solution for iterating over kinds and/or properties might be out of reach. Is that an accurate assessment?

(2) I didn’t go back to check the behavior in 9.2 or 10.1 yet, but in the latest version, counting the number of objects of a given kind can produce incorrect results. For example, if a “widget” is a kind of thing, and the story has five statically defined widgets, then the following:

say "There are currently [the number of widgets] widgets.";

correctly reports that there are 5 widgets. But if we dynamically create five more (by cloning one or more of the originals), the above expression STILL only reports that there are five.

This only happens with counting the number of object of a kind, with no other criteria applied. If any other definition is used, such as, say …

A widget can be large or small. A widget is usually small.

To take story-wide inventory of widgets:
say "There are presently [number of small widgets] small widgets and [number of large widgets] large widgets scattered throughout the story.";

this accurately includes any dynamically created objects in the count. I am guessing that under the covers, Inform takes a shortcut when there are no conditions to check, and returns what is probably a compiler-generated constant for the number of objects of a given kind? Wheras the presence of any additional condition causes the “number of …” logic to walk through the objects (which Dynamic Objects takes pains to ensure continues to work correctly), testing the condition for each one.

A workaround for the basic object kind situation (also noted in the documentation) is to write something like:

Definition: a thing is total: decide yes.
To print the inventory of total widgets:
say "A total of [number of total widgets] widgets are currently scattered throughout the story.";

But if anyone knows off the top of their heads whether it would be simple enough to either override Inform’s behavior for these special cases, or update whatever counter Inform makes use of when responding to these requests – or can even save me a bit of time by pointing out where to look to try to find out – that would be appreciated.

Neither is a show-stopper, so nothing critical here.

(3) Finally, on a separate note, I have a couple of other extensions in mind that are ready to submit to the library. One is an improved update to an old extension (Print Stage Detection) I submitted a good way back to workaround a quirk in “printing the name” rules that evidently still exists, as I’m finding I still needed the same sort of fix in current development. The other is a “Nested Text Capture” extension, essentially a reentrant-safe version of text capture, layered atop Eric Eve’s extension.

Daniel mentioned that it might be possible to get write access to the repository? If so, that might be helpful in the days ahead, as I’m hoping to be able to contribute a good bit more as time goes on. If you’d prefer a different process, that’s fine too. Either way, I’d expect to continue to run things past folks here ahead of merging anything at all significant… unless there’s a better place for such discussion? Let me know …

Thanks!

2 Likes

I’ve checked that in 9.2 & 9.3, “[the number of widgets]” correctly takes dynamic objects into account.

Thanks … I appreciate your checking that out. I’ll have a look at the previous source as a guide, then.

1 Like

I believe the compiler now emits a constant like IK3_Count holding the number of objects of kind 3. Which unfortunately means it can’t be changed at runtime; the compiler would have to be altered for that.

Iterating over the properties of an object is actually pretty easy, because property keys are just numbers, and you can check at runtime if an object provides a certain one. The hard part is getting the kinds of those properties.

You could make the simplifying assumption that a numeric property value will probably never be a valid pointer to a data block on the heap, so if the value points to a data block, it’s a block-valued property and needs to be fixed. But this could fail if someone happened to put a very specific number in a numeric property or the like, and got very very unlucky.

Or, you could make a feature request to have the compiler output an array with the kinds of all the properties in it, since that information is easily available to the compiler, it’s just not made accessible at runtime currently.

2 Likes

I think it would be possible, with a lot of hacky code. The phrase in question is

To decide which number is number of (S - description of values)
	(documented at ph_numberof):
	(- {-primitive-definition:number-of} -

We can replace this phrase. If you do a bit of sneaky bytecode introspection, then you can work out if it’s a constant or a function call. If it’s a constant then we can check if the kind is a subkind of object, and if that’s true then calculate the new number of objects in the kind. Otherwise return the original result.

Bytecode introspection

Through the @catch instruction you can get the address of a statement, and thereby find out what its opcode is. Here’s an example I’ve been working on for an inline hyperlinks extension which gets the address of the text between the text substitutions without printing it.

To say link -- beginning say_replacement_command_link -- running on:
	(-
		{-counter-up:InlineLink_Catch}
		{-counter-up:InlineLink_CatchC}
		{-counter-up:InlineLink_After}
		! Get the address of the next statement via @catch
		jump {-label:InlineLink_Catch};
		.{-label:InlineLink_CatchC};
		@pull {-my:1};
		@pull {-my:1};
		TAGGED_HYPERLINK_TY_Inline_Text({-my:1});
		! Clean up the rest of the call stub
		@pull {-my:1};
		@pull {-my:1};
		jump {-label:InlineLink_After};
		.{-label:InlineLink_Catch};
		@catch {-my:1} {-label:InlineLink_CatchC};
	-).

To say as -- ending say_replacement_command_link -- running on:
	(- .{-label:InlineLink_After}; -).

Include (-
[ TAGGED_HYPERLINK_TY_Inline_Text addr;
	print "TAGGED_HYPERLINK_TY_Inline_Text ", addr, ": ", (addr->0), " ", (addr->1), " ", (addr->2), "^";
	! Expect a @callf to ParaContent
	if (addr->0 == 129 && addr->1 == 96 && addr->2 == 3) {
		if ((addr + 3)-->0 == ParaContent) {
			! Now @streamstr
			if (addr->7 == 114 && addr->8 == 3) {
				print  (addr + 9)-->0, (string) (addr + 9)-->0, "^";
				TAGGED_HYPERLINK_TY_New(inline_replacement_hyperlink, (addr + 9)-->0, NUMBER_TY, 1);
				rfalse;
			}
		}
	}
	print "err";
];
-).
3 Likes

Wow! Also, Yikes! :zany_face:

Out of curiousity, is that still I6, or are you into the guts of glulx and/or the Z-machine instructions (i.e., roughly speaking, “assembly language”) at that point?

Also, as with your code block there, I’ve seen a number of other constructs used in various extensions that I haven’t found documented anywhere. Passing a “nonexistent” variable (a variable reference, returning a value, perhaps) as a parameter from I7 code comes to mind. I’m not saying they aren’t necessarily documented, but in that case, I obviously don’t know where to look. A couple, I’ve figured out well enough to use – others remain a total mystery.

It’s Inter/I6. The lines beginning with @ are assembly yes.

I think most of the {- } invocations are documented in the Extensions chapter of Writing With Inform. They do change from time to time, so ones that worked in older releases of I7 won’t always work in newer ones.

1 Like

I’m not sure how to make that solution work here, since the phrase is compiled as an expression but @catch can only be used as a statement…

However, there’s an easier way. You can bypass the constant count and force it to be calculated dynamically:

To decide which number is number of (S - description of values):
	(- ({S}(-3)) -).
2 Likes

I was expecting a lot of comma operators would be needed. I don’t know if it would actually work with the inline assembly etc?

But luckily it’s not needed!

Simpler sounds good. Would there be any extra overhead with that approach, though, for the vast number of cases where Inform already gets it right? As far as I can tell, it’s ONLY in the instance where the description is JUST a kind of object that the issue arises. (And you’ll have to excuse me if that’s a “dumb” question, but I have to ask, as you guys may know from what appears to be long experience – but I haven’t the slightest clue what the expression (- ({S}(-3)) -). is doing, and not much of a clue how to find out, either.

But also, putting together what Florian said (i.e., the problem didn’t exist in 9.x), my guess would be somewhere along the way, someone “optimized” the compiler to skip traversal in the special case that a constant value could be queried instead – so would the even easier way possibly be to just find out how and where that’s been done and undo it? Or make it a Use Option, so it’s undone conditionally?

It generates a bit more code, but it won’t run any slower.

Looking at Build/auto.inf, here’s what Inform normally generates for counting when we don’t override the phrase:

    ! for a "just a kind" description
    (tmp_0 = 10);

    ! for a more complex description
    (tmp_1 = (call_U1967)());

Where call_U1967 is a routine that does the counting:

[ call_U1967 x x_ix counter;
    for ((x = K16_bar_First):x:(x = (x.K16_bar_Next))) {
        if (call_U1937(x)) {
            (counter)++;
            jump NextOuterLoop_5;
        }
        .NextOuterLoop_5;
    }
    return counter;
];

If we override the phrase to compile as ({S}(-3)), then instead we get:

    ! for a "just a kind" description
    (tmp_0 = (call_U1967)(-3));

    ! for a more complex description
    (tmp_1 = (call_U1968)(-3));

Where the routines call_U1967 and call_U1968 each have a big switch statement that implements everything a description can be used for: picking a random item, counting, deciding whether an object fits the description, etc., and -3 happens to be the parameter that makes it count.

3 Likes

Is it safe to assume that @catch will be given an address? I thought the format of a catch token was implementation-defined.

For Glulx, it’s specified:

When catch is executed, a four-value call stub is pushed on the stack – result destination, PC, and FramePtr. (See section 1.3.2, “Call Stubs”. The PC is the address of the next instruction after the catch.) The catch token is the value of the stack pointer after these are pushed.

1 Like

That’s very helpful – thanks for the explanation. I see no reason not to update the extension with that change, then. (I’ll add a brief summary of your explanation in a comment as well – not just for anyone else, but for me, too, for if I have to look back at it who knows how many months down the road and can’t make heads or tails of it again :wink:

1 Like

Well, I tried it. Unfortunately, it results quite a few compile-time errors related to various descriptions in the standard library, all of the same form:

Problem. You wrote ‘if the number of things carried by the actor is at least the carrying capacity of the actor’ [](source:C:\Program Files\Inform\Internal\Extensions\Graham Nelson\Standard Rules.i7xd\Source\Contents.w#line1903), but descriptions used as values are not allowed to contain references to temporary values (defined by ‘let’, or by loops, or existing only in certain rulebooks or actions, say) - unfortunately ‘the actor’ is just such a temporary value. The problem is that it may well not exist any more when the description needs to be used, in another time and another place.

Unless another obvious – and preferably not super deep into the weeds of bytecode – solution springs to mind, my feeling is to put a pin in it for now. The issue is noted in the extension documentation, along with a simple enough workaround that gets the job done well enough.

I think dynamic objects are potentially useful enough, though, that it really would be nice if the compiler could accomodate the requirement down the road. The other bit that would be equally helpful is if the compiler created a readily iterable list of object properties AND THEIR KINDS, which would allow the extension to automate the task of “fixing” block properties for newly cloned objects.

2 Likes

As written, it’s replacing the existing:

To decide which number is number of (S - description of values)
        (documented at ph_numberof):
        (- {-primitive-definition:number-of} -).

It’ll work with a distinct name.

To decide which number is count of (S - description of values):
        (- ({S}(-3)) -).

I made just such a feature request (I7-2684) on Friday.

3 Likes