Building a Binary with Chicken Scheme

So that was fucking fun. Seems Chicken's docs aren't correct on how to build with modules, because those were added after the dark-ages R5RS it was modelled on.

There is an "egg" system which is used for building the libraries, but it's difficult to use for making binaries in your own destination dir, and fills your work dir with spam temp files. Unfortunately basically useless for work.

Finally got this working:

src/somelib.scm:

(declare (unit somelib))
(module somelib
    (hello)

(import scheme)

(define (hello) (display "Hello!\n"))
)

src/somemain.scm:

(import scheme
    (chicken load)
)

(cond-expand
    (compiling (declare (uses somelib)))
    (else (load-relative "somelib.scm")))
(import somelib)

(hello)

Don't you just love that muffin-man-muffin-man repetitious bullshit? declare is for the compiler, load is for the interpreter, then import for both. You import builtins like chicken, but use libraries (aforementioned sdl) without an import. Bizarre and contradictory.

build.zsh: [script updated 2019-06-08]

#!/bin/zsh

# EXE is binary filename
# MAIN is main script
# LIBS is space-delimited, must not include main script

function usage {
    echo "Usage: build.zsh PROJECT || MAIN.scm [LIBS] || ls || -?"
    exit 1
}

if [[ $# -eq 0 || "$1" == "-?" || "$1" == "--help" ]]; then
    usage
fi

case $1 in
    ls)
        echo "Known projects:"
        # Add known projects here
        echo "eldritch test"
        exit 0
    ;;
    eldritch)
        EXE=eldritch
        MAIN=eldritch.scm
        LIBS="marklib.scm marklib-geometry.scm marklib-ansi.scm"
    ;;
    test)
        EXE=marklib-test
        MAIN=marklib-test.scm
        LIBS="marklib.scm marklib-geometry.scm"
    ;;
    *)
        # command-line project: MAIN=$1, LIBS=$2...
        EXE=`basename $1 .scm`
        MAIN=$1
        shift
        LIBS="$*"
    ;;
esac

mkdir -p bin

cd src
for SF in ${=LIBS}; do
    SNAME=`basename $SF .scm`
    echo "Compiling $SF"
    csc -c -j $SNAME $SF -o $SNAME.o || exit 1
done
echo "Compiling $MAIN"
csc -c $MAIN -o `basename $MAIN .scm`.o || exit 1
echo "Linking..."
csc *.o -o ../bin/$EXE || exit 1
rm -f *.o
rm -f *.import.scm
cd ..

echo "Built bin/$EXE"
% ./build.zsh
Built bin/something
% ./bin/something
Hello!

Hello, working native binary! There's a bunch more stuff about making a deployable app, but I'll take this for now. My actual program can show a blank green graphics window!

More Fun and Swearing with Scheme

I have ideas for some little games, which aren't suitable as giant Electron-powered applications. My preference in language for this would be Objective-C, Scheme, Pascal, C if I absolutely had to. Obj-C's lack of portability rules it out for now, but if GNUstep gets their crap together it may go back in the running. Scheme looked promising, I've liked using Chez Scheme for some data processing.

So after 2 days of experimentation and abuse, and none of my tools working right in the process, I was unable to get thunderchez/sdl2 to link. I can explicitly load the dylib, and it doesn't find SDL_Init. I wrote a little C program to prove that SDL is reachable, and it is. Chez just won't load it.

Frustrated, I grabbed Chicken Scheme again, did a chicken-install -sudo sdl2, ran the sample code(!), and bam, it just worked. 15 minutes of effort at most, and I'm up and running.

Down side, Chicken compiles slow, and the interpreter is PAINFULLY slow; Chez "interprets" by compiling fast with low optimizations. And Chicken defaults to R5RS which is… 1990 called and wants me to watch MIT SICP lectures on VHS. It has some R7RS support, but I prefer the guarantees of portability, safety, and specified behavior in R6RS. Have to go looking thru SRFI docs to find any feature, it's not batteries-included. Oh well, I'll probably live just fine without ideological purity.

Programming is a Joy

"Programming is a joy. That's why people do it. No one should spend hours in front of a computer terminal out of some dreary sense of duty, or because they have some vague notion of becoming "computer literate". That's not the point. Programming ought to be fun—and if you're not having fun, you shouldn't waste your time."
—Michael Eisenberg, "Programming in Scheme" (1988)

A Little Scheme

I go through phases of playing with Scheme for utility code, maybe even portable dev; while FreePascal is a better language for this, I'm frustrated by the lack of library support and the useless iOS situation.

Scheme's always been an emergency backup language; it was fun to learn back in the '80s and early '90s, and both SICP and TSPL are good books, but nobody wanted to pay for Scheme dev, and anyway the language is very annoying to write. I often treat it as a logic puzzle to get anything done, not a useful tool. But it does have good library support, and it can compile to very fast binaries, despite having GC pauses and consuming 2x as much memory as a C program. Maybe I can get better at solving problems in it, build up some libraries, and make it useful?

So the current landscape is:

  • Chez Scheme:
    • Pros:
      • Very fast to compile and at runtime, competitive with C compilers.
      • Great interactive REPL, not just a half-broken readline history like pretty much every other Scheme.
      • Debugger is reasonably good, and integrated in the REPL. All I really use myself is (trace FOO) and (inspect BAR), but non-caveman coders will make better use of it.
      • Current R6RS implementation plus extensive chezscheme library.
      • By R. Kent Dybvig, author of TSPL, and Cisco currently employs him to maintain Chez Scheme.
      • REPL environment is called a café, which I find charming. Yes, I also liked all the coffee puns and iconography from early Java programming.
    • Cons:
      • Not as widely supported by tools & documentation as Racket.
  • Racket:
    • Pros:
      • Very nice GUI.
      • Current R6RS implementation plus extensive racket library.
      • Built around making multiple languages; I don't really care about this. I loathe "Typed Racket", one of the worst combinations of ideas in history.
      • Tons of documentation.
    • Cons:
      • Mediocre performance. There's a project to rehost Racket on Chez Scheme, which would fix this, but then why use Racket?
      • Doing anything in the GUI destroys your environment, all the objects you've made, unlike any LISP or Scheme ever. So it's utterly fucking useless as an interactive REPL. I can't say enough bad things about this. ★☆☆☆☆ Kill On Sight.
  • Chicken:
    • Pros:
      • Compiles to C and thence to native binaries, with nice FFI to C libraries.
    • Cons:
      • Mostly old R5RS, with a few extension libraries.
      • Terrible REPL, only really usable as a compiled language.
  • Scheme R7RS benchmarks

Chez Scheme is the clear winner for me; if I was a novice, I might choose Racket and not realize that the REPL is a broken abomination for a while. If I was only doing C interop, Chicken would be better.

Editing in BBEdit works OK, but it doesn't know how to find function definitions. I guess Vim has current syntax, but I'm kinda over that habit unless I have to sysadmin. I have never been emacsulated and never will.

Atom's symbols list doesn't do any better. But if you do want to use it, install package language-racket (all other language-schemes are R5RS at best), and then add some file types to config.cson:

"*":
  core:
    customFileTypes:
      "source.racket": [
        "scm"
        "ss"
      ]

In any editor, any language, I use hard tabs (1 char = 1 logical indentation level, obviously), and normally tabstop at 8 chars which discourages very long nesting and encourages me to extract functions. Scheme is indentation hell, so set the tabstop to 4 spaces. (The code blocks below won't show that.)

Do not criticize my C-like paren/brace placement; I prefer clear readability of code structure to some obsolete Emacs dogma.

So, let's see it work, with hello.ss:

#!/usr/bin/env scheme-script
(import (chezscheme))
(format  "Cheers 🍻 , ~a!~%" (car (command-line-arguments)))
(exit)
% chmod 755 hello.ss
% ./hello.ss Mark
Cheers 🍻 , Mark!

Now for something more serious:

stdlib.ss:

;; stdlib.ss
;; Copyright © 2015,2018 by Mark Damon Hughes. All Rights Reserved.
(library (stdlib)
    (export inc! dec! currentTimeMillis randomize input atoi)
    (import (chezscheme))

;; Variables

(define-syntax inc!
    (syntax-rules ()
        ((_ x)      (begin (set! x (+ x 1)) x))
        ((_ x n)    (begin (set! x (+ x n)) x))
    )
)

(define-syntax dec!
    (syntax-rules ()
        ((_ x)      (inc! x -1))
        ((_ x n)    (inc! x (- n)))
    )
)

;; Date-Time

(define (currentTimeMillis)
    (let [(now (current-time))]
        (+ (* (time-second now) 1000)
            (div0 (time-nanosecond now) 1000000))
    )
)

;; Random Numbers
;; "Anyone who attempts to generate random numbers by deterministic means is, of course, living in a state of sin." —John von Neumann

(define (randomize)
    (random-seed (bitwise-and (currentTimeMillis) #xffffffff) )
)

;; Input/Output

;; Reads a line from stdin, ends program on EOF
(define (input)
    (let [(s (get-line (current-input-port)) )]
        (if (eof-object? s)
            [begin (display "Bye!\n")
                (exit)
            ]
            s
        )
    )
)

;; Strings

;; Converts a string to an integer, 0 if invalid
(define (atoi s)
    (let [(n (string->number s))]
        (if (eqv? n #f)
            0
            (inexact->exact (truncate n))
        )
    )
)

)

guess.ss:

#!/usr/bin/env scheme-script
;; guess.ss
;; Copyright © 2015,2018 by Mark Damon Hughes. All Rights Reserved.

(import (chezscheme))
(import (stdlib))

(define (guess)
    (display "I'm thinking of a number from 1 to 100, try to guess it!\n")
    (let [(theNumber (+ (random 100) 1))]
        (define guesses 1)
        (do [(break #f)] (break)
            (format  "Guess #~a? " guesses)
            (let [(g (atoi (input)))]
                (cond
                    [(or (<= g 0) (>= g 100))
                        (display "Try a number from 1 to 100.\n")
                    ]
                    [(< g theNumber)
                        (display "Too low!\n")
                        (inc! guesses)
                    ]
                    [(> g theNumber)
                        (display "Too high!\n")
                        (inc! guesses)
                    ]
                    [else
                        (display "You got it!\n")
                        (set! break )
                    ]
                )
            )
        )
    )
    (display "***GAME OVER***\n")
)

(randomize)
(guess)
(exit)

chez-compile.zsh, with my thanks to Graham Watt for explaining wpo and libraries:

#!/bin/zsh
if [ $# -ne 1 ]; then
    echo "Usage: chez-compile.zsh MAINNAME"
    exit 1
fi
rm -f *.so
rm -f *.wpo
mkdir -p bin

cat <<ENDTEXT |scheme -q --optimize-level 3
(compile-imported-libraries )
(generate-wpo-files )
(compile-program "$1.ss")
(compile-whole-program "$1.wpo" "bin/$1")
ENDTEXT

rm -f *.so
rm -f *.wpo
if [ -f "bin/$1" ]; then
    chmod 755 "bin/$1"
fi

Now I just:

% chez-compile.zsh guess
compiling guess.ss with output to guess.so
compiling stdlib.ss with output to stdlib.so
((stdlib))
()
% bin/guess
I'm thinking of a number from 1 to 100, try to guess it!
Guess #1? 50
Too low!
Guess #2? ^DBye!

Well, that was an adventure to get the equivalent of my first BASIC program from 1980, which can be run in Chipmunk BASIC if you don't happen to have a TRS-80 Model I handy:

1 REM GUESS. COPYRIGHT (C) 1980,2018 BY MARK DAMON HUGHES. ALL RIGHTS RESERVED.
5 RANDOMIZE INT(TIMER()):FOR I=1 TO 10:A=RND(1):NEXT I:REM CHIPMUNK'S RANDOMIZE SUCKS
10 N=INT(100*RND(1))+1:T=1
20 PRINT "I'M THINKING OF A NUMBER FROM 1 TO 100, TRY TO GUESS IT!"
100 PRINT "GUESS #";T;"? ";:INPUT "",G
110 IF G<=0 OR G>=100 OR G<>INT(G) THEN 200
120 IF G<N THEN 210
130 IF G>N THEN 220
140 GOTO 230
200 PRINT "TRY A NUMBER FROM 1 TO 100.":GOTO 100
210 PRINT "TOO LOW!":T=T+1:GOTO 100
220 PRINT "TOO HIGH!":T=T+1:GOTO 100
230 PRINT "YOU GOT IT!":PRINT "*** GAME OVER ***"
240 EXIT:REM CHIPMUNK

But now I can think about more complex problems in Chez Scheme!

Here's the tiniest piece of what I've been thinking about next:

Island in emoji