I haven't set up an automated way to push my Gopher phlogs from Cyberhole Online up here yet, so I'm just going to manually paste for the moment. So this may be a bit random.
mdh@Aegura:~/Code/CodeJS/Cyberhole/gopher/hole/phlog% cat `ls -1 *.txt|sort -r`|pbcopy
i=== Digital Mark's Phlog ===
2025-11-04 19:06:46 UTC
Momentary Thoughts About Game Design
I've been making games for 46-ish years, and I still don't know how to
make a "compelling narrative". Sometimes I have good gameplay and "kill
the thing" plots.
A few times I've worked at studios and had a writer who could produce
lore text, but we still didn't have much story.
That's why my games are mostly interface first, what does the technology
let me do, then work out some game logic behind it.
I generally reject the narrativist view of games. Games are interaction
+ challenge = fun. Books and movies are characters + plot = fun. Visual
novels combine book style with illustrations and a homeopathic level of
interaction, maybe one choice every 10-20 minutes for most. Modern MMOs
& mobage are largely predefined characters on a rails plot, with a
shooting or action game in between. Designers for these hate the idea
of you having your own character or making plot choices.
What I find interesting is exploring a world, fighting, and building. So
Atari 2600 games are still fascinating; the absolute focus on gameplay
was required by the limitations of the machine. Luanti (and that "mines
craft" game that's like it) is endlessly fun. Juice Galaxy is amazing,
just weird blobby monsters, toys, and vehicles in procgen world. AAA
titles are basically all crap, they have characters I can't stand, doing
things I don't want to do, minutes of fun after hours of hitting X.
Text adventures are in a weird space, where there's varying levels of
exploration or plot railroad, varying levels of character predefinition.
Hitchhiker's Guide to the Galaxy is fun once (possibly frustrating for
months), but it's very linear and you are Arthur Dent, of the late
Earth. Zork and Colossal Cave Adventure are still fun, because there's
no fixed path to solve everything, and you're not defined by the game.
A lot of people do like the excruciatingly long cutscenes and "Press F
to Pay Respects" pseudo-gameplay, I'm well aware I'm an outlier. But
that's why I make my games for me. If someone else has fun, too, that's
a sweet bonus!
2025-10-31 05:10:56 UTC
Cyber Mudhole
Starting on the Lisp Game Jam Autumn 2025
https://itch.io/jam/autumn-lisp-game-jam-2025
Plan is to put up a Terminal that talks to a MUD written in my Kawa
servlet platform.
I wrote a long, mostly aspirational, list of MUD commands inspired by
LambdaMoo and TinyMUD; it's not very like my old combat-oriented
Circle MUD. I doubt I'll get more than a fraction of the commands
done, but if multiple players can move around and say and emote, it's
good enough.
The way this works is there's a Java servlet container, and a single
servlet that takes POST requests for commands, then returns all your
chat history since last call. If it polls every so often with empty
command, it should look real-time-ish, and connection keep-alive
should prevent it from hammering the server.
The servlets are in Kawa Scheme, which compiles to normal enough Java
classes. I'll put the whole framework up on gitlab or whatever as I
get towards the end, it's definitely changing a lot each update right
now.
2025-09-02 13:03:13 UTC
Phlogging Like a Typewriter
One thing that occurs to me when writing gopher phlogs, is the 70-
column limit feels like typing on paper. Get near the end and I have
to guess if the next word will fit or not, and whack the carriage
return DING all the way back. I haven't got in the habit of hyphen-
ating words but I did it right there!
Is the physical act of typing different when you don't have auto
word wrap on? I could vi :set tw=70, but I don't.
2025-09-02 12:32:03 UTC
Mystic Dungeon beta
I have a barely-working version of the Mystic Dungeon, my dungeon
exploration game. Everything in the game system works again,
and I've added three new stores (one you won't find for a while!),
and some secrets to find outside of town.
Right now messaging is incredibly spammy and ugly, using up the full
screen if you move fast. Just wait a few seconds and the messages
will dissipate. I'll fix those in the next pass.
You can save [F3] and load [F2] and it's saved to local storage.
https://cyberhole.online/dungeon
Have fun, and let me know what you think!
2025-08-18 08:34:55 UTC
Amazing (converting MysticDungeon games)
I've now managed to convert one of the MysticDungeon.club games, see
front page of https://cyberhole.online/
That wasn't too hard. It's using like 1/2 of the code of the previous
version, and doesn't rely on Node or have to be built, I can just empty
cache and reload a page. But it's a little harder to debug, eslint
doesn't work well on pile-o-random-js.
Amazing's not that impressive, and I'll probably do LostTreasure and
some other small ones, then get the big stuff converted. PortalWorlds
and Dungeon both rely on a shitload of libraries.
The high score server's gone, which is unfortunate. There's a few
options to replace it. A server running Scheme, of course. Or maybe some
web service that I can ajax out to? Cross-site posting is complicated
now.
2025-08-01 03:30:30 UTC
TinyBasicWeb
Now I have a working boot-to-READY prompt BASIC!
It's pretty primitive still, just enough to do interactive
forms, and I plan to put in some web tech (REMOTE and REDIR)
commands which will make it useful for that.
Currently some BASIC microcomputer games will work in it.
So with that basically (o_o) functional, I can focus on
adding more services to the CYBER HOLE.
2025-07-23 02:06:47 UTC
Gopher Holed
And now the CYBER HOLE has my phlog! I can either log in and
write-phlog.zsh, or do it locally and scp into place, refresh and
it's rebuilt by script.
Just finished the Lispy Gopher Show
gopher://gopher.club/1/users/screwtape
(who hasn't been updating his phlog, but like I'm Mr Reliability?)
RIP Ozzy Osbourne. The best and drunkest of us.
2025-04-27 14:39:03 UTC
Dayglo Decade
I was listening to Children of the CPU
https://childrenofthecpu.bandcamp.com
While testing out my write-phlog script.
2025-04-27 13:47:08 UTC
First post
So driven by my Hypertext post
https://mdhughes.tech/2025/04/22/some-of-my-history-of-hypertext/
I needed to set up Gopher again, and not just on SDF or tilde.town.
Talking to Screwtape & CatK got me moving on it.
Currently I'm running Motsognir,
https://motsognir.sourceforge.net
since it works well on BSD; on MacOS, I had to comment out the "dot
feature" to make it compile, but I changed NoTxtPeriod=1, the default is
just wrong, so it's fine.
Which has made it pretty easy to set up CGI (Common Gateway Interface!)
scripts to generate header, footer, and phlog indexes.
And I use shell scripts to start, stop the server, and generate phlog
posts in my format (TIME.txt filename; TIME, TITLE, blank for body); I
would use RFC822 but it's a little more parsing, and the date format is
wrong.
For client, I mostly test in Lynx
https://lynx.invisible-island.net
but also Lagrange
https://github.com/skyjake/lagrange
Which has revealed a few bugs in how I write gophermaps!
In particular, Lynx doesn't care if you list a text file as a dir or
text or binary, it'll render it anyway. Lagrange is very picky, so a
dumb directory doesn't work with it.
Hey hey hey good evening! Tonight a quick note on wastrel, a new WebAssembly implementation.
Wastrel compiles Wasm modules to standalone binaries. It does so by emitting C and then compiling that C.
Compiling Wasm to C isn’t new: Ben Smith wrote wasm2c back in the day and these days most people in this space use Bastien Müller‘s w2c2. These are great projects!
Wastrel has two or three minor differences from these projects. Let’s lead with the most important one, despite the fact that it’s as yet vaporware: Wastrel aims to support automatic memory managment via WasmGC, by embedding the Whippet garbage collection library. (For the wingolog faithful, you can think of Wastrel as a Whiffle for Wasm.) This is the whole point! But let’s come back to it.
The other differences are minor. Firstly, the CLI is more like wasmtime: instead of privileging the production of C, which you then incorporate into your project, Wastrel also compiles the C (by default), and even runs it, like wasmtime run.
Unlike wasm2c (but like w2c2), Wastrel implements WASI. Specifically, WASI 0.1, sometimes known as “WASI preview 1”. It’s nice to be able to take the wasi-sdk‘s C compiler, compile your program to a binary that uses WASI imports, and then run it directly.
In a past life, I once took a week-long sailing course on a 12-meter yacht. One thing that comes back to me often is the way the instructor would insist on taking in the bumpers immediately as we left port, that to sail with them was no muy marinero, not very seamanlike. Well one thing about Wastrel is that it emits nice C: nice in the sense that it avoids many useless temporaries. It does so with a lightweight effects analysis, in which as temporaries are produced, they record which bits of the world they depend on, in a coarse way: one bit for the contents of all global state (memories, tables, globals), and one bit for each local. When compiling an operation that writes to state, we flush all temporaries that read from that state (but only that state). It’s a small thing, and I am sure it has very little or zero impact after SROA turns locals into SSA values, but we are vessels of the divine, and it is important for vessels to be C worthy.
Finally, w2c2 at least is built in such a way that you can instantiate a module multiple times. Wastrel doesn’t do that: the Wasm instance is statically allocated, once. It’s a restriction, but that’s the use case I’m going for.
Oh buddy, who knows?!? What is real anyway? I would love to have proper perf tests, but in the meantime, I compiled coremark using my GCC on x86-64 (-02, no other options), then also compiled it with the current wasi-sdk and then ran with w2c2, wastrel, and wasmtime. I am well aware of the many pitfalls of benchmarking, and so I should not say anything because it is irresponsible to make conclusions from useless microbenchmarks. However, we’re all friends here, and I am a dude with hubris who also believes blogs are better out than in, and so I will give some small indications. Please obtain your own salt.
So on coremark, Wastrel is some 2-5% percent slower than native, and w2c2 is some 2-5% slower than that. Wasmtime is 30-40% slower than GCC. Voilà.
My conclusion is, Wastrel provides state-of-the-art performance. Like w2c2. It’s no wonder, these are simple translators that use industrial compilers underneath. But it’s neat to see that performance is close to native.
OK this is going to sound incredibly arrogant but here it is: writing Wastrel was easy. I have worked on Wasm for a while, and on Firefox’s baseline compiler, and Wastrel is kinda like a baseline compiler in shape: it just has to avoid emitting boneheaded code, and can leave the serious work to someone else (Ion in the case of Firefox, GCC in the case of Wastrel). I just had to use the Wasm libraries I already had and make it emit some C for each instruction. It took 2 days.
WASI, though, took two and a half weeks of agony. Three reasons: One, you can be sloppy when implementing just wasm, but when you do WASI you have to implement an ABI using sticks and glue, but you have no glue, it’s all just i32. Truly excruciating, it makes you doubt everything, and I had to refactor Wastrel to use C’s meager type system to the max. (Basically, structs-as-values to avoid type confusion, but via inline functions to avoid overhead.)
Two, WASI is not huge but not tiny either. Implementing poll_oneoff is annoying. And so on. Wastrel’s WASI implementation is thin but it’s still a couple thousand lines of code.
Three, WASI is underspecified, and in practice what is “conforming” is a function of what the Rust and C toolchains produce. I used wasi-testsuite to burn down most of the issues, but it was a slog. I neglected email and important things but now things pass so it was worth it maybe? Maybe?
WASI preview 1 has this “rights” interface that associated capabilities with file descriptors. I think it was an attempt at replacing and expanding file permissions with a capabilities-oriented security approach to sandboxing, but it was only a veneer. In practice most WASI implementations effectively implement the sandbox via a permissions layer: for example the process has capabilities to access the parents of preopened directories via .., but the WASI implementation has to actively prevent this capability from leaking to the compiled module via run-time checks.
Wastrel takes a different approach, which is to use Linux’s filesystem namespaces to build a tree in which only the exposed files are accessible. No run-time checks are necessary; the system is secure by construction. He says. It’s very hard to be categorical in this domain but a true capabilities-based approach is the only way I can have any confidence in the results, and that’s what I did.
The upshot is that Wastrel is only for Linux. And honestly, if you are on MacOS or Windows, what are you doing with your life? I get that it’s important to meet users where they are but it’s just gross to build on a corporate-controlled platform.
The current versions of WASI keep a vestigial capabilities-based API, but given that the goal is to compile POSIX programs, I would prefer if wasi-filesystem leaned into the approach of WASI just having access to a filesystem instead of a small set of descriptors plus scoped openat, linkat, and so on APIs. The security properties would be the same, except with fewer bug possibilities and with a more conventional interface.
So Wastrel is Wasm to native via C, but with an as-yet-unbuilt GC aim. Why?
This is hard to explain and I am still workshopping it.
Firstly I am annoyed at the WASI working group’s focus on shared-nothing architectures as a principle of composition. Yes, it works, but garbage collection also works; we could be building different, simpler systems if we leaned in to a more capable virtual machine. Many of the problems that WASI is currently addressing are ownership-related, and would be comprehensively avoided with automatic memory management. Nobody is really pushing for GC in this space and I would like for people to be able to build out counterfactuals to the shared-nothing orthodoxy.
Secondly there are quite a number of languages that are targetting WasmGC these days, and it would be nice for them to have a good run-time outside the browser. I know that Wasmtime is working on GC, but it needs competition :)
Finally, and selfishly, I have a GC library! I would love to spend more time on it. One way that can happen is for it to prove itself useful, and maybe a Wasm implementation is a way to do that. Could Wastrel on wasm_of_ocaml output beat ocamlopt? I don’t know but it would be worth it to find out! And I would love to get Guile programs compiled to native, and perhaps with Hoot and Whippet and Wastrel that is a possibility.
Welp, there we go, blog out, dude to bed. Hack at y’all later and wonderful wasming to you all!
This SRFI defines a language to describe control-flow graphs (CFGs) suitable for formulating iterative and recursive algorithms. Using the notion of a CFG term, this language can be seamlessly embedded in the Scheme language. Complex CFG terms can be composed from simple CFG terms.
The language described in this SRFI is not meant to be directly used in programs but by library authors to build abstractions like loop facilities on top of it.
For this complex topic I needed some help. I explained the process to an AI and had it help me write this blog post. Questions and comments are welcome.
Managed runtimes like the Java Virtual Machine (JVM) and the Common Language Runtime (CLR) provide robust, high-performance environments for software execution. A key feature of these platforms is a rigidly structured call stack, which manages function calls and returns in a strict last-in, first-out (LIFO) order. While this model is efficient and simplifies memory management, it precludes certain powerful control flow constructs, most notably first-class continuations.
A first-class continuation is the reification of the current point of execution—essentially, "the rest of the program"—as an object that can be stored, passed around, and invoked. Invoking a continuation effectively discards the current execution stack and replaces it with the captured one. This document details a methodology for implementing such a mechanism within an interpreter running on a managed runtime, circumventing the limitations of the native call stack.
This document provides a comprehensive technical overview of a method for implementing first-class continuations within an interpreter executing on a managed runtime, such as the JVM or CLR. These platforms enforce a strict, stack-based execution model that is incompatible with the control-flow manipulations required for first-class continuations. The technique described herein circumvents this limitation by creating a custom, manually-managed execution model based on a trampoline and a universal "step" contract, enabling the capture, storage, and invocation of the program's execution state.
The foundation of this system is an interpreter where every evaluatable entity—from primitive operations to user-defined functions—adheres to a single, uniform execution contract. This approach abstracts execution away from the host's native call stack.
All computable objects implement a `Step` method. This method performs one atomic unit of computation. Its precise signature is critical to the entire mechanism:
bool Step(out object ans, ref IControl ctl, ref IEnvironment env)
The parameters of the Step method function as the registers of our virtual machine. Their specific modifiers are essential:
out object ans: The Answer Register. This is an output parameter used to return the final value of a computation.ref IControl ctl: The Control Register. This reference parameter holds a pointer to the next computational object (`IControl`) to be executed.ref IEnvironment env: The Environment Register. This reference parameter holds the context necessary for the execution of the control object, such as lexical variable bindings.The use of reference (ref) and output (out) parameters is the key that allows a callee function to directly modify the state of its caller's execution loop, which is fundamental to achieving tail calls and other advanced control transfers.
A Step method executes its atomic portion of work and then relinquishes control in one of four distinct ways:
Step method of a callee function, initiating a deeper, nested computation.ans parameter to its result value and returning false. The false return value signals to the caller that a value has been produced and normal execution can proceed.ctl parameter to the callee and the env parameter to the callee's required environment, and then returning true. The true return value signals to the caller's execution loop that it should not proceed, but instead immediately re-execute with the new ctl and env values.To avoid consuming the native call stack and prevent stack overflow exceptions during deep recursion, we employ a trampoline. This is a controlling loop that manages the execution of Step methods.
// Variables to hold the current state
IControl control = ...;
IEnvironment environment = ...;
object answer;
// The trampoline loop
while (control.Step(out answer, ref control, ref environment)) {}
// Execution continues here after a normal return (false)
The operation is as follows: When a callee wishes to tail call, it mutates the control and environment variables through the ref parameters and returns true. The while loop's condition evaluates to true, its (empty) body executes, and the loop condition is evaluated again, this time invoking the Step method on the newly specified control object. When a callee returns a value, it mutates the answer variable via the out parameter and returns false. This terminates the loop, and the ultimate value of the call is available in the answer variable.
The continuation is captured by hijacking the established return mechanism. This is a cooperative process that propagates upward from the point of capture.
A special function (e.g., the primitive for `call/cc`) initiates the capture. It sets the answer register to a magic constant (e.g., `UNWIND`) and mutates the environment register to hold a new `UnwinderState` object, which will accumulate the stack frames. It then returns false, causing its immediate caller's trampoline to exit.
Crucially, every call site must check for the unwind signal immediately after its trampoline loop terminates.
while (control.Step(out answer, ref control, ref environment)) { };
if (answer == MagicValues.UNWIND) {
// An unwind is in progress. We must participate.
// 1. Create a Frame object containing all necessary local state
// to resume this function from this point.
Frame resumeFrame = new Frame(this.localState1, this.localState2, ...);
// 2. Add the created frame to the list being accumulated.
((UnwinderState)environment).AddFrame(resumeFrame);
// 3. Propagate the unwind to our own caller. Since this code is
// inside our own Step method, we have access to our caller's
// registers via our own parameters. We set *their* answer to UNWIND
// and *their* environment to the UnwinderState, and return false
// to drop *their* trampoline.
return false; // Assuming 'ans' and 'env' are our own out/ref parameters.
}
This process creates a chain reaction. Each function up the conceptual call stack catches the unwind signal, preserves its own state in a Frame object, adds it to the list, and then triggers its own caller to unwind. This continues until the top-level dispatch loop is reached.
The main entry point of the interpreter requires a master loop that can handle the three possible outcomes of an unwind event.
while (true) {
answer = null;
while (control.Step(out answer, ref control, ref environment)) { };
if (answer == MagicValues.UNWIND) {
UnwinderState unwindState = (UnwinderState)environment;
// Outcome 3: The unwind was an instruction to exit the interpreter.
if (unwindState.IsExit) {
answer = unwindState.ExitValue;
break;
}
else {
// Outcome 1 & 2: A continuation was captured (cwcc) or is being invoked.
// In either case, we must restore a control point.
ControlPoint stateToRestore = unwindState.ToControlPoint();
IControl receiver = unwindState.Receiver;
// The RewindState holds the list of frames to be reloaded.
environment = new RewindState(stateToRestore, receiver);
control = ((RewindState)environment).PopFrame();
}
} else {
// Normal termination of the entire program
break;
}
}
// Interpreter has exited.
return answer;
This top-level handler serves as the central arbiter. It runs the normal trampoline, but if an unwind reaches it, it inspects the UnwinderState to determine whether to exit the program entirely or to begin a rewind process to install a new (or previously captured) execution stack.
Invoking a continuation involves rebuilding the captured stack. This is managed by the `RewindState` environment and the `Step` methods of the captured `Frame` objects.
The `Step` method for a `Frame` object being restored is complex. Its primary responsibility is to first restore the part of the stack that was deeper than itself. It does this by calling `PopFrame` on the `RewindState` to get the next frame and then running a local trampoline on it. The code that represents its own original pending computation is encapsulated in a separate `Continue` method.
// Simplified Step method for a Frame during rewind.
public override bool Step(out object answer, ref IControl control, ref IEnvironment environment)
{
// First, set up and run a trampoline for the deeper part of the stack.
object resultFromDeeperCall;
IControl deeperFrame = ((RewindState)environment).PopFrame();
IEnvironment rewindEnv = environment;
while (deeperFrame.Step(out resultFromDeeperCall, ref deeperFrame, ref rewindEnv)) { };
// Check if a NEW unwind occurred during the rewind of the deeper frame.
if (resultFromDeeperCall == MagicValues.UNWIND) {
// If so, we must participate again. Append our remaining frames to
// the new UnwinderState and propagate the new unwind upwards.
((UnwinderState)rewindEnv).AppendContinuationFrames(this.myRemainingFrames);
environment = rewindEnv;
answer = MagicValues.UNWIND;
return false;
}
// If the deeper call completed normally, now we can execute our own pending work.
control = this.originalExpression;
environment = this.originalEnvironment;
return Continue(out answer, ref control, ref environment, resultFromDeeperCall);
}
This structure ensures that the stack is rebuilt in the correct order and that the system can gracefully handle a new continuation capture that occurs while a previous one is still being restored.
The rewind chain must end. The innermost frame of a captured continuation corresponds to the `call/cc` primitive itself. Its `Step` method does not reload any deeper frames. Its sole purpose is to invoke the continuation receiver—the lambda function that was passed to `call/cc`—and provide it with the fully reified continuation object.
public override bool Step(out object answer, ref IControl control, ref IEnvironment environment)
{
// The rewind is complete. Deliver the continuation to the waiting function.
ControlPoint continuation = ((RewindState)environment).ControlPoint;
return this.receiver.Call(out answer, ref control, ref environment, continuation);
}
With this final call, the stack is fully restored, the RewindState is discarded, and normal execution resumes within the receiver function, which now holds a reference to "the rest of the program" as a callable object.
by Joe Marshall (noreply@blogger.com) at Tuesday, October 28, 2025
This SRFI defines an interface to hash tables, which are widely recognized as a fundamental data structure for a wide variety of applications. A hash table is a data structure that:
- Is disjoint from all other types.
- Provides a mapping from objects known as keys to corresponding objects known as values.
- Keys may be any Scheme objects in some kinds of hash tables, but are restricted in other kinds.
- Values may be any Scheme objects.
- Provides an equality predicate which defines when a proposed key is the same as an existing key. No table may contain more than one value for a given key.
- Provides a hash function which maps a candidate key into a non-negative exact integer.
- Supports mutation as the primary means of setting the contents of a table.
- Provides key lookup and destructive update in (expected) amortized constant time, provided that a satisfactory hash function is available.
- Does not guarantee that whole-table operations work in the presence of concurrent mutation of the whole hash table. (Values may be safely mutated.)
Unlike the hash tables of SRFI 125, which is the direct ancestor of this specification, the hash tables described here are ordered by insertion: that is, associations inserted earlier in the history of the hash table appear earlier in the ordering. Advances in the implementations of hash tables, as provided by C++, Python, JavaScript, etc., make the provision of this new facility practical. As a result, the hash tables of this SRFI do not necessarily interoperate with the hash tables of SRFI 125, SRFI 126, or existing R6RS implementations.
by John Cowan and Daphne Preston-Kendal at Monday, October 27, 2025