Skip to content

Commit 6a47be9

Browse files
committed
Add macro expansion and some other missing features to Ruby implementation
1 parent dc97df6 commit 6a47be9

1 file changed

Lines changed: 233 additions & 19 deletions

File tree

ports-r/scheme.rb

Lines changed: 233 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
require_relative "parser"
22

33
module Scheme
4+
# Macro table to store defined macros
5+
MACRO_TABLE = {}
6+
47
class Environment
58
def initialize(params, args, outer_environment = nil)
69
@outer_environment = outer_environment
@@ -29,6 +32,14 @@ def find(key)
2932
raise "Lookup error for #{key}"
3033
end
3134
end
35+
36+
def unset(key)
37+
if @env.key?(key)
38+
@env.delete(key)
39+
else
40+
raise "Cannot unset non-existent variable #{key}"
41+
end
42+
end
3243
end
3344

3445
class Procedure
@@ -52,21 +63,223 @@ def call(*args)
5263
:>= => proc { |a, b| a >= b },
5364
:< => proc { |a, b| a < b },
5465
:<= => proc { |a, b| a <= b },
66+
:"=" => proc { |a, b| a == b },
5567
:list => proc { |*args| args },
5668
:cons => proc { |a, b| [a] + b },
5769
:car => :first.to_proc,
5870
:cdr => proc { |a| a[1..] },
59-
:append => proc { |*args| args.flatten },
71+
:append => proc { |*args| args.flatten(1) },
72+
:display => proc { |a| print a },
73+
:inexact => proc { |a| a.to_f },
6074
:length => :length.to_proc,
61-
:null? => proc { |a| a.nil? || a == [] }
75+
:not => proc { |a| !a },
76+
:null? => proc { |a| a.nil? || a.empty? },
77+
:pair? => proc { |x| x.is_a?(Array) && !x.empty? },
78+
:sqrt => proc { |a| Math.sqrt(a) }
6279
}.entries.transpose
6380
GLOBAL_ENV = Environment.new(GLOBAL_DICT[0], GLOBAL_DICT[1])
6481

82+
# Helper methods for expand
83+
def is_pair(x)
84+
x.is_a?(Array) && !x.empty?
85+
end
86+
87+
def to_string(x)
88+
case x
89+
when true then "#t"
90+
when false then "#f"
91+
when Symbol then x.to_s
92+
when String then "\"#{x.gsub('"', '\"')}\""
93+
when Array then "(#{x.map { |e| to_string(e) }.join(" ")})"
94+
else x.to_s
95+
end
96+
end
97+
98+
def require_syntax(x, predicate, msg = "wrong length")
99+
raise "#{to_string(x)}: #{msg}" unless predicate
100+
end
101+
102+
# Main expand function
103+
def expand(x, toplevel = false)
104+
# Check for empty list
105+
require_syntax(x, !(x.is_a?(Array) && x.empty?))
106+
107+
# Constant/non-list => unchanged
108+
return x unless x.is_a?(Array)
109+
110+
case x.first
111+
when :include
112+
# (include string1 string2 ...)
113+
require_syntax(x, x.length > 1)
114+
expand_include(x)
115+
when :quote
116+
# (quote exp)
117+
require_syntax(x, x.length == 2)
118+
x
119+
when :if
120+
# (if test conseq) => (if test conseq nil)
121+
if x.length == 3
122+
x += [nil]
123+
end
124+
require_syntax(x, x.length == 4)
125+
x.map { |xi| expand(xi) }
126+
when :set
127+
# (set! var exp)
128+
require_syntax(x, x.length == 3)
129+
var = x[1]
130+
require_syntax(x, var.is_a?(Symbol), "can set! only a symbol")
131+
[:set!, var, expand(x[2])]
132+
when :unset
133+
# (variable-unset! var)
134+
require_syntax(x, x.length == 2)
135+
var = x[1]
136+
require_syntax(x, var.is_a?(Symbol), "can unset! only a symbol")
137+
[:"variable-unset!", var]
138+
when :define, :"define-macro"
139+
# Check correct length
140+
require_syntax(x, x.length >= 3)
141+
142+
_def, v, body = x[0], x[1], x[2..]
143+
144+
if v.is_a?(Array) && !v.empty?
145+
# (define (f args) body) => (define f (lambda (args) body))
146+
f, *args = v
147+
expand([_def, f, [:lambda, args] + body])
148+
else
149+
require_syntax(x, x.length == 3, "wrong length in definition")
150+
require_syntax(x, v.is_a?(Symbol), "can define only a symbol")
151+
exp = expand(x[2])
152+
153+
if _def == :"define-macro"
154+
require_syntax(x, toplevel, "define-macro only allowed at top level")
155+
proc = evaluate(exp)
156+
require_syntax(x, proc.respond_to?(:call), "macro must be a procedure")
157+
MACRO_TABLE[v] = proc
158+
return nil
159+
end
160+
161+
[:define, v, exp]
162+
end
163+
when :begin
164+
# (begin exp*)
165+
return nil if x.length == 1
166+
x.map { |xi| expand(xi, toplevel) }
167+
when :lambda
168+
# (lambda (vars) exp1 exp2...)
169+
require_syntax(x, x.length >= 3)
170+
171+
vars, *body = x[1..]
172+
173+
# Check that vars is a symbol or list of symbols
174+
is_valid_vars = vars.is_a?(Symbol) ||
175+
(vars.is_a?(Array) && vars.all? { |v| v.is_a?(Symbol) })
176+
require_syntax(x, is_valid_vars, "illegal lambda argument list")
177+
178+
# Wrap multiple expressions in begin
179+
exp = (body.length == 1) ? body[0] : [:begin] + body
180+
[:lambda, vars, expand(exp)]
181+
when :quasiquote
182+
# `x => expand_quasiquote(x)
183+
require_syntax(x, x.length == 2)
184+
expand_quasiquote(x[1])
185+
when :cond
186+
# (cond (test exp) ...)
187+
expanded_clauses = x[1..].map do |clause|
188+
require_syntax(x, clause.is_a?(Array) && clause.length == 2,
189+
"Invalid cond clause format")
190+
[expand(clause[0]), expand(clause[1])]
191+
end
192+
[:cond] + expanded_clauses
193+
else
194+
# Check for macro expansion
195+
if x.first.is_a?(Symbol) && MACRO_TABLE.key?(x.first)
196+
# (m arg...) => macroexpand if m is a macro
197+
expand(MACRO_TABLE[x.first].call(*x[1..]), toplevel)
198+
else
199+
# (f arg...) => expand each
200+
x.map { |xi| expand(xi) }
201+
end
202+
end
203+
end
204+
205+
# Expand quasiquote expression
206+
def expand_quasiquote(x)
207+
# 'x => 'x
208+
unless is_pair(x)
209+
return [:quote, x]
210+
end
211+
212+
# Check for invalid splicing
213+
require_syntax(x, x[0] != :"unquote-splicing", "can't splice here")
214+
215+
if x[0] == :unquote
216+
# ,x => x
217+
require_syntax(x, x.length == 2)
218+
x[1]
219+
elsif is_pair(x[0]) && x[0][0] == :"unquote-splicing"
220+
# (,@x y) => (append x y)
221+
require_syntax(x[0], x[0].length == 2)
222+
[:append, x[0][1], expand_quasiquote(x[1..])]
223+
else
224+
# `(x . y) => (cons `x `y)
225+
[:cons, expand_quasiquote(x[0]), expand_quasiquote(x[1..])]
226+
end
227+
end
228+
229+
# Expand include directive
230+
def expand_include(x)
231+
result = [:begin]
232+
233+
x[1..].each do |file_name|
234+
File.open(file_name, "r") do |include_file|
235+
content = include_file.read
236+
include_result = Parser.parse_string(content)
237+
238+
if include_result
239+
result << expand(include_result, true)
240+
else
241+
raise SchemeException, "Could not include content of #{file_name}"
242+
end
243+
end
244+
end
245+
246+
result
247+
end
248+
249+
# Let macro implementation
250+
def let_macro(*args)
251+
args = [:let] + args.to_a
252+
require_syntax(args, args.length > 2)
253+
254+
bindings, *body = args[1..]
255+
256+
# Validate bindings format
257+
valid_bindings = bindings.is_a?(Array) &&
258+
bindings.all? { |b| b.is_a?(Array) && b.length == 2 && b[0].is_a?(Symbol) }
259+
require_syntax(args, valid_bindings, "illegal binding list")
260+
261+
# Extract variables and values
262+
vars = bindings.map { |b| b[0] }
263+
vals = bindings.map { |b| b[1] }
264+
265+
# Create lambda expression
266+
lambda_expr = [[:lambda, vars] + body.map { |b| expand(b) }]
267+
268+
# Add expanded values
269+
lambda_expr + vals.map { |v| expand(v) }
270+
end
271+
272+
# Register the let macro
273+
MACRO_TABLE[:let] = proc { |*args| let_macro(*args) }
274+
275+
# Main evaluation function
65276
def evaluate(tokens, environment = GLOBAL_ENV)
66277
if tokens.is_a?(String)
67278
tokens
68279
elsif tokens.is_a?(Numeric)
69280
tokens
281+
elsif tokens == true || tokens == false
282+
tokens
70283
elsif tokens.is_a?(Array)
71284
case tokens.first
72285
when :if
@@ -90,6 +303,10 @@ def evaluate(tokens, environment = GLOBAL_ENV)
90303
Procedure.new(tokens[1], tokens[2], environment)
91304
when :begin
92305
tokens[1..].map { |t| evaluate(t, environment) }.last
306+
when :set!
307+
raise "`set!` expected 2 argument, got #{tokens.length - 1}" unless tokens.length == 3
308+
309+
environment.find(tokens[1])[tokens[1]] = evaluate(tokens[2], environment)
93310
else
94311
token = tokens.first
95312
procedure = evaluate(token, environment)
@@ -102,24 +319,21 @@ def evaluate(tokens, environment = GLOBAL_ENV)
102319
end
103320
end
104321

105-
def expand(tokens)
106-
return tokens unless tokens.is_a?(Array)
107-
108-
case tokens.first
109-
when :if
110-
if tokens.length == 3
111-
tokens + [nil]
112-
else
113-
tokens
114-
end
115-
else
116-
tokens.map { |t| expand(t) }
117-
end
118-
end
119-
120322
def evaluate_string(source)
121-
Scheme.evaluate(Scheme.expand(Parser.parse_string(source)))
323+
Scheme.evaluate(Scheme.expand(Parser.parse_string(source), true))
122324
end
123325

124-
module_function :evaluate_string, :evaluate, :expand
326+
module_function :evaluate_string, :evaluate, :expand, :is_pair, :require_syntax,
327+
:expand_quasiquote, :expand_include, :let_macro, :to_string
328+
329+
# Get current directory
330+
dirname = File.dirname(__FILE__)
331+
332+
begin
333+
stdlib = File.join(dirname, "../ports/stdlib.scm")
334+
evaluate_string(File.read(stdlib))
335+
rescue => error
336+
puts "Error loading standard library: #{error.message}"
337+
exit(1)
338+
end
125339
end

0 commit comments

Comments
 (0)