ASM development log 1: OliviaBOT

Posted on by Idorobots

Hello fellow programmers. Look at your favourite programming language.

Now back to Lisp.

Now back to your favourite language.

Now back to Lisp.

Sadly, it isn't Lisp, but if you stopped wasting your time, it could feel like it's Lisp.

Look down.

Back up.

Where are you?

Reading in OldSpice-guy's voice as I introduce the ASM programming language.

What's on your mind?

Back at me.

ASM has it. The expressiveness and brevity augmented by powerful and extensible features.

Look again.

ASM is now usable.

Anything is possible when your programming language is functional and not imperative.

It's highly munctional.



Quite a few things have changed since the last dev log entry, but I'll try to keep it short.I gave up on the new parser for now, because I first need to fix the specs (as in actually write them) and think about formalizing the reader and the reader macro facility. The test version can be found in experimental.test module, it's fun, though a little bug-ridden.

All you need to do is:

> (import 'experimental.parser)
> (parse "some code")


> (import 'experimental.parser)
> (repl 0) # Which is kind of buggy.

Here's some stuff to try out:

foo: x,y ->
         (* x y);

bar: do sum:  (+ 222 3333);
        mult: (* 123 321);
        (- mult sum);

baz: (foo bar 23);

Here we define a function foo taking two parameters and multiplying them, a variable bar and a variable baz being a result of foo called with bar and 23. The scanner doesn't recognise numbers, so it's pretty limited, but it's still fun. Let's look at the syntax:

var : expressions ; # Defines a new variable.
a , b               # Pairs two sub-expressions.
args -> body        # Creates an anonymous closure.

This code translates to:

(var foo (lambda (x y)
                 (* x y)))
(var bar (do (var sum (+ 222 3333))
             (var mult (* 123 321))
             (- mult sum)))
(var baz (foo bar 23))

The former looks a tad better if you ask me, but it's nothing near the official syntax I intend ASM to support out of the box.

You'll probably notice, that even though the syntax is redefined to suit the specific domain, ASM still makes it easy to understand it, even after some time, because it translates it to unified, lispy representation. That's a feature I'd like to explore more in the future.

In other news, I spent some time thinking about the UnitType in ASM and figured that CommonLispy way (nil = 'nil = () = '()) is not quite cutting it. Translating 0-Tuple to fnord introduced more problems than it solved, so I've settled for a Schemy/Haskelly compromise - the 0-Tuple is the UnitType, and fnord is an alias to (). It doesn't sacrifice any symmetry at all and surprisingly it didn't break any old code.

I also had some fun with lazy evaluation, implemented native Promise, lazy macro and a bunch of basic functions in imports.lazy. There's even a little example in samples.lazy:

(import 'imports.lazy)

(function ++ [ref]
  (+ 1 ref))

(var all-integers ([n ->
                      (join~ n (self (++ n)))]

(function squareduce [n]
  (reduce [a b ->
               (+ (* a a) (* b b))]
          (take n (map~ [x -> (* x x)]

It creates a lazy list of all the positive integers greater than 0 and defines a function named squareduce which does some math on this list. You would be surprised how quickly the values grow!

> (import 'samples.lazy)
> (map squareduce (range 1 15))
    (1 17 370 137156 1.88118e+10 3.53883e+20
     1.25233e+41 1.56833e+82 2.45965e+164
     6.0499e+328 3.66013e+657 1.33966e+1315
     1.79468e+2630 inf)

ASM does not support BigNums yet, so (squareduce 15) is equal infinity. The lazy evaluation is such a mind bending feature that I'm considering making it default in the language, just like the Haskell) guys.

Lastly, I fixed the error messages. Debugging is now WAY easier:

> (import 'tests.test2)
    tests/test2.asm(4): Expected exactly 1 argument instead of 2.
    tests/test2.asm(4): Issued here: `(error "test lol" (quote wat?))'.

It still needs some care for the built-in values and functions because they don't have any source positions that could be displayed.

All these changes should be available from the master branch of ASM repo at google.code GitHub.


While debugging and doing general maintenance arround ASM I figured it'd be easier for me to catch bugs early if I started some small hackish project using ASM, and since ASM is a dynamic language aimed at anything AI I figured I could use it for my IRC bot.The bot, called OliviaBOT, was written entirely in the D programming language, and even though it worked well it was really clumsy.Each patch, new extension and whatnot made it disconnect and reload the binary, and since I intended it to be not only a simple IRC helper, but more of a long-running daemon-helper-startrek-thing in the future, it was unacceptable.

So there we go. I have rewritten Olivia in ASM.

Since ASM doesn't have a working IO library I've decided to glue it to my ready-made IRC Client written in D instead of writing some crap I would surely scrap later. It required some shallow bindings translating IRC Client's API to ASM functions, but was rather straightforward:

auto irc = new IRCConnection(host, port);
auto ASM = new Interpreter();

delegate Expression (ref Scope s, Expression[] args){
    auto nick = args[ 0].eval(s);

    irc.user(nick.toString[1 .. $-1]);
    return nick;
}, 1);

delegate Expression (ref Scope s, Expression[] args){
    auto nick = args[ 0].eval(s);

    irc.nick(nick.toString[1 .. $-1]);
    return nick;
}, 1);

delegate Expression (ref Scope s, Expression[] args){
    auto channel = args[ 0].eval(s);

    irc.join(channel.toString[1 .. $-1]);
    return channel;
}, 1);

delegate Expression (ref Scope s, Expression[] args) {
    auto to = args[ 0].eval(s).toString[1 .. $-1];
    auto msg = args[ 1].eval(s);

    irc.msg(to, msg.toString[1 .. $-1]);
    return msg;
}, 2);

Now these functions are accessible from ASM:

# Bot setup:
(var *nick* "Olivia")
(var *password* *coolface*)
(var *channels* '["#asmpl"])

# Bot init:
(irc-user *nick*)
(irc-nick *nick*)
(irc-msg "NickServ" (append "identify " *password*))

(map irc-join *channels*)

For simplicity reasons it doesn't wait for any responses from the server. I've also thrown out the logging code from these snippets for readability.

For now the commands framework is fairly simple, it uses two short macros and two alist for defining and storing the commands:

# Command alists:
(var *irc-command-alist*)
(var *command-alist*)

# Defines an irc-command.
# Example:
# (irc-command name [args]
#   body)
(macro irc-command [name args .tuple body]
  `(push! (tuple $name (lambda $args $(append '(do) body)))

# defines a user command.
# Example:
# (command name [args]
#   "Doc string."
#   "Usage string."
#   body)
(macro command [name args doc usage .tuple body]
  `(push! (tuple '$name
                 (lambda $args $(append '(do) body))
                 $(append "Usage: " usage "."))

Whenever there's any input from the IRC server pending, the bot preprocesses and tokenizes it for convenience, then executes irc-commands if need be and in case of a PRIVMSG it dispatches the message looking for bot commands. Boring code, so I'll skip it.

This is how you define commands:

# Nick already taken:
(irc-command "433" [from asterix nick msg]
  (log "--- Nick " nick " already taken." \newline)
  (set! *nick* (append *nick* "_"))
  (irc-nick *nick*)    	      # Try another nick!
  (map irc-join *channels*))

# Somebody quits:
(irc-command "QUIT" [nick msg]
  (log "--- "
       (strip-nick nick) " left the channel (" msg ")." \newline))

# ...

# Command list:
(command ?commands [nick channel]
  "Lists available commands."
  (irc-msg channel (list-alist "Commands: " *command-alist*)))

# RPG anybody?
(command !roll [nick channel dice]
  "Rolls some dice."
  "`!roll dice'"
  (irc-msg channel
           (stringof (random (second (assoc dice *dice-alist*))))))

# Search:
(command ?d [nick channel phrase]
  "DuckDuckGoes `phrase' on the interwebs."
  "`?d phrase'"
  (irc-msg channel (append ""
                           (trspace (stringof phrase)))))

Now, there are some helper functions obviously, such as strip-nick or list-alist, but I'm sure these are self-explanatory and I'll skip them aswell. As you can see these are tiny!

Here's a list of all available commands so far:

  • ?apropos - Writes docstring and usage info for a command.
  • ?commands - Lists the available commands.
  • !eval - Disabled by default, evaluates some ASM code.
  • !set - Sets a description to a variable. Variables can be queried later using ?query syntax.
  • ?stuff - Lists !set variables.
  • ?g - Searches the net using Google.
  • ?d - Searches the net using DuckDuckGo. This is a special command. Whenever somebody ?queries Olivia and there is no !set variable for that query, she'll ?d it on the net. It's especially useful for DuckDuckGoes !bang syntax: ?!wiki koza will search Wikipedia for the term koza.
  • ?dice - Lists available dice.
  • !roll - Rolls a ?dice.
  • !note - Leaves a note for a user, that will be sent to him whenever he becomes active (either by JOINing the channel, or PRIVMSGing it).

Here's a test run (you can check it out yourself most of the time on #asmpl at freenode):

(17:40:01) Kajtek: ?!cpp socket
(17:40:02) Olivia:!cpp+socket
(17:40:28) Kajtek: ?dice
(17:40:29) Olivia: Dice: d4 d6 d8 d10 d100 d% d12 d20
(17:40:34) Kajtek: !roll d%
(17:40:35) Olivia: 86
(17:40:55) Kajtek: !set socket "Some C networking crap."
(17:40:59) Kajtek: ?socket
(17:41:00) Olivia: Some C networking crap.
(17:41:27) Kajtek: ?commands
(17:41:27) Olivia: Commands: !note ?stuff !set ?dice !roll ?d ?g ?commands ?apropos
(17:41:43) Kajtek: ?apropos !note
(17:41:44) Olivia: Usage: `!note nick msg'. Leaves a `msg' to `nick'.
(17:42:41) Kajtek: Olivia: thanks!
(17:42:42) Olivia: Kajtek: no probs.
(17:44:31) Kajtek: Olivia, you're kind of neat!
(17:44:32) Olivia: Kajtek: thanks!

2016-02-18: Adjusted some links & tags.