Journal of an Inform "Clone"

Starting a thread to record my journey as I explore possibly making an inform-inspired programming language. I don’t really have anything substantial done yet, but I have started doing some research.

The thought started here after me thinking about what it would take to make an LSP for inform7.

Now I’m thinking I’d rather just experiment with making my own language that is very inspired by inform.

From my research so far it looks like training a CRF is really simple and can get nearly state-of-the-art accuracy on part of speech tagging. One example is Part-of-Speech Tagging using Conditional Random Fields: Exploiting Sub-Label Dependencies for Improved Accuracy, though we probably don’t need the optimization that that paper presents so much since it didn’t seem to increase accuracy all that much on English.

I can use the crfs crate for training the part-of-speech tagger, instead of implementing the algorithm myself.

For lemmatization, I can use a dictionary taken from the Universal Dependencies word trees that maps words + their part of speech to their lemma. I’ve already got the dictionary generator working, and I can add some simple suffix-stripping fallback rules for verbs and adverbs to create guessed lemmas for out-of-vocabulary words.

I still need to figure out the best algorithm for constructing the tree of the words in the sentence from the (word, lemma, part-of-speech) tags. Stanza is a useful reference, but it uses more neural networks, and I’d like to keep things simpler and faster than that if the accuracy is acceptable.

Inform7 uses an extremely limited subset of English grammar so I should at least be able to tag most inform sentences I’d think, but I’d also like it to be more flexible. It doesn’t need to work for everything, though. The LSP should help show where it can’t understand things, so that you aren’t too confused or annoyed while writing, and you can just simplify the sentence if it doesn’t understand it.

Anyway, that’s about as far as I’ve gotten. If I can create a parser that can decently tag the lemma, part of speech, and the tree structure of a sentence, then I can do more research on how inform bootstraps grammar meaning and work out what that might look in this new language.


Eventually I’ll need a name for the project. I’ve thought of reform which sounds too high and mighty, deform which sounds too negative, and conform which would make more sense if I was trying to be inform compatible, which I don’t think is a useful goal.

It probably shouldn’t be anything form because I’ll probably do this in layers and make the language not even specific to interactive fiction anyway.

I’ll figure the name out later.


I’ll just go ahead and make comments as I make progress and we’ll see how far I get. This will probably come to nothing, but you never know… :slight_smile:

4 Likes

I’d be interested to hear what you figure out!

Not sure if this is useful to you since it doesn’t sound like you’ll be using Preform, but what I did for my Preform generator was pretty simple:

  • First I create the inflection pattern for each word/lexeme and lemma pair (if you’ve seen a Preform trie, the patterns look like “0ed”, “3ook”, etc).

  • Then I create one suffix tree/trie per part-of-speech, where each leaf node in the tree is a (word, inflection pattern) pair, and each parent node is its children’s common suffix and a list of their inflections.

  • Then, to generate the Preform grammar, I “collapse” the tree by always finding the smallest subtree with a single inflection pattern (ie. most specific rule, or most irregular inflection) and making it a Preform rule, removing the subtree and its inflection pattern from the tree. The root of the tree will then be the last rule; its common suffix will be “*” and its inflection will be the most regular pattern.

The most important thing! I called my Preform generator “Preforge”.

1 Like

Enjoy your journey. I’ve been on a similar one for several years. Are you working on the parser for the player input or for a Domain Specific Language in which to write the IF?

1 Like

I’m focusing on a language for writing IF.

But it’s definitely possible that the parser could be useful for player input too. :thinking:

I’m not sure what it takes to compile to Glulx yet, so I don’t know how easy it would be to port my Rust implementation of the parser to it.

Edit: A quick read of the intro to glulx sounds like it might be remotely possible I could compile to risc-v or WASM and then translate it to glulx, but I’m not really sure.

Wasm2Glulx may be of interest.

1 Like

Woah, yeah, that might be really handy, thx!

After a little bit of confusion on how to run inference with the CRF I finally got a 93% accuracy for part-of-speech tagging on one of the test datasets!

My confusion was that you train the CRF with a bunch of attributes derived from the words: word, suffix2, suffix3, suffix4, wordAfter1, wordAfter2, wordBefore1, wordBefore2, etc.

But when I was running inference I was only passing the word. :man_facepalming: After getting a little stuck on that, and finally figuring it out I went from ~20% accuracy to 93%!

That was when I trained with the fastest training method which was “averaged perceptron” and took less than 10 minutes. I’m running the much slower l2 normalized stochastic gradient descent trainer now and we’ll see what kind of accuracy I get from that.

And this is with no big machine learning dependencies or anything. The binary is 1.6 MB so far, and the trained model is 6.9MB.

Edit: So far at 94.14% accuracy on the English LinES dataset after training for only 19 seconds! More training doesn’t make it better since it actually starts overfitting. I think the more expensive training methods don’t have much more accuracy and they’re way slower so I’m not going to wait for those now.

Edit 2: And I got 94.25% accuracy on the GUM dataset which is more than 2x larger than LinES in only 6.5 minutes of training.

Edit 3: On the CHILDS dataset it gets 94.8% accuracy in 10 seconds with a 3.7MB model file. That might actually be a really appropriate dataset for inform-like sentences.

I got 93.4% accuracy on GUM + LinES + EWT in 21 minutes of training with a 19MB model file. But most of the sentences in those datasets are far more complicated than the ones that we’ll be using for world modeling most of the time, I think.

1 Like

Status Summary: Part of speech tagging is a success. :tada:

Training time: 10 seconds
Model size: 3.6 MB
Program size: 1.6 MB

Example sentence:

$ reform tagger run Instead of talking to John while cooking in the kitchen is happening .
Instead - ADV
of - ADP
talking - VERB
to - ADP
John - PROPN
while - SCONJ
cooking - VERB
in - ADP
the - DET
kitchen - NOUN
is - AUX
happening - VERB
. - PUNCT

Next step, train for extracting the tree structure of the sentence. :evergreen_tree:


I write this fun little status report myself, and it drives me crazy that it looks like something an LLM would do, but whatever.

2 Likes

So as soon as I had this figured out I realized that fuzzy pattern-matching on part of speech really wasn’t as much as a super power as it seemed.

I realized that I still needed a way to bootstrap meaning, and the fuzzy, “the grammar will defined the meaning of the other grammar and we’ll figure it out when we get there” didn’t hold up as soon as I thought about it in depth.

That’s OK, since this was all about an experiment anyway, but it was a total change in direction…

I realized that what I needed was a foundational, like, “theory of compute” to run the thing on.

i.e. regardless of how we write about the world we still have to have the computer operate and compute on that world somehow. I had initially imagined maybe the whole “world” was a matter of transformation over grammar, but that’s not really a great idea.

So I started doing some researching around different “calculus” that might be suitable for my goals. Things similar to lambda calculus where everything is built around substitution seemed like a good idea.

After doing some AI chatting / searching I got it narrowed down to an extremely simple system that I could hypothetically build everything on:

  • You have facts, which, after some iteration, are just tuples such as (Kitchen, is, Room), (Room, is, Type).
  • You have rules, which match on facts in the world, with potential placeholders, and when they match, they can create new rules and delete old ones.
  • And that’s about it. Even rules are just facts, so they can create or delete other rules.

If you add a little bit of syntax sugar that automatically maps non-tuple-style facts like John is a man to (sentence, John, is, a, man), and now the language can actually “write itself”, by having rules that match on (sentence, ..) facts and parse them to create new facts about the world.

I had the AI write a prototype of the language so that I could get experimenting with the idea faster and see if it seemed like a good direction. In about 1k lines of Rust, including some tests, the engine is already usable for creating primitive interactive fiction.

I manually refactored the parser to simplify things and now I’m working out how to build the foundational world model and rules to see how well this is going to scale.

Here’s an example of the latest version and a simple demo game where you can walk between rooms.


# Rules for going in a direction
(rule, go_north,
    ( -(go, north), -(here, is, ?here), (?going, north, of, ?here) ),
    ( (here, is, ?going), (print, (You are in the ), ?going, .) )
)
(rule, go_south,
    ( -(go, south), -(here, is, ?here), (?going, south, of, ?here) ),
    ( (here, is, ?going), (print, (You are in the ), ?going, .) )
)
(rule, go_fail,
    ( -(go, ?dir), (here, is, ?here), !(?going, ?dir, of, ?here) ),
    ( (print, (You can't go that way.)) )
)

# Everything that has a "north of" also has a reversed "south of"
(rule, north_of_south,
    ( (?x, north, of, ?y) ),
    ( (?y, south, of, ?x) ),
)
(rule, south_of_north,
    ( (?y, south, of, ?x) ),
    ( (?x, north, of, ?y) ),
)

# Sentence parsing, so we can define the world in simpler language

(rule, parse_dir_of,
    ( -(sentence, ?room1, is, ?dir, of, ?room2) ),
    ( (?room1, ?dir, of, ?room2) )
)
(rule, parse_you_are_in,
    ( -(sentence, You, are, in, ?here) ),
    ( (here, is, ?here) )
)

# Prmopt parsing

(rule, parse_go,
    ( -(prompt, go, ?where) ),
    ( (go, ?where) )
)
(rule, parse_whereami,
    ( -(prompt, where, am, I), (here, is, ?here) ),
    ( (print, (You are in the ), ?here) )
)

# Sentences defining our game world

Bedroom is south of Kitchen
Frontroom is north of Kitchen

You are in Bedroom

And a simple play through:

> where am I
You are in the Bedroom
> go north
You are in the Kitchen.
> go east
You can't go that way.
> go north
You are in the Frontroom.
> go south
You are in the Kitchen

The AI prototype a much more fleshed out version with things that could be taken, examined, scenery objects, room descriptions, and inventory, all in their most simple forms.

I’m not sure what the performance will be. The perf cost is going to be dominated by pattern matching over all the facts in the world, but I’m hopefully it will be fast enough, and that there’s ways to optimize things.

What I’m really excited about is how brutally simple the engine is, and how the rest of the game can be written in this simple language, built up from smaller pieces. I just need to get some actually substantial world modeling going in it so I can see how well it fares. There are definitely some oddities to doing everything rule-based like this.

Sequential processing, for example, isn’t the most natural thing, though it isn’t impossible.

1 Like

Very cool work. Looking forward to seeing how this progresses.

What you have there is what is known as an Expert System. There are interesting papers from the 80’s and 90’s about them. You may be interested in the Rete algorithm if you are concerned about efficiency.

I’m working on something similar although I only bothered to implement the alpha nodes of the Rete and not the beta nodes. My DSL is a little more English-like than yours though. I’ve name the system Reify. I’m hand-coding the whole thing. It’s all in JavaScript and is web centric. Hoping to have something vaguely presentable around January to show folks. It’s been slow going.

2 Likes

Ooh neat!

Yeah, that came up in my searches. I’m thinking that if it looks like I can continue with this general strategy that I’m going to go through and clean up the source code ( AI tends to write a lot more than necessary for any given problem ), get it nice and solid while focusing on clarity rather than performance.

Then I can start trying out different performance optimizations and see what might be low hanging fruit and how to compares to the “reference” simple implementation.

Someone I know just finished a SIMD library and I’m curious whether or not that can be applied somehow.

I haven’t heard that term before, that’ll give me more to search for, thanks. I’ve noticed that there was a lot of really smart thinking going on in the 80’s and 90’s. I think all the software and stuff we have nowadays has given us more to think about and there’s less brain energy to focus on the fundamentals of computing now that we’ve got all these existing, massive systems and APIs we have to deal with to get anything done in the “real” world.

I’m not exactly sure how hard it’s going to be to get nice English DSL yet, so I’m definitely interested in seeing how it’s been done in other projects.

Yeah, I really like the web as far as how accessible websites are for showing things to people.

I like Rust because it’s a really solid language that I can compile to WASM to put on the web.

I’m not 100% sure how this is going to pan out and whether I will care to target Glulx or not, since I quite possibly might as well just ship a web page and a commandline interpreter.

I wrote a CFG parser for it. I like the flexibility of it because I keep changing my mind about the DSL. I think I’ve finally settled on something I like now, though.

Ah, yeah, looking around I just found this which seems to be essentially the same thing with more features:

CLIPS is ~90k lines of C code, though, so my 1k lines of Rust still seems cool for the minimalism. :smile:

Conflict resolution was one of the biggest things I was unsure bout handling in reform, and CLIPS has multiple different strategies you can pick from which is cool.

More progress on the natural language parsing aspect. The iflib.rf file implements the english-style sentence parsing, and then game.rf imports it and uses it to define the game world.

What’s fun is that the language can define it’s own English parser. It’s not built-in to the language.

iflib.rf

# We have to bootstrap one article for the rules below
(an, is, article)

# [The] ?x is [a] ?y
(rule, def_x_is_y,
    (
        -(sentence, [?a1], ?x, is, [?a2], ?y),
        (?a1, is, article),
        (?a2, is, article),
    ),
    (
        (?x, is, ?y)
    )
)

# "is" transitivity: i.e. if John is a person, and a person is a thing,
# then john is also a thing. This works for anything that "is" not just
# "kinds".
(rule, is_transitivity,
    (
        (?x, is, ?y),
        (?y, is, ?z),
    ),
    (
        (?x, is, ?z)
    )
)

# [The] ?prop of [the] ?thing is ?value
(rule, prop_of_thing,
    (
        -(sentence, [?a1], ?prop, of, [?a2], ?thing, is, ?value),
        (?a1, is, article),
        (?a2, is, article),
        (?prep, is, preposition),
    ),
    (
        (?value, is, ?prop, of, ?thing)
    )
)

# [The] ?noun is ?rel [of] [a] ?other
(rule, rel_somthing_other,
    (
        -(sentence, [?a1], ?thing, is, ?rel, [?prep], [?a2], ?other),
        (?a1, is, article),
        (?a2, is, article),
        (?prep, is, preposition),
    ),
    (
        (?thing, is, ?rel, [?prep], ?other)
    )
)

# "is" elmination rule: if something "is" another thing and "is not" another
# thing, then both rules are destroyed.
(rule, rel_something_not_other,
    (
        -(?thing, is, not, ?other),
        -(?thing, is, ?other)
    ),
    ()
)

# Auto reverse relations: lets you define relations that automatically
# define their inverses.
#
# ?rel1 of is the reverse of ?rel2 of
(rule, def_reverse_rel,
    (
        -(sentence, ?rel1, [?prep1], is, the, reverse, of, ?rel2, [?prep2]),
    ),
    (
        (rule, reverse_rel,
            ( (?thing, is, ?rel1, [?prep1], ?other) ),
            ( (?other, is, ?rel2, [?prep2], ?thing) )
        ),
        (rule, reverse_rel,
            ( (?thing, is, ?rel2, [?prep2], ?other) ),
            ( (?other, is, ?rel1, [?prep1], ?thing) )
        ),
    )
)

# The say command for printing text
Say is say-token
say is say-token
(rule, say,
    (
        -(sentence, ?cmd, ..?args),
        (?cmd, is, say-token)
    ),
    (
        (print, ..?args)
    )
)

# ?act is an action
(rule, def_action,
    (
        -(sentence, ?act, is, an, action),
    ),
    (
        (?act, is, action),
    )
)

# Understanding prompts as actions.
# This converts the action to an (enact, action, ...args) fact.
#
# Understand ..word as ?act
Understand is understand-token
understand is understand-token
(rule, understand_action,
    (
        -(sentence, ?understand, ?word, as, ?act),
        (?act, is, action),
        (?understand, is, understand-token)
    ),
    (
        (rule, enact_action,
            ( -(prompt, ?word, ..?args) ),
            ( (enact, ?act, ..?args) ),
        )
    )
)

# When ?acting ..?facts
when is when-token
When is when-token

(rule, when_1,
    (
        -(sentence, ?when, ?act, ..?factChain),
        (?act, is, action),
        (?when, is, when-token)
    ),
    (
        (rule, when_1_out,
            ( -(enact, ?act, ..?args) ),
            ( (when_2, ..?factChain) )
        )
    )
)

(rule, when_2a,
    ( -(when_2, ..?fact, and, [now], ..?rest) ),
    ( (sentence, ..?fact), (when_2, ..?rest) )
)

(rule, when_2b,
    ( -(when_2, ..?fact) ),
    ( (sentence, ..?fact) )
)

game.rf:

$ load ./iflib/iflib.rf

# Prelude

## Define the basic english articles

# lowercase "an" is defined in bootstrap
a is an article
the is an article
A is an article
An is an article
The is an article

## Define prepositions
of is a preposition
to is a preposition
from is a preposition
in is a preposition

# Core world modeling

# In reform we don't need to make "kinds" explictly.
# We just say one thing is another.
# So now all bedrooms are considered rooms
bedroom is a room

# And the player is a person. Notice that we didn't need to 
# say that "person is a kind". It's implicit. There are no
# "kinds" just something that "is" something else.
The player is a person

## Direction relationships
north of is the reverse of south of
east of is the reverse of west of
up from is the reverse of down from
under is the reverse of over
below is the reverse of above
in is the reverse of containing


# Game

The Kitchen is a room
The Kitchen is north of the (Master Bedroom)
The description of the kitchen is (The place where things are cooked.)

The (tea kettle) is a thing
The (tea kettle) is in the Kitchen
The description of the (tea kettle) is (A cute little kettle for tea.)
The print-name of the (tea kettle) is (little kettle)

(My Room) is east of the Kitchen

(My Room) is containing the player

# Start the game with a message

say (
    You wake up.
    
    Who you are, and what you are doing here is a mystery...
)

## Game actions
looking is an action
understand look as looking
understand l as looking

When looking say (You look around tentatively.) and now the player is a moose and say (Now you are a moose 🦛 👀)

unmoosing is an action
understand unmoose as unmoosing

When unmoosing say (You are not a moose anymore.) and now the player is not a moose

So I did a lot of designing work on the language with my brother, hand-wrote a parser for most of the language, finished it off and implemented the rest of the engine with AI, and I think the core language and ( unoptimized ) engine is about done!

Me and my brother revamped the syntax and it is much nicer now. The engine also orders rule execution by specificity so that we can have default rules without having to worry about the order the rules are defined in in the source.

The engine core is 1066 lines of Rust so far, not counting tests, and I have 100% test coverage over library ( i.e. excluding the CLI ).

I’m hoping that this is all the features in the core that we’ll need to make a full game, and now the real work of making an interactive fiction standard library for it begins. I’m very excited and curious to see how well that goes.


Quick sample of the new syntax:

# This rule will print a warning if any sentence was not understood
$ rule default_sentence_rule
    (
        - sentence $( $arg )*
    )
    (
        $ println (Warning: I did not understand this sentence:) ($($arg )*)
    )
    
# Rule for handling prompts that are not understood
$ rule default_prompt_rule
    (
        - prompt $( $arg )*
    )
    (
        $ println (Sorry, I don't know what you mean.)
    )

$ rule x_is_y
    (
        - sentence $( $a1 )? $x is $( $a2 )? $y
        $( $a1 is article )?
        $( $a2 is article )?
    )
    (
        $x is $y
    )

$ rule x_is_rel_of_y
    (
        - sentence $( $a1 )? $x is $rel $( $prep )? $( $a2 )? $y
        $( $a1 is article )?
        $( $a2 is article )?
        $( $prep is preposition )?
    )
    (
        $x is $rel $( $prep )? $y
    )

# Define the articles and prepositions

an is article
a is an article
the is an article
An is an article
A is an article
The is an article

of is a preposition
to is a preposition

Fixed a few bugs. I’ve gotten a nice test-driven development cycle going where I notice something wrong, write a reform file that isn’t doing what I think I should with some failing assertions, and fix it, and keep going.

So as I go I’m accumulating little reform files describing what the behavior should be.

A linux release build of the engine optimized for size is only 498 kilobytes!

Hopefully I’m near the end of the basic bugs and I’ll be able to focus on my world modeling & English parsing.

Latest demo code:

$ load ./iflib/lib.rf

The Kitchen is a room. The Kitchen is north of the bedroom.
Properties of the Kitchen:
    description: (The best room in the house.)
    brightness: dark
    
The location of the player is Kitchen.

Before looking if the brightness of Kitchen is dark:
    say (It is too dark in here!)

Understand look as looking. Understand l as looking.
After looking:
    say You are in {the location of the player}

Understand illuminate as illuminating.
Carry-out illuminating:
    now the brightness of the Kitchen is light
After illuminating:
    say (You have illuminated the room.)

Things like {the location of the player} and Before looking if the brightness of Kitchen is dark were surprisingly easy to get working. It’s looking like this could be pretty nice.

There’s going to be a bit of machinery to get really up-to-par dynamic printing possibly. But I’m hopeful that the direction is going to work well.