Formatting Strings in Scheme

Most of the time I use primitive display, or print functions:

;; displays a series of args to stdout
(define (print . args) (for-each display args) (flush-output-port) )

;; displays a series of args to stdout, then newline
(define (println . args) (for-each display args) (newline) (flush-output-port) )

;; displays a series of args to given port
(define (fprint port . args) (for-each (λ (x) (display x port)) args) (flush-output-port port) )

;; displays a series of args to given port, then newline
(define (fprintln port . args) (for-each (λ (x) (display x port)) args) (newline port) (flush-output-port port) )

;; displays a series of args to stderr, then newline
(define (errprintln . args)  (let [ (port (current-error-port)) ]
    (for-each (λ (x) (display x port)) args) (newline port) (flush-output-port port)

but sometimes I actually need to format things:

(import (prefix (srfi s19 time) tm: ))

(format  "~8a ~10:d ~20a" name score
    (tm:date->string (tm:current-date) "~Y-~m-~d ~H:~M:~S") )

Common Lisp format works as described in Chez Scheme, using /#f for destination, and some other Schemes as well; but most Schemes only have the nearly-useless SRFI 28. I'm aware of cat/fox/etc combinatorial formatters, but they're very verbose.

Chez also has date/time functions, but no formatter, so using SRFI 19 - nicely, SRFI 19 mostly does sane things, it's not like C's strftime.

Gambit hits a mark

(see, the X-Men Gambit has perfect aim and a stupid accent, which still makes him more interesting than Hawkeye; and of course I'm Mark and so is Marc)

With much appreciated help from Marc Feeley, got maintest running.

A couple of lessons: I very much think include paths should include the path of the main source doing the including. Chibi's default is correct, Gambit's default is wrong and requires fixing in every user program. It's "more secure", but if you're running source code from a directory, you can probably trust whatever else is in that dir.

main was frustrating: Gambit manual 2.6 (highlighting mine)

After the script is loaded the procedure main is called with the command line arguments. The way this is done depends on the language specifying token. For scheme-r4rs, scheme-r5rs, scheme-ieee-1178-1990, and scheme-srfi-0, the main procedure is called with the equivalent of (main (cdr (command-line))) and main is expected to return a process exit status code in the range 0 to 255. This conforms to the “Running Scheme Scripts on Unix SRFI” (SRFI 22). For gsi-script and six-script the main procedure is called with the equivalent of (apply main (cdr (command-line))) and the process exit status code is 0 (main’s result is ignored). The Gambit system has a predefined main procedure which accepts any number of arguments and returns 0, so it is perfectly valid for a script to not define main and to do all its processing with top-level expressions (examples are given in the next section).

So your code that looks fine with 1 arg will break with 2, depending on the version. (main . argv) works. I'm in the process of making sure every one of my maintests parses args consistently, and every Scheme disagrees.

Gambit's compiler worked very simply once I got the library on the command line; it doesn't seek out & include them the way Chez does, even though it takes what looks like a search path.

The upside of all this is at least now there's one maintained, fast, R7-compatible Scheme compiler. I'm sticking with Chez (R6) for my code, but it's nice having something 100x faster (gut feeling, not benchmarked) than Chibi to test R7 code on.

Gambit is a risky scheme

(puns about "scheme" names are mandatory)

Neat: New version 4.9.4 of Gambit Scheme is out and they have a web site again after like 3 years.

OK: So I start adapting my little module/how do you run example: here

Bad: Not only does the R7 library system not work, their version of this hello example
will load code from fucking github at runtime! NPM viruses & sabotage are baked into the system. See Modules in Gambit at 30


Haunted Dungeon early beta

Hey, it's a new and slightly more usable build (still Mac only) of the Haunted Dungeon! You can now use all the weapons & armor, eat & drink food & potions, might even make it down to floor 2 or 3!

Up in the next few days:

  • Tossing out items. I need to write a new event mode for selecting it, so I didn't feel like it today.
  • Levelling up. Right now you're doomed because you can't heal except by food & potions; or improve, except by very rare (and a long ways down) stat potions.
  • Start getting the actual story into the game. But only the first hints will be in the upper levels, since you can't get far anyway.
  • Main release on Halloween!

Probably next month:

  • Backpack for storing more items. One of the premises of this game is every item's a singleton, there's no stacking. So every item must be useful by itself, and item slot management is hard.
  • Ranged weapons have range.
  • Magic spells.
  • Elemental effects. It's already true that hitting some monsters with sharp or blunt weapons is better, but there's many more interactions when magic gets involved.
  • Much more dungeon dressing, I'm using Vexed's Demonic Dungeon art for that late-'80s, hi-res but low color count effect.

I'd love to get some feedback if it actually works on everyone's machine, because I'm doing some weird tricks to make it launch. As noted on itch, if it doesn't launch, or crashes, try looking at ~/Documents/haunted-dungeon.log, email me with that file.

The game's written in Scheme, running on Chez Scheme, with SDL2 from Thunderchez. Then I have an excessively complex Scheme script that compiles it, and builds a .app structure for Mac, and should also make Linux & Windows builds on those platforms (or in a VM, which is how I do it); been a while since I tried those, but once this is solid I'll include those.

I normally discuss ongoing projects on fediverse,

Script the Scheme REPL with Expect

Routinely I want to open the Chez Scheme REPL in my code dir and load my standard libraries; I've been copy-pasting from a Stickies to get my setup each time, because you can't easily set a common prelude from command line. Finally solved that.

In ~/bin/scheme-repl, I put:

#!/usr/bin/env expect -f
log_user 0
cd "$env(HOME)/Code/CodeChez"
spawn scheme
expect "> "
send -- "(import (chezscheme) (marklib) (marklib-os)"
send -- "  (only (srfi s1 lists) delete drop-right last)"
send -- "  (only (srfi s13 strings) string-delete string-index string-index-right string-join string-tokenize) )\n"
log_user 1

(make sure to change the path to wherever you keep your Scheme scripts, and whatever imports you like)

Added alias s=scheme-repl to my .zshrc

Now I can just hit one letter for shortcut:

% s
(import (chezscheme) (marklib) (marklib-os)  (only (srfi s1 lists) delete drop
-right last)  (only (srfi s13 strings) string-delete string-index string-index-r
ight string-join string-tokenize) )
> (os-path 'pwd)

You can sort of do the same thing with a Chez boot file, but I wasn't able to get it to load libraries from thunderchez, even with --libdirs flag, so screw it.

I'd forgotten everything I ever knew about expect, and the only resources online are exact copies of the same "log into ssh with expect!" (which you should never do! Set up SSH keys for Cthulhu's sake!) tutorial over and over again, so had to read the man page to make even this trivial thing work.

Basic Games in Scheme

The first project I write in any new language is usually a guess-the-number game, a die roller, or an RPN calculator, then I start collecting those and other toys and utilities into a single "main menu" program, and use that to drive me to develop my libraries, play with different algorithms. Occasionally it's useful, mostly it's just a pile of stuff.

The Scheme version has a couple useful things. I was mostly thinking about old BASIC games, so it's "BasicSS" (SS being the Chez Scheme file extension, not anything more nautical or sinister).

I wrote a fairly malevolent wordsearch generator in the process of testing some file parsing, so here's one for 20 programming languages. I can tell you that B, C, C#, and D are not in my list. I'm doubtful that anyone can find all of them, or even half.

Hangman depends on /usr/share/dict/words, 235,886 lines on my system, which is very unfair:

 #     |
 #    ---
 # \ (o o) /
 #  \ --- /
 #   \ X /
 #    \X/
 #     X
 #     X
 #    / \
 #   /   \
Word: TE---EN--
Guesses: E, T, A, O, I, N, B, R, S
YOU LOSE! You have been hung.
The word was TEMULENCY.

Seabattle ("you sunk my…") sucks, it just picks targets at random; teaching it some AI would help.

Hurkle, like all the early-'70s "find a monster on a grid" games, is awful, but the map display makes it a little easier to track your shots. "The Hurkle is a Happy Beast" by Theodore Sturgeon is one of his 10% good stories, but it provides only a little context.

Some of this I can release source for, some I probably shouldn't, so it's just a binary for now.

Reinventing the Wheel

Sure, there's existing code. Somebody Else's Code. It works fine, maybe not as fast as you'd like, or the interface isn't quite right. That's how it often is with me and SRFI-13. Olin Shivers is a skilled Schemer, back when that wasn't cool (OK, it's still not cool), but some of his APIs enshrined in early SRFIs drive me a little nuts, and the implementation is slow because it's so generalized.

So after a few false starts and failed tests, I now have these pretty things: (updated 2020-11-10, enforced hstart, hend boundaries)

;; Returns index of `needle` in `haystack`, or  if not found.
;; `cmp`: Comparator. Default `char=?`, `char-ci=?` is the most useful alternate comparator.
;; `hstart`: Starting index, default 0.
;; `hend`: Ending index, default (- haystack-length needle-length)
(define string-find (case-lambda
    [(haystack needle)  (string-find haystack needle char=? 0 )]
    [(haystack needle cmp)  (string-find haystack needle cmp 0 )]
    [(haystack needle cmp hstart)  (string-find haystack needle cmp hstart ) ]
    [(haystack needle cmp hstart hend)
        (let* [ (hlen (string-length haystack))  (nlen (string-length needle)) ]
            (set! hstart (max 0 (min hstart (sub1 hlen))))
            (unless hend (set! hend (fx- hlen nlen)))
            (set! hend (max 0 (min hend hlen)) )
            (if (or (fxzero? hlen) (fxzero? nlen))
                (let loop [ (hi hstart)  (ni 0) ]
                    ;; assume (< ni nlen)
                    ;(errprintln "hi=" hi ", ni=" ni ", hsub=" (substr haystack hi hlen) ", bsub=" (substr needle ni nlen))
                        [(cmp (string-ref haystack (fx+ hi ni)) (string-ref needle ni))  (set! ni (fx+ ni 1))
                            ;; end of needle?
                            (if (fx>=? ni nlen)  hi  (loop hi ni) )
                        [else  (set! hi (fx+ hi 1))
                            ;; end of haystack?
                            (if (fx>? hi hend)    (loop hi 0) )

;; Test whether 'haystack' starts with 'needle'.
(define (string-has-prefix? haystack needle)
    (let [ (i (string-find haystack needle char=? 0 0)) ]
        (and i (fxzero? i))

;; Test whether 'haystack' ends with 'needle'.
(define (string-has-suffix? haystack needle)
    (let* [ (hlen (string-length haystack))  (nlen (string-length needle))
            (i (string-find haystack needle char=? (fx- hlen nlen)))
        (and i (fx=? i (fx- hlen nlen)))

Written for Chez Scheme, caveat implementor. BSD license, do what thou wilt. If you find a bug, send me a failing test case.

I don't normally bother with fx (fixnum) operations, but in tight loops it makes a difference over generic numeric tower +, etc.

Racket CS 7.8

Big plus: ARM64 support!

Big minus: Neither DRRacket nor any built .app launches on the Mac, you have to call them from command line. Gross incompetence & zero testing.

My basic gridtest.rkt demo still takes 100ms to render a frame, needs to be <16ms. But that's interpreting from the shell, and I may look and see if they have a more efficient graphics API.

The bigger problem is that compiling doesn't work with packages, or at least I don't have the right magic invocations:

% cat build.zsh

rm -rf build
mkdir build
# Windows: --ico foo.ico
# Mac: --icns foo.icns
time raco exe --cs --gui ++lib memoize ++lib rsound ++lib portaudio -o build/gridtest gridtest.rkt

% ./build.zsh && ./build/
raco exe --cs --gui ++lib memoize ++lib rsound ++lib portaudio -o    39.14s user 2.40s system 87% cpu 47.390 total
ffi-lib: could not load foreign library
  path: libportaudio.2.dylib
  system error: dlopen(libportaudio.2.dylib, 6): image not found
   .../ffi/unsafe.rkt:131:0: get-ffi-lib
   body of '#%embedded:portaudio/portaudio:
   [repeats 6 more times]

Well, that's fucked. 40 seconds and a fuck you because it doesn't bring in the dylib it needs.

Obviously I could just say "install Racket, run this script", but that's lame and distributes source, which in a real game I don't want to do.

LISP Machines

I really wish we had a front-end to software even half as useful as this in the 21st C, but technology has regressed massively since the '80s and '90s. Some of the old IDEs, before they became bloated "enterprise" software (because giant mega-corporations paid for them, not individual programmers, so the IDE makers serve their paymasters), started to slouch towards this kind of usefulness but fast and small. CodeWarrior, Project Builder/Interface Builder, Borland's Turbo Whatever.

emacs isn't the answer, it's an abandonment of the question; "modern" emacs turned away from the LM zmacs model, it's now just a terrible LISP interpreter with a bad text-only-editor front end; you can make tools in it, but nobody sane will want to use them. I'm perfectly comfortable with the emacs editing keys (well, obviously, Macs use emacs keys for all text areas), but the machinery in it is just broken.

I get excited about Chez Scheme having a nice REPL, with history and can edit multi-line blocks in the REPL, unlike the crappy readline almost every other Scheme uses. But it doesn't do hypertext, it doesn't do graphics, it barely has tab completion (procedure names only), it doesn't have any inline documentation & source inspector; all of those were in the LISP Machine.

When I'm working, I have my text editor (BBEdit or Atom mostly), a terminal with the REPL running and I copy-paste to it, 3-4 PDFs open (R6RS, R6RS-lib, CSUG, sometimes TSPL), and a web browser pointed at the SRFIs. If the Internet connection went down, I'd have to search the SRFI sources to figure out what's in there. I really need a better tool for this.

DrRacket can do some graphics inline, and the tab completion shows documentation hints in the top right corner, but as I note every time, it doesn't really have a REPL because it destroys the interactive environment every time you edit code; utterly useless for code exploration. And in practice, Racket is really horrifyingly slow; it does fine in focused benchmarks but real-world use it just falls over drooling.

The graphics part's sort of irrelevant, and sort of not; the way the LISP Machine worked was getting a "presentation" form for an object, which would render as text or drawings or images, and interacting with it sent messages back to that object. That's probably out of scope for anything except a complete new OS and terminal. A simplistic number-tagged hypertext would be good enough and orders of magnitude easier.

So. I'm not sure what to do here, I don't want to just complain about tools and not do anything about them. I could try to extract the docs to make a hypertext doc system; a lot of text processing on TeX source sounds painful, and a one-off job, I want a more universal solution. It may be possible to hook into Chez's completion to call a help system. Or it could be a standalone program that you feed several doc sources into, and it lets you search against them. Dash does that, but it's Mac and C/Objective-C primarily, and does poorly at other docsets.