Project

iode

0.0
No commit activity in last 3 years
No release in over 3 years
Iode is a work in progress real language on LLVM. This Ruby Gem exists solely so the author can experiment with new language features before committing those ideas to the real language. It is not intended for general use, nor is it intended to be fast or concise.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 1.5
>= 0

Runtime

~> 0.0.8
 Project Readme

Iode RB

An experimental lisp-family language hosted on Ruby.

Installation

gem install iode

Usage

This project is really a playground for language exploration while I build a real language on the LLVM. Nothing here is intended to be fast. I'm just going for expressiveness. If you try iode, please understand it will be slow and somewhat lacking in features.

Command Line

Program source files end in ".io". You can run them like so:

iode-rb path/to/file.io

Or you can send the source code to STDIN:

iode-rb < path/to/file.io

The basic hello world looks like so.

;; this is a comment
(puts "Hello World!")

Some built-in data types (e.g. fractions) are enriched with literals in iode.

(+ 1/2 2/3) ; 7/6

Functions are defined in terms of func.

((func (x y) (* x y)) 6 3) ; 18

As you'd expect, functions are first-class objects in Iode.

Of course, functions can be defined recursively too.

(def loop
 (func (n)
   (if (= n 0)
     'done
     (progn
       (puts n)
       (loop (- n 1))))))

(loop 20)

Similarly, closures can be returned from functions.

(def dec
 (func (n)
   (- n 1)))

(def expt
 (func (n x)
   (if (= x 0)
      1
      (* n (expt n (dec x))))))

(def make-expt-fn
 (func (x)
   (func (n) (expt n x))))

(def square
 (make-expt-fn 2))

(def cube
 (make-expt-fn 3))

(puts (square 4))
(puts (cube 4))

Or something that updates some internal state.

(def make-counter
 (func (n)
   (func () (set! n (inc n)))))

(def counter
 (make-counter 0))

(puts (counter)) ; 1
(puts (counter)) ; 2
(puts (counter)) ; 3
(puts (counter)) ; 4

Variadic functions

A function may accept a variable number of arguments by using the & symbol before the parameter name.

(def sprintf
  (func (str &values)
    (apply format (cons str values))))

(sprintf "It is %.2f degrees today in %s"
         23.7
         "Melbourne")

Whitespace after the & symbol is permitted and has no effect.

The variadic parameter may not necessarily be the last parameter of the func. If any parameters are specified after the variadic parameter, they will increase the minimum arity of the func and will cause the variadic parameter to receive fewer arguments.

Only one variadic parameter is permitted per func definition.

Support for passing arguments to functions using variadic style is also planned (like the splat in Ruby).

Tail call optimization

If the last thing a function returns is a call to another function (or itself), iode will replace the current stack frame with the new call, giving limitless recursion. This is practically essential for any functional language.

The following will loop forever and make the script "hang":

(let ((forever (func ()
                 (forever))))
  (forever))

This works for mutual recursion too.

(let ((odd? (func (n)
              (if (= n 0)
                false
                (even? (- n 1)))))
      (even? (func (n)
               (if (= n 0)
                 true
                 (odd? (- n 1))))))
  (even? 200000))
; true

Of course, the above routine is not efficient, but it demonstrates the ability to recurse without blowing the stack.

Data types

Iode has a rich set of supported data types.

Integers

(def x 42)

Floats

(def x 42.5)

Fractions

(def x 1/2)

Symbols

(def x 'foo)

Strings

(def x "this is a string")

Lists

(def x (list 1 2 3))
(def y '(1 2 3)) ; same thing

(head x) ; 1
(tail x) ; '(2 3)
(empty? (tail (tail (tail x)))) ; true
(nth x 1) ; 2
(x 1) ; 2 (same thing)

Maps (Hashes)

(def x {'a 42, 'b 7})

(get x 'b) ; 7
(x 'b) ; 7 (same thing)
(assoc x 'b 9) ; {'a 42, 'b 9}
(dissoc x 'b) ; {'a 42}

Regular expressions

(def re /[a-z]*_class/)

Modules

Note: This is a big work in progress and is feature incomplete.

Source files (a.k.a. modules) may be loaded from a path using require.

;; foo.io

(puts "Foo loaded")

(def test
  (func () "Can't be reached"))
;; bar.io

(require "foo.io") ; Foo loaded
(test) ; Error, no such function!

By design, definitions are kept local to individual modules. This means when you require another module, you don't gain its definitions, nor can it see your definitions.

In order to share definitions between modules, iode provides the symmetric functions export and import.

;; foo.io

(def one
  (func () "Called one"))

(def two
  (func () "Called two"))

(export '(one two))

Any module needing access to one and two may not import the foo module.

;; bar.io

(import "foo.io")

(one) ; Called one
(two) ; Called two

This is definitely going to change, since the current implementation is only a step towards the end goal of loading modules by naming convention, and namespacing within modules. Since one and two were not explicitly referred to the current scope, the above example would be better written as:

;; bar.io

(import 'foo)

(foo/one) ; Called one
(foo/two) ; Called two

Macros

Macros in iode are first class and have some powerful features. They are objects and can therefore be assigned to variables and passed as arguments etc.

In many ways, macros are just like funcs, except that they receive unevaluated iode data as input and return unevaluated iode data as output. This transformation is done at runtime, which is what gives iode's macros the quality of being handled as objects.

The syntax for returning code is a little cumbersome at this point, since I haven't yet added quasiquoting. That is very soon on my list of things to do.

Since iode doesn't yet have the boolean operators and and or, let's define them with macros.

(def and
  (macro (a b)
    (list (quote if) a
            b a)))

(def or
  (macro (a b)
    (list (quote if) a
            a b)))

(p (and false 42)) ; false
(p (and nil 1000)) ; nil
(p (and 1000 nil)) ; nil
(p (and 1000 888)) ; 888
(p (and 888 1000)) ; 1000

(p (or false 42)) ; 42
(p (or nil 1000)) ; 1000
(p (or 1000 nil)) ; 1000
(p (or 1000 888)) ; 1000
(p (or 888 1000)) ; 888

Ok, so the above macros assume their components are side-effect free, since they evaluate the condition twice. It is left as an exercise to the reader to find a way to correct this issue.

In Ruby Code

Using Iode from inside Ruby code can be interesting, as it will interoperate with Ruby.

require "iode"

result = Iode.run <<-PROG
(if ((func (x) x) false)
  "x = true"
  "x = false")
PROG

puts result

This returns the string "x = false" to Ruby. Hopefully you can see what the code does.

Here's another example showing how you can pass values from Ruby into Iode.

require "iode"

prog = Iode.run <<-PROG
(func (x)
  (if x
    (puts 42)
    (puts 7)))
PROG

prog.call(false) #=> 7
prog.call(true)  #=> 42

This works because internally, iode funcs are represented as Procs.

Incidentally, that means you can even pass higher-order functions from Ruby to iode.

require "iode"

prog = Iode.run <<-PROG
(func (f)
  (f 42))
PROG

prog.call(->(x){ x * 2 }) #=> 84
prog.call(->(x){ x + 4 }) #=> 46

Extending iode

If you want to add a native Ruby function to be applied like an iode function, put it in a Module and register it into Iode::Core:

require "iode"

module MyFunctions
  def example(a, b)
    a + b
  end
end

Iode::Core.register MyFunctions

Iode.run('(example 7 5)') #=> 12

Of course, you can always use the built-in module support to write iode source code to be imported too.

Development

If you feel inclined to poke around in the source, start with the Interpreter#eval method. You'll see it's a simple recursive algorithm that operates on native Ruby data. The native Ruby data is equivalent to native iode data. This is how all lisps work.

The Reader class converts source code to this data format.

Native ruby functions (things that can't be written in iode itself) are all found under lib/iode/core/. Built-in functions and macros written in iode itself are found under lib/iode/src/. The Iode::Core module handles loading these definitions into a Hash.

The class Iode::Scope is the basis for all lexical scoping. It loads the core definitions into the root scope by default, then new scopes are chained from there.

Copyright & Licensing

Licensed under the Apache License, Version 2.0. See the LICENSE.txt file for full details.