delorean_lang 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +145 -0
- data/Rakefile +2 -0
- data/delorean.gemspec +21 -0
- data/lib/delorean/base.rb +54 -0
- data/lib/delorean/container.rb +40 -0
- data/lib/delorean/delorean.rb +3157 -0
- data/lib/delorean/delorean.treetop +172 -0
- data/lib/delorean/engine.rb +359 -0
- data/lib/delorean/error.rb +45 -0
- data/lib/delorean/functions.rb +134 -0
- data/lib/delorean/model.rb +25 -0
- data/lib/delorean/nodes.rb +401 -0
- data/lib/delorean/version.rb +3 -0
- data/lib/delorean_lang.rb +12 -0
- data/spec/dev_spec.rb +98 -0
- data/spec/eval_spec.rb +609 -0
- data/spec/func_spec.rb +192 -0
- data/spec/parse_spec.rb +688 -0
- data/spec/spec_helper.rb +100 -0
- metadata +138 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
grammar Delorean
|
2
|
+
rule line
|
3
|
+
f:formula sp? ('#' .*)? <Line>
|
4
|
+
end
|
5
|
+
|
6
|
+
rule formula
|
7
|
+
sp i:identifier sp? '=?' sp? e:expression <ParameterDefault>
|
8
|
+
/
|
9
|
+
sp i:identifier sp? '=?' <Parameter>
|
10
|
+
/
|
11
|
+
sp i:identifier sp? '=' sp? e:expression <Formula>
|
12
|
+
/
|
13
|
+
n:node_name ':' sp? mod:(m:node_name '::')? p:node_name <SubNode>
|
14
|
+
/
|
15
|
+
n:node_name ':' <BaseNode>
|
16
|
+
/
|
17
|
+
'import' sp n:node_name sp v:integer <Import>
|
18
|
+
end
|
19
|
+
|
20
|
+
rule node_name
|
21
|
+
[A-Z] [a-zA-Z0-9_]*
|
22
|
+
end
|
23
|
+
|
24
|
+
rule expression
|
25
|
+
op:unary_op sp? e:expression <UnOp>
|
26
|
+
/
|
27
|
+
'if' sp? v:expression sp?
|
28
|
+
'then' sp? e1:expression sp?
|
29
|
+
'else' sp? e2:expression <IfElse>
|
30
|
+
/
|
31
|
+
v:value sp? op:binary_op sp? e:expression <BinOp>
|
32
|
+
/
|
33
|
+
value
|
34
|
+
end
|
35
|
+
|
36
|
+
rule list_expr
|
37
|
+
'[]' <ListExpr>
|
38
|
+
/
|
39
|
+
'[' sp? e2:expression sp
|
40
|
+
'for' sp i:identifier sp 'in' sp e1:expression sp?
|
41
|
+
ifexp:('if' sp e3:expression sp?)?
|
42
|
+
']' <ListComprehension>
|
43
|
+
/
|
44
|
+
'[' sp? args:fn_args sp? ']' <ListExpr>
|
45
|
+
end
|
46
|
+
|
47
|
+
rule hash_expr
|
48
|
+
'{}' <HashExpr>
|
49
|
+
/
|
50
|
+
'{' sp? args:hash_args sp? '}' <HashExpr>
|
51
|
+
end
|
52
|
+
|
53
|
+
# NOTE: some operations such as << have side-effects (e.g. on
|
54
|
+
# Arrays). So, be cautious about which opertaions are added.
|
55
|
+
rule binary_op
|
56
|
+
'+' / '-' / '*' / '/' / '%' / '==' / '!=' / '>=' / '<=' / '>' / '<' / '&&' / '||'
|
57
|
+
end
|
58
|
+
|
59
|
+
rule unary_op
|
60
|
+
'!' / '-'
|
61
|
+
end
|
62
|
+
|
63
|
+
rule value
|
64
|
+
number /
|
65
|
+
string /
|
66
|
+
boolean /
|
67
|
+
nil_val /
|
68
|
+
script_call /
|
69
|
+
fn /
|
70
|
+
model_fn_getattr /
|
71
|
+
model_fn /
|
72
|
+
node_getattr /
|
73
|
+
getattr /
|
74
|
+
list_expr /
|
75
|
+
hash_expr /
|
76
|
+
'(' sp? e:expression sp? ')' <Expr>
|
77
|
+
end
|
78
|
+
|
79
|
+
# built-in functions
|
80
|
+
rule fn
|
81
|
+
fn:fn_name '(' sp? ')' <Fn>
|
82
|
+
/
|
83
|
+
fn:fn_name '(' sp? args:fn_args sp? ')' <Fn>
|
84
|
+
end
|
85
|
+
|
86
|
+
rule fn_args
|
87
|
+
arg0:expression args_rest:(sp? ',' sp? args:fn_args?)? <FnArgs>
|
88
|
+
end
|
89
|
+
|
90
|
+
rule model_fn
|
91
|
+
m:model_name '.' fn:identifier '(' sp? ')' <ModelFn>
|
92
|
+
/
|
93
|
+
m:model_name '.' fn:identifier '(' sp? args:fn_args sp? ')' <ModelFn>
|
94
|
+
end
|
95
|
+
|
96
|
+
rule model_name
|
97
|
+
class_name '::' model_name
|
98
|
+
/
|
99
|
+
class_name
|
100
|
+
end
|
101
|
+
|
102
|
+
rule fn_name
|
103
|
+
[A-Z] [A-Z0-9]*
|
104
|
+
end
|
105
|
+
|
106
|
+
rule class_name
|
107
|
+
[A-Z] [a-zA-Z0-9]*
|
108
|
+
end
|
109
|
+
|
110
|
+
# script calling
|
111
|
+
rule script_call
|
112
|
+
'@' mod:(m:node_name '::')? c:class_name '(' sp? al:kw_args sp? ')' <ScriptCallNode>
|
113
|
+
/
|
114
|
+
'@' i:identifier? '(' sp? al:kw_args sp? ')' <ScriptCall>
|
115
|
+
end
|
116
|
+
|
117
|
+
rule hash_args
|
118
|
+
e0:expression sp? ':' sp? e1:expression args_rest:(sp? ',' sp? al:hash_args?)? <HashArgs>
|
119
|
+
end
|
120
|
+
|
121
|
+
rule kw_args
|
122
|
+
k:(i:identifier ':' sp?)? arg0:expression args_rest:(sp? ',' sp? al:kw_args)? <KwArgs>
|
123
|
+
end
|
124
|
+
|
125
|
+
rule node_getattr
|
126
|
+
n:node_name '.' i:identifier <NodeGetAttr>
|
127
|
+
end
|
128
|
+
|
129
|
+
rule model_fn_getattr
|
130
|
+
mfn:model_fn '.' i:identifier <ModelFnGetAttr>
|
131
|
+
end
|
132
|
+
|
133
|
+
rule getattr
|
134
|
+
i:identifier '.' ga:getattr <GetAttr>
|
135
|
+
/
|
136
|
+
identifier
|
137
|
+
end
|
138
|
+
|
139
|
+
rule number
|
140
|
+
decimal / integer
|
141
|
+
end
|
142
|
+
|
143
|
+
rule decimal
|
144
|
+
[0-9]+ '.' [0-9]+ <Literal>
|
145
|
+
end
|
146
|
+
|
147
|
+
rule integer
|
148
|
+
[0-9]+ <Literal>
|
149
|
+
end
|
150
|
+
|
151
|
+
rule identifier
|
152
|
+
[a-z] [a-zA-Z0-9_]* <Identifier>
|
153
|
+
end
|
154
|
+
|
155
|
+
rule boolean
|
156
|
+
'true' <Literal> / 'false' <Literal>
|
157
|
+
end
|
158
|
+
|
159
|
+
rule nil_val
|
160
|
+
'nil' <Literal>
|
161
|
+
end
|
162
|
+
|
163
|
+
rule sp
|
164
|
+
[\s]+
|
165
|
+
end
|
166
|
+
|
167
|
+
rule string
|
168
|
+
'"' ('\"' / !'"' .)* '"' <String>
|
169
|
+
/
|
170
|
+
"'" [^']* "'" <String>
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,359 @@
|
|
1
|
+
require 'delorean/base'
|
2
|
+
|
3
|
+
module Delorean
|
4
|
+
SIG = "_SIG"
|
5
|
+
MOD = "DELOREAN__"
|
6
|
+
POST = "__D"
|
7
|
+
|
8
|
+
class Engine
|
9
|
+
attr_reader :last_node, :module_name, :line_no, :comp_set, :pm, :m, :imports
|
10
|
+
|
11
|
+
def initialize(module_name)
|
12
|
+
# name of current module
|
13
|
+
@module_name = module_name
|
14
|
+
reset
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset
|
18
|
+
@m, @pm = nil, nil
|
19
|
+
@last_node, @node_attrs = nil, {}
|
20
|
+
@line_no, @multi_no = 0, nil
|
21
|
+
|
22
|
+
# set of comprehension vars
|
23
|
+
@comp_set = Set.new
|
24
|
+
|
25
|
+
# set of all params
|
26
|
+
@param_set = Set.new
|
27
|
+
|
28
|
+
@imports = {}
|
29
|
+
end
|
30
|
+
|
31
|
+
def curr_line
|
32
|
+
@multi_no || @line_no
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_import(sset, name, version)
|
36
|
+
err(ParseError, "No script set") unless sset
|
37
|
+
|
38
|
+
err(ParseError, "Module #{name} importing itself") if
|
39
|
+
name == module_name
|
40
|
+
|
41
|
+
begin
|
42
|
+
@imports[name] = sset.import(name, version)
|
43
|
+
rescue => exc
|
44
|
+
err(ImportError, exc.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
@pm.const_set("#{MOD}#{name}", @imports[name].pm)
|
48
|
+
end
|
49
|
+
|
50
|
+
def gen_import(name, version)
|
51
|
+
@imports.merge!(@imports[name].imports)
|
52
|
+
|
53
|
+
@m.const_set("#{MOD}#{name}", @imports[name].m)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_import_engine(name)
|
57
|
+
@imports[name] || err(ParseError, "#{name} not imported")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Check to see if node with given name is defined. flag tell the
|
61
|
+
# method about our expectation. flag=true means that we make sure
|
62
|
+
# that name is defined. flag=false is the opposite.
|
63
|
+
def parse_check_defined_node(name, flag)
|
64
|
+
isdef = @pm.constants.member? name.to_sym
|
65
|
+
|
66
|
+
if isdef != flag
|
67
|
+
isdef ? err(RedefinedError, "#{name} already defined") :
|
68
|
+
err(UndefinedError, "#{name} not defined yet")
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def super_name(pname, mname)
|
73
|
+
mname ? "#{MOD}#{mname}::#{pname}" : pname
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_check_defined_mod_node(pname, mname)
|
77
|
+
engine = mname ? get_import_engine(mname) : self
|
78
|
+
engine.parse_check_defined_node(pname, true)
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_define_node(name, pname, mname=nil)
|
82
|
+
parse_check_defined_node(name, false)
|
83
|
+
parse_check_defined_mod_node(pname, mname) if pname
|
84
|
+
|
85
|
+
sname = pname ? super_name(pname, mname) : 'Object'
|
86
|
+
|
87
|
+
code = "class #{name} < #{sname}; end"
|
88
|
+
@pm.module_eval(code)
|
89
|
+
|
90
|
+
# latest defined node
|
91
|
+
@last_node = name
|
92
|
+
|
93
|
+
# mapping of node name to list of attrs it defines
|
94
|
+
@node_attrs[name] = []
|
95
|
+
end
|
96
|
+
|
97
|
+
# Parse-time check to see if attr is available. If not, error is
|
98
|
+
# raised.
|
99
|
+
def parse_call_attr(node_name, attr_name)
|
100
|
+
return [] if comp_set.member?(attr_name)
|
101
|
+
|
102
|
+
# get the class associated with node
|
103
|
+
klass = @pm.module_eval(node_name)
|
104
|
+
|
105
|
+
# puts attr_name, "#{attr_name}#{POST}".to_sym, klass.methods.inspect
|
106
|
+
|
107
|
+
begin
|
108
|
+
klass.send("#{attr_name}#{POST}".to_sym, [])
|
109
|
+
rescue NoMethodError
|
110
|
+
err(UndefinedError, "'#{attr_name}' not defined in #{node_name}")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Parse-time check to see if attr is available on current node.
|
115
|
+
def parse_call_last_node_attr(attr_name)
|
116
|
+
err(ParseError, "Not inside a node") unless @last_node
|
117
|
+
parse_call_attr(@last_node, attr_name)
|
118
|
+
end
|
119
|
+
|
120
|
+
def parse_define_var(var_name)
|
121
|
+
err(RedefinedError,
|
122
|
+
"List comprehension can't redefine variable '#{var_name}'") if
|
123
|
+
comp_set.member? var_name
|
124
|
+
|
125
|
+
comp_set.add var_name
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_undef_var(var_name)
|
129
|
+
err(ParseError, "internal error") unless comp_set.member? var_name
|
130
|
+
comp_set.delete var_name
|
131
|
+
end
|
132
|
+
|
133
|
+
# parse-time attr definition
|
134
|
+
def parse_define_attr(name, spec)
|
135
|
+
err(ParseError, "Can't define '#{name}' outside a node") unless
|
136
|
+
@last_node
|
137
|
+
|
138
|
+
err(RedefinedError, "Can't redefine '#{name}' in node #{@last_node}") if
|
139
|
+
@node_attrs[@last_node].member? name
|
140
|
+
|
141
|
+
@node_attrs[@last_node] << name
|
142
|
+
|
143
|
+
checks = spec.map { |a|
|
144
|
+
n = a.index('.') ? a : (@last_node + "." + a)
|
145
|
+
"_x.member?('#{n}') ? raise('#{n}') : #{a}#{POST}(_x + ['#{n}'])"
|
146
|
+
}.join(';')
|
147
|
+
|
148
|
+
code = "class #{@last_node}; def self.#{name}#{POST}(_x); #{checks}; end; end"
|
149
|
+
|
150
|
+
# pp code
|
151
|
+
|
152
|
+
@pm.module_eval(code)
|
153
|
+
|
154
|
+
begin
|
155
|
+
parse_call_attr(@last_node, name)
|
156
|
+
rescue RuntimeError
|
157
|
+
err(RecursionError, "'#{name}' is recursive")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def parse_define_param(name, spec)
|
162
|
+
parse_define_attr(name, spec)
|
163
|
+
@param_set.add(name)
|
164
|
+
end
|
165
|
+
|
166
|
+
def parse_class(class_name)
|
167
|
+
begin
|
168
|
+
# need the runtime module here (@m) since we need to
|
169
|
+
# introspect methods/attrs.
|
170
|
+
klass = @m.module_eval(class_name)
|
171
|
+
rescue NoMethodError, NameError
|
172
|
+
err(UndefinedError, "Can't find class: #{class_name}")
|
173
|
+
end
|
174
|
+
|
175
|
+
err(UndefinedError, "Access to non-class: #{class_name}") unless
|
176
|
+
klass.instance_of?(Class)
|
177
|
+
|
178
|
+
klass
|
179
|
+
end
|
180
|
+
|
181
|
+
def err(exc, msg)
|
182
|
+
raise exc.new(msg, @module_name, curr_line)
|
183
|
+
end
|
184
|
+
|
185
|
+
def parse_check_call_fn(fn, argcount, class_name=nil)
|
186
|
+
klass = class_name ? parse_class(class_name) : (@m::BaseClass)
|
187
|
+
|
188
|
+
err(UndefinedFunctionError, "Function #{fn} not found") unless
|
189
|
+
klass.methods.member? fn.to_sym
|
190
|
+
|
191
|
+
# signature methods must be named FUNCTION_NAME_SIG
|
192
|
+
sig = "#{fn}#{SIG}".upcase.to_sym
|
193
|
+
|
194
|
+
err(UndefinedFunctionError, "Signature #{sig} not found") unless
|
195
|
+
klass.constants.member? sig
|
196
|
+
|
197
|
+
min, max = klass.const_get(sig)
|
198
|
+
|
199
|
+
err(BadCallError, "Too many args to #{fn} (#{argcount} > #{max})") if
|
200
|
+
argcount > max
|
201
|
+
|
202
|
+
err(BadCallError, "Too few args to #{fn} (#{argcount} < #{min})") if
|
203
|
+
argcount < min
|
204
|
+
end
|
205
|
+
|
206
|
+
def parser
|
207
|
+
@@parser ||= DeloreanParser.new
|
208
|
+
end
|
209
|
+
|
210
|
+
def generate(t, sset=nil)
|
211
|
+
t.check(self, sset)
|
212
|
+
|
213
|
+
# generate ruby code
|
214
|
+
gen = t.rewrite(self)
|
215
|
+
|
216
|
+
# puts gen
|
217
|
+
|
218
|
+
begin
|
219
|
+
# evaluate generated code in @m
|
220
|
+
@m.module_eval(gen, "#{MOD}#{module_name}", curr_line)
|
221
|
+
rescue => exc
|
222
|
+
# bad ruby code generated, shoudn't happen
|
223
|
+
err(ParseError, "codegen error: " + exc.message)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def parse(source, sset=nil)
|
228
|
+
raise "can't call parse again without reset" if @pm
|
229
|
+
|
230
|
+
# @m module is used at runtime for code evaluation. @pm module
|
231
|
+
# is only used during parsing to check for errors.
|
232
|
+
@m, @pm = BaseModule.clone, Module.new
|
233
|
+
|
234
|
+
multi_line, @multi_no = nil, nil
|
235
|
+
|
236
|
+
source.each_line do |line|
|
237
|
+
@line_no += 1
|
238
|
+
|
239
|
+
# skip comments
|
240
|
+
next if line.match(/^\s*\#/)
|
241
|
+
|
242
|
+
# remove trailing blanks
|
243
|
+
line.rstrip!
|
244
|
+
|
245
|
+
next if line.length == 0
|
246
|
+
|
247
|
+
if multi_line
|
248
|
+
# Inside a multiline and next line doesn't look like a
|
249
|
+
# continuation => syntax error.
|
250
|
+
err(ParseError, "syntax error") unless line =~ /^\s+/
|
251
|
+
|
252
|
+
multi_line += line
|
253
|
+
t = parser.parse(multi_line)
|
254
|
+
|
255
|
+
if t
|
256
|
+
multi_line, @multi_no = nil, nil
|
257
|
+
generate(t, sset)
|
258
|
+
end
|
259
|
+
|
260
|
+
else
|
261
|
+
t = parser.parse(line)
|
262
|
+
|
263
|
+
if !t
|
264
|
+
err(ParseError, "syntax error") unless line =~ /^\s+/
|
265
|
+
|
266
|
+
multi_line = line
|
267
|
+
@multi_no = @line_no
|
268
|
+
else
|
269
|
+
generate(t, sset)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# left over multi_line
|
275
|
+
err(ParseError, "syntax error") if multi_line
|
276
|
+
end
|
277
|
+
|
278
|
+
######################################################################
|
279
|
+
# Script development/testing
|
280
|
+
######################################################################
|
281
|
+
|
282
|
+
# enumerate all nodes
|
283
|
+
def enumerate_nodes
|
284
|
+
n = SortedSet.new
|
285
|
+
@node_attrs.keys.each {|k| n.add(k) }
|
286
|
+
n
|
287
|
+
end
|
288
|
+
|
289
|
+
# enumerate qualified list of all attrs
|
290
|
+
def enumerate_attrs
|
291
|
+
enumerate_attrs_by_node(nil)
|
292
|
+
end
|
293
|
+
|
294
|
+
# enumerate qualified list of attrs by node (or all if nil is passed in)
|
295
|
+
def enumerate_attrs_by_node(node)
|
296
|
+
@node_attrs.keys.inject({}) { |h, n|
|
297
|
+
klass = @m.module_eval(n)
|
298
|
+
h[n] = klass.methods.map(&:to_s).select {|x| x.end_with?(POST)}.map {|x|
|
299
|
+
x.sub(/#{POST}$/, '')
|
300
|
+
} if node == n || node.nil?
|
301
|
+
h
|
302
|
+
}
|
303
|
+
end
|
304
|
+
|
305
|
+
# enumerate all params
|
306
|
+
def enumerate_params
|
307
|
+
@param_set
|
308
|
+
end
|
309
|
+
|
310
|
+
# enumerate params by a single node
|
311
|
+
def enumerate_params_by_node(node)
|
312
|
+
attrs = enumerate_attrs_by_node(node)
|
313
|
+
ps = Set.new
|
314
|
+
attrs.each_value {|v| v.map {|p| ps.add(p) if @param_set.include?(p.to_s)}}
|
315
|
+
ps
|
316
|
+
end
|
317
|
+
|
318
|
+
######################################################################
|
319
|
+
# Runtime
|
320
|
+
######################################################################
|
321
|
+
|
322
|
+
def evaluate(node, attr, params={})
|
323
|
+
evaluate_attrs(node, [attr], params)[0]
|
324
|
+
end
|
325
|
+
|
326
|
+
def evaluate_attrs(node, attrs, params={})
|
327
|
+
raise "bad node '#{node}'" unless node =~ /^[A-Z][a-zA-Z0-9_]*$/
|
328
|
+
|
329
|
+
begin
|
330
|
+
klass = @m.module_eval(node)
|
331
|
+
rescue NameError
|
332
|
+
err(UndefinedNodeError, "node #{node} is undefined")
|
333
|
+
end
|
334
|
+
|
335
|
+
params[:_engine] = self
|
336
|
+
|
337
|
+
attrs.map {|attr|
|
338
|
+
raise "bad attribute '#{attr}'" unless attr =~ /^[a-z][A-Za-z0-9_]*$/
|
339
|
+
klass.send("#{attr}#{POST}".to_sym, params)
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
# FIXME: should be renamed to grok_runtime_exception so as to not
|
344
|
+
# be confused with other parse_* calls which occur at parse time.
|
345
|
+
def parse_runtime_exception(exc)
|
346
|
+
# parse out the delorean-related backtrace records
|
347
|
+
bt = exc.backtrace.map{ |x|
|
348
|
+
x.match(/^#{MOD}(.+?):(\d+)(|:in `(.+)')$/);
|
349
|
+
$1 && [$1, $2.to_i, $4.sub(/#{POST}$/, '')]
|
350
|
+
}.reject(&:!)
|
351
|
+
|
352
|
+
[exc.message, bt]
|
353
|
+
end
|
354
|
+
|
355
|
+
######################################################################
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
end
|