rcas 0.2.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 (117) hide show
  1. checksums.yaml +7 -0
  2. data/CITATION.cff +17 -0
  3. data/DESIGN.md +783 -0
  4. data/LICENSE +21 -0
  5. data/MANUAL.md +6265 -0
  6. data/README.md +267 -0
  7. data/bin/rcas +9 -0
  8. data/bin/rcas-app +9 -0
  9. data/bin/rcas-chat +9 -0
  10. data/lib/rcas/algebraic.rb +481 -0
  11. data/lib/rcas/analysis.rb +966 -0
  12. data/lib/rcas/app/launcher.rb +203 -0
  13. data/lib/rcas/app/public/app.css +402 -0
  14. data/lib/rcas/app/public/app.js +449 -0
  15. data/lib/rcas/app/public/index.html +46 -0
  16. data/lib/rcas/app/server.rb +220 -0
  17. data/lib/rcas/app/window.rb +94 -0
  18. data/lib/rcas/app/worksheet.rb +290 -0
  19. data/lib/rcas/app.rb +168 -0
  20. data/lib/rcas/background.rb +758 -0
  21. data/lib/rcas/chat/assistant.rb +199 -0
  22. data/lib/rcas/chat/picker.rb +164 -0
  23. data/lib/rcas/chat/repl.rb +583 -0
  24. data/lib/rcas/chat/session.rb +137 -0
  25. data/lib/rcas/chat/settings.rb +71 -0
  26. data/lib/rcas/chat/style.rb +30 -0
  27. data/lib/rcas/chat/tool.rb +53 -0
  28. data/lib/rcas/chat/ui.rb +316 -0
  29. data/lib/rcas/chat/usage.rb +62 -0
  30. data/lib/rcas/chat/workspace.rb +132 -0
  31. data/lib/rcas/chat.rb +54 -0
  32. data/lib/rcas/coefficients.rb +170 -0
  33. data/lib/rcas/combinatorics.rb +274 -0
  34. data/lib/rcas/complex_parts.rb +160 -0
  35. data/lib/rcas/constants.rb +129 -0
  36. data/lib/rcas/core_ext.rb +35 -0
  37. data/lib/rcas/decide.rb +501 -0
  38. data/lib/rcas/decompositions.rb +241 -0
  39. data/lib/rcas/differentiate.rb +144 -0
  40. data/lib/rcas/discussion.rb +558 -0
  41. data/lib/rcas/distributions.rb +980 -0
  42. data/lib/rcas/dixon.rb +95 -0
  43. data/lib/rcas/docs.rb +321 -0
  44. data/lib/rcas/domains.rb +728 -0
  45. data/lib/rcas/expand.rb +174 -0
  46. data/lib/rcas/expression.rb +613 -0
  47. data/lib/rcas/factor.rb +605 -0
  48. data/lib/rcas/finite_field.rb +577 -0
  49. data/lib/rcas/fourier.rb +118 -0
  50. data/lib/rcas/fps.rb +678 -0
  51. data/lib/rcas/fraction.rb +126 -0
  52. data/lib/rcas/functions.rb +1136 -0
  53. data/lib/rcas/gcd.rb +112 -0
  54. data/lib/rcas/geometry.rb +266 -0
  55. data/lib/rcas/groebner.rb +162 -0
  56. data/lib/rcas/hold.rb +277 -0
  57. data/lib/rcas/hypothesis.rb +364 -0
  58. data/lib/rcas/inequalities.rb +689 -0
  59. data/lib/rcas/integral_functions.rb +260 -0
  60. data/lib/rcas/integrate.rb +1589 -0
  61. data/lib/rcas/integrate_substitutions.rb +434 -0
  62. data/lib/rcas/interpolate.rb +40 -0
  63. data/lib/rcas/irb.rb +146 -0
  64. data/lib/rcas/laplace.rb +159 -0
  65. data/lib/rcas/latex.rb +556 -0
  66. data/lib/rcas/lattice.rb +172 -0
  67. data/lib/rcas/linear_algebra.rb +117 -0
  68. data/lib/rcas/linear_program.rb +416 -0
  69. data/lib/rcas/lint.rb +79 -0
  70. data/lib/rcas/matrix.rb +531 -0
  71. data/lib/rcas/matrix_multiply.rb +202 -0
  72. data/lib/rcas/multimodular.rb +286 -0
  73. data/lib/rcas/named_polynomials.rb +274 -0
  74. data/lib/rcas/number_theory.rb +443 -0
  75. data/lib/rcas/numerics.rb +825 -0
  76. data/lib/rcas/ode.rb +488 -0
  77. data/lib/rcas/openmath/objects.rb +364 -0
  78. data/lib/rcas/openmath/phrasebook.rb +551 -0
  79. data/lib/rcas/openmath/popcorn.rb +518 -0
  80. data/lib/rcas/openmath/xml.rb +309 -0
  81. data/lib/rcas/openmath.rb +49 -0
  82. data/lib/rcas/petkovsek.rb +165 -0
  83. data/lib/rcas/piecewise.rb +488 -0
  84. data/lib/rcas/plot.rb +763 -0
  85. data/lib/rcas/plot3d.rb +419 -0
  86. data/lib/rcas/poly_matrix.rb +318 -0
  87. data/lib/rcas/poly_recurrence.rb +117 -0
  88. data/lib/rcas/polynomial.rb +466 -0
  89. data/lib/rcas/precision.rb +925 -0
  90. data/lib/rcas/printer.rb +150 -0
  91. data/lib/rcas/product.rb +155 -0
  92. data/lib/rcas/q_difference.rb +296 -0
  93. data/lib/rcas/q_functions.rb +158 -0
  94. data/lib/rcas/q_summation.rb +308 -0
  95. data/lib/rcas/q_zeilberger.rb +199 -0
  96. data/lib/rcas/random.rb +506 -0
  97. data/lib/rcas/rational_function.rb +186 -0
  98. data/lib/rcas/recurrence.rb +323 -0
  99. data/lib/rcas/render.rb +431 -0
  100. data/lib/rcas/results.rb +192 -0
  101. data/lib/rcas/scalar.rb +219 -0
  102. data/lib/rcas/series.rb +726 -0
  103. data/lib/rcas/simplify.rb +649 -0
  104. data/lib/rcas/solve.rb +2002 -0
  105. data/lib/rcas/special.rb +163 -0
  106. data/lib/rcas/statistics.rb +175 -0
  107. data/lib/rcas/steps.rb +835 -0
  108. data/lib/rcas/summation.rb +532 -0
  109. data/lib/rcas/trig.rb +264 -0
  110. data/lib/rcas/van_hoeij.rb +241 -0
  111. data/lib/rcas/vector.rb +175 -0
  112. data/lib/rcas/vector_calculus.rb +411 -0
  113. data/lib/rcas/version.rb +5 -0
  114. data/lib/rcas/zeilberger.rb +358 -0
  115. data/lib/rcas.rb +91 -0
  116. data/package.json +8 -0
  117. metadata +206 -0
@@ -0,0 +1,518 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+
5
+ module RCAS
6
+ module OpenMath
7
+ # POPCORN [HR09]: a linear notation for OpenMath objects meant for people
8
+ # to read and write, where XML is meant for machines.
9
+ #
10
+ # RCAS.openmath(:x + 1).to_popcorn # => "$x + 1"
11
+ # RCAS.from_popcorn("$x^2 + 1") # => x**2 + 1, unevaluated
12
+ #
13
+ # A third encoding of the objects in objects.rb, beside XML - and the one
14
+ # a person types. Variables carry a $ so that a bare name can be short for
15
+ # a symbol (`sin` for transc1.sin), which is the trick the whole notation
16
+ # turns on.
17
+ #
18
+ # Two spellings of the same object: `render` sugars what it can, and
19
+ # `render(sugar: false)` writes every symbol out as cd.name, which is what
20
+ # Node#to_s uses - a beginner should see arith1.plus before they see +.
21
+ #
22
+ # plain: arith1.plus(arith1.power($x, 2), 1)
23
+ # sugared: $x^2 + 1
24
+ #
25
+ # The grammar is the one of the reference implementation
26
+ # (org.symcomp.openmath, Apache-2.0; this is an independent
27
+ # implementation from the published grammar, not a translation of it).
28
+ # Two details of it are easy to get wrong: ~ is relation2.approx, not
29
+ # relation1, and a minus sign in front of a literal is part of the
30
+ # literal - -17 is the integer -17, not unary_minus(17).
31
+ #
32
+ # Not supported: the typed-expression form `a::b`. It raises rather than
33
+ # guessing.
34
+ module Popcorn
35
+ # symbol => [text, precedence, association]. The levels are the ones of
36
+ # the published grammar; the associations are what keep the writing
37
+ # faithful, which the reference implementation's precedences do not:
38
+ # it gives plus 70 and minus 75, and then writes plus($a, minus($b, $c))
39
+ # as "$a + $b - $c", which reads back as minus(plus($a, $b), $c). Here
40
+ # the two share a level and the right operand is written one level
41
+ # tighter, so the tree that is read is the tree that was written.
42
+ INFIX = {
43
+ %w[prog1 block] => [";", 10, :flat],
44
+ %w[prog1 assign] => [":=", 20, :single],
45
+ %w[logic1 implies] => ["==>", 30, :single],
46
+ %w[logic1 equivalent] => ["<=>", 30, :single],
47
+ %w[logic1 or] => ["or", 40, :left],
48
+ %w[logic1 and] => ["and", 50, :left],
49
+ %w[relation1 eq] => ["=", 60, :single],
50
+ %w[relation1 lt] => ["<", 60, :single],
51
+ %w[relation1 leq] => ["<=", 60, :single],
52
+ %w[relation1 gt] => [">", 60, :single],
53
+ %w[relation1 geq] => [">=", 60, :single],
54
+ %w[relation1 neq] => ["!=", 60, :single],
55
+ %w[relation2 approx] => ["~", 60, :single],
56
+ %w[interval1 interval] => ["..", 70, :single],
57
+ %w[arith1 plus] => ["+", 80, :flat],
58
+ %w[arith1 minus] => ["-", 80, :left],
59
+ %w[arith1 times] => ["*", 90, :flat],
60
+ %w[arith1 divide] => ["/", 90, :left],
61
+ %w[arith1 power] => ["^", 100, :single],
62
+ %w[complex1 complex_cartesian] => ["|", 110, :single],
63
+ %w[nums1 rational] => ["//", 120, :single]
64
+ }.freeze
65
+
66
+ UNARY_MINUS = %w[arith1 unary_minus].freeze
67
+ UNARY_PRECEDENCE = 85
68
+ ATOM = 130
69
+
70
+ BRACKETS = { %w[list1 list] => %w{[ ]}, %w[set1 set] => %w[{ }] }.freeze
71
+
72
+ # The reference renderer spaces the additive operators and writes the
73
+ # multiplicative ones tight: a + b, but a*b and a^2. We follow it.
74
+ TIGHT = ["*", "/", "^", "|", "//"].freeze
75
+
76
+ KEYWORDS = %w[and or if then else endif while do endwhile].freeze
77
+
78
+ IDENTIFIER = /[A-Za-z_][A-Za-z0-9_]*/.freeze
79
+
80
+ # A bare name stands for a symbol. The reference implementation carries
81
+ # a table of some 570 of them; ours is derived from the phrasebook, so
82
+ # every symbol rcas knows may be written short and nothing else may.
83
+ # Names of our own content dictionary never sugar - rcas1.gamma is
84
+ # written out, because no one else would know what `gamma` meant.
85
+ def self.sugar_table
86
+ @sugar_table ||= begin
87
+ keys = Phrasebook::DECODE_APPLY.keys + Phrasebook::DECODE_CONST.keys +
88
+ [%w[fns1 lambda], %w[list1 list], %w[set1 set], UNARY_MINUS]
89
+ counts = Hash.new(0)
90
+ keys.each { |cd, name| counts[name] += 1 unless cd == RCAS_CD }
91
+ keys.reject { |cd, _| cd == RCAS_CD }
92
+ .select { |_, name| counts[name] == 1 }
93
+ .to_h { |cd, name| [name, [cd, name]] }
94
+ .freeze
95
+ end
96
+ end
97
+
98
+ def self.symbol_for(name) = sugar_table[name]
99
+ def self.sugar_for(key) = sugar_table[key.last] == key ? key.last : nil
100
+
101
+ module_function
102
+
103
+ # ---- writing -----------------------------------------------------------
104
+
105
+ def render(node, sugar: true)
106
+ node = node.object while node.is_a?(Root)
107
+ emit(node, 0, sugar)
108
+ end
109
+
110
+ def emit(node, prec, sugar)
111
+ text, own = emit_bare(node, sugar)
112
+ text = "#{atomic(text, own)}:#{node.id}" if node.id
113
+ own = ATOM if node.id
114
+ own < prec ? "(#{text})" : text
115
+ end
116
+
117
+ def atomic(text, own) = own < ATOM ? "(#{text})" : text
118
+
119
+ # The rendered text and the precedence it binds at.
120
+ def emit_bare(node, sugar)
121
+ case node
122
+ when Int then [node.value.to_s, node.value.negative? ? UNARY_PRECEDENCE : ATOM]
123
+ when Double then [float_text(node.value), node.value.negative? ? UNARY_PRECEDENCE : ATOM]
124
+ when Text then [string_text(node.value), ATOM]
125
+ when Bytes then ["%#{[node.value].pack('m0')}%", ATOM]
126
+ when Variable then ["$#{node.name}", ATOM]
127
+ when ContentSymbol then [symbol_text(node, sugar), ATOM]
128
+ when Application then application(node, sugar)
129
+ when Bind then [binding_text(node, sugar), ATOM]
130
+ when Attribution then [attribution_text(node, sugar), ATOM]
131
+ when Error then ["#{emit(node.symbol, ATOM, sugar)}!(#{list_text(node.args, sugar)})", ATOM]
132
+ when Reference then [reference_text(node), ATOM]
133
+ when Foreign then ["`#{node.content}`", ATOM]
134
+ else raise EncodeError, "no POPCORN notation for #{node.class}"
135
+ end
136
+ end
137
+
138
+ def symbol_text(node, sugar)
139
+ short = sugar && node.official? ? Popcorn.sugar_for(node.key) : nil
140
+ short || "#{node.cd}.#{node.name}"
141
+ end
142
+
143
+ def application(node, sugar)
144
+ head = node.head
145
+ return [plain_call(node, sugar), ATOM] unless head.is_a?(ContentSymbol) && sugar && head.official?
146
+
147
+ if (brackets = BRACKETS[head.key])
148
+ return ["#{brackets.first}#{list_text(node.args, sugar)}#{brackets.last}", ATOM]
149
+ end
150
+ if head.key == UNARY_MINUS && node.args.size == 1
151
+ inner = emit(node.args.first, UNARY_PRECEDENCE, sugar)
152
+ inner = "(#{inner})" if inner.start_with?("-") # -(-$x), -(-2): never --
153
+ return ["-#{inner}", UNARY_PRECEDENCE]
154
+ end
155
+ if (op = INFIX[head.key]) && infix?(op, node.args.size)
156
+ return [infix_text(op, node.args, sugar), op[1]]
157
+ end
158
+ [plain_call(node, sugar), ATOM]
159
+ end
160
+
161
+ def infix?(op, count) = op.last == :flat ? count >= 2 : count == 2
162
+
163
+ def infix_text(op, args, sugar)
164
+ text, precedence, association = op
165
+ parts = args.each_with_index.map do |arg, i|
166
+ # the first operand of a left-associative or n-ary operator may
167
+ # share its level; everything else is written one level tighter
168
+ inner = precedence + (i.zero? && association != :single ? 0 : 1)
169
+ # a + (-b) is written a - b, as the reference implementation does
170
+ if i.positive? && text == "+" && negated(arg)
171
+ " - #{emit(negated(arg), inner, sugar)}"
172
+ else
173
+ separator = i.zero? ? "" : (TIGHT.include?(text) ? text : " #{text} ")
174
+ "#{separator}#{emit(arg, inner, sugar)}"
175
+ end
176
+ end
177
+ parts.join
178
+ end
179
+
180
+ # The argument of an arith1.unary_minus application, or nil.
181
+ def negated(node)
182
+ return nil unless node.is_a?(Application) && node.head.is_a?(ContentSymbol)
183
+ return nil unless node.head.key == UNARY_MINUS && node.args.size == 1
184
+ node.args.first
185
+ end
186
+
187
+ def plain_call(node, sugar) = "#{emit(node.head, ATOM, sugar)}(#{list_text(node.args, sugar)})"
188
+
189
+ def binding_text(node, sugar)
190
+ vars = node.vars.map { |v| emit(v, 0, sugar) }.join(", ")
191
+ "#{emit(node.binder, ATOM, sugar)}[#{vars} -> #{emit(node.body, 0, sugar)}]"
192
+ end
193
+
194
+ def attribution_text(node, sugar)
195
+ pairs = node.pairs.map { |p| "#{emit(p.key, 0, sugar)} -> #{emit(p.value, 0, sugar)}" }
196
+ "#{emit(node.object, ATOM, sugar)}{#{pairs.join(', ')}}"
197
+ end
198
+
199
+ def reference_text(node)
200
+ name = node.href.sub(/\A#/, "")
201
+ node.href.start_with?("#") && name.match?(/\A#{IDENTIFIER}\z/o) ? "##{name}" : "###{node.href}##"
202
+ end
203
+
204
+ def list_text(nodes, sugar) = nodes.map { |n| emit(n, 0, sugar) }.join(", ")
205
+
206
+ def float_text(value)
207
+ raise EncodeError, "POPCORN has no notation for #{value}" unless value.finite?
208
+ text = value.to_s.sub("e+", "e")
209
+ text.include?(".") ? text : "#{text}.0"
210
+ end
211
+
212
+ def string_text(value) = %("#{value.gsub('\\', '\\\\\\\\').gsub('"', '\"')}")
213
+
214
+ # ---- reading -----------------------------------------------------------
215
+
216
+ def parse(source) = Root.new(Parser.new(source).parse)
217
+
218
+ # Recursive descent over the published grammar, one method per level of
219
+ # the precedence chain.
220
+ class Parser
221
+ def initialize(source)
222
+ @s = StringScanner.new(source.to_s)
223
+ end
224
+
225
+ def parse
226
+ value = expression
227
+ skip
228
+ raise ParseError, "unexpected #{@s.rest[0, 20].inspect}" unless @s.eos?
229
+ value
230
+ end
231
+
232
+ def expression = block
233
+
234
+ private
235
+
236
+ def skip
237
+ loop do
238
+ @s.skip(/\s+/)
239
+ break unless @s.scan(%r{/\*.*?\*/}m)
240
+ end
241
+ end
242
+
243
+ def accept(pattern)
244
+ skip
245
+ @s.scan(pattern)
246
+ end
247
+
248
+ # An operator that must not swallow the start of a longer one, and a
249
+ # word operator that must not swallow an identifier.
250
+ def operator(*texts)
251
+ skip
252
+ texts.sort_by { |t| -t.size }.each do |text|
253
+ next unless @s.check(/#{Regexp.escape(text)}/)
254
+ next if text.match?(/\A\w+\z/) && @s.check(/#{text}#{Popcorn::IDENTIFIER}/o)
255
+ next if text == "<" && @s.check(/<[=>]/)
256
+ next if text == ">" && @s.check(/>=/)
257
+ next if text == "<=" && @s.check(/<=>/)
258
+ next if text == "-" && @s.check(/->/)
259
+ next if text == "/" && @s.check(%r{//})
260
+ next if text == "!" && @s.check(/!=/)
261
+ next if text == ":" && @s.check(/:[:=]/)
262
+ next if text == "=" && @s.check(/==>/)
263
+ return @s.scan(/#{Regexp.escape(text)}/)
264
+ end
265
+ nil
266
+ end
267
+
268
+ def apply(cd, name, *args) = Application.new(OpenMath.sym(cd, name), args)
269
+
270
+ # arith1.plus, arith1.times and prog1.block are n-ary: a + b + c is
271
+ # one application of three arguments, not two of two.
272
+ def flat(cd, name, left, right)
273
+ key = [cd, name]
274
+ if left.is_a?(Application) && left.head.is_a?(ContentSymbol) && left.head.key == key
275
+ Application.new(left.head, [*left.args, right])
276
+ else
277
+ apply(cd, name, left, right)
278
+ end
279
+ end
280
+
281
+ def block
282
+ value = assign
283
+ value = flat("prog1", "block", value, assign) while operator(";")
284
+ value
285
+ end
286
+
287
+ def assign
288
+ value = implication
289
+ operator(":=") ? apply("prog1", "assign", value, implication) : value
290
+ end
291
+
292
+ def implication
293
+ value = disjunction
294
+ if (op = operator("==>", "<=>"))
295
+ return apply("logic1", op == "==>" ? "implies" : "equivalent", value, disjunction)
296
+ end
297
+ value
298
+ end
299
+
300
+ def disjunction
301
+ value = conjunction
302
+ value = apply("logic1", "or", value, conjunction) while operator("or")
303
+ value
304
+ end
305
+
306
+ def conjunction
307
+ value = relation
308
+ value = apply("logic1", "and", value, relation) while operator("and")
309
+ value
310
+ end
311
+
312
+ RELATIONS = { "=" => %w[relation1 eq], "<" => %w[relation1 lt], "<=" => %w[relation1 leq],
313
+ ">" => %w[relation1 gt], ">=" => %w[relation1 geq], "!=" => %w[relation1 neq],
314
+ "<>" => %w[relation1 neq], "~" => %w[relation2 approx] }.freeze
315
+
316
+ def relation
317
+ value = interval
318
+ return value unless (op = operator(*RELATIONS.keys))
319
+ cd, name = RELATIONS[op]
320
+ apply(cd, name, value, interval)
321
+ end
322
+
323
+ def interval
324
+ value = sum
325
+ operator("..") ? apply("interval1", "interval", value, sum) : value
326
+ end
327
+
328
+ def sum
329
+ value = negation
330
+ loop do
331
+ if operator("+")
332
+ value = flat("arith1", "plus", value, negation)
333
+ elsif operator("-")
334
+ value = apply("arith1", "minus", value, negation)
335
+ else
336
+ return value
337
+ end
338
+ end
339
+ end
340
+
341
+ # -17 is the integer -17; -$x is an application of unary_minus. The
342
+ # minus belongs to the literal only when the literal follows it
343
+ # directly: -(-2) is unary_minus(-2), and --$x is two minuses (the
344
+ # writer used to emit that and this parser refused it: C8).
345
+ def negation
346
+ return product unless operator("-")
347
+ skip
348
+ literal = @s.check(/[0-9.]/)
349
+ value = @s.check(/-(?!>)/) ? negation : product
350
+ case value
351
+ when Int then literal ? Int.new(-value.value) : apply("arith1", "unary_minus", value)
352
+ when Double then literal ? Double.new(-value.value) : apply("arith1", "unary_minus", value)
353
+ else apply("arith1", "unary_minus", value)
354
+ end
355
+ end
356
+
357
+ def product
358
+ value = power
359
+ loop do
360
+ if operator("*")
361
+ value = flat("arith1", "times", value, power)
362
+ elsif operator("/")
363
+ value = apply("arith1", "divide", value, power)
364
+ else
365
+ return value
366
+ end
367
+ end
368
+ end
369
+
370
+ def power
371
+ value = complex
372
+ operator("^") ? apply("arith1", "power", value, complex) : value
373
+ end
374
+
375
+ def complex
376
+ value = rational
377
+ operator("|") ? apply("complex1", "complex_cartesian", value, rational) : value
378
+ end
379
+
380
+ def rational
381
+ value = postfix
382
+ operator("//") ? apply("nums1", "rational", value, postfix) : value
383
+ end
384
+
385
+ # Everything that attaches to an atom: a call, an error call, an
386
+ # attribution, a binding and an id.
387
+ def postfix
388
+ value = atom
389
+ loop do
390
+ if operator("!")
391
+ expect("(")
392
+ value = Error.new(value, arguments(")"))
393
+ elsif accept(/\(/)
394
+ value = Application.new(value, arguments(")"))
395
+ elsif accept(/\{/)
396
+ value = attribution(value)
397
+ elsif accept(/\[/)
398
+ value = binding(value)
399
+ elsif operator(":")
400
+ name = accept(Popcorn::IDENTIFIER) or raise ParseError, "expected an id after :"
401
+ value = value.identified(name)
402
+ elsif @s.check(/\s*::/)
403
+ raise ParseError, "typed expressions (a::b) are not supported"
404
+ else
405
+ return value
406
+ end
407
+ end
408
+ end
409
+
410
+ def arguments(closing)
411
+ return [] if accept(/#{Regexp.escape(closing)}/)
412
+ args = [expression]
413
+ args << expression while operator(",")
414
+ expect(closing)
415
+ args
416
+ end
417
+
418
+ def attribution(object)
419
+ pairs = []
420
+ loop do
421
+ key = expression
422
+ expect("->")
423
+ pairs << AttrPair.new(key, expression)
424
+ break unless operator(",")
425
+ end
426
+ expect("}")
427
+ Attribution.new(pairs, object)
428
+ end
429
+
430
+ def binding(binder)
431
+ vars = [expression]
432
+ vars << expression while operator(",")
433
+ expect("->")
434
+ body = expression
435
+ expect("]")
436
+ raise ParseError, "a binding binds variables" unless vars.all? { |v| v.is_a?(Variable) }
437
+ Bind.new(binder, BVar.new(vars), body)
438
+ end
439
+
440
+ def expect(text)
441
+ operator(text) or raise ParseError, "expected #{text} at #{@s.rest[0, 20].inspect}"
442
+ end
443
+
444
+ def atom
445
+ skip
446
+ if accept(/\(/)
447
+ value = expression
448
+ expect(")")
449
+ return value
450
+ end
451
+ literal || bracketed || conditional || name_atom ||
452
+ raise(ParseError, "expected an object at #{@s.rest[0, 20].inspect}")
453
+ end
454
+
455
+ def literal
456
+ if (hex = accept(/0f\h+/)) then Double.new([hex[2..]].pack("H*").unpack1("G"))
457
+ elsif (hex = accept(/0x\h+/)) then Int.new(hex[2..].to_i(16))
458
+ elsif (f = accept(/\d+\.\d+(?:[eE]-?\d+)?/)) then Double.new(Float(f))
459
+ elsif (i = accept(/\d+/)) then Int.new(i.to_i)
460
+ elsif (s = accept(/"(?:[^"\\]|\\.)*"/)) then Text.new(unescape(s[1..-2]))
461
+ elsif (b = accept(%r{%[A-Za-z0-9+/=]*%})) then Bytes.new(b[1..-2].unpack1("m") || "")
462
+ elsif (f = accept(/`[^`]*`/)) then Foreign.new(f[1..-2])
463
+ elsif (g = accept(/##.*?##/m)) then Reference.new(g[2..-3])
464
+ elsif (r = accept(/\##{Popcorn::IDENTIFIER}/o)) then Reference.new(r)
465
+ end
466
+ end
467
+
468
+ def bracketed
469
+ if accept(/\[/) then apply("list1", "list", *arguments("]"))
470
+ elsif accept(/\{/) then apply("set1", "set", *arguments("}"))
471
+ end
472
+ end
473
+
474
+ def conditional
475
+ if operator("if")
476
+ condition = expression
477
+ expect("then")
478
+ yes = expression
479
+ expect("else")
480
+ no = expression
481
+ expect("endif")
482
+ apply("prog1", "if", condition, yes, no)
483
+ elsif operator("while")
484
+ condition = expression
485
+ expect("do")
486
+ body = expression
487
+ expect("endwhile")
488
+ apply("prog1", "while", condition, body)
489
+ end
490
+ end
491
+
492
+ # $x is a variable, cd.name a symbol, and a bare name is short for a
493
+ # symbol the phrasebook knows.
494
+ def name_atom
495
+ if accept(/\$/)
496
+ Variable.new(identifier || raise(ParseError, "expected a name after $"))
497
+ elsif (name = identifier)
498
+ raise ParseError, "#{name} is a keyword" if Popcorn::KEYWORDS.include?(name)
499
+ if accept(/\./)
500
+ ContentSymbol.new(name, identifier || raise(ParseError, "expected a symbol name"))
501
+ else
502
+ key = Popcorn.symbol_for(name) or raise ParseError, "unknown symbol #{name}"
503
+ OpenMath.sym(*key)
504
+ end
505
+ end
506
+ end
507
+
508
+ def identifier
509
+ quoted = accept(/'[^']*'/)
510
+ return quoted[1..-2] if quoted
511
+ accept(Popcorn::IDENTIFIER)
512
+ end
513
+
514
+ def unescape(text) = text.gsub(/\\(.)/) { Regexp.last_match(1) }
515
+ end
516
+ end
517
+ end
518
+ end