Tower of Babble

Programmers almost compulsively make new languages; within just a few years of there being computers, multiple competing languages appeared:

It proliferated from there into millions; probably half of all programmers with 10+ years of experience have written one or more.

I've written several, as scripting systems or toys. I really liked my Minimal script in Hephaestus 1.0, which was like BASIC+LISP, but implemented as it was in Java the performance was shitty and I had better options to replace it. My XML game schemas in GameScroll and Aiee! were half programmer humor, but very usable if you had a good XML editor. Multiple apps have shipped with my tiny lisp interpreter Aspic, despite the fruit company's ban on such things at the time. A Brainfuck/FORTH-like Stream, working-but-incomplete tbasic, and a couple PILOT variants (I think PILOT is hilariously on the border of "almost useful").

Almost every new language is invented as marketing bullshit based on a few Ur-languages:

  • C++: Swift
  • Java: Javascript (sorta), C#, Go
  • Awk: Perl, Python, PHP, Julia
  • C: Rust
  • Smalltalk: Objective-C
  • Prolog: Erlang, Elixir
  • ALGOL: C, Pascal, PL/1, Simula, Smalltalk, Java
  • LISP: Scheme, ML, Haskell, Clojure, Racket
  • BASIC: None, other than more dialects of BASIC.
  • FORTRAN: None in decades, but is the direct ancestor of ALGOL & BASIC.
  • COBOL: None in decades.

A few of these improve on their ancestors in some useful way, often performance is better, but most do nothing new; it's plausible that ALGOL 68 is a better language than any of its descendants, it just has mediocre compiler support these days.

Certainly I've made it clear I think Swift is a major regression, less capable, stable, fast, or even readable than C++, a feat I would've called impossible except as a practical joke a decade ago. When Marzipan comes out, I'll be able to rebuild all my 15 years of Objective-C code and it'll work on 2 platforms. The Swift 1.0 app I wrote and painfully ported to 2.0 is dead as a doornail, and current Swift apps will be uncompilable in 1-2 years; and be lost when Apple abandons Swift.

When I want to move my Scheme code to a new version or any other Scheme, it's pretty simple, I made only a handful of changes other than library importing from MIT Scheme to Chez to Chicken 4 to Chicken 5. When I tested it in Racket (which I won't be using) I had to make a handful of aliases. Probably even CLISP (which is the Swift of LISPs, except it fossilized in 1994) would be 20 or 30 aliases; their broken do iterator would be hard but the rest is just naming.

Javascript is a pernicious Herpes-virus-like infection of browsers and desktops, and nothing can ever kill it, so where it fits the problem, there's no reason not to use it. But there's a lot it doesn't do well.

I was leery of using FreePascal because it has a single implementation (technically Delphi still exists, but it's $X,000 per seat on Windows) and minimal libraries, and in fact when it broke on OS X Mojave, I was disappointed but I-told-you-so.

I'm not saying we should quit making new Brainfuck and LOLCODE things, I don't think it's possible for programmers to stop without radical brain surgery. But when you're evaluating a language for a real-world problem, try moving backwards until you find the oldest and most stable thing that works and will continue to work, not piling more crap into a rickety new framework.

The Biblical reference in the title amuses me, because we know now that it requires no malevolent genocidal war deity scared of us invading Heaven to magically confuse our languages and make us work at cross purposes; anyone who can write and think splinters their thought into a unique language and then argues about it.

Lost Treasure

In 1979, I learned to program in BASIC on a TRS-80 Model I. Sometime in the next year, I read one of my first programming books:

I played Monster Chase and Lost Treasure, modified them extensively, and combined them, so the cave on the island had a monster chase to reach the exit. I recall having problems getting Starship Alpha and Devil's Dungeon to work, but they joined my software library eventually.

One of my earliest and happiest programming memories was sitting at the dining room table, reading Monster Chase, and writing out a smarter movement system and obstacles in a notebook; at the time the only computers were at school, so I wrote code on paper and typed them in later.

So when I found the book again on archive.org last night, I was very excited, and had to reimplement it. I actually typed this into Pythonista on my phone with the PDF open on an iPad, only moved it to the computer to do some final cleanup and upload it.

The book suggests some modifications, and I did some minor ones: Lowered the movement error to 10%, and risk of shark attack to 10%, rising by 1.5x rather than a flat +50% each time; being anywhere near the island edge killed you too often in the original. I also don't move you out of the water automatically, that should cost a turn.

I realized in converting it that I hate, hate, hate Row,Column coordinates instead of Cartesian X,Y; tons of mainframe-era computing resources used Row,Column, and you can still see it in some APIs like Curses. Note that the original program is 74 lines, mine's 214; BASIC is a terrible language, but it's terse.

I could adapt this into another doorgame for my Mystic Dungeon BBS, but I'm not sure what the multiplayer aspect would be, and it has limited replayability without doing some randomization.

Return of the Objective-C Jedi

[[[ These ]]] are your father's square brackets, the weapons of a Jedi Knight.
Not as clumsy or random as C++.
Elegant weapons for a more civilized age.

What's Different in Mulle-ObjC

This is like Objective-C circa 2010(?), good but not fully baked. Far better than circa 1986-2009, when it was a very thin translation layer over C.

  • No ARC (Automatic Reference Counting). This is just invisible sugar to hide retain/release/autorelease, and while ARC's convenient, it's trivial if you actually know how reference counting works. Don't really miss it.
  • No dot property syntax. [[myObj name] length] instead of myObj.name.length, and [myObj setName:newName] instead of myObj.name = newName. I can live with it, but I really did like dot syntax, even if it does "overload" the . operator and hide the distinction between methods and variables.
    • When dot syntax came out, Objective-C nerds came close to fistfights over this. You would not believe the venom some people had for it. Most of those nerds died or quit or got old & tired before fucking Swift came around, I guess.
  • No array syntax. [myList objectAtIndex:i] instead of myList[i]. This is a pain in the ass, I'll have to write some shorthand macros (or rather, go dig them out of my very oldest code).
  • No blocks. This one hurts, but it's a reasonable pick-your-battles decision. Classic: Write a method, dispatch to it, and call back success somehow. Blocks: create a weakSelf reference, enclose it, search-replace self in your block, pick one of a half-dozen complex GCD methods, get a memory leak because you retained something across the block boundary. This is annoying but logically simpler:
    [self performSelectorInBackground:@selector(computeData) withObject:inputData];
    
    - (void)computeData:(id)inputData {
        // create outputData
        [self setOutputData:outputData];
        [[NSNotificationCenter defaultCenter] postNotification:NOTI_DataComputed];
    }
    
  • Has object literals: @42 and @(var) create an NSNumber, @[] creates an NSArray, @{} creates an NSDictionary; dicts use key:value order, not the reverse order used in -[NSDictionary dictionaryWithObjectsAndKeys:], and array and dicts don't need a trailing nil, which was a constant source of mystifying bugs back in the day. Big win!
    • Hmn, crashes if you do something janky like [@[] mutableCopy]: mulle_objc_universe 0x1006adef0 fatal: unknown method 5e1b0403 "-getObjects:range:" in class 7aa0d636 "_MulleObjCEmptyArray"
  • Has for (id x in container) loops, using NSFastEnumeration. The 1.0 process of looping enumerations was awful, so this is very nice.
  • Huh, does have @autoreleasepool, so maybe I should use that instead of NSAutoreleasePool like a caveman? It compiles and seems to work.
  • Properties have properties assign/retain nonatomic/atomic nonnullable readonly, default is assign nonatomic, no "nullable" or "readwrite" flags needed. As it should be.
  • Weird isa define instead of pointer: blog post

TODO

  • I haven't set up an NSRunLoop or the equivalent of NSApplication (which is in AppKit, not Foundation), need to do that and then I'll have a working app template.

Writing Objective-C with Mulle-Objc

mkdir CLICalc
cd CLICalc
mulle-sde init -m foundation/objc-developer executable

This takes more or less forever.

… Still going. OK, finally done. I hate to think it's gonna do that every new project? Or whenever it updates?

Anyway, bbedit . (fuck Xcode), and add at the bottom of import.h and import-private.h:

#import <Foundation/Foundation.h>

Make src/main.m useful:

// main.m
 "import-private.h"
 "CLICalc.h"

int main(int argc, char *argv[]) {
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    CLICalc *calc = [[CLICalc alloc] init];
    [calc push:42.0];
    [calc push:69.0];
    double a = [calc pop];
    double b = [calc pop];
    NSLog(@"a=%f, b=%f", a, b);

    [pool release];
    return 0;
}

Create an Objective-C class, src/CLICalc.h:

// CLICalc.h
 "import-private.h"

@interface CLICalc : NSObject

@property (retain) NSMutableArray *stack;

- (void)push:(double)n;
- (double)pop;

@end

and src/CLICalc.m:

// CLICalc.m
 "CLICalc.h"

@implementation CLICalc

@synthesize stack = _stack;

- (id)init {
    self = [super init];
    _stack = [[NSMutableArray alloc] init];
    return self;
}

- (void)dealloc {
    NSLog(@"CLICalc dealloc");
    [_stack release];
    [super dealloc];
}

- (void)push:(double)n {
    [_stack addObject:@(n)];
}

- (double)pop {
    if ( ! [_stack count]) {
        // ERROR: stack underflow
        return 0.0;
    }
    double n = [[_stack lastObject] doubleValue];
    [_stack removeLastObject];
    return n;
}

@end

Doing that without a template was a little hard on the old memory, and I had to use DDG to look up some method names without autocompletion. But I'm pretty sure that's fine.

In mulle-ide, type update to add the new class to cmake: If you look in cmake/_Sources.cmake you should now see CLICalc.m listed.

Now craft to compile. You'll get a spew of crap, but hopefully no errors.

I am getting this, which I can't resolve:

/Users/mdh/Code/CodeMac/CLICalc/src/main.m:21:55: warning: 'NSAutoreleasePool'
      may not respond to 'init'
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
                                   ~~~~~~~~~~~~~~~~~~~~~~~~~ ^
1 warning generated.

But NSAutoreleasePool certainly has init, and it seems to not die?

% ./build/Debug/CLICalc
a=69.000000, b=42.000000

Hooray!

Yeah, this isn't amazing. Except: It's supposedly portable now. I can maybe rebuild this on Linux, or Windows? I dunno.

This is almost classic Objective-C, slightly enhanced from 1.0: We didn't have property/synthesize, or nice object wrappers like @() when I were a lad. I typed so many [NSNumber numberWithInteger:n]. So get used to the retain/release/autorelease dance. There's no dot-syntax for property access, type them [] like old-school. But hey, it's a proper compiled language with a nice object system and no GC pausing.

I tried importing Cocoa and got a ludicrous spew of errors, so Mac GUI is gonna be a challenge. But I could import SDL and use that for portable UI, since Objective-C is just C.

Sweet. I'll finish up the calculator's parser in a bit, but then see about doing something useful in it.

Writing a Doorgame

So, how did I write this? Mystic BBS has a Pascal-based scripting, but I didn't really like using that; it's very powerful, but a pain in the ass to code/compile/test on, and it's just different enough from normal FreePascal to annoy me. And then tried to use Mystic's "integrated Python", which just doesn't work; I tried everything to make it find Python. And it's 2.7, so bleh. BUT: Adding a doorgame with the menu system lets you specify any executable, including a Python 3.7 script, and pass in a few command-line arguments about user and directories, so that's what I did (though the server I'm using only has Python 3.6, which hasn't been a problem yet, but be aware)…

The BBS handles all the redirection from network to stdin/stdout, otherwise I could use inetd for the same thing, but then I'd have to deal with all the login and networking state. Doing this as a door simplifies programming back to 1970s level!

I'll eventually open source it, but while I'm iterating and early on, I want to keep some of my secrets.

ANSI Color

There's ANSI color everywhere, of course. curses is fucking awful and archaic, and built to handle many incompatible terminals, which isn't a problem anymore. Mystic uses the same trick I'm using, and many others back in the day: Strings have escape codes |XX in them. Mystic uses numbers for colors, which is awful, and has hundreds of BBS-specific strings, which I have no need of. So all I did is this:

# set to 1200, 9600, etc. to simulate modem transmission rate
kModemSpeed = 0
kLinesPerScreen = 24

kAnsiEsc = "\x1b["

#      0   1   2   3   4   5   6   7   8   9   10
kAnsiBoxLine =  ["─",   "│",    "┌",    "┬",    "┐",    "├",    "┼",    "┤",    "└",    "┴",    "┘",]
kAnsiBoxDouble =["═",   "║",    "╔",    "╦",    "╗",    "╠",    "╬",    "╣",    "╚",    "╩",    "╝",]
kAnsiBoxHeavy = ["━",   "┃",    "┏",    "┳",    "┓",    "┣",    "╋",    "┫",    "┗",    "┻",    "┛",]

# ansiFilter, called by write/writeln, replaces |XX with escape codes:
kAnsiEscapeCode = {
    "CD": kAnsiEsc+"J",         # Clear Down
    "CL": kAnsiEsc+"2K",            # Clear Line
    "CS": kAnsiEsc+"2J"+kAnsiEsc+"H",   # Clear Screen
    "DI": kAnsiEsc+"2m",            # Dim
    "ES": "\x1b",               # Escape ASCII 27
    "GG": "\x07",               # Bell ASCII 7
    "HR": kAnsiBoxDouble[0] * 79,       # Horizontal Ruler
    "LI": kAnsiEsc+"1m",            # Light/Bright
    "PI": "|",              # Pipe char |
    "RE": kAnsiEsc+"0m",            # Reset Attributes
    "UN": kAnsiEsc+"4m",            # Underscore

    "FK": kAnsiEsc+"30m",           # Foreground Black
    "FR": kAnsiEsc+"31m",           # Foreground Red
    "FG": kAnsiEsc+"32m",           # Foreground Green
    "FY": kAnsiEsc+"33m",           # Foreground Yellow
    "FB": kAnsiEsc+"34m",           # Foreground Blue
    "FM": kAnsiEsc+"35m",           # Foreground Magenta
    "FC": kAnsiEsc+"36m",           # Foreground Cyan
    "FW": kAnsiEsc+"37m",           # Foreground White

    "LK": kAnsiEsc+"1;30m",         # Foreground Light Black
    "LR": kAnsiEsc+"1;31m",         # Foreground Light Red
    "LG": kAnsiEsc+"1;32m",         # Foreground Light Green
    "LY": kAnsiEsc+"1;33m",         # Foreground Light Yellow
    "LB": kAnsiEsc+"1;34m",         # Foreground Light Blue
    "LM": kAnsiEsc+"1;35m",         # Foreground Light Magenta
    "LC": kAnsiEsc+"1;36m",         # Foreground Light Cyan
    "LW": kAnsiEsc+"1;37m",         # Foreground Light White

    "BK": kAnsiEsc+"40m",           # Background Black
    "BR": kAnsiEsc+"41m",           # Background Red
    "BG": kAnsiEsc+"42m",           # Background Green
    "BY": kAnsiEsc+"43m",           # Background Yellow
    "BB": kAnsiEsc+"44m",           # Background Blue
    "BM": kAnsiEsc+"45m",           # Background Magenta
    "BC": kAnsiEsc+"46m",           # Background Cyan
    "BW": kAnsiEsc+"47m",           # Background White
}

def ansiFilter(s):
    """Replaces |XX codes with values defined in `kAnsiEscapeCode`."""
    out = ""
    slen = len(s)
    i = 0
    while i < slen:
        c = s[i]
        i += 1
        if c == "|":
            esc = s[i:i+2]
            i += 2
            out += kAnsiEscapeCode.get(esc, esc)
        else:
            out += c
    return out

def ansiGoto(pt):
    return "%s%s;%sH" % (kAnsiEsc, pt[1], pt[0])

def readln(prompt):
    """Calls input after filtering prompt."""
    return input(ansiFilter(prompt))

def write(s, end=None):
    """Filters `s` and writes to stdout, writes `end` if needed, flushes stdout."""
    if type(s) != str:
        s = str(s)
    lines = 0
    out = ansiFilter(s)
    for c in out:
        sys.stdout.write(c)
        if kModemSpeed:
            time.sleep(1.0/kModemSpeed)
        if c == '\n':
            lines += 1
        if lines >= kLinesPerScreen:
            readln("|RE|FC[MORE]|RE")
            lines = 0
    if end:
        sys.stdout.write(end)
    sys.stdout.flush()

def writeln(s):
    """Calls write with `end='\n'`."""
    write(s, end='\n')

Now if I want someone to talk in bold cyan (which I chose as my standard NPC color), I just
writeln("|RE|LC%s says, \"Mellow greetings, sir!\"|RE" % (npc["name"],))
RE before and after text is important, resetting colors from previous strings.

The one problem is I can't use | in ASCII art, so to generate my big character logos, I write a script mysticFiglet.zsh, making : the vertical char:

figlet -f small "$*"|sed "-es/|/:/g"

Flow Control

The great part about a doorgame is, no event loop. Oh, there's a REPL of sorts:

    def run(self):
        self.running = True
        try:
            : Python 3.8: while line := self.prompt():
            while self.running:
                line = self.prompt()
                if line == None:
                    break
                words = line.split()
                if len(words) == 0:
                    continue
                self.parseWords(words)
            self.save()
            self.doHelp("?", "_outro")
            return 0
        except (EOFError, KeyboardInterrupt):
            self.save()
            return 0
        except Exception:
            writeln("|GG|RE|BR|FWERROR! Something went wrong.|RE")
            logging.error("", exc_info=True)
            self.save()
            return 1

It just blocks at input (called by readln, called by prompt), and continues on until self.running = False, or an exception is thrown. Whee! The usual game loop makes it impossible to get stateful input without leaving giant memos of current state, so the event loop can keep cycling at 60fps.

Data Files

One solution would be to just make everything in classes, and use Python's pickle to archive everything. But the problem is that's pure binary, you can't easily hack on it or see what's happened, and on load you can't change any values. So instead I only use primitive types and archive everything to and from JSON. Floor maps and Players have a toData() method which produces a JSON-compatible data structure, and their initializers take a data structure and pull out or translate the values they need. It's a little more work, but means I can debug faster and add new fields faster.

The floors use a super-simple data file format, with an ASCII roguelike map and a series of command lines to create NPCs, traps, text, etc. The weapons and armor are listed in an even simpler format, with space-delimited fields.

Both the command lines and item formats are just run thru a tokenizer I wrote, that reads words, "strings", numbers, and [lists]:

type    name        cost    stat    bonus   damage  inflicts    twoh
wpn "Club"      5   Str 0   3   [blunt]     0

becomes:

{'type': 'wpn', 'name': 'Club', 'cost': 5, 'bonus': 0, 'damage': 3, 'inflicts': ['blunt'], 'twoh': 0}

Bookmarklets

I just fixed a bug and re-uploaded these. They're little scripts that sit in a bookmark, like on your favorites bar, and do something useful to the web page. Monochrome Dark & Light change the page style; View Source puts source and plain text in boxes you can easily view and copy-paste from.

Learn2JS Updates

Added to my Learn2JS project, and it's fairly usable now for rapid development, I can move over the application logic of little tools and they just show up in the catalog and work. Still no live editor, you have to drop a script in lib or a user dir, but it's getting closer to instant-on coding!

Try it out, then look at the scripts, alien is just block sprites but it's a decently hard shooter (there's some oddness about hit detection, and it needs upgrade drops to be a real game). maze is the usual maze generator; drawing text right now because I haven't hooked up the sprite graphics. Both are about as close to minimal code needed for the task as you can get.

Runes

A text filter to convert ASCII sequences into nice Unicode or emoji. Call it from your favorite editor, or on the command line:

% echo "BEFORE {circle:this is some hollow text.} AFTER" |runes.py
BEFORE ⓉⒽⒾⓈ ⒾⓈ ⓈⓄⓂⒺ ⒽⓄⓁⓁⓄⓌ ⓉⒺⓍⓉ⊙ AFTER

More instructions in the README file.

Spread of Terrible Programming Languages

Abstract—The English-like business programming language COBOL saw widespread use from its introduction in 1960 well into the 1980s, despite being disdained by computer science academics. This article traces out decisions made during COBOL’s development, and argues that its English-like appearance was a rhetorical move designed to make the concept of code itself more legible to non-programming management at computer-using companies.

I found some of the references much more interesting than the paper, which is a pretty high-level history avoiding the actual boots on the ground details.

COBOL was designed (and fought over very hard on this point) so that unskilled managers could "read" it, but in my view that had little to do with its spread. Middle management where that would matter has no buying power, and executives won't read more than a sentence on a slideshow.

Ubiquity made much more of a difference; no two computer installations were compatible until the late '60s, so the alternatives were COBOL, FORTRAN, LISP, and a hundred weird languages invented at each facility. Given those choices, I'd pick FORTRAN or LISP, but even COBOL would beat rewriting on every machine. A bunch of companies and government agencies ended up clustered on that choice, so it became widespread, not on any merits but because programmers could move code semi-automatically.

I know this because it happened at least five more times that I can think of, and only once with unskilled readability as a goal:

  1. BASIC is a tutorial language for children, very poor for large programs, very slow compared to C or ASM, grossly inferior to Pascal or Logo for any role. BASIC became ubiquitous because it can be implemented in a few K of RAM and worked nearly the same on hundreds of incompatible timesharing and microcomputer systems.
  2. Java is a mediocre Objective-C/Smalltalk replacement, applets turned out to be too heavyweight for the web and insecure, but cross-platform on servers turned out to be very valuable; cross-compiling C++ is a total crapshoot. Developers can have nice Macs and still compile Java code that runs on non-Mac servers.
  3. Linux (not a language, I know, but same pattern) is hot garbage, the product of a drunk, belligerent Finn student putting a kernel that'd get him a failing grade in an OS class on his 386. But because it's so quarter-assed and has no device driver support, it runs on anything like a virus. So now UNIX is all but dead, killed by a nematode parasite that fills the niche.
  4. PHP is a cruel joke, a gross hack to put server-side script in HTML instead of generating HTML in code or templating. But it was easily installed in Apache, runs everywhere with no setup. So half the web runs on this shit, from WordPress to Facebook.
  5. JavaScript started life as a six week hack to get LISP & Self-like programming, with C-like syntax for marketing reasons, in a web browser. And until early 2000s, it wasn't portable enough for anything useful. But when IE died and the other browsers implemented ECMAScript consistently, it became the universal language. It's still weird and fragile; I don't dare write it without eslint. But it may be the language of the century.

There's the similar case of IBM PC/DOS/Windows vs microcomputers and Macintosh, which were better tools but fragmented, but that's more about central authorities imposing Nazi-supporting IBM, and convicted criminal organization Microsoft bribing and extorting to kill competition. Common languages would likely have been enough to keep competition and diversity going if IBM & MS had been burned to the ground and their scatterlings shot as they ran back in the '70s.

The author of the paper sort of slouches in this direction but doesn't quite get it, when pointing out how science and technical culture has standardized on English. We are all incompatible machines, but a common language lets us argue.

I hate when papers list references without URLs:

  1. 10 PRINT CHR$(205.5+RND(1)):GOTO 10: Fun little book, not at all relevant to the paper.
  2. N. Wardrip-Fruin, Expressive Processing
  3. M.C. Marino, Critical Code Studies
  4. B. Schneiderman, The Relationship Between Cobol And Computer Science
  5. J. McCarthy, "Memo To P. M. Morse: A Proposal For A Compiler" Memo CC-56
  6. D. Nofre , M. Priestley , and G. Alberts, "When Technology Became Language: The Origins Of The Linguistic Conception Of Computer Programming, 1950–1960"
  7. M.D. Gordin , Scientific Babel: How Science Was Done Before And After Global English

Debugging in C

I've spent a horribly long time tonight staring at nested stack manipulation code now to get algebraic expression parsing (mostly?) working. I hate unary minus; life is pretty good except for that ugly little weiner with its binary operator twin, and then BAM weird compromises in your code.

For most errors, I rely on testing (even if just firing a test script through the language, as I'm doing with tbasic) and debug mode with verbose stderr logging. But this is C, where the slightest mistake can be EXC_BAD_ACCESS with no clue where. So then I need a debugger…

% make && lldb -o run -- tbasic -d test1.bas

[update: Forgot the -- before the program, which prevents lldb from reading those params. Command lines without parens are easy to get wrong!]

I don't really do much serious with lldb, I just need to see where an error occurred, backtrace (bt), and sometimes print some variables, to usually be able to solve a crash. It's a little frustrating that the lldb environment is so primitive, though, doesn't even have stdout, stderr (weirdly \<stdio.h> is callable), so how do I call utility functions? Had to rewrite some functions to take default NULL values.

Anyway, let and print (and error, my idiosyncratic stderr print) work, there's not a lot left in BASIC then I can get back to more serious things.