Bubbling Beaker Awards (Award #33, February 07 2025)

The Glulx specification has very little to say about this capability. The basic description of the opcode in section 2.11 reads:

@setiosys L1 L2

Set the I/O system mode and rock. If the system L1 is not supported by the interpreter, it will default to the “null” system (0).

These systems are currently defined:

  • 0: The null system. All output is discarded. (When the Glulx machine starts up, this is the current system.)
  • 1: The filtering system. The rock (L2) value should be the address of a Glulx function. This function will be called for every character output (with the character value as its sole argument). The function’s return value is ignored.
  • 2: The Glk system. All output will be handled through Glk function calls, sent to the current Glk stream.
  • 20: The FyreVM channel system. See section 0.2, “Glulx and Other IF Systems”.

The values 140-14F are reserved for extension projects by ZZO38. These are not documented here.

It is important to recall that when Glulx starts up, the Glk I/O system is not set. And when Glk starts up, there are no windows and no current output stream. To make anything appear to the user, you must first do three things: select the Glk I/O system, open a Glk window, and set its stream as the current one. (It is illegal in Glk to send output when there is no stream set. Sending output to Glulx’s “null” I/O system is legal, but pointless.)

There’s also a little more in section 1.3.5 about how the “filtering” subsystem interacts with the call stack, but it doesn’t do much to illuminate the utility of this opcode.

The normal mode of a running game is mode 2, but the part of interest here is the “filtering system”, which is mode 1. As hinted above, when this mode is set, every character of output is submitted to this routine instead of being handled normally by the interpreter, and the return value is ignored. The utility may not be immediately apparent, but what it allows you to do as the author is inspect every outbound character and decide how to handle it, either by ignoring it, printing it, or transforming it in some way.

Conveniently, from the compiler’s perspective, the name of a routine is functionally the same as its address here.

Since the return value is ignored, the only thing that matters is what your “filtering” routine prints in response to a given input character. As a simple example, imagine a routine to produce “static” in output, such as might happen in a radio transmission:

Include (-

Global	chance_of_static_pct;

[ StaticText char    lc r ;
	lc = glk_char_to_lower(char);
	r = random(100);
	if (r <= (+ percent chance of static +) && lc >= 'a' && lc <= 'z')
		glk_put_char_uni('*');
	else
		glk_put_char_uni(char);
];

-).

Percent chance of static is initially 30.

To activate static filter:
	(- @setiosys 1 StaticText; -).

To return to unfiltered output:
	(- @setiosys 2 0; -).

To say (T - text) with (N - number) percent static:
	now percent chance of static is N;
	activate static filter;
	say T;
	return to unfiltered output.

When play begins:
	say "We're getting close to the energy source, Captain. It's starting to affect our communicators." with 15 percent static.

The logic of the “filtering” routine can be arbitrarily complex. What eu1’s post does is allow any I7 phrase to be swapped in as the “filtering” routine, with the following widget:

Include (-
[ set_io_system phrase;
phrase = phrase-->1;
@setiosys 1 phrase;
];
-).

To switch to printing by (P - a phrase Unicode character -> nothing): (- set_io_system({P}); -).

So, to modify the static example to use this method:

Include (-
[ set_io_system phrase;
phrase = phrase-->1;
@setiosys 1 phrase;
];
-).

To switch to printing by (P - a phrase Unicode character -> nothing): (- set_io_system({P}); -).

Include Unicode Character Names by Graham Nelson.

To add (C - a Unicode character) to the story’s output: (- glk_put_char_uni({C}); -).

newline translates into Unicode as 10. [not in the extension]

Definition: a Unicode character is white space if it is Unicode space or it is Unicode newline.

Percent chance of static is initially 30.

To print (C - a Unicode character) with static (this is static filter):
	if a random chance of percent chance of static in 100 succeeds and C is not white space:
		add Unicode asterisk to the story's output;
	otherwise:
		add C to the story's output.

To say (T - text) with (N - number) percent static:
	now percent chance of static is N;
	switch to printing by static filter;
	say T;
	return to unfiltered output.

When play begins:
	say "The interference is getting worse, Captain." with 35 percent static.
7 Likes

This week’s prize, Bubbling Beaker Award® #25 is presented to @Zed. (EDIT: I’m also pleased to announce that it is belatedly given the Peer Revue™ distinction!) His award-winning post provides some new phrases for looping through the values in relations. While two of them are primarily performance improvements over what the Standard Rules provide for iterating through the members of the left or right sides of a relation, the third provides the ability to loop through relation loading both left and right values into temporary variables. (It never even occurred to me that such a thing would be possible, and it’s not necessary to use I6 to obtain value from that technique.)

This is Zed’s fifth BBA, once again putting him at the top of the leaderboard. Congratulations, Zed! There’s noone madder than the King of Loopiness! If you’re willing to say a few words, I would love to hear more about your motivation in developing it and the kinds of uses you’ve envisioned.

Honorable mention goes to @Celtic_Minstrel, for a technique to manage a command like >WAIT FOR 10 MINUTES (see this thread). At first glance it doesn’t seem that different from the examples in the documentation for this type of command (RB Ex 388 Nine AM Appointment and RB Ex 389 Delayed Gratification), but Celtic_Minstrel’s solution strikes me as more elegant. Rather than trying to interrupt the turn sequence rules or drive them in an abnormal way, using this approach it’s the parse command rule that’s suppressed, allowing the model world to run without waiting for human player input.

3 Likes

Just last month I was talking about how useful custom loops are.

A disadvantage to my solution as written is that it’ll only work in the current development version of Inform, thus it’s only of use right now to someone who’s building Inform from source. Otis, however, provided a 9.3/6M62 translation of looping through relations. I just tried it in 10.1, and with just a small modification (removing the final 2 closing right braces) Otis’ solution works there. 10.1-ready code is below for easy copy-pasting.

10.1 version
To repeat for/with/-- (x - nonexisting K variable) running/-- through/in keys of (r - relation of values of kind K to values of kind L) begin -- end loop:
(-
	for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
		  {-my:0} >= 0 : {-my:0}--, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
		if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
			if (KOVIsBlockValue({-strong-kind:K}))
				BlkValueCopy({-by-reference:x}, BlkValueRead({-by-reference:r}, {-my:1} + 1));
			else
				{-by-reference:x} =  BlkValueRead({-by-reference:r}, {-my:1} + 1);
			if (1) {-block}
-).

To repeat for/with/-- (y - nonexisting L variable) running/-- through/in values of (r - relation of values of kind K to values of kind L) begin – end loop:
(-
for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
{-my:0} >= 0 : {-my:0}–, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:y}, BlkValueRead({-by-reference:r}, {-my:1} + 2));
else
{-by-reference:y} = BlkValueRead({-by-reference:r}, {-my:1} + 2);
if (1) {-block}
-).

To repeat for/with/-- (x - nonexisting K variable) and (y - nonexisting L variable) running/-- through/in (r - relation of values of kind K to values of kind L) begin – end loop:
(-
for ({-my:0} = BlkValueRead({-by-reference:r}, RRV_STORAGE), {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) :
{-my:0} >= 0 : {-my:0}–, {-my:1} = RRV_DATA_BASE + (3 * {-my:0}) ) {
if (BlkValueRead({-by-reference:r}, {-my:1}) & RRF_USED) {
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:x}, BlkValueRead({-by-reference:r}, {-my:1} + 1));
else
{-by-reference:x} = BlkValueRead({-by-reference:r}, {-my:1} + 1);
if (KOVIsBlockValue({-strong-kind:K}))
BlkValueCopy({-by-reference:y}, BlkValueRead({-by-reference:r}, {-my:1} + 2));
else
{-by-reference:y} = BlkValueRead({-by-reference:r}, {-my:1} + 2);
if (1) {-block}
-).

Though the 9.3 → 10.1 differences are slight, both have conspicuous differences from my version which makes use of something new in 11.0: an abstraction layer for dealing with kinds conforming to the pointer value type that’s built on top of the block value layer.

A really fundamental data structure with nigh-infinite application is a collection of mappings from X → Y. In Perl or Ruby these are called Hashes. In Python, it’s a dict. A more generic name is “associative array” (which is also what they’re called in PHP). Javascript objects can be employed this way, too, with some effort.

In inform, a one-to-various relation is the closest thing. But, historically, relations have been buggy, and there are undocumented gotchas. For instance, we can refer to the number of things that enclose a person but the number of numbers that boop a text would get a runtime error:

*** Run-time problem P59: You can't implicitly repeat through the values of this kind: a problem arising from a description which started out here [...]

And the absence of an ability to loop through keys and values at once severely limited utility as a hash. I think this is going to change my Inform programming and to decide what relation [...] will pop up a lot going forward.

A week ago, I wrote another novel loop I find useful for looping through the characters of a text, something that is infeasibly slow for all but the shortest of strings with the best way I7 directly offers.

To repeat with/for (c - nonexisting unicode character variable) running/-- through/in chars/characters of/in/for (t - text) begin -- end loop:
(-
  {-my:0} = {t}-->0;
  @push {-my:0};
  {-my:1} = TEXT_TY_Temporarily_Transmute({-by-reference:t});
  @push {-my:1};
  for ( {-my:1} = 0, {c} = BlkValueRead({-by-reference:t}, 0) : {c} : {c} = BlkValueRead({-by-reference:t}, ++{-my:1} ))
    {-block}
   @pull {-my:1};
   @pull {-my:0};
   TEXT_TY_Untransmute({-by-reference:t}, {-my:1}, {-my:0});
-)

(Also in that thread, Zarf posted a setiosys solution akin to previous BBA award #24.)

I will cherish the King of Loopiness title. :laughing:

5 Likes

Zed is being very generous. My “translation” was about 90% done by him pointing out where to find analagous code in the template and explaining what the new template routines did. My only real contribution was fighting with the 6M62 compiler to find out how the handling of loop inclusions differed. Structurally speaking it’s all his work.

Thanks for explaining, Zed – and for these interesting new toys!

Late breaking news: Although word from the assorted mad scientists wasn’t received in time for the original post, Zed’s latest invention was granted the Peer Revue™ distinction by unanimous acclaim. Zed’s the first person to receive that honor twice – double congratulations are in order!

4 Likes

This week’s prize, Bubbling Beaker Award® #26 is presented to @vaporware. The award-winning post demonstrates a method for using I7 list objects as temporary storage from I6. This is an excellent tool for the tool kit when tinkering in I6 – as vaporware points out, list constructs are a very easy way to leverage heap memory for many purposes.

Although the award is for the general technique, the winning post shows specifically how to use this to save data about the state of the match list, an aspect of the system not covered by anything in the Standard Rules. (One technical note: The code was written in 2012 and uses an outdated syntax for the parameters used by I7 phrases invoking I6 routines. Where it says -pointer-to one should substitute -by-reference to get it to work today.)

This is the first BBA awarded to vaporware, who has a long history with Inform and has managed some very impressive feats (such as the Dynamic Objects extension). Congratulations! If you would like to say a few words about your invention, please feel free to take the floor.

5 Likes

I thought I’d adapt the technique to construct something a little easier to work with in I7:

a match-list-kind is a kind of object.
a match-list-kind has a number called the number-matched.
a match-list-kind has a number called match-from.
a match-list-kind has a number called token-filter.
a match-list-kind has a number called match-length.
a match-list-kind has a number called number-of-classes.
a match-list-kind has a number called oops-from.
a match-list-kind has a list of objects called the match-list.
a match-list-kind has a list of numbers called the match-classes.
a match-list-kind has a list of numbers called the match-scores.

The number-matched property translates into I6 as "number_matched_prop".
The match-from property translates into I6 as "match_from_prop".
The token-filter property translates into I6 as "token_filter_prop".
The match-length property translates into I6 as "match_length_prop".
The number-of-classes property translates into I6 as "number_of_classes_prop".
The oops-from property translates into I6 as "oops_from_prop".
The match-list property translates into I6 as "match_list_prop".
The match-classes property translates into I6 as "match_classes_prop".
The match-scores property translates into I6 as "match_scores_prop".

the global match list is a match-list-kind.
the global match list object translates into I6 as "global_match_list".

To set the/-- match globals: (- SetMatchGlobals(); -).

Include (-
[ SetMatchGlobals i;
global_match_list.number_matched_prop = number_matched;
global_match_list.match_from_prop = match_from;
global_match_list.token_filter_prop = token_filter;
global_match_list.match_length_prop = match_length;
global_match_list.number_of_classes_prop = number_of_classes;
global_match_list.oops_from_prop = oops_from;
LIST_OF_TY_SetLength(global_match_list.match_list_prop, number_matched, 0);
LIST_OF_TY_SetLength(global_match_list.match_classes_prop, number_matched, 0);
LIST_OF_TY_SetLength(global_match_list.match_scores_prop, number_matched, 0);
for ( i = 0 : i < number_matched : i++ ) {
LIST_OF_TY_PutItem(global_match_list.match_list_prop, i + 1, match_list-–>i);
LIST_OF_TY_PutItem(global_match_list.match_classes_prop, i + 1, match_classes-–>i);
LIST_OF_TY_PutItem(global_match_list.match_scores_prop, i + 1, match_scores-–>i);
}
];
-)

to dump match list:
  say "number matched: [number-matched of global match list].";
  say "match from: [match-from of global match list].";
  say "token filter: [token-filter of global match list].";
  say "match length: [match-length of global match list].";
  say "number of classes: [number-of-classes of global match list].";
  say "oops from: [oops-from of global match list].";
  repeat with i running from 1 to number-matched of global match list begin;
    say "[i]. match: [entry i in match-list of global match list] ; ";
    say "class: [entry i in match-classes of global match list] ; ";
    say "score: [entry i in match-scores of global match list].";
  end repeat;

lab is a room.
the red paper is in the lab.
the red tree is scenery in the lab.
the red ghost is an undescribed thing in the lab.
the player holds the red pen.

every turn: set match globals;
dump match list;

test me with "x red".

Output is:

lab
You can see a red paper here.
 
>test me
(Testing.)
 
>[1] x red
(the red pen)
You see nothing special about the red pen.
 
number matched: 4.
match from: 2.
token filter: 0.
match length: 1.
number of classes: 0.
oops from: 0.
1. match: red paper ; class: 0 ; score: 2156.
2. match: red tree ; class: 0 ; score: 2146.
3. match: red ghost ; class: 0 ; score: 2056.
4. match: red pen ; class: 0 ; score: 2176.
1 Like

posted that and promptly had a better idea.

number-matched is a number variable.
the number-matched variable translates into I6 as "number_matched".

match-from is a number variable.
the match-from variable translates into I6 as "match_from".

token-filter is a number variable.
the token-filter variable translates into I6 as "token_filter".

match-length is a number variable.
the match-length variable translates into I6 as "match_length".

number-of-classes is a number variable.
the number-of-classes variable translates into I6 as "number_of_classes".

oops-from is a number variable.
the oops-from variable translates into I6 as "oops_from"

match-list-entry is a kind of object.
a match-list-entry has an object called the matched-object.
a match-list-entry has a number called the match-classes.
a match-list-entry has a number called the match-score.

dummy-mle is a match-list-entry.
the dummy-mle object translates into I6 as "dummy_mle".

the matched-object property translates into I6 as "matched_object_prop".
the match-classes property translates into I6 as "match_classes_prop".
the match-score property translates into I6 as "match_score_prop".

to repeat with/for (mle - nonexisting match-list-entry variable) running/-- through/in the/-- global match list begin -- end loop:
(- for ({-my:0} = 0, {mle} = dummy_mle, 
dummy_mle.matched_object_prop = match_list-->{-my:0},
  dummy_mle.match_classes_prop = match_classes-->{-my:0},
  dummy_mle.match_score_prop = match_scores-->{-my:0}
: {-my:0} < number_matched :
{-my:0}++,
dummy_mle.matched_object_prop = match_list-->{-my:0},
  dummy_mle.match_classes_prop = match_classes-->{-my:0},
  dummy_mle.match_score_prop = match_scores-->{-my:0}
)
-)

lab is a room.
the red paper is in the lab.
the red tree is scenery in the lab.
the red ghost is an undescribed thing in the lab.
the player holds the red pen.

every turn:
  say "number matched: [number-matched].";
  say "match from: [match-from].";
  say "token filter: [token-filter].";
  say "match length: [match-length].";
  say "number of classes: [number-of-classes].";
  say "oops from: [oops-from].";
let i be 1;
repeat for mle in the global match list begin;
  say "[i]. [matched-object of mle] ; class: [match-classes of mle] ; score [match-score of mle].";
increment i;
end repeat;

test me with "x red".
1 Like

This week’s prize, Bubbling Beaker Award® #27 is presented to @Erik_Temple, another figure who makes frequent appearance in the archives of this forum. His award-winning post refines a method introduced by @Ron_Newcomb at the start of the thread, which demonstrates a method for using the Glulx @linearsearch opcode to speed up row access for large tables.

This is the first BBA awarded to Erik_Temple. Congratulations! (Since he last visited in the fall of last year, I’ll post a brief write-up of the technique soon.)

More-than-honorable mention goes to Ron_Newcomb for the breakthrough conception and initial implementation of a working program using the technique; I chose Erik_Temple as the award recipient because his construction is easier to work with and to understand… and because ties would create problems for the leaderboard. (Though Ron_Newcomb is at another level entirely compared to me, so perhaps I just didn’t understand why his version was superior!)

Also: Extra special thanks to @Zed for suggesting this topic as a BBA candidate. As a reminder, anyone can nominate a post for consideration, so please do if you happen to come across something notable.

4 Likes

The technique depends on a working knowledge of the I6 infrastructure underlying the implementation of tables in I7, plus knowledge of Glulx opcodes. In short, the demonstration code:

  1. leverages the I7 template layer to find the address of the array used to store a particular table column’s data, then
  2. uses a specialized Glulx opcode to iterate through the values stored in that column much faster than can be accomplished via an I6 loop.

If the sought value is found, the return result from the opcode is used to determine in which row of the table it is located, and internal variables are set accordingly to make that the current row.

The syntax of some of the special inclusion bits have changed since the post originally went up, but here’s a working version for 6M62:

Section - Newcomb-Temple Table Search Acceleration

Include (-
Global seekStartAddress;
Global seekRows;
Global seekVal;
Global seekReturnValue;
-) after “Definitions.i6t”.

To select a/the/-- row with (TC - a word value valued table column) of (W - a word value) in/from (T - table name):
(-
	{-my:ct_0} = {T};
	{-my:ct_1} = -1;
	seekVal = {W};
	seekRows = TableRows({T});
	seekStartAddress = ({T})-->(TableFindCol({T}, {TC}, true)) + (COL_HSIZE * WORDSIZE);
	@linearsearch seekVal WORDSIZE seekStartAddress WORDSIZE seekRows 0 4 {-my:ct_1};
	if ({-my:ct_1} < 1) return RunTimeProblem(RTP_TABLE_NOCORR, {T});
-).


Table of Smiles
smile number	key
0	“: )”
1	“; )”
2	“:->”
with 2 blank rows.

When play begins:
	showme the contents of Table of Smiles;
	say "Rows: [number of rows in the Table of Smiles].";
	repeat with N running from 0 to 2:
		select row with a smile number of N in the Table of Smiles;
		say "smile [N] is [key entry] by the lights of the new select… phrase."
1 Like

This week’s prize, Bubbling Beaker Award® #28, is presented to Aaron Reed (@aaronius). Aaron Reed is the author of the game Blue Lacuna (among many others) and the book Creating Interactive Fiction with Inform 7. In this award-winning post from 2011, he looked into the problem of trying to make parser failure more intelligible to the untutored player, and in doing so dug deep into the parser code to tinker with the parts that decode a game’s recognizable grammar lines. This portion of the code has very limited documentation outside the code itself, much of it found in the obscure Inform Technical Manual.

This is the first BBA awarded to aaronius. Congratulations! The code in the post has unfortunately been a bit mangled by the forum’s conversion, but it will work more or less as-is once proper spacing is restored and em-dashes are converted back to double hyphens (--) when part of array access constructions. I believe that this technique (or its refined successor), along with many other pioneering user interface experiments, became part of the Smarter Parser extension, so it’s recommended to just look at that.

2 Likes

I’ve taken the opportunity to fix the code tags, so you can now see it properly.

3 Likes

I did a 6M62 update of some of the code by Aaron and Andrew there along with some other mucking around with it: Parser Error Details.

3 Likes

Hey, great! Thanks, Zed!

This week’s prize, Bubbling Beaker Award® #29, is presented to @Zed, who needs no introduction. In this award-winning post, he provides a speed-enhanced method of looping through the individual characters of an I7 text, a task which he notes above is infeasibly slow except for very small strings. It’s another valuable tool in a series produced by the King of Loopiness!

This is Zed’s sixth BBA, so I think it’s become pretty clear who’s the maddest of the mad. Congratulations, Zed! If you would like to explain a little bit about the guts of how it works (such as defining block values, transmutation, etc.), I’m sure that would be of interest to many a future apprentice.

7 Likes

I’m working on a write-up but tackling explaining the low-level details of text storage is taking some time…

1 Like

OK, I’m going to do the relatively easy parts for now.

A cool thing about Inform is that you have a lot of latitude to create your own control structures, like special forms of if or repeat. You can look through the Basic Inform extension and see that it’s how Inform’s own structures are defined. (Prior to 10.1 these were is the Standard Rules extension.)

I dislike the gratuitous verbosity of Inform’s repeat statement, so the below includes something I’ve been using since my Strange Loopiness extension of a couple years ago so that one can say just repeat for c in t instead of repeat with c running through t.

This is an example routine from Text.i6t in the Basic Inform Kit:

[ TEXT_TY_CharacterLength txt ch i dsize p cp r;
        if (txt==0) return 0;
        cp = txt-->0;
        p = TEXT_TY_Temporarily_Transmute(txt);
        dsize = BlkValueLBCapacity(txt); r = dsize;
        for (i=0:i<dsize:i++) {
                ch = BlkValueRead(txt, i);
                if (ch == 0) { r = i; break; }
        }
        TEXT_TY_Untransmute(txt, p, cp);
        return r;
];

A whole bunch of text routines begin with

        cp = txt-->0;
        p = TEXT_TY_Temporarily_Transmute(txt);

and have last thing before the return statement

        TEXT_TY_Untransmute(txt, p, cp);

This is the part that’s hard to give the background for, so for now I’ll just say that my routine is doing the same thing. The way you create local variables in I6 inclusions is to name them, e.g., {-my:0}, {-my:1}, etc.

To repeat with/for (c - nonexisting unicode character variable) running/-- through/in chars/characters of/in/for (t - text) begin -- end loop:
(-
! store the value of {t}-->0 for untransmute at end
  {-my:0} = {t}-->0;
! temporarily transmute t. Since this will change t, pass it by-reference. Store the result.
  {-my:1} = TEXT_TY_Temporarily_Transmute({-by-reference:t});
! We're going to reuse {-my:1} as the loop index variable instead of creating another temporary variable, so we stash its value on the stack so we can get it back to untransmute at the end
  @push {-my:1};
! This is the for loop. We assign to c, the nonexisting unicode character value. The loop terminates when c = 0... that means we're through the real text
  for ( {-my:1} = 0, {c} = BlkValueRead({-by-reference:t}, 0) : {c} : {c} = BlkValueRead({-by-reference:t}, ++{-my:1} ))
! in I6 inclusions that have `begin -- end loop`, the loop body from the I7 code is `{-block}` within the inclusion, so this is whatever the author-specified loop body was
    {-block}
! we restore {-my:1}'s original value
   @pull {-my:1};
! we untransmute t
   TEXT_TY_Untransmute({-by-reference:t}, {-my:1}, {-my:0});
-)

Why is this so much faster than the obvious approach Inform allows?

repeat with x running from 1 to number of characters in t begin;
  Let c be character number x in t; [...]

Well, let’s modify Transmute, Untransmute, and Length to print a character upon invocation:

Include (-
[ TEXT_TY_CharacterLength txt ch i dsize p cp r;
        if (txt==0) return 0;
print "X";
        cp = txt-->0; p = TEXT_TY_Temporarily_Transmute(txt);
        dsize = BlkValueLBCapacity(txt); r = dsize;
        for (i=0:i<dsize:i++) {
                ch = BlkValueRead(txt, i);
                if (ch == 0) { r = i; break; }
        }
        TEXT_TY_Untransmute(txt, p, cp);
        return r;
];
-) replacing "TEXT_TY_CharacterLength".

Include (-
[ TEXT_TY_Temporarily_Transmute txt  x;
print "Y";
        if ((txt) && (txt-->0 & BLK_BVBITMAP_LONGBLOCKMASK == 0)) {
                x = txt-->1; ! The old value was a packed string

                txt-->0 = UNPACKED_TEXT_STORAGE;
                txt-->1 = FlexAllocate(32, TEXT_TY, TEXT_TY_Storage_Flags);
                if (x ~= EMPTY_TEXT_PACKED) TEXT_TY_CastPrimitive(txt, false, x);

                return x;
        }
        return 0;
];
-) replacing "TEXT_TY_Temporarily_Transmute".

Include (-
[ TEXT_TY_Untransmute txt pk cp x;
print "Z";
        if ((pk) && (txt-->0 == UNPACKED_TEXT_STORAGE)) {
                x = txt-->1; ! The old value was an unpacked string
                FlexFree(x);
                txt-->0 = cp;
                txt-->1 = pk; ! The value earlier returned by TEXT_TY_Temporarily_Transmute
        }
        return txt;
];
-) replacing "TEXT_TY_Untransmute".

and have the obvious approach operate on just two characters:

when play begins:
Repeat with x running from 1 to number of characters in wotw begin;
Let c be character number x in wotw;
say "[unicode 822][c]";
end repeat;

wotw is initially "No".

We see XYZYYXYZZ̶NXYZYYXYZYZ̶oXYZ

That’s 9 function calls to just these 3 routines per character (and doesn’t even include a complete accounting of routines those routines call). My repeat loop just transmutes/untransmutes once for the whole loop and gets away without checking the length at all.

5 Likes

I’ll emphasize that this is the norm with text manipulation in Inform: there are a whole lot of gratuitous calls to transmute/untransmute. If it’s ever the case that you need faster text manipulation, it pays to know enough I6 and enough about Text.i6t to write a custom routine that avoids it.

2 Likes

This week’s prize, Bubbling Beaker Award® #30, is presented to @eu1, the handle of Brady Garvin, who has also been known in the community as emacsuser. In this award-winning post, he demonstrates a method for allowing the properties of a set of objects to be associated with table columns, along with a routine for setting the objects’ property state in accordance with the table state at any point during run-time. This is potentially useful because, although objects can be defined using a table at compile time, the states of the table and the objects are not linked at run-time, so changes to properties do not change the table and vice-versa.

Although the same effect could be achieved in a less technically-complicated way, any mad scientist will appreciate this approach, and the related discussion on the thread.

This is eu1’s third BBA, which is pretty good for someone who probably doesn’t even know the award exists. Congratulations, eu1! Since he is not likely to be by to discuss his invention, I will provide a quick overview soon.

Special thanks to @Zed for nominating this post!

1 Like

First off, this is a post that got a little mangled during the forum migration, so here’s a functional version of the code for 6M62:

Brady Garvin's Table Antics
"Brady Garvin's Table Antics"

[from https://intfiction.org/t/i7-tables-and-functional-programming/2045]

Kitchen is a room.

To decide what number is description converted to a number:
	(- description -).

To decide what number is locked converted to a number:
	(- locked -).

When play begins: [demonstrates property and attribute having same underlying value]
	showme description converted to a number;
	showme locked converted to a number.
	
To decide what number is (V - a value) converted to a number:
	(- {V} -).

To decide whether (O - an object) has property number (P - a number):
	(- WhetherProvides({O},false,{P}) -).

To store non-block value (V - a value) as property number (P - a number) in (O - an object):
	(- {O}.{P}={V}; -).

To store block value (V - a value) as property number (P - a number) in (O - an object):
	(- BlkValueCopy(GProperty(OBJECT_TY,{O},{P}),{V}); -).

To decide what number is the number of columns in (T - a table name):
	(- ({T}-->0) -).

To decide what number is table column (I - a number) in (T - a table name):
	(- ((({T}-->{I})-->1)&TB_COLUMN_NUMBER) -).

To decide whether table column (C - a number) is a block-value table column:
	(- KOVIsBlockValue(TC_KOV({C})) -)

To decide whether there is a value at row (R - a number) and column (C - a number) in (T - a table name):
	(- ExistsTableLookUpEntry({T},{C},{R}) -).

To decide what number is the value at row (R - a number) and column (C - a number) in (T - a table name):
	(- TableLookUpEntry({T},{C},{R}) -).

Column correspondence relates numbers to numbers. The verb to be the column corresponding to implies the column correspondence relation.

To associate (C - a value of kind K valued table column) with (P - a K valued property):
	let the column number be C converted to a number;
	let the property number be P converted to a number;
	say "<associating: col = [column number], prop = [property number]>[line break]";
	now the column number is the column corresponding to the property number.

To apply the properties in (T - a table name):
	say "<STARTED: T = [T] with [number of columns in T] cols>[line break]";
	repeat with the column index running from two [not one; that’s the object column] to the number of columns in T:
		say "<col ind: [column index]>";
		let the column number be the table column column index in T;
		say "<col # = [column number]>";
		unless the column number relates to a number by the column correspondence relation:
			say line break;
			next;
		let the property number be the number that the column number relates to by the column correspondence relation;
		say "<prop # = [property number]>";
		repeat with the row index running from one to the number of rows in T:
			say "<row ind = [row index]>";
			let the named object be the object column in row row index of T;
			say "<obj = [named object]>";
			unless the named object has property number property number:
				say line break;
				next;
			unless there is a value at row row index and column column number in T:
				say line break;
				next;
			let V be the value at row row index and column column number in T;
			say "<V = [V]>[line break]";
			if table column column number is a block-value table column:
				store block value V as property number property number in the named object;
			otherwise:
				store non-block value V as property number property number in the named object.

Section “Demonstration”

The Kitchen is a room.
Here is an animal called the salamander.

Every object has a number called strength.
Every object has a number called size.
Every animal has some indexed text called tagline.

Table of Object Parameters
object	description	strength	size	tagline
salamander	“A mythical lizardlike creature.”	12	201234	“x”
Kitchen	--	--	800	“y”
north	--	92	--	“z”

When play begins:
	associate the description column with description;
	associate the strength column with strength;
	associate the size column with size;
	associate the tagline column with tagline;
	showme the contents of Table of Object Parameters.

Instead of jumping:
	apply the properties in the Table of Object Parameters;
	say “Done.”

Test me with “showme salamander / showme north / jump / showme salamander / showme north”.

My version has extra output on individual steps, originally for debugging but now mostly serving to “show the work” as it’s being done. The particular sample scenario is from the original post.

Because the I7 compiler does not like the idea of iterating through properties or table columns, eu1’s method has to fight its type checker the whole way. At its crux is a relation of numbers to numbers, which is used to link the underlying integer ID of a property or attribute to the underlying integer ID of a table column. Some custom phrases are built to use template code routines for tables that are not exposed by the Standard Rules. The to apply the properties in <table>... phrase iterates through the provided table’s underlying data structure, looking for columns that are mapped to properties, and if it finds one, it cycles through the table’s rows, applying the value in the column of interest to the corresponding object (which must be in the first column, in a column named “object”).

2 Likes