fable 0.5.0

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +30 -0
  3. data/.gitignore +57 -0
  4. data/.ruby-version +1 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +30 -0
  8. data/LICENSE +21 -0
  9. data/README.md +2 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/bin/test +8 -0
  14. data/fable.gemspec +34 -0
  15. data/fable.sublime-project +8 -0
  16. data/lib/fable.rb +49 -0
  17. data/lib/fable/call_stack.rb +351 -0
  18. data/lib/fable/choice.rb +31 -0
  19. data/lib/fable/choice_point.rb +65 -0
  20. data/lib/fable/container.rb +218 -0
  21. data/lib/fable/control_command.rb +156 -0
  22. data/lib/fable/debug_metadata.rb +13 -0
  23. data/lib/fable/divert.rb +100 -0
  24. data/lib/fable/glue.rb +7 -0
  25. data/lib/fable/ink_list.rb +425 -0
  26. data/lib/fable/list_definition.rb +44 -0
  27. data/lib/fable/list_definitions_origin.rb +35 -0
  28. data/lib/fable/native_function_call.rb +324 -0
  29. data/lib/fable/native_function_operations.rb +149 -0
  30. data/lib/fable/observer.rb +205 -0
  31. data/lib/fable/path.rb +186 -0
  32. data/lib/fable/pointer.rb +42 -0
  33. data/lib/fable/profiler.rb +287 -0
  34. data/lib/fable/push_pop_type.rb +11 -0
  35. data/lib/fable/runtime_object.rb +159 -0
  36. data/lib/fable/search_result.rb +20 -0
  37. data/lib/fable/serializer.rb +560 -0
  38. data/lib/fable/state_patch.rb +47 -0
  39. data/lib/fable/story.rb +1447 -0
  40. data/lib/fable/story_state.rb +915 -0
  41. data/lib/fable/tag.rb +14 -0
  42. data/lib/fable/value.rb +334 -0
  43. data/lib/fable/variable_assignment.rb +20 -0
  44. data/lib/fable/variable_reference.rb +38 -0
  45. data/lib/fable/variables_state.rb +327 -0
  46. data/lib/fable/version.rb +3 -0
  47. data/lib/fable/void.rb +4 -0
  48. data/zork_mode.rb +23 -0
  49. metadata +149 -0
@@ -0,0 +1,13 @@
1
+ module Fable
2
+ class DebugMetadata
3
+ attr_accessor :start_line_number, :end_line_number, :file_name, :source_name
4
+
5
+ def to_s
6
+ if !file_name.nil?
7
+ "line #{start_line_number} of #{file_name}"
8
+ else
9
+ "line #{start_line_number}"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,100 @@
1
+ module Fable
2
+ class Divert < RuntimeObject
3
+ attr_accessor :target_path, :target_pointer,
4
+ :variable_divert_name, :pushes_to_stack, :stack_push_type, :is_external,
5
+ :external_arguments, :is_conditional
6
+
7
+ alias_method :pushes_to_stack?, :pushes_to_stack
8
+ alias_method :is_external?, :is_external
9
+ alias_method :is_conditional?, :is_conditional
10
+
11
+ def target_path
12
+ # Resolve any relative paths to global paths as we come across them
13
+ if !@target_path.nil? && @target_path.relative?
14
+ target_object = target_pointer.resolve!
15
+ if !target_pointer.nil?
16
+ @target_path = target_object.path
17
+ end
18
+ end
19
+
20
+ return @target_path
21
+ end
22
+
23
+ def target_path=(value)
24
+ @target_path = value
25
+ @target_pointer = Pointer.null_pointer
26
+ end
27
+
28
+ def target_pointer
29
+ if @target_pointer.null_pointer?
30
+ target_object = resolve_path(@target_path).object
31
+ if @target_path.components.last.is_index?
32
+ @target_pointer.container = target_object.parent
33
+ @target_pointer.index = @target_path.components.last.index
34
+ else
35
+ @target_pointer = Pointer.start_of(target_object)
36
+ end
37
+ end
38
+
39
+ return @target_pointer
40
+ end
41
+
42
+ def target_path_string
43
+ return nil if target_path.nil?
44
+
45
+ return compact_path_string(target_path)
46
+ end
47
+
48
+ def target_path_string=(value)
49
+ if value.nil?
50
+ self.target_path = nil
51
+ else
52
+ self.target_path = Path.new(value)
53
+ end
54
+ end
55
+
56
+ def ==(other_divert)
57
+ if other_divert.is_a?(Divert) && !other_divert.nil?
58
+ if self.has_variable_target? == other_divert.has_variable_target?
59
+ if self.has_variable_target?
60
+ return self.variable_divert_name == other_divert.variable_divert_name
61
+ else
62
+ return self.target_path == other_divert.target_path
63
+ end
64
+ end
65
+ end
66
+ return false
67
+ end
68
+
69
+ def has_variable_target?
70
+ !variable_divert_name.nil?
71
+ end
72
+
73
+ def to_s
74
+ if has_variable_target?
75
+ return "Divert(variable: #{variable_divert_name})"
76
+ elsif target_path.nil?
77
+ return "Divert(null)"
78
+ else
79
+ result = ""
80
+
81
+ target_string = target_path.to_s
82
+ target_line_number = debug_line_number_of_path(target_path)
83
+ if !target_line_number.nil?
84
+ target_string = "line #{target_line_number}"
85
+ end
86
+
87
+ push_type = ""
88
+ if pushes_to_stack?
89
+ if stack_push_type == :FUNCTION
90
+ push_type = " function"
91
+ else
92
+ push_type = " tunnel"
93
+ end
94
+ end
95
+
96
+ "Divert#{'?' if is_conditional?}#{push_type} -> #{target_path_string} (#{target_string})"
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,7 @@
1
+ module Fable
2
+ class Glue < RuntimeObject
3
+ def to_s
4
+ return "Glue"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,425 @@
1
+ module Fable
2
+ # The InkList is the underlying type that's used to store an instance of a
3
+ # list in ink. It's not used for the *definition* of the list, but for a list
4
+ # value that's stored in a variable.
5
+ class InkList
6
+ # The underlying type for a list item in ink. It stores the original list definition
7
+ # name as well as the item name, but without the value of the item. When the value is
8
+ # stored, it's stored in a Dictionary of InkListItem and an integer.
9
+ class InkListItem
10
+ #The name of the list where the item was originally defined.
11
+ attr_accessor :origin_name
12
+
13
+ # The main name of the item as defined in ink
14
+ attr_accessor :item_name
15
+
16
+ def initialize(options)
17
+ # Create an item from a dot-separated string of the form
18
+ # `list_definition_name.list_item_name`
19
+ if options.has_key?(:full_name)
20
+ name_parts = options[:full_name].split(".")
21
+ self.origin_name = name_parts[0]
22
+ self.item_name = name_parts[1]
23
+ else
24
+ self.origin_name = options[:origin_name]
25
+ self.item_name = options[:item_name]
26
+ end
27
+ end
28
+
29
+ def self.Null
30
+ return self.new(origin_name: nil, item_name: nil)
31
+ end
32
+
33
+ def null_item?
34
+ origin_name.nil? && item_name.null?
35
+ end
36
+
37
+ # Get the full dot-separated name of the item, in the form of
38
+ # `list_definition_name.list_item_name`
39
+ def full_name
40
+ return "#{origin_name.nil? ? "?" : origin_name}.#{item_name}"
41
+ end
42
+
43
+ alias_method :to_s, :full_name
44
+
45
+ def equal?(other_object)
46
+ return false if !other_object.is_a?(InkListItem)
47
+
48
+ return (
49
+ other_object.item_name == self.item_name &&
50
+ other_object.origin_name == self.origin_name
51
+ )
52
+ end
53
+ end
54
+
55
+
56
+ attr_accessor :list, :origins
57
+
58
+ # Create a new empty ink list
59
+ def initialize
60
+ self.list = {}
61
+ self.origins = []
62
+ end
63
+
64
+ def self.copy_list(other_list)
65
+ ink_list = self.new
66
+ other_list_data = other_list.list.map{|item, int_value| [InkList::InkListItem.new(full_name: item.full_name), int_value] }
67
+ ink_list.list = Hash[other_list_data]
68
+ ink_list.origins = other_list.origins
69
+ ink_list.set_initial_origin_names(other_list.origin_names)
70
+
71
+ return ink_list
72
+ end
73
+
74
+ def self.new_with_single_item(item, value)
75
+ ink_list = self.new
76
+ ink_list.list[item] = value
77
+
78
+ return ink_list
79
+ end
80
+
81
+ # Create a new empty ink list that's intended to hold items from a
82
+ # particular origin list definition. The origin story is needed in order
83
+ # to be able to look up that definition
84
+ def self.new_for_origin_definition_and_story(single_origin_list_name, origin_story)
85
+ ink_list = self.new
86
+ ink_list.set_initial_origin_name(single_origin_list_name)
87
+
88
+ list_definition = origin_story.list_definitions.find_list(single_origin_list_name)
89
+ if list_definition.nil?
90
+ raise Error("InkList origin could not be found in story when constructing new list: #{single_origin_list_name}")
91
+ else
92
+ ink_list.origins = [list_definition]
93
+ end
94
+
95
+ return ink_list
96
+ end
97
+
98
+ # Converts a string to an ink list, and returns for use in the Story
99
+ def self.from_string(my_list_item, origin_story)
100
+ list_value = origin_story.list_definitions[my_list_item]
101
+ if list_value.nil?
102
+ raise Error("Could not find the InkListItem from the string '#{my_list_item}' to create an InkList because it doesn't exist in the original list definition in ink.")
103
+ else
104
+ ink_list = self.new
105
+ ink_list.list = list_value
106
+ end
107
+
108
+ return ink_list
109
+ end
110
+
111
+ def item_with_value(value)
112
+ list.key(value)
113
+ end
114
+
115
+ def add_item(item_or_item_name)
116
+ if item_or_item_name.is_a?(InkListItem)
117
+ add_ink_list_item(item_or_item_name)
118
+ else
119
+ add_item_from_string(item_or_item_name)
120
+ end
121
+ end
122
+
123
+ # Adds the given item to the ink list. Note that the item must come from a list
124
+ # definition that is already "known" to this list, so that teh item's value can be
125
+ # looked up. By "known", we mean that it alreadyu has items in it from that source,
126
+ # or did at one point. It can't be a completely fresh empty list, or a list that only
127
+ # contains items from a different list definition
128
+ def add_ink_list_item(item)
129
+ if item.origin_name.nil?
130
+ return add_item_from_string(item.item_name)
131
+ end
132
+
133
+ origins.each do |origin|
134
+ if origin.name == item.origin_name
135
+ integer_value = origin.items[item]
136
+
137
+ if integer_value.nil?
138
+ raise Error("Could not add the item '#{item.item_name}' to this list because it doesn't exist in the original list definition in ink.")
139
+ else
140
+ self.list[item] = integer_value
141
+ end
142
+ end
143
+ end
144
+
145
+ raise Error("Failed to add item to list because the item was from a new list definition that wasn't previously known to this list. Only items from previously known lists can be used, so that the int value can be found.")
146
+ end
147
+
148
+ # Adds the given item to the ink list, attempting to find the origin list definition
149
+ # that it belongs to. The item must therefore come from a list definition that is already
150
+ # "known" to this list, so that the item's value can be looked up. By "known", we mean that it
151
+ # already has items in it from that source, or it did at one point. It can't be a completely
152
+ # fresh empty list, or a list that only contains items from a different list definition
153
+ def add_item_from_string(item_name)
154
+ found_list_definition = nil
155
+ origins.each do |origin|
156
+ if origin.items.any?{|item, int_value| item.name == item_name }
157
+ if found_list_definition.nil?
158
+ found_list_definition = origin
159
+ else
160
+ raise Error("Could not add the item '#{item_name}' to this list because it could come from either '#{origin.name}' or '#{found_list_definition.name}'")
161
+ end
162
+ end
163
+ end
164
+
165
+ if found_list_definition.nil?
166
+ raise Error("Could not add the item '#{item_name}' to this list because it isn't known to any list definitions previously associated with this list.")
167
+ end
168
+
169
+ item = InkListItem.new(origin_name: found_list_definition.name, item_name: item_name)
170
+ item_value = found_list_definition.value_for_item(item)
171
+ self.items[item] = item_value
172
+ end
173
+
174
+ def include_item_named?(item_name)
175
+ list.any?{|item, int_value| item.name == item_name}
176
+ end
177
+
178
+ # Story has to set this so that the value knows its origin, necessary for
179
+ # certain operations (eg: iteracting with ints). Only the story has access
180
+ # to the full set of lists, so that the origin can be resolved from the
181
+ # origin_list_name
182
+ def origin_of_max_item
183
+ return nil if origins.nil?
184
+ max_origin_name = max_item.origin_name
185
+ origins.find{|origin| origin.name == max_origin_name }
186
+ end
187
+
188
+ # Origin name needs to be serialized when content is empty, assuming
189
+ # a name is available, for list definitions with variable that is currently
190
+ # empty
191
+ def origin_names
192
+ if self.list.any?
193
+ @origin_names = self.list.map{|item, int_value| item.origin_name }.compact.uniq
194
+ end
195
+
196
+ return @origin_names
197
+ end
198
+
199
+ def set_initial_origin_name(initial_origin_name)
200
+ @origin_names = [initial_origin_name]
201
+ end
202
+
203
+ def set_initial_origin_names(initial_origin_names)
204
+ @origin_names = initial_origin_names
205
+ end
206
+
207
+ def count
208
+ list.size
209
+ end
210
+
211
+ # Get the maximum item in the list, equivalent to calling LIST_MAX(list) in ink.
212
+ def max_item
213
+ list.max do |a, b|
214
+ return -1 if a[0].null_item?
215
+ return 1 if b[0].null_item?
216
+ a[1] <=> b[1]
217
+ end
218
+ end
219
+
220
+ # Get the minimum item in the list, equivalent to calling LIST_MIN(list) in ink.
221
+ def min_item
222
+ list.min do |a, b|
223
+ return -1 if a[0].null_item?
224
+ return 1 if b[0].null_item?
225
+ a[1] <=> b[1]
226
+ end
227
+ end
228
+
229
+ # The inverse of the list, equivalent to colling LIST_INVERSE(list) in ink.
230
+ def inverse
231
+ new_list = self.class.new
232
+ origins.each do |origin|
233
+ origin.items.each do |item, int_value|
234
+ if self.list.none?{|item_to_search, other_value| item_to_search.equal?(item)}
235
+ new_list.list[item] = int_value
236
+ end
237
+ end
238
+ end
239
+
240
+ new_list
241
+ end
242
+
243
+ # The list of all items from the original list definition, equivalent to
244
+ # calling LIST_ALL(list) in ink.
245
+ def all
246
+ new_list = self.class.new
247
+ origins.each do |origin|
248
+ origin.items.each do |item, int_value|
249
+ new_list.list[item] = int_value
250
+ end
251
+ end
252
+
253
+ new_list
254
+ end
255
+
256
+ # Return a new list that is a combination of the current list and
257
+ # one that's passed in. The equivalent of calling (list1 + list2) in ink.
258
+ def +(other_list)
259
+ union_list = self.class.copy_list(self)
260
+ other_list.list.each do |item, int_value|
261
+ union_list.list[item] = int_value
262
+ end
263
+
264
+ return union_list
265
+ end
266
+
267
+ # Return a new list that is the intersection of the current list and
268
+ # one that's passed in. The equivalent of calling (list1 ^ list2) in ink.
269
+ def &(other_list)
270
+ intersection_list = self.class.new
271
+ self.list.each do |item, int_value|
272
+ if other_list.list.any?{|other_item, other_value| other_item.equal?(item)}
273
+ intersection_list.list[item] = int_value
274
+ end
275
+ end
276
+
277
+ return intersection_list
278
+ end
279
+
280
+ # Returns a new list that's the same as the current one, except with the
281
+ # given items removed that are in the passed-in list. Equivalent to calling
282
+ # (list1 - list2) in ink.
283
+ def -(other_list)
284
+ without_list = self.class.copy_list(self)
285
+ other_list.list.each do |item, int_value|
286
+ without_list.list.delete_if{ |without_list_item, other_value| without_list_item.equal?(item)}
287
+ end
288
+
289
+ return without_list
290
+ end
291
+
292
+ # Returns true if the current list contains all the items that are in the
293
+ # list that is passed in. Equivalent to calling (list1 ? list2) in ink.
294
+ def contains?(other_list)
295
+ other_list.list.all?{|other_item, other_value| self.list.any?{|item, value| other_item.equal?(item) }}
296
+ end
297
+
298
+ # Returns true if all the item values in the current list are greater than
299
+ # all the item values in the passed-in list. Equivalent to calling
300
+ # (list1 > list2) in ink.
301
+ def >(other_list)
302
+ return false if self.list.empty?
303
+ return true if other_list.list.empty?
304
+
305
+ return self.min_item[1] > other_list.max_item[1]
306
+ end
307
+
308
+ # Returns true if the item values in the current list overlap, or are all
309
+ # greater than the item values in the passed-in list. None of the item values
310
+ # in the current list must fall below the item values in the passed-in list
311
+ # Equivalent to (list1 >= list2) in ink, or LIST_MIN(list1) >= LIST_MIN(list2) &&
312
+ # LIST_MAX(list1) >= LIST_MAX(list2)
313
+ def >=(other_list)
314
+ return false if self.list.empty?
315
+ return true if other_list.list.empty?
316
+
317
+ return (
318
+ self.min_item[1] >= other_list.min_item[1] &&
319
+ self.max_item[1] >= other_list.max_item[1]
320
+ )
321
+ end
322
+
323
+ # Returns true if all the item values in the current list are less than all the
324
+ # item values in the passed-in list. Equivalent to calling (list1 < list2 in ink)
325
+ def <(other_list)
326
+ return false if other_list.list.empty?
327
+ return true if self.list.empty?
328
+
329
+ return self.max_item[1] < other_list.min_item[1]
330
+ end
331
+
332
+ # Returns true if the item values in the current list overlap, or are all less than
333
+ # the item values in the passed in list. None of the item values in the current list
334
+ # must go above the item values in the passed in list. Equivalent to (list1 <= list2)
335
+ # in ink, or LIST_MAX(list1) <= LIST_MAX(list2) && LIST_MIN(list1) <= LIST_MIN(list2)
336
+ def <=(other_list)
337
+ return false if other_list.list.empty?
338
+ return true if self.list.empty?
339
+
340
+ return (
341
+ self.max_item[1] <= other_list.max_item[1] &&
342
+ self.min_item[1] <= other_list.min_item[1]
343
+ )
344
+ end
345
+
346
+ def max_as_list
347
+ if self.list.empty?
348
+ return self.class.new
349
+ else
350
+ return self.class.new_with_single_item(max_item)
351
+ end
352
+ end
353
+
354
+ def min_as_list
355
+ if self.list.empty?
356
+ return self.class.new
357
+ else
358
+ return self.class.new_with_single_item(min_item)
359
+ end
360
+ end
361
+
362
+ # Returns a sublist with the elements given in the minimum & maximum
363
+ # bounds. The bounds can either be ints, which are indicies into the entire (sorted)
364
+ # list, or they can be InkLists themsevles. These are intended to be single-item lists,
365
+ # so you can specify the upper & lower bounds. If you pass in multi-item lists, it'll use
366
+ # the minimum and maximum items in those lists, respectively.
367
+ def list_with_subrange(min_bound, max_bound)
368
+ return self.class.new if self.list.empty?
369
+
370
+ ordered = self.ordered_items
371
+
372
+ min_value = 0
373
+ max_value = Float::INFINITY
374
+
375
+ if min_bound.is_a?(Numeric)
376
+ min_value = min_bound
377
+ elsif min_bound.is_a?(InkList) && !min_bound.list.empty?
378
+ min_value = min_bound.min_item[1]
379
+ end
380
+
381
+ if max_bound.is_a?(Numeric)
382
+ max_value = max_bound
383
+ elsif max_bound.is_a?(InkList) && !max_bound.list.empty?
384
+ max_value = max_bound.max_item[1]
385
+ end
386
+
387
+ sublist = self.class.new
388
+ sublist.set_initial_origin_names(origin_names)
389
+ ordered.each do |item, int_value|
390
+ if int_value >= min_value && int_value <= max_value
391
+ sublist.list[item] = int_value
392
+ end
393
+ end
394
+
395
+ sublist
396
+ end
397
+
398
+ # Returns true if the passed object is also an ink list that contains the
399
+ # same items as the current list, false otherwise.
400
+ def ==(other_list)
401
+ return false if !other_list.is_a?(InkList)
402
+ return false if other_list.list.size != self.list.size
403
+
404
+ return self.list.all?{|item, int_value| other_list.list.has_key?(item) }
405
+ end
406
+
407
+ def ordered_items
408
+ self.list.sort do |a, b|
409
+ # ensure consistent ordering of mixed lists
410
+ if(a[1] == b[1])
411
+ a[0].origin_name <=> b[0].origin_name
412
+ else
413
+ a[1] <=> b[1]
414
+ end
415
+ end
416
+ end
417
+
418
+ # Returns a string in the form "a, b, c" with the names of the items in the
419
+ # list, without the origin list definition names. Equivalent to writing
420
+ # {list} in ink
421
+ def to_s
422
+ ordered_items.map{|item, int_value| item.item_name }.join(", ")
423
+ end
424
+ end
425
+ end