Class: Crisp::Functions::Core

Inherits:
Object
  • Object
show all
Defined in:
lib/crisp/functions/core.rb

Overview

Defining core crisp functions

Class Method Summary collapse

Class Method Details

.load(current_env) ⇒ Object

load the functions and bind them into the given environment



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/crisp/functions/core.rb', line 6

def self.load(current_env)

  # reserve nil/true/false in environment
  current_env['nil'] = nil
  current_env['true'] = true
  current_env['false'] = false

  # println
  # print arguments (seperated by whitspace) including a newline to the standard output
  #
  #  (println 123)
  #  123
  Function.new do
    print args_evaled.collect(&:to_s).join(' ') + "\n"
  end.bind('println', current_env)

  # def
  # bind the second argument to the symbol name of the first argument
  # actually a key/value pair will be stored in the environment
  #
  #  (def foo 1)
  #  => foo
  #  (println foo)
  #  1
  Function.new do
    validate_args_count(2, args.size)

    key = args[0].text_value
    value = args[1].resolve_and_eval(env)

    if value.class.name == "Crisp::Function"
      value.bind(key, env)
    else
      env[key] = value
    end
  end.bind('def', current_env)

  # fn
  # creates a function
  # the first argument has to be an array containing the argumentlist
  # the second argument is the function body
  #
  #  (fn [a b] (+ a b)
  #  => ...)
  Function.new do
    validate_args_count(2, args.size)

    if args[0].class.name != "Crisp::Nodes::ArrayLiteral"
      raise ArgumentError, "no argument list defined"
    end

    fn_arg_list = args[0].raw_elements
    fn_operation = args[1]

    # create and return the new function
    Function.new do
      validate_args_count(fn_arg_list.size, args.size)

      local_env = Env.new
      fn_arg_list.each_with_index do |key, idx|
        local_env[key.text_value] = args[idx].resolve_and_eval(env)
      end

      chained_env = ChainedEnv.new(local_env, env)

      fn_operation.resolve_and_eval(chained_env)
    end
  end.bind('fn', current_env)

  # if
  # the if function evaluates the second argument if the condition (first argument) returns not nil or false.
  # Otherwise the third argument will be evaluated (optional)
  #
  #  (if (= 1 2) 1 2)
  #  => 2
  Function.new do
    validate_args_count((2..3), args.size)

    result = args[0].resolve_and_eval(env)

    res = if ![nil, false].include?(result)
      args[1]
    elsif args[2]
      args[2]
    end

    res ? res.resolve_and_eval(env) : res
  end.bind('if', current_env)

  # cond
  # cond works like a switch/case statement, evaluating the first expression where the condition matches
  #
  #  (cond
  #   false (println "should not be printed")
  #   true (println "should be printed"))
  Function.new do
    if args.size.odd?
      raise ArgumentError, "argument list has to contain even list of arguments"
    end

    result = nil

    args.each_with_index do |arg, idx|
      next if idx.odd?
      if (arg.class.name == 'Crisp::Nodes::SymbolLiteral' and arg.text_value == 'else') or
         (![nil, false].include?(arg.resolve_and_eval(env)))
        result = args[idx + 1].resolve_and_eval(env)
        break
      end
    end

    result
  end.bind("cond", current_env)

  # let
  # create local binding only valid within let and evaluate expressions
  #
  #  (let [x 1 y 2] (+ x y))
  #  => 3
  Function.new do
    if args[0].class.name != "Crisp::Nodes::ArrayLiteral"
      raise ArgumentError, "no argument list defined"
    end

    if args[0].raw_elements.size.odd?
      raise ArgumentError, "argument list has to contain even list of arguments"
    end

    local_env = Env.new
    chained_env = ChainedEnv.new(local_env, env)
    binding_array = args[0].raw_elements

    binding_array.each_with_index do |key, idx|
      next if idx.odd?
      local_env[key.text_value] = binding_array[idx+1].resolve_and_eval(chained_env)
    end

    args[1..-1].map do |op|
      op.resolve_and_eval(chained_env)
    end.last
  end.bind('let', current_env)

  # loop
  # create loop with a local binding, perform a loopjump using recur
  #
  #  (loop [cnt 5 acc 1]
  #    (if (= 0 cnt)
  #      acc
  #      (recur (- cnt 1) (* acc cnt))))
  #  => 120
  Function.new do
    if env.global_loop_data
      raise LoopError, "nested loops are not allowed"
    end

    if args[0].class.name != "Crisp::Nodes::ArrayLiteral"
      raise ArgumentError, "no argument list defined"
    end

    if args[0].raw_elements.size.odd?
      raise ArgumentError, "argument list has to contain even list of arguments"
    end

    local_env = Env.new
    chained_env = ChainedEnv.new(local_env, env)
    binding_array = args[0].raw_elements
    argument_name_array = []

    binding_array.each_with_index do |key, idx|
      next if idx.odd?
      local_env[key.text_value] = binding_array[idx+1].resolve_and_eval(chained_env)
      argument_name_array << key.text_value
    end

    env.global_loop_data = {
      :operations => args[1..-1],
      :argument_names =>  argument_name_array
    }

    result = args[1..-1].map do |op|
      op.resolve_and_eval(chained_env)
    end.last

    env.global_loop_data = nil

    result
  end.bind('loop', current_env)

  # recur
  # only used inside a loop. recur will trigger a new run of the loop
  # see 'loop' for an example
  Function.new do
    if !env.global_loop_data
      raise LoopError, "recur called outside loop"
    end

    validate_args_count(env.global_loop_data[:argument_names].size, args.size)

    local_env = Env.new
    chained_env = ChainedEnv.new(local_env, env)

    env.global_loop_data[:argument_names].each_with_index do |key, idx|
      local_env[key] = args[idx].resolve_and_eval(env)
    end

    env.global_loop_data[:operations].map do |op|
      op.resolve_and_eval(chained_env)
    end.last
  end.bind('recur', current_env)

  # .
  # perform a native call on an object with optional arguments
  #
  #  (. upcase "abc")
  #  => "ABC"
  Function.new do
    meth = args[0].text_value.to_sym
    target = if args[1].class.name == "Crisp::Nodes::SymbolLiteral" and !env.has_key?(args[1].text_value)
      Object.const_get(args[1].text_value)
    else
      args[1].resolve_and_eval(env)
    end
    values = args[2..-1].map { |arg| arg.resolve_and_eval(env) }

    NativeCallInvoker.new(target, meth, values).invoke!
  end.bind('.', current_env)

  # load
  # include content of another crisp source file
  #
  #  (load "business_logic")
  #  => true
  Function.new do
    args_evaled.collect(&:to_s).each do |filename|
      pwd = `pwd`.strip
      file = if filename[0..1] == '/'
        File.join(pwd, filename)
      else
        filename
      end.sub(".crisp$", '') + '.crisp'

      if !File.exists?(file)
        raise Crisp::ArgumentError, "file #{file} not found"
      end

      filecontent = File.read(file)
      ast = Crisp::Parser.new.parse(filecontent)
      Crisp::Runtime.new(env).run(ast)
    end

    true
  end.bind('load', current_env)

  # alias
  # create alias from one symbol to another
  # 
  #  (alias p println)
  #  (p 123)
  #  123 
  Function.new do
    validate_args_count(2, args.size)

    to = args[0].text_value
    from = args[1].text_value

    env.alias(to, from)
  end.bind('alias', current_env)

end