Announcing Beguile 0.1.0-preview.4

Note: I put the initial announcement for Beguile in the I6 channel, because of Beguile’s close relationship with I6; however, at its core this is really is a language-and-tooling project, so I think this is where update announcements really belong.

In case you missed it, Beguile is a modern, statically-typed language for interactive fiction with a C#/TypeScript-flavored syntax. It transpiles to Inform 6, so you keep the whole I6 ecosystem underneath while writing in something contemporary. It adds a step-through your code, run-time debugging tool as a VS Code extension for Z-Machine and Glulx games.

This update adds…

1. Inline object declarations with type inference

This new syntax makes object arrays more concise to declare. Here’s a snippet:

//setup for the example...
enum Mood { calm, wary, hostile }

Mood npcMood = Mood.wary;
int  trust   = 1;

//example...  first define a new Response type...
class Response : object {
    inline func<bool> when;    // inline tells the compiler these members
    inline string     text;    // can be used in the new inline syntax
}

/* Three Response objects declared in an array.  Response is inferred from 
   the array type, so no need to declare it redundantly. */

array<Response> guard = {
    { () => npcMood == Mood.hostile, "The guard levels his pike. Not one step more." },
    { () => trust < 0,               "“I don't trust you,” he mutters." },
    { () => true,                    "The guard nods, and waves you through." },
};

Note that inline marks the positional slots; however, every field is still settable by name (i.e., { when = …; text = …; }) when you’d rather be explicit.

Reading the array back is ordinary code:

string speak(array<Response> lines) {
    for(Response r in lines)       // iterate the elements directly
        if(r.when())               // call the stored function
            return r.text;
    return "";
}

void main() {
    print($"The guard: {speak(guard)}^");   // string interpolation
}

2. Editing an array after it’s declared

Because these are just arrays, you can amend them declaratively, adding, removing, or reordering them without touching the original. These operations are resolved at compile time:

bool bribed() { return trust > 3; }

extend guard {
    inject { bribed, "“...for you, friend, the gate is always open.”" } first;
}

inject / remove / move let one file lay down a default list while later files (or optional extensions) adjust it.

Just a reminder: Beguile is in preview state; however, its stable enough to make functional games, with Puny or the I6StdLib. Feel free to kick the tires. The compiler, language runtime, and the VS Code extension are on the release pages. Feedback, “why won’t this compile,” and design arguments all welcome.

Thanks!

I’m unable to compile this on Windows. Here is the output from clang++:

beguiler.cpp:521:31: error: use of undeclared identifier 'popen'
  521 |                 if(FILE* up = popen(uCmd.c_str(), "r")){
      |                               ^~~~~
beguiler.cpp:530:21: error: use of undeclared identifier 'pclose'
  530 |                     pclose(up);
      |                     ^~~~~~
beguiler.cpp:596:22: error: use of undeclared identifier 'popen'
  596 |         FILE* pipe = popen(popenCmd.c_str(), "r");
      |                      ^~~~~
beguiler.cpp:651:18: error: use of undeclared identifier 'pclose'
  651 |         int rc = pclose(pipe);
      |                  ^~~~~~
4 errors generated.
bglLanguageService.cpp:115:12: error: use of undeclared identifier 'format'
  115 |     return format("{0}:{1}", src.file, src.line);
      |            ^~~~~~
bglLanguageService.cpp:162:29: error: use of undeclared identifier 'format'
  162 |         parser.parsingError(format("'{0}' is already defined", name));
      |                             ^~~~~~
bglLanguageService.cpp:183:29: error: use of undeclared identifier 'format'
  183 |         parser.parsingError(format("'{0}' is already defined (originally declared at {1})", dspName.empt...
      |                             ^~~~~~
bglLanguageService.cpp:208:29: error: use of undeclared identifier 'format'
  208 |         parser.parsingError(format("'{0}' is already defined (originally declared at {1})", dspName.empt...
      |                             ^~~~~~
bglLanguageService.cpp:234:29: error: use of undeclared identifier 'format'
  234 |         parser.parsingError(format("'{0}' is already defined (originally declared at {1})", dspName.empt...
      |                             ^~~~~~
bglLanguageService.cpp:267:33: error: use of undeclared identifier 'format'
  267 |             parser.parsingError(format("'{0}' is already defined (originally declared at {1})", varDef.n...
      |                                 ^~~~~~
bglLanguageService.cpp:277:29: error: use of undeclared identifier 'format'
  277 |         parser.parsingError(format("'{0}' is already defined as a type (originally declared at {1})", va...
      |                             ^~~~~~
7 errors generated.

@blindHunter: I’m oddly delighted that you posted this error, since I see these opportunities as steps toward stabilization.

It looks like the README provides an early command line, which changed quite a bit as the code evolved. My apologies. I’ll update the README presently to make sure it matches what’s in the build pipeline. For now, try this on a Windows box, instead of what is in the README:

clang++ -std=c++20 -O2 -Wno-deprecated-declarations -include format -include functional -Disnumber=isdigit -Dstrncasecmp=_strnicmp -Dpopen=_popen -Dpclose=_pclose *.cpp -o beguiler.exe

That should compile cleanly. DM me and let me know if it doesn’t and we’ll address.

Thanks for the bug report!

Jim

This preview release (4) of Beguiler includes a handful of language features and bug fixes.


1. Inline, automatically typed objects

An expansion of the last update, which brought inline declaration of objects with known types, this release now allows those objects to be anonymously typed. That is, you can declare an object instance inline, and let the compiler synthesize the type for you via the auto pseudo type:

object player {
    auto stats = {          // no name, beguiler creates the type
        int strength = 10;
        int agility  = 7;
        void sayStatTot(){ print(strength + agility);}
    }
}

The above is equivalent to…

object statsObj {          // named object
    int strength = 10;
    int agility  = 7;
    void sayStatTot(){ print(strength + agility);}
}
    
object player {
    object stats = statsObj;
}

This makes declaring getters and setters easier

If you are familiar with C#, you’ve probably heard of getters and setters. Since Beguile’s support for this pattern is built on overloaded operators, adding getters/setters to objects can seem like more trouble than its worth. The new, inline, automatically-typed syntax simplifies things:

object gadget {
    int scale = 3;
    auto level = {  
        int _raw = 0;
        int  operator()        { return _raw; }            //getter
        void operator = (int v){ _raw = v * outer.scale; } //setter
    }
}

The above creates a level property object with getters and setters implemented using the normal cast and assignment operators. This lets you run arbitrary code when accessing or writing to the property:

gadget.level = 4;        // setter: _raw = 4 * 3
int a = gadget.level;    // getter: a = 12

2. New operator: := (The reference-binding operator)

This release introduces a new operator which allows you to change the object instance a ref member actually references at runtime:

object holder { ref inventory pack; }   // no instance created

inventory spare;

holder.pack := spare; // holder.pack & spare point to the same object

Unlike the assignment operator (=), which assigns a value to an object, the := operator binds the reference to a different object altogether. It only works with references and cannot be redefined.


3. New operator: <=> (The spaceship operator)

The <=> operator, also called the three-way comparison operator, rolls the <, >, and == operators into one. The canonical behavior of <=> is to compare two values and return -1 if the first is less than the second, 0 if they are the same, and 1 if the first is greater:

class Item {
    int weight;
    int operator <=> (Item o) { 
         if(weight < o.weight) return -1;
         if(weight > o.weight) return  1; return 0; 
    }
}

This convention is commonly used for sorting. In fact, if you are defining a class that needs to be sortable in an array, you’ll want to define the spaceship. Without it, sort() falls back to comparing addresses.


4. children lets you place a room’s contents in one line

Prior to this update, objects needed to place themselves in the world model using their parent property. Now a container can alternatively declare its contents in the children property, so you can populate a room in a single declaration:

object table {}
object chair {}
object kitchen {
    children = { table, chair };
}

The children and parent properties represent the same information from two perspectives; changing one at runtime changes the other.

Unlike I6’s statement with the same name, children is a collection and you can do collection-like things with it:

for (object o in kitchen.children) { o.give(seen); }

bowl.children += { apple, pear };     // moves both into the bowl

5. #declare — persistent, order-independent #define

This new directive, #declare, is similar to #define. Like #define, you can create compile time symbols and test them using #if. Unlike #define, #declared symbols are scoped to the entire compilation. That is, they can be tested even before their declarations appear in code. Additionally, once specified, they are permanent and cannot subsequently be changed; attempting to #undef or #redef a #declared symbol will raise a compile time error.

// a core file, parsed early:
#if I6_STANDARD_LIBRARY
    // use something the standard library provides
#endif
#if SNAP_VAL
    // won't be compiled because defined later
#endif

// the standard-library binding, even if #included later, sets the symbol...
#declare I6_STANDARD_LIBRARY

// SNAP_VAL will only be visible after this point
#define  SNAP_VAL 

The #if above sees I6_STANDARD_LIBRARY even though it is #declared afterward. It would not see SNAP_VAL unless referenced later.


6. Magic strings are dead. Long live string objects

Beguile Preview 1 gave you basic I6 strings by default, then silently translated them into mutable string objects when the <string> language extension was included. Although nifty, this practice hid technical realities which rightly should be visible to the developer. With this release, strings no longer magically transform into buffer-backed objects. Instead, the <string> extension introduces mutable strings as a separate stringObj type, which you choose intentionally.

Although complementary, and designed to work together, string and stringObj are different:

  • string is a pointer to static text, the packed literal address, exactly what I6 provides.
  • stringObj is an object which owns a buffer. Assignable and changeable, it’s the type of string you get in other languages.
string  title = "Cloak";  // a pointer to static text
stringObj name = title;   // copies the TEXT into name's own buffer
name = name + " of Darkness!";  // name is "Cloak of Darkness!"; title unchanged 

Simplified build-it-yourself

If you are building Beguiler yourself, there are now reduced C++ requirements. Previous versions of the source used idioms from the C++ 20 standard, making newer compilers a requirement. These dependencies have been factored out, bring this requirement down to C++ 17 and opening up the build process to older versions of the C++ compiler.

Additionally, I’ve endeavored to simplify and standardize the command used to build Beguiler. The README reflects the latest commandline.