LISP/MDL/ZIL routine to JS function - Am I doing this right?

The original routine in ZIL:

<ROUTINE RUNNING? (RTN "AUX" C E TICK)
	 <SET E <REST ,C-TABLE ,C-TABLELEN>>
	 <SET C <REST ,C-TABLE ,C-INTS>>
	 <REPEAT ()
		 <COND (<==? .C .E> <RFALSE>)
		       (<EQUAL? <GET .C ,C-RTN> .RTN>
			<COND (<OR <0? <GET .C ,C-ENABLED?>>
				   <0? <SET TICK <GET .C ,C-TICK>>>
				   <G? .TICK 1>>
			       <RFALSE>)
			      (T <RTRUE>)>)>
		 <SET C <REST .C ,C-INTLEN>>>>

My attempt:

function runningCheck(RTN){
    let C, E, TICK;
    E = C_TABLE.slice(C_TABLELEN);
    C = C_TABLE.slice(C_INTS);
    let bool = false;// because JS can't return from a while loop!
    while(C !== E){
        if(C === E){ bool = false;break;}
        if(C[C_RTN] === RTN){
            TICK = C[C_TICK];
            if (C[C_ENABLED_CHECK] === 0 || TICK === 0 || TICK > 1){
                bool = false;
                break;
            } else {
                bool = true;
                break;
            }
        }
        C = C.slice[C_INTLEN];
    }
    return bool;
}

It looks like REST is the same thing as array.shift() if there is only one arg, but a second arg would mean array.slice(secondArg).

So:

<SET E <REST ,ARRAY ,INT>>

. . . is this in JS:

let E = ARRAY.slice(INT);

image

https://ifarchive.org/if-archive/programming/mdl/manuals/MDL_Programming_Language.pdf (pages 52 - 53)


Also, I'm guessing that ==? and EQUAL? are (basically) the same thing (as far as I should be concerned)?

image
image


This is all I could find concerning EQUAL (not from the MDL manual):

image

https://eblong.com/infocom/other/Learning_ZIL_Meretzky_1995.pdf (pages 15 - 16)


Wait. . .

I just realized . . .

Those poor guys didn't have for or foreach to work with lists/tables. That's why they keep using REPEAT() loops! They're just iterating lists, aren't they!?!?!

In fact, it looks like nearly all the "mechanics" of a ZIL game are using lists as the gears.

...and that's why Inform 7 code tends to use tables for anything more complex than iterating through a list! It was reverse-engineered to behave like ZIL did, without anyone involved having ever seen any ZIL code.


Those poor guys didn't have for or foreach to work with lists/tables.

Interestingly, LISP stood for LISt Processor. I believe it was the first to have lists as a basic data type. However, "lists" wasn't really an accurate name, as a list had exactly 2 elements. The commands car and cdr return those elements. If I remember correctly, those two became GET and REST respectively in MDL.

Basically, a list had 2 elements, "a" and "d". Usually, you would have "a" pointing to a data element, and "d" pointing to the next list; with the last "d" pointing to null. So when you loop over a list, it's usually recursive. "If 'a' is the element we want, return it. Otherwise return the result of calling this function on the contents of 'd'"

It was also common to have a double list; where every "a" points to another list whose "a" is the key and "d" is the value.

In scheme (a more human-readable LISP variant), you had functions named things like cdddddar which could look deeper into a list; but these were implemented essentially as macros; so (cdddar l) would expand to (car (cdr (cdr (cdr l)))).

MDL makes it a bit more elegant by giving REST a second parameter. So you can write <REST l ,6> instead of (cddddddr l). And you also had a function to assemble a table (a structure made up of lists), so you could do <DTABLE 'foo 'bar 'baz> instead of (cons 'foo (cons 'bar (cons 'baz #end))). (cons takes 2 parameters; the "a" and "d" elements to compose into a list)

It looks like ZIL also has strings; the basic data type in LISP was an "atom", which can either be a number, a reserved token like #null or #end, a function, or a word. However Scheme (and maybe ZIL; I'm not sure) provides a string type with double quotes which is treated internally as a table of individual words.

(Yes, I said function. The biggest strong point of LISP and MDL compared to contemporary languages was their LAMBDA operator, which is basically the => operator in JS. Despite being the language's biggest strength, I remember reading that this feature was dropped early in ZIL's development because of memory constraints)

Look at those data types… it seems like these languages were optimised very heavily for writing a specific type of software: chatbots with machine learning. And this really is the case. LISP was the language of choice for AI research until the last decade.


Wow. I actually understood most of that.

So, if I have :

let arr1 = [ "foo", "bar", "XanMagSux" ];

In ZIL/MDL/LISP, there was no way to just do arr1[2]?

All I could do was arr[0] (which is "foo") or arr[1] (which is "bar"). Beyond that, as long as arr[1] isn't null, that means I've still got more items to access.

..and to access more I'd have to use a variable to copy the REST of the list then work from there all over again.

Also, I could get the REST of a list starting from whatever index I choose, to narrow the "search".

Right?


All I could do was arr[0] (which is "foo") or arr[1] (which is "bar")

Not really. Your list would look more like: [ "foo", ["bar", ["XanMagSux", null ]]]

So you do arr[0] if you want the first element (GET).
Or you do arr[1] (REST) to get the rest of the list, which is itself a list.

So to get the nth element of a list, you'd have a recursive function that looks like:

getelement = function (n, arr) {
  if (arr == null) {
    return NOTFOUND;
  } elseif (n == 0) {
    return arr[0];
  } else {
    return getelement(n-1. arr[1]);
  }
}

This is really inefficient, and memory hungry. There is a reason LISP wasn't widely used on desktop until memory was cheap.
But… it turns out that once you get your head around thinking of lists like this rather than a numbered array, some of the weirder AI stuff feels more natural in this mental paradigm. It's weird and fun :)

EDIT: And if you think "why not just use an array?", remember that at the time arrays in other languages were treated as pointers with some syntactic sugar.

A pointer is an int which represents a physical address in memory. To get the nth element of an array, you would multiply n by the size in bytes of whatever data type the array contained, and add it onto the pointer. That means that you need enough space in memory in one continuous range to accommodate the whole array. And because the system doesn't know which ints are going to be used as pointers, it can't shuffle things around to find a big enough area of memory if you want to add to an existing array. You need to know how many elements an array will have when you allocate the memory for it, and all elements in an array need to be the same size.

Having every list being a load of two-element lists pointing to each other means you don't need to know nearly as much about your data when writing the program.

Modern languages give you much more flexibility because they mostly use a hybrid of these two methods behind the scenes, and do a hell of a lot of work so you don't need to think about it.

Oddly enough, it appears that Quest's dictionaries owe a lot to LISP dictionary tables.
That is, the difference between a list and a dictionary is that where a list stores each value, a dictionary stores a two-element mini-list whose elements are a key and a value.

Translating old LISP-style code into JS, your dictionary might look like:

var dict = [
["color", "blue"], [
    ["fruit", "peaches"], [
        ["fish", "herring"],
            null
        ]
    ]
];

and you'd access it with a function like:

getdictionaryelement = function (key, arr) {
  if (arr == null) {
    return NOTFOUND;
  } elseif (key == arr[0][0]) {
    return arr[0][1];
  } else {
    return getdictionaryelement(key. arr[1]);
  }
}

A pointer is an int which represents a physical address in memory.

Ah.

                <SET PT <GETPT ,HERE ,PRSO>>
		<COND (<EQUAL? <SET PTS <PTSIZE .PT>> ,UEXIT>

A bit of the walk verb from Hitchhiker's.

PT is a local variable.

GETPT is getting the property PRSO of HERE and setting PT to that value.

PRSO is the parser object (a global variable; sometimes the object after X FOO; sometimes just text after TYPE "HELLO WORLD"). HERE is a global variable that targets the current room.

In the ZIL world, there are a few types of exits: UEXIT (unconditional exit), NEXIT (nonexit (can't go that way)), CEXIT (conditional exit; to BASEMENT if CYCLOPS-FLED else "The door is nailed shut."), FEXIT (function exit; just a scipt exit), and DEXIT (door exit).

Here's another bit:

                  (<EQUAL? .PTS ,FEXIT>
		       <COND (<SET RM <APPLY <GET .PT ,FEXITFCN>>>

So, I type GO SOUTH.

Crap, I forgot that part. Here's how rooms are set up:

<ROOM BEDROOM
      (LOC ROOMS)
      (SYNONYM TRAVEL)
      (ADJECTIVE TIME)
      (DESC "Bedroom")
      (SOUTH PER BEDROOM-EXIT-F)
      (OUT PER BEDROOM-EXIT-F)
      (EAST TO KITCHEN)
      (WEST TO STRANGE-PASSAGE IF CYCLOPS-FLED ELSE"The wooden door is nailed shut.")

So, I enter GO EAST.

Now, the PRSO is EAST.

(The PRSA is GO, but that's besides the point.)

So, the code will now set PT to the value of HERE[PRSO].

So, HERE is the current location, and it has a "EAST" property which is "TO KITCHEN". This is an unconditional exit (UEXIT).

So...

<COND (<EQUAL? <SET PTS <PTSIZE .PT>> ,UEXIT>

To break that down, first we set the local variable PTS to <PTSIZE PT>.

We already set PT to HERE[PRSO], which is a UEXIT value: "TO KITCHEN".

So, PTSIZE is getting the pointer size of the current room object's SOUTH property and matching that with the pointer value that matches the value of UEXIT?

Am I on the right track?

(If this is boring you, feel free to ignore me. I think I'm slowly figuring it out.)


ALSO . . . .

Most objects (in-game objects are actually objects (I think?)) have a FLAGS property, like:

<FLAGS NDESCBIT TAKEBIT TRYTAKEBIT NARTICLEBIT>

NDESCBIT = object.scenery = true

TAKEBIT = object.take = true

TRYTAKEBIT =

object.take = { 
  if(foo) {
    AddToInventory(object)
    this.take = true
  }
  else { 
    msg("Bar first!")
  }

NARTICLEBIT = object.usedefaultprefix = false


The way it sets, clears, and checks flags:

<FSET ,OBJECT TRYTAKEBIT>
<FCLEAR ,OBJECT TRYTAKEBIT>
<FSET? ,OBJECT TRYTAKEBIT>


So, if I'm understand this, the FLAGS property would be like this:

object.flags = [ [NDESCBIT, TAKEBIT], [ [TRYTAKEBIT, NARTICLEBIT], [NULL] ]] 

And anything not wrapped in quotation marks is a variable, I think.

But it seems that they couldn't simply do if (object.ndescbit) back then, plus they could only have so N amount of objects and X amount of properties (and Z amount of space to work with).

At first, I thought simply splitting the string into a string list by " " then adding to, removing from, and checking if the list contained whatever string was the best way to try to emulate this, but now I realize it's more efficient to just add each separate boolean attribute to the object (I think).


Also also, I'm guessing there's no reason for me to try to decipher all the code in the parser file.

It has to be using the table/list things to do everything, and, even if I can fully understand all the inner-workings, I don't think I'd ever want to actually try to have something parse commands with strings of strings of strings of strings. I can use these new things called RegExp patterns for most of that, then check the synonym dictionaries if necessary to provide the detailed parser errors.


A pointer is an int which represents a physical address in memory.

No, I was talking about pointers in contemporary languages of the time - an alternative to the way LISP-style langauges handle lists and tables.

I don't know if PT in the code you're quoting stands for 'pointer', but even if it's the same word I strongly doubt it's the same concept. To me, it looks like they're using PT as an abbreviation for "property", and PTSIZE is a strangely-named equivalent of TypeOf. I could be wrong; I don't have much experience with OO LISP.

But that's entirely unrelated to what I was saying :p

So, if I'm understand this, the FLAGS property would be like this:

Why are you putting the flags in pairs?

I suspect that FLAGS is a set of flags rather than a table.
You give each flag a value (1, 2, 4, 8, 16…) and retrieving them just checks the specified bit of the flags property. So you can easily check one of them by extracting a single bit from a property. Saves memory that way.

But if they were a table, it would either look like:

object.flags = [ [NDESCBIT, true],
  [[TAKEBIT, false], [
    [[TRYTAKEBIT, true], 
      [[NARTICLEBIT, false],
        NULL
      ]
    ]
  ]
];

or

object.flags = [NDESCBIT,
  [TRYTAKEBIT, 
    NULL
  ]
];

(without the false ones)

More likely, each flag has a fixed value which is taken from 1, 2, 4, 8, 18, 32, etc…, and the flags property is just the sum of the ones which are true. This was a common way to store flags in all kinds of languages, because you're only using 1 bit in memory for each flag; a byte can hold 8, an int could hold up to 32 (depending on what datatype your ints are).


I don't know if PT in the code you're quoting stands for 'pointer', but even if it's the same word I strongly doubt it's the same concept. To me, it looks like they're using PT as an abbreviation for "property", and PTSIZE is a strangely-named equivalent of TypeOf.

This was my original theory (and how I coded this part in JS).

But that's entirely unrelated to what I was saying :p

Oh. I get overly excited when exposed to new information.

Why are you putting the flags in pairs?

I don't know! I've gone mental!!!

:O)

You give each flag a value (1, 2, 4, 8, 16…) and retrieving them just checks the specified bit of the flags property. So you can easily check one of them by extracting a single bit from a property. Saves memory that way.

Aha.

I'm pretty sure the flag is there or it's not there. It doesn't look like true or false is used.

I was wondering if PTSIZE was the length of the string when I first saw it in the WALK routine. The exit properties are "TO KITCHEN", "TO KITCHEN IF FOO ELSE 'EXIT NOT AVAILABLE RIGHT NOW', "TO KITCHEN PER EXIT-F".

I decided that couldn't be the case because there was never any telling how long a room name or ELSE string might be.

I ended up just making regexen to determine the exit type in the WALK script.

I'm trying to make it so I have to change as little of the source code as possible, as far as the game objects' code is concerned, I mean. I don't want to have to think about how I changed each way to handle every property on every object in the game.

I'm only porting up until you leave Earth, just like I did in Quest 5, but that's still a lot of code.


The following is from "ZIL Language Guide" by Jesse McGrew (which I can only find in some ZILF documentation that is hard to find, so I'm posting it here -- since it is relevant):


Tables

Tables are arrays or buffers that can be located in either static memory (ROM) or dynamic memory (RAM).

<ITABLE [length-type] count [(flags...)] [default...]>

Defines a table of count elements filled with default values: either zeros or, if the default list is specified, the specified list of values repeated until the table is full.

The optional length-type may be the atoms “NONE”, “BYTE”, or “WORD”. “BYTE” and “WORD” change the type of the table and also turn on the length marker, the same as giving them as flags together with “LENGTH”. (For an explanation of flags, see below.)

<[P][L]TABLE [(flags...)] values...>

Defines a table containing the specified values.

If this command is invoked as PLTABLE, the “PURE” and “LENGTH” flags are implied; as PTABLE, “PURE” is implied; as LTABLE, “LENGTH” is implied; and as TABLE, none are implied. In all cases, additional flags may be given. (For an explanation of flags, see below.)

Note: all of the table generating commands produce a table address, which must be assigned to a constant, variable, or property to be used within the game. For example:

<CONSTANT MYTABLE <ITABLE BYTE 50>>

Types of Tables

These flags control the format of the table:
• “WORD” causes the elements to be 2-byte words. This is the default.
• “BYTE” causes the elements to be single bytes.
• “LEXV” causes the elements to be 4-byte records. If default values are given to ITABLE with this flag, they will be split into groups of three: the first compiled as a word, the next two compiled as bytes. The table is also prefixed with a byte indicating the number of records, followed by a zero byte.
• “STRING” causes the elements to be single bytes and also changes the initializer format. This flag may not be used with ITABLE. When this flag is given, any values given as strings will be compiled as a series of individual ASCII characters, rather than as string addresses.

Table Options

These flags alter the table without changing its basic format:
• “LENGTH” causes a length marker to be written at the beginning of the table, indicating the number of elements that follow. The length marker is a byte if “BYTE” or “STRING” are also given; otherwise the length marker is a word. This flag is ignored if “LEXV” is given.
• “PURE” causes the table to be compiled into static memory (ROM).


Also:

So you doarr[0] if you want the first element (GET).
Or you do arr[1] (REST) to get the rest of the list, which is itself a list.

I think really get it (the table/array thing) now.

If had the list 1, 2, 3, 4, 5, it would be like:

list = [1, [2, [3, [4, [5, null]]]]]

If I just had 1,2,3:

list = [1, [2, [3, null]]]

1,2

list = [1, [2, null]]

Do I have it right now?


Think so :)

That explains why looping over an array in LISP is almost always recursive.

On the other hand, the mention of size declarations in that bit of manual implies that they might have made a table datatype that behaves like a LISP table, but is stored on the backend as a bunch of fixed-length records, like a C array or a COBOL table. Changing the storage structure for lower-memory machines, but keeping the idiosyncratic language style that grew out of the old structure.


(RE) Types of Tables

These flags control the format of the table:
...

  • “BYTE” causes the elements to be single bytes.
  • “LEXV” causes the elements to be 4-byte records. If default values are given to ITABLE with this flag, they will be split into groups of three: the first compiled as a word, the next two compiled as bytes. The table is also prefixed with a byte indicating the number of records, followed by a zero byte.
    ...

EDITED

So, what the heck is this?

<GLOBAL P-LEXV <ITABLE 60 (LEXV) 0 <BYTE 0> <BYTE 0>>>

Is it:

let P_LEXV = [60, "PLACEHOLDER STRING", 0, 0, 0];

OR (TECHNICALLY):

let P_LEXV = [60, ["PLACEHOLDER STRING", [0, [0, [0, null]]]]];

Also this:

<GLOBAL OOPS-INBUF <ITABLE 120 (BYTE LENGTH) 0>>

Is that:

let OOPS_INBUF = [120, OOPS_INBUF.length, 0];

TECHNICALLY:

let OOPS_INBUF = [120, [OOPS_INBUF.length, [0, null]]];

I think these are the last two questions I'll have concerning this.

These tables are kicking my butt!


EDIT: I missed that the options said (BYTE LENGTH), which adds an extra element to the array, a single byte containing the number of elements.

I think your second example will be simpler.
I expect that

<GLOBAL OOPS-INBUF <ITABLE 120 (BYTE LENGTH) 0>>

is equivalent to something like:

let OOPS_INBUF = Array(120).fill(0).unshift(120);

Creating an array of 120 0s, with an extra element stuck on the beginning (thanks to the LENGTH option) saying how many elements it has.

Although, as we previously saw, LISP-style tables have GET and REST functions, and in some ways act as if they are arrays-in-arrays-in-arrays…

If you're handling them like that, you could do:

let OOPS_INBUF = [null];
for (i=0 ; i<120 ; i++) {
  OOPS_INBUF = [0, OOPS_INBUF];
}
OOPS_INBUF = [120, OOPS_INBUF];

or as a single statement:

let OOPS_INBUF = [120, new Array(120).fill().reduce((d, a)  => ([a, d]), [null])];

I'm not sure if it's useful to handle tables in this way or not. You're working with code in which it seems to be common to access either the first element of a table or the remainder. But it may be easier just to use JS arrays for the tables, and use slice.

Even if your data isn't stored in the same way, you can do equivalent things with it, which is probably close enough for learning to port something. Really I only included the stuff about endlessly-nested 2-element arrays in the hope of explaining while LISP-style languages ended up evolving with a pair of functions like GET and REST, or car and cdr – coming from a language where arrays are arbitrarily sized, it seems odd to treat the first element specially, unless you know how they worked behind the scenes.


Anyway… on to the first example.

It looks like a LEXV is some kind of data structure that contains 3 elements; an int and 2 bytes. They're presumably treated similarly to a C struct; it's a fixed-length record for storing 3 numbers in a group, with the first one larger than the others. As it's a fixed-size thing, I assume you can represent it in JS as a 3-element array.

So I think that means 0 <BYTE 0> <BYTE 0> is a LEXV - an int 0, and two byte 0s. In JS I'd probably represent that as [0,0,0].

<GLOBAL P-LEXV <ITABLE 60 (LEXV) 0 <BYTE 0> <BYTE 0>>>

The (LEXV) in the table definition is an option, which causes an alternate version of the ITABLE function to be used. Like passing command-line switches to a program, before the rest of the arguments. When that is specified, the number immediately before it is the initial length of the table, and the number after it is the value to be placed in the row.

So

<GLOBAL P-LEXV <ITABLE 60 (LEXV) 0 <BYTE 0> <BYTE 0>>>

creates a table (which acts like a nested set of 2-element lists), 60 elements deep, in which each element has type LEXV and is initialised to 0 <BYTE 0> <BYTE 0> – which we can assume is the same as [0,0,0] because JS doesn't usually care about the number of bytes of memory allocated to store a variable.

So that line would be translated to something like:

let P_LEXV = new Array(60).fill().map(() => [0,0,0]);

or a little easier to understand:

let P_LEXV = [];
while (P_LEXV.length < 60) {
  P_LEXV.push([0,0,0]);
}

Depending on the implementation behind the scenes, it may well be something like:

let P_LEXV = [
  [0,0,0], [
    [0,0,0], [
      [0,0,0], [
        [0,0,0], [
          [0,0,0], [
            [0,0,0], [
.... and so on for 6 repetitions
          ]
        ]
      ]
    ]
  ]
]

so you'll be using GET and REST to recurse down into the table. Just in this case, each "element" in the table is a 3-element struct containing an int and 2 bytes.

Note:
I think the line is probably equivalent to:

let P_LEXV = new Array(60).fill([0,0,0]);

but I stated:

let P_LEXV = new Array(60).fill().map(() => [0,0,0]);

above, because otherwise it would behave weirdly when you try to change the values. Because the LEXV type is something which behaves like a struct in C, which JS (as a dynamically-typed language) has no direct equivalent for.

Sorry if I'm repeating myself; my blood sugar's a little low and I'm finding it hard to focus.


let OOPS_INBUF = Array(120).fill(0).unshift(120);

Brilliant!

I wasn't piecing that 1st value (120) and the length together. This makes perfect sense!


[...] it may be easier just to use JS arrays for the tables, and useslice.
[...]
Even if your data isn't stored in the same way, you can do equivalent things with it, which is probably close enough for learning to port something. Really I only included the stuff about endlessly-nested 2-element arrays in the hope of explaining while LISP-style languages ended up evolving with a pair of functions like GET and REST, or

Oh, yeah. I got ya'.

That's what I've been doing.

I'm just trying to think of the original source code in the "list-in-list-in-list" kind of way, just in case I might overlook something important otherwise. :)


let P_LEXV = new Array(60).fill().map(() => [0,0,0]);

Also brilliant!

Hot dog! Even if I had known the first value was to be the length of the array/table, I'd never have known about Array.prototype.fill()!


because otherwise it would behave weirdly when you try to change the values

Oh, I'll definitely be changing the values.

P_LEXV is the Quest equivalent of game.pov.currentcommand.

That's another confusing thing. It looks like LEXV is the text last entered by the player, but they also say "“LEXV” causes the elements to be 4-byte records. If default values are given to ITABLE with this flag, they will be split into groups of three: the first compiled as a word, the next two compiled as bytes. The table is also prefixed with a byte indicating the number of records, followed by a zero byte."

I might be wrong. LEXV might not be a global at all. (I haven't made it all the way through this bit (the parser) of the code yet, but I used CTRL+F to search this particular file for "LEXV", and found nothing but "P_LEXV".)

My theory is that LEXV is either a flag when creating a table, or a global variable. I'm assuming it couldn't be both in MDL. (Also, when I say "MDL", it seems that I mean "a mod of LISP".)


Sorry if I'm repeating myself; my blood sugar's a little low and I'm finding it hard to focus.

Man, go get your blood sugar right! (Don't make me threaten to come find you and make you do it!)

Just kidding!

Sort of...

:o)


Hot dog! Even if I had known the first value was to be the length of the array/table, I'd never have known about Array.prototype.fill()!

The big gotcha there is things like:

var sometable = new Array(60).fill([0,0,0]);

That creates a new array with 60 elements, and then calls fill to change all its values to [0,0,0].

Source of a common mistake which confuses people when they first use it. Because you're creating an array [0,0,0] and then filling the array with 60 references to the same array. Meaning that if you do sometable[5][0]++, it will change all of them to [1,0,0].

fill fills all the elements with its argument; in this case setting them to undefined I think. map then converts those undefined values to [0,0,0], but because the map function is run once per element, it creates a new [0,0,0] each time. (The fill is still necessary, because new Array(60) creates an array whose length is 60 but has no elements. So just using map would still give you no elements. In this usage, new Array(60) sets the length, fill fills in the keys, and map gives them values.

My theory is that LEXV is either a flag when creating a table,

In this code, it seems to be a flag indicating the data type of the table's contents.


Awesome. I've got all that coded.

I lied about that being the last question, though. (Surprised?)


I really think this is the last thing:

;"For AGAIN purposes, put contents of one LEXV table into another."
<ROUTINE STUFF (DEST SRC "OPTIONAL" (MAX 29) "AUX" (PTR ,P-LEXSTART) (CTR 1)
						   BPTR)
	 <PUTB .DEST 0 <GETB .SRC 0>>
	 <PUTB .DEST 1 <GETB .SRC 1>>
	 <REPEAT ()
	  <PUT .DEST .PTR <GET .SRC .PTR>>
	  <SET BPTR <+ <* .PTR 2> 2>>
	  <PUTB .DEST .BPTR <GETB .SRC .BPTR>>
	  <SET BPTR <+ <* .PTR 2> 3>>
	  <PUTB .DEST .BPTR <GETB .SRC .BPTR>>
	  <SET PTR <+ .PTR ,P-LEXELEN>>
	  <COND (<IGRTR? CTR .MAX>
		 <RETURN>)>>>

Alright. . .

I don't see them ever use the third parameter.

This is how it used in the code:

<STUFF OOPS-TABLE P_LEXV>

I was doing OOPS_TABLE.push(P_LEXV), but now that I found the actual STUFF routine, it looks like I just need to do OOPS_TABLE = P_LEXV.

Plus, that would make actual sense, as we're dealing with the OOPS command, which will only work with the last entered command when it was an "object not found" parser error.


FULL DISCLOSURE

A good challenge releases a good amount of dopamine into my brain.

Coding in general releases a small amount of dopamine into my brain.

I've been porting this ZIL code for about two weeks now, every day, most of the day and night. First, I ported the things necessary to complete the first part of the game to Quest 5. Since then, I've been porting all the under-the-hood ZIL code to JS.

All the while, I've been high on the process. (Don't judge me! Coding is addictive!!!)


So, well, damn... I forget the point I wanted to make, because I'm high on coding.

Oh yeah. I remember. I wanted to thank you (mrangel).

I get lost in the process lots of times, and I usually forget to take the proper time to show my appreciation during those times. (Because I'm high on coding! Plus, I'm an ass-half. It takes two of me to make an asshole.)


After translating doing my best (with a LOT of help from mrangel) to translate quite a bit of ZIL to JS, I'm thinking all the while() loops are not something I want to use that much in JS. Most of the time in ZIL, they use REPEAT() to run through tables, which I think would be safer to do via Array.prototype.forEach() in JS.

I did learn a nifty trick concerning while while messing with while, though.

let i = 10;
myLoop:
while (i > 0) {
  console.log (i);
  i--;
  if (i === 3) {
    console.log ("RESTARTING");
    continue myLoop; // JS version of <AGAIN> in ZIL
  }
  console.log ("Still going...");
}

Hi! Author of ZILF here with a few notes.

  1. PT, as in GETPT and PTSIZE, stands for property table -- i.e., the span of memory that contains the data for a particular property. Whereas <GETP ,OBJ ,P?PROP> returns the value stored in the property, <GETPT ,OBJ ,P?PROP> returns the address where that value is stored, which can then be accessed with table functions like GET.

  2. PTSIZE returns the size (in bytes) of a property table whose address was previously obtained with GETPT, by decoding the header stored just before the property data. In practice, it functions as a "TypeOf" for direction properties, because the compiler ensures that each type of exit has a different length when it's stored as a property.

  3. MDL has both Lisp-style linked lists, written with (), and vectors (arrays), written with []. Conveniently, they can be accessed with the same functions. To get the nth element of a list or vector, you can use the NTH function (<NTH ,FOO 3>) or simply use a number as a function name (<3 ,FOO>). To iterate through the elements of a list or vector, you can use MAPF (or occasionally MAPR), which is basically a foreach loop that uses a lambda for the loop body and can return a value. But...

  4. Code that runs on the Z-machine (i.e. code inside a ROUTINE) doesn't have access to dynamic memory allocation, and therefore doesn't use linked lists. Or lambdas. In fact, none of the dynamic features of MDL are available at runtime: value types are lost during compilation, so there's no way to test whether the value in a variable is a number, a string, an object, or a table. You might still encounter uses of things like MAPF in ZIL code, but they'll be in code that runs at compile time (i.e. MDL). Therefore...

  5. ZIL uses TABLE for data that has to be accessible at runtime. A table is a fixed length vector with some special properties; for example, different elements can take up different amounts of space in the table. LEXV is the main example of that: every third element is stored as a word (2 bytes), and the rest are stored as single bytes. That metadata, like everything else about value types, is lost during compilation.

  6. If you want to know more about low-level details like this, one way is to compile some code with ZILF and then look at the assembler output (.zap files). There's also Henrik Åsman's ZILF Reference Guide.


Hi!

Hello!


Author of ZILF here with a few notes.

Wow! This is unexpected. I have so many questions. I shall narrow it down to three.

...after thoroughly reviewing these notes, of course -- as they will likely answer questions I don't even know I'm going to have yet!

Thanks, by the way! For the input here, but mostly for ZILF! I've been perusing the Hitchhiker's code posted by historicalsource on GitHub for quite a few days now, and it's been equally educational and entertaining. Plus, when I'm not sure how a certain bit of the code is "behaving", I can add a call to TELL to print "debugging" messages in games after compiling with ZILF and ZAPF. (It's all freaking awesome, but I digress.)


Regarding your notes (and thanks again!):

  1. Cool; that's pretty much how mrangel said he believes all this works. The bit about returning the address of where a value is stored is the bit about which I am ignorant (not using the word pejoratively, mind you). I think I mostly understand, though. It seems like ...well, similar to when I create an object in JS like frob = { take: function(){ console.log("Taken.")} (keeping it super, super simple), window["frob"] is one way to refer to the property of the window object (like when I tell someone to go to "my house"), and from that JS knows the actual physical location "on the drive" from which to retrieve the data?

  2. Rock and roll! That was the theory posed by mrangel concerning this, too. (Smart fellow, that one!) I decided to use regular expressions to match the string values of the exits in JS. (It seemed like the all-around easiest way to handle the exits. I'm leaning away from using regular expressions to handle the player's commands, though -- because all the synonyms and whatnot seem much, much more intuitive.)

  3. I've seen plenty of linked lists, like () (mrangel explained those to me, too), but I don't recall coming across any vectors (or arrays), like []. I went through all of SYNTAX.ZIL, VERBS.ZIL, EARTH.ZIL, I did bits and pieces that I needed from GLOBALS.ZIL, and I believe I made it about 2/3 of the way through PARSER.ZIL before I went cross-eyed and decided to take a break from attempting to port any more of it for a couple of days. That was two days ago. Now that I am aware of the vectors, I shall most likely discover one (or more) of them sooner than later (due to the interconnectdeness of all things and all that). From what I've gathered so far (which is very little), a vector is like a ... huh. I don't know what a vector is (yet). I have seen <NTH ,FOO 3> and I figured it out. I seem to recall seeing a <3 ,FOO>, but I don't remember how I handled it, which suggests I skipped over it -- leaving a //TODO in its place. Also, I am fairly certain I saw MAPF when I scanned ahead a little before closing PARSER.ZIL the other day. So, you just helped me with that up front -- as soon as I go learn what the heck a lambda is!

  4. I oddly understood most of that -- because of my level of understanding, of course; not that the explanation wasn't well-written. So, if I understand, this is why it checks for the sizes of properties in bytes (because it's the only way it can identify the TypeOf)? And that's also why it needs an identifying header stored in front of the property value in a table?

  5. I'm going to have to go learn what a vector is understand the tables. I've also previously noted that I need to learn about 2 byte words and single bytes. I called myself researching the latter for an hour or so, but all I could find online were forum threads with people discussing them in a way which was not elementary enough for me to grasp the concept. My issue (if you haven't guessed) is a serious lack of exposure. I hadn't even heard of LISP or MDL until I posted a link to all the Infocom source code and mrangel said it did not look unlike LISP. Ha ha! Anyway, I know good and well I've seen LEXV numerous times and wondered what it is (expecting to find out later in the code).

This is so much fun! The inner-workings of text adventures are their own adventures! Plus, I'm learning about lots of unknown unknowns along the way, which is always a good time! And I decided to take on Hitchhiker's right off the bat, so, when I'm messing with the code on Thursdays (which I never could quite get the hang of) and having problems understanding a bit of code, I'll eventually work through it to come to some dialogue written by Douglas or Steve that makes me laugh aloud and get back to enjoying the code.

By the way (and back on track). . .

  1. I've already learned three things I always needed to know but didn't know I needed to know it until perusing Henrik Åsman's ZILF Reference Guide. I hadn't found that one. I also haven't even looked at the contents of any .zap files, and now I don't know why I never thought to do so. I'm always working from the ZIL files, so I search for a string like grep -in lexv *zil, totally omitting anything noteworthy I might of noticed in a .zap file. Now I'm curious as to what all I'll find in those!

PS

I will hold off on those three questions I mentioned at the beginning of this, but I do wonder if this is the version of ZILF everyone should be using (because it is, in fact, the version I am using): https://foss.heptapod.net/zilf/zilf/-/wikis/Releases/0.9/Downloads


PPS

Everything I've said in this thread that is correct is due to people like you, mrangel, The Pixie (the current developer of Quest), and many, many others who have provided the necessary information (and lots of times a boost in the right direction).

Everything I've said in this thread that is incorrect is due to my own misunderstandings (and probably a few typographical errors here and there), but I always do my best to correct any misinformation I have shared once I've learned about it.

Anyway, I've got some good reading to go do now. I shall return once I've read up about vectors and lambdas and tables (oh my).

Thanks, again! To everyone!


Another resource you might find helpful is the Z-Machine Standards Document, especially sections 1 (the memory map), 12 (the object table), and 13 (the dictionary and lexical analysis).

The bit about returning the address of where a value is stored is the bit about which I am ignorant

Here's one way to think about it.

If you open the compiled story file in a hex editor, you can search through it and find the initial values of every global variable, every property of every object, and so on, all laid out the same way they're laid out in the game's memory (because the interpreter copies the whole story file into memory when you start the game).

For example, the object GOWN in HHGG (defined here) has a property (SIZE 15), so somewhere in nhitch.zip you'll find the bytes 00 0f, which is 15 written in hex as a two-byte word. Well, you'll probably find many copies of 00 0f, but one specific copy represents the SIZE of GOWN.

The number of bytes you have to skip ahead from the beginning of the file to find it is the address of that property value.

So, if I understand, this is why it checks for the sizes of properties in bytes (because it's the only way it can identify the TypeOf)? And that's also why it needs an identifying header stored in front of the property value in a table?

It may help to see what a property table looks like. Here's the one for HATCHWAY in HHGG, from nhitchdat.zap.

Each time the game accesses a property, the interpreter has to search through the object's property table, top to bottom, to find the data for that property. The headers in front of each property value (represented in the ZAP code by .PROP directives) let it know when it has found the right property, as well as how far ahead to skip to find the next property.

Using the property size to indicate what kind of data is stored in the property is a convention Infocom came up with for direction properties. It makes sense for those because most types of exits need different amounts of data anyway: a UEXIT only has a destination room (an object number, which is 1 byte on Z-machine version 3), a NEXIT only has an optional error message (a 2 byte string address), a DEXIT has a destination and door object plus an optional error message (1 + 1 + 2 bytes), etc.

Some other properties can have varying length, but usually it's just because they can contain more than one value. For example, objects often have more than one value in their SYNONYM, ADJECTIVE, or GLOBAL. In those cases, the game will still use PTSIZE when iterating through the values in the property so it knows when to stop.

(As mrangel pointed out, FLAGS might look like another example of this, but it's actually a special case and isn't stored as a property at all -- in the ZAP files, you'll find each object's flags listed in the .OBJECT directives, far from the property tables.)


If you open the compiled story file in a hex editor, you can search through it and find the initial values of every global variable, every property of every object, and so on, all laid out the same way they're laid out in the game's memory (because the interpreter copies the whole story file into memory when you start the game).

For example, the object GOWN in HHGG [...] has a property (SIZE 15), so somewhere in nhitch.zip you'll find the bytes00 0f, which is 15 written in hex as a two-byte word. Well, you'll probably find many copies of 00 0f, but one specific copy represents the SIZE of GOWN.

The number of bytes you have to skip ahead from the beginning of the file to find it is the address of that property value.

Oh! I think I'm beginning to see the light now.


image

I need to learn about binary to fully understand all of this.

...but 15 as a two byte word is 00 0f, because 00 is 0 and 0f is 15, and together they are 15. I get that part of it, but I don't understand how 0f is 15. In binary, I can figure that 00001111 is 15 without looking it up, but that's about the extent of my binary skills.

I have no clue how I would figure out how 0f is 15, without putting my thinking cap on and doing some research.

This looks like a good starting point: https://userweb.cs.txstate.edu/~js236/201112/cs1428/lecture16.pdf

image

image


So, the SIZE of GOWN seems like it should be somewhere in 002f0 in the memory. . .

I'm off to find a hex editor to investigate!


It may help to see what a property table looks like. Here's the one for HATCHWAY in HHGG, from nhitchdat.zap.

Each time the game accesses a property, the interpreter has to search through the object's property table, top to bottom, to find the data for that property. The headers in front of each property value (represented in the ZAP code by .PROP directives) let it know when it has found the right property, as well as how far ahead to skip to find the next property.

Using the property size to indicate what kind of data is stored in the property is a convention Infocom came up with for direction properties. It makes sense for those because most types of exits need different amounts of data anyway: a UEXIT only has a destination room (an object number, which is 1 byte on Z-machine version 3), a NEXIT only has an optional error message (a 2 byte string address), a DEXIT has a destination and door object plus an optional error message (1 + 1 + 2 bytes), etc.

Okay. . .

So, this is one exit:

	.PROP 4,P?EAST		; CONDITIONAL EXIT
	ACCESS-SPACE-ENTER-F		; PER FUNCTION
	.BYTE 0
	.BYTE 0

Let's see if I've got it.

.PROP signifies that this is where a property begins.

4 lets us know that we can skip ahead 4 bytes to find the next property, past ACCESS-SPACE-ENTER-F (which is 2 bytes?) and both instances of .BYTE 0.

Did they add the two instances of .BYTE 0 just to make an FEXIT a different size than an NEXIT?


SIDENOTE

I had assumed that P? and W? were routines until I looked at that .zap file. Like, I thought W?ALL was like W? (ALL) for some reason. Now I see that there is actually a W?ALL, and now I feel silly, which is usually what happens not too long after I assume anything. :)


(Apparently "zork" was a nonsense word used at MIT for the current uninstalled program in progress, and stuck. Just as this document uses the term "Z-machine" for both the machine and its loaded program (which is also sometimes called the "story file"), so ZIP (Zork Implementation Program) was used to mean either the interpreter or the object code it interpreted. Code was written in ZIL (Zork Implementation Language), which was derived from MDL (informally called "muddle"), a particularly unhelpful form of LISP. It was then compiled by ZILCH to assembly code which was passed to ZAP to make the ZIP.)

Ha!

"MDL [...] a particularly unhelpful form of LISP."


Last time I tried to read the Z-machine Standards Document, it was, as the say, "Greek to me."

I was ignorant of a couple of the fundamental building blocks. Now I get it, though! I understand what mrangel was trying to tell me about the address! And I understand the stuff vaporware is telling me, too!

Whoo-hoo!

I really do very much appreciate all the help!


I'm not sure if I'm right about many of my guesses… I was half guessing, and half randomly reminiscing about my experience with other LISP variants. But hopefully some of it was useful :)


I'm not sure if I'm right about many of my guesses… I was half guessing, and half randomly reminiscing about my experience with other LISP variants. But hopefully some of it was useful :)

I haven't been actively trying to prove anything you said wrong, but I have pretty much covered everything you threw out there. Between the super-useful posts by vaporware and all the documentation, everything I recalled you saying was right on point.

Looking at that .zap file was the final key, and the lock was the Z-machine Standards Document. Piecing those together with all the stuff you two have posted, along with all the ZIL source code I've been trying to translate to (something very similar) in JS, I finally see how it's all working.

"A moment of realization is worth a thousand prayers."
- M. Knox


post moved to a new thread


Let's see if I've got it.

.PROP signifies that this is where a property begins.

4 lets us know that we can skip ahead 4 bytes to find the next property, past ACCESS-SPACE-ENTER-F (which is 2 bytes?) and both instances of .BYTE 0.

Correct.

Did they add the two instances of .BYTE 0 just to make an FEXIT a different size than an NEXIT?

Exactly.

I had assumed that P? and W? were routines until I looked at that .zap file. Like, I thought W?ALL was like W? (ALL) for some reason. Now I see that there is actually a W?ALL, and now I feel silly, which is usually what happens not too long after I assume anything. :)

Indeed, the ? is just part of the name.

By convention, ? is used at the end of a name to show that something returns a boolean value (e.g. the ==? function). A ? in the middle or beginning is used when the compiler generates names, usually by adding a prefix.

For example, the word "in" can appear in player input, so it has an entry in the dictionary (i.e. the VOCAB table). The constant pointing to that entry is W?IN. The word can be used as a preposition, so the constant for its preposition number is PR?IN. And since IN is also a direction property, there's a constant for its property number called P?IN.


Splendid!

I am beginning to get the hang of this.

Thanks for all your help!


This topic is now closed. Topics are closed after 60 days of inactivity.

Support

Forums