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.
- checksums.yaml +7 -0
- data/CITATION.cff +17 -0
- data/DESIGN.md +783 -0
- data/LICENSE +21 -0
- data/MANUAL.md +6265 -0
- data/README.md +267 -0
- data/bin/rcas +9 -0
- data/bin/rcas-app +9 -0
- data/bin/rcas-chat +9 -0
- data/lib/rcas/algebraic.rb +481 -0
- data/lib/rcas/analysis.rb +966 -0
- data/lib/rcas/app/launcher.rb +203 -0
- data/lib/rcas/app/public/app.css +402 -0
- data/lib/rcas/app/public/app.js +449 -0
- data/lib/rcas/app/public/index.html +46 -0
- data/lib/rcas/app/server.rb +220 -0
- data/lib/rcas/app/window.rb +94 -0
- data/lib/rcas/app/worksheet.rb +290 -0
- data/lib/rcas/app.rb +168 -0
- data/lib/rcas/background.rb +758 -0
- data/lib/rcas/chat/assistant.rb +199 -0
- data/lib/rcas/chat/picker.rb +164 -0
- data/lib/rcas/chat/repl.rb +583 -0
- data/lib/rcas/chat/session.rb +137 -0
- data/lib/rcas/chat/settings.rb +71 -0
- data/lib/rcas/chat/style.rb +30 -0
- data/lib/rcas/chat/tool.rb +53 -0
- data/lib/rcas/chat/ui.rb +316 -0
- data/lib/rcas/chat/usage.rb +62 -0
- data/lib/rcas/chat/workspace.rb +132 -0
- data/lib/rcas/chat.rb +54 -0
- data/lib/rcas/coefficients.rb +170 -0
- data/lib/rcas/combinatorics.rb +274 -0
- data/lib/rcas/complex_parts.rb +160 -0
- data/lib/rcas/constants.rb +129 -0
- data/lib/rcas/core_ext.rb +35 -0
- data/lib/rcas/decide.rb +501 -0
- data/lib/rcas/decompositions.rb +241 -0
- data/lib/rcas/differentiate.rb +144 -0
- data/lib/rcas/discussion.rb +558 -0
- data/lib/rcas/distributions.rb +980 -0
- data/lib/rcas/dixon.rb +95 -0
- data/lib/rcas/docs.rb +321 -0
- data/lib/rcas/domains.rb +728 -0
- data/lib/rcas/expand.rb +174 -0
- data/lib/rcas/expression.rb +613 -0
- data/lib/rcas/factor.rb +605 -0
- data/lib/rcas/finite_field.rb +577 -0
- data/lib/rcas/fourier.rb +118 -0
- data/lib/rcas/fps.rb +678 -0
- data/lib/rcas/fraction.rb +126 -0
- data/lib/rcas/functions.rb +1136 -0
- data/lib/rcas/gcd.rb +112 -0
- data/lib/rcas/geometry.rb +266 -0
- data/lib/rcas/groebner.rb +162 -0
- data/lib/rcas/hold.rb +277 -0
- data/lib/rcas/hypothesis.rb +364 -0
- data/lib/rcas/inequalities.rb +689 -0
- data/lib/rcas/integral_functions.rb +260 -0
- data/lib/rcas/integrate.rb +1589 -0
- data/lib/rcas/integrate_substitutions.rb +434 -0
- data/lib/rcas/interpolate.rb +40 -0
- data/lib/rcas/irb.rb +146 -0
- data/lib/rcas/laplace.rb +159 -0
- data/lib/rcas/latex.rb +556 -0
- data/lib/rcas/lattice.rb +172 -0
- data/lib/rcas/linear_algebra.rb +117 -0
- data/lib/rcas/linear_program.rb +416 -0
- data/lib/rcas/lint.rb +79 -0
- data/lib/rcas/matrix.rb +531 -0
- data/lib/rcas/matrix_multiply.rb +202 -0
- data/lib/rcas/multimodular.rb +286 -0
- data/lib/rcas/named_polynomials.rb +274 -0
- data/lib/rcas/number_theory.rb +443 -0
- data/lib/rcas/numerics.rb +825 -0
- data/lib/rcas/ode.rb +488 -0
- data/lib/rcas/openmath/objects.rb +364 -0
- data/lib/rcas/openmath/phrasebook.rb +551 -0
- data/lib/rcas/openmath/popcorn.rb +518 -0
- data/lib/rcas/openmath/xml.rb +309 -0
- data/lib/rcas/openmath.rb +49 -0
- data/lib/rcas/petkovsek.rb +165 -0
- data/lib/rcas/piecewise.rb +488 -0
- data/lib/rcas/plot.rb +763 -0
- data/lib/rcas/plot3d.rb +419 -0
- data/lib/rcas/poly_matrix.rb +318 -0
- data/lib/rcas/poly_recurrence.rb +117 -0
- data/lib/rcas/polynomial.rb +466 -0
- data/lib/rcas/precision.rb +925 -0
- data/lib/rcas/printer.rb +150 -0
- data/lib/rcas/product.rb +155 -0
- data/lib/rcas/q_difference.rb +296 -0
- data/lib/rcas/q_functions.rb +158 -0
- data/lib/rcas/q_summation.rb +308 -0
- data/lib/rcas/q_zeilberger.rb +199 -0
- data/lib/rcas/random.rb +506 -0
- data/lib/rcas/rational_function.rb +186 -0
- data/lib/rcas/recurrence.rb +323 -0
- data/lib/rcas/render.rb +431 -0
- data/lib/rcas/results.rb +192 -0
- data/lib/rcas/scalar.rb +219 -0
- data/lib/rcas/series.rb +726 -0
- data/lib/rcas/simplify.rb +649 -0
- data/lib/rcas/solve.rb +2002 -0
- data/lib/rcas/special.rb +163 -0
- data/lib/rcas/statistics.rb +175 -0
- data/lib/rcas/steps.rb +835 -0
- data/lib/rcas/summation.rb +532 -0
- data/lib/rcas/trig.rb +264 -0
- data/lib/rcas/van_hoeij.rb +241 -0
- data/lib/rcas/vector.rb +175 -0
- data/lib/rcas/vector_calculus.rb +411 -0
- data/lib/rcas/version.rb +5 -0
- data/lib/rcas/zeilberger.rb +358 -0
- data/lib/rcas.rb +91 -0
- data/package.json +8 -0
- 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
|