Created
November 4, 2011 04:01
-
-
Save macournoyer/1338630 to your computer and use it in GitHub Desktop.
mio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# mio -- minimalist language inspired by Io for your own careful | |
# and private delectation w/ friends of the the family, | |
# if you want to. | |
# usage: | |
# mio # starts the REPL | |
# mio mio_on_rails.mio | |
# (c) macournoyer | |
module Mio | |
class Error < RuntimeError | |
attr_accessor :current_message | |
def message | |
super + " in message `#{@current_message.to_s}` at line #{@current_message.line}" | |
end | |
end | |
class Object | |
attr_accessor :slots, :protos, :value | |
def initialize(proto=nil, value=nil) | |
@protos = [proto].compact | |
@value = value | |
@slots = {} | |
end | |
def [](name) | |
return @slots[name] if @slots.key?(name) | |
message = nil | |
@protos.each { |proto| return message if message = proto[name] } | |
raise Mio::Error, "Missing slot: #{name.inspect}" | |
end | |
def []=(name, message) | |
@slots[name] = message | |
end | |
# The call method is used to eval an object. | |
# By default objects eval to themselves. | |
def call(*) | |
self | |
end | |
def clone(val=nil) | |
val ||= @value && @value.dup rescue TypeError | |
Object.new(self, val) | |
end | |
end | |
# Message is a chain of tokens produced when parsing. | |
# 1 print. | |
# is parsed to: | |
# Message.new("1", | |
# Message.new("print")) | |
# You can then +call+ the top level Message to eval it. | |
class Message < Object | |
attr_accessor :next, :name, :args, :line, :cached_value | |
def initialize(name, line) | |
@name = name | |
@args = [] | |
@line = line | |
@cached_value = case @name | |
when /^\d+/ | |
Lobby["Number"].clone(@name.to_i) | |
when /^"(.*)"$/ | |
Lobby["String"].clone($1) | |
end | |
@terminator = [".", "\n"].include?(@name) | |
super(Lobby["Message"]) | |
end | |
def terminator? | |
@terminator | |
end | |
# Call (eval) the message on the +receiver+. | |
def call(receiver, context=receiver, *args) | |
if terminator? | |
# reset receiver to object at begining of the chain. | |
# eg.: | |
# hello there. yo | |
# ^ ^__ "." resets back to the receiver here | |
# \________________________________________________/ | |
value = context | |
elsif @cached_value | |
# We already got the value | |
value = @cached_value | |
else | |
# Lookup the slot on the receiver | |
slot = receiver[name] | |
# Eval the object in the slot | |
value = slot.call(receiver, context, *@args) | |
end | |
# Pass to next message if some | |
if @next | |
@next.call(value, context) | |
else | |
value | |
end | |
rescue Mio::Error => e | |
e.current_message ||= self | |
raise | |
end | |
def to_s(level=0) | |
s = " " * level | |
s << "<Message @name=#{@name}" | |
s << ", @args=" + @args.inspect unless @args.empty? | |
s << ", @next=\n" + @next.to_s(level + 1) if @next | |
s + ">" | |
end | |
# Parse a string into a chain of messages | |
def self.parse(code) | |
parse_all(code, 1).last | |
end | |
private | |
def self.parse_all(code, line) | |
code = code.strip | |
i = 0 | |
message = nil | |
messages = [] | |
# Marrrvelous parsing code! | |
while i < code.size | |
case code[i..-1] | |
when /\A("[^"]*")/, # string | |
/\A(\.)+/, # dot | |
/\A(\n)+/, # line break | |
/\A(\w+|[=\-\+\*\/<>]|[<>=]=)/i # name | |
m = Message.new($1, line) | |
if messages.empty? | |
messages << m | |
else | |
message.next = m | |
end | |
line += $1.count("\n") | |
message = m | |
i += $1.size - 1 | |
when /\A(\(\s*)/ # arguments | |
start = i + $1.size | |
level = 1 | |
while level > 0 && i < code.size | |
i += 1 | |
level += 1 if code[i] == ?\( | |
level -= 1 if code[i] == ?\) | |
end | |
line += $1.count("\n") | |
code_chunk = code[start..i-1] | |
message.args = parse_all(code_chunk, line) | |
line += code_chunk.count("\n") | |
when /\A,(\s*)/ | |
line += $1.count("\n") | |
messages.concat parse_all(code[i+1..-1], line) | |
break | |
when /\A(\s+)/, # ignore whitespace | |
/\A(#.*$)/ # comments | |
line += $1.count("\n") | |
i += $1.size - 1 | |
else | |
raise "Unknown char #{code[i].inspect} at line #{line}" | |
end | |
i += 1 | |
end | |
messages | |
end | |
end | |
class Method < Object | |
def initialize(context, message) | |
@definition_context = context | |
@message = message | |
super(Lobby["Method"]) | |
end | |
def call(receiver, calling_context, *args) | |
# Woo... lots of contexts here... lemme clear dat up, ya: | |
# @definition_context: where the method was defined | |
# calling_context: where the method was called | |
# method_context: where the method body (message) is executing | |
method_context = @definition_context.clone | |
method_context["self"] = receiver | |
method_context["arguments"] = Lobby["List"].clone(args) | |
# Note: no argument is evaluated here. Our lil' language only has lazy argument evaluation. | |
# If you pass args to a method, you have to eval them explicitly, using the following | |
# method. | |
# Handy method to eval an argument in it's original context. | |
method_context["eval_arg"] = proc do |receiver, context, at| | |
(args[at.call(context).value] || Lobby["nil"]).call(calling_context) | |
end | |
@message.call(method_context) | |
end | |
end | |
def self.eval(code) | |
# Parse | |
message = Message.parse(code) | |
# puts message.to_s | |
# Eval | |
message.call(Lobby) | |
end | |
# Bootstrap | |
object = Object.new | |
object["clone"] = proc { |receiver, context| receiver.clone } | |
object["set_slot"] = proc { |receiver, context, name, value| receiver[name.call(context).value] = value.call(context) } | |
object["print"] = proc { |receiver, context| puts receiver.value; Lobby["nil"] } | |
# Introducing the Lobby! Where all the fantastic objects live and also the root context of evaluation. | |
Lobby = object.clone | |
Lobby["Lobby"] = Lobby | |
Lobby["Object"] = object | |
Lobby["nil"] = object.clone(nil) | |
Lobby["true"] = object.clone(true) | |
Lobby["false"] = object.clone(false) | |
Lobby["Number"] = object.clone(0) | |
Lobby["String"] = object.clone("") | |
Lobby["List"] = object.clone([]) | |
Lobby["Message"] = object.clone | |
Lobby["Method"] = object.clone | |
Lobby["method"] = proc { |receiver, context, message| Method.new(context, message) } | |
eval <<-EOS | |
## OO | |
set_slot("dude", Object clone) | |
dude set_slot("name", "Bob") | |
# dude name print | |
dude set_slot("say_name", method( | |
arguments print | |
eval_arg(0) print | |
self name print | |
)) | |
dude say_name("hello...") | |
## Boolean logic | |
Object set_slot("and", method( | |
eval_arg(0) | |
)) | |
Object set_slot("or", method( | |
self | |
)) | |
nil set_slot("and", nil) | |
nil set_slot("or", method( | |
eval_arg(0) | |
)) | |
false set_slot("and", false) | |
false set_slot("or", method( | |
eval_arg(0) | |
)) | |
"yo" or("hi") print | |
1 and(2 or(3)) print | |
## Implementing if | |
set_slot("if", method( | |
# eval condition | |
set_slot("condition", eval_arg(0)) | |
condition and( # if true | |
eval_arg(1) | |
) | |
condition or( # if false (else) | |
eval_arg(2) | |
) | |
)) | |
if(true, | |
"condition is true!" print, | |
# else | |
"nope" print | |
) | |
EOS | |
end | |
if __FILE__ == $PROGRAM_NAME | |
if ARGV.empty? | |
require "readline" | |
loop do | |
line = Readline::readline('>> ') | |
Readline::HISTORY.push(line) | |
puts Mio.eval(line) rescue puts $! | |
end | |
else | |
Mio.eval(File.read(ARGV[0])) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment