gen-text 0.0.2
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/.yardopts +2 -0
- data/LICENSE +21 -0
- data/README.md +170 -0
- data/bin/gen-text +73 -0
- data/lib/gen_text/compile.rb +547 -0
- data/lib/gen_text/vm.rb +269 -0
- data/lib/io/with_dummy_pos.rb +20 -0
- data/yardopts_extra.rb +4 -0
- metadata +75 -0
data/.yardopts
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016-9999 Humanity
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
Description
|
2
|
+
-----------
|
3
|
+
|
4
|
+
A generator of texts from EBNF-like grammars.
|
5
|
+
|
6
|
+
Install
|
7
|
+
-------
|
8
|
+
|
9
|
+
- Install [Ruby](http://ruby-lang.org) 1.9.3 or higher.
|
10
|
+
- `gem install gen-text`
|
11
|
+
|
12
|
+
Usage
|
13
|
+
-----
|
14
|
+
|
15
|
+
Run it with the command:
|
16
|
+
|
17
|
+
gen-text file.g
|
18
|
+
|
19
|
+
Here `file.g` is a file containing the grammar description which consists of the rule definitions:
|
20
|
+
|
21
|
+
nonterminal1 = expr1 ;
|
22
|
+
|
23
|
+
nonterminal2 = expr2 ;
|
24
|
+
|
25
|
+
nonterminal3 = expr3 ;
|
26
|
+
|
27
|
+
Nonterminals start from a letter or "\_" and may contain alphanumeric characters, "\_", "-" or ":".
|
28
|
+
|
29
|
+
You may also use backquoted nonterminals:
|
30
|
+
|
31
|
+
`nonterminal1` = expr1 ;
|
32
|
+
|
33
|
+
`Nonterminal with arbitrary characters: [:|:]\/!` = expr2 ;
|
34
|
+
|
35
|
+
You may use the following expressions in the right part:
|
36
|
+
|
37
|
+
<table>
|
38
|
+
<thead>
|
39
|
+
<tr> <td><strong>Expression</strong></td> <td><strong>Meaning</strong></td> </tr>
|
40
|
+
</thead>
|
41
|
+
<tbody>
|
42
|
+
<tr>
|
43
|
+
<td>
|
44
|
+
<tt>"str"</tt><br/>
|
45
|
+
<tt>'str'</tt>
|
46
|
+
</td>
|
47
|
+
<td>
|
48
|
+
<p>Generate a string.</p>
|
49
|
+
<p>Following escape sequences are allowed: "\n", "\t", "\e" and "\." where "." is an arbitrary character.</p>
|
50
|
+
</td>
|
51
|
+
</tr>
|
52
|
+
<tr>
|
53
|
+
<td><tt>U+HHHH</tt></td>
|
54
|
+
<td>Generate an UTF-8 character sequence corresponding to the Unicode code. E. g.: <tt>U+000A</tt> is equivalent to <tt>"\n"</tt>.</td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<td><tt>n</tt> (a number)</td>
|
58
|
+
<td>Generate a number</td>
|
59
|
+
</tr>
|
60
|
+
<tr>
|
61
|
+
<td><tt>m...n</tt></td>
|
62
|
+
<td>Generate a random number between <tt>m</tt> and <tt>n</tt> (inclusive).</td>
|
63
|
+
</tr>
|
64
|
+
<tr>
|
65
|
+
<td><tt>nonterm</tt></td>
|
66
|
+
<td>–</td>
|
67
|
+
</tr>
|
68
|
+
<tr>
|
69
|
+
<td colspan="2"><center><strong>Combinators</strong></center></td>
|
70
|
+
</tr>
|
71
|
+
<tr>
|
72
|
+
<td> <tt>expr expr</tt> </td>
|
73
|
+
<td>Sequence.</td>
|
74
|
+
</tr>
|
75
|
+
<tr>
|
76
|
+
<td>
|
77
|
+
<tt>expr | expr</tt>
|
78
|
+
</td>
|
79
|
+
<td>Random choice.</td>
|
80
|
+
</tr>
|
81
|
+
<tr>
|
82
|
+
<td>
|
83
|
+
<tt>
|
84
|
+
| expr <br/>
|
85
|
+
| expr
|
86
|
+
</tt>
|
87
|
+
</td>
|
88
|
+
<td>Random choice (another form).</td>
|
89
|
+
</tr>
|
90
|
+
<tr>
|
91
|
+
<td>
|
92
|
+
<tt>
|
93
|
+
| [m%] expr <br/>
|
94
|
+
| [n%] expr <br/>
|
95
|
+
| expr
|
96
|
+
</tt>
|
97
|
+
</td>
|
98
|
+
<td>
|
99
|
+
<p>Random choice with specific probabilities.</p>
|
100
|
+
<p>If probability is unspecified then it is calculated automatically.</p>
|
101
|
+
</td>
|
102
|
+
</tr>
|
103
|
+
<tr>
|
104
|
+
<td>
|
105
|
+
<tt>
|
106
|
+
| [0.1] expr <br/>
|
107
|
+
| [0.3] expr <br/>
|
108
|
+
| expr
|
109
|
+
</tt>
|
110
|
+
</td>
|
111
|
+
<td>
|
112
|
+
The same as above. Probabilities may be specified as floating point numbers between 0.0 and 1.0.
|
113
|
+
</td>
|
114
|
+
</tr>
|
115
|
+
<tr>
|
116
|
+
<td>
|
117
|
+
<tt>expr*</tt> <br/>
|
118
|
+
<tt>expr+</tt> <br/>
|
119
|
+
<tt>expr?</tt> <br/>
|
120
|
+
<tt>expr*[n]</tt> <br/>
|
121
|
+
<tt>expr*[m...n]</tt> <br/>
|
122
|
+
</td>
|
123
|
+
<td>
|
124
|
+
<p>Repeat <tt>expr</tt> many times:</p>
|
125
|
+
<ul>
|
126
|
+
<li>0 or more times</li>
|
127
|
+
<li>1 or more times</li>
|
128
|
+
<li>0 or 1 time</li>
|
129
|
+
<li>exactly <tt>n</tt> times</li>
|
130
|
+
<li>between <tt>m</tt> and <tt>n</tt> times</li>
|
131
|
+
</ul>
|
132
|
+
<p><strong>Note:</strong> you may use <tt>inf</tt> ("infinity") instead of <tt>m</tt> or <tt>n</tt>.</p>
|
133
|
+
</td>
|
134
|
+
</tr>
|
135
|
+
<tr>
|
136
|
+
<td colspan="2"><center><strong>Ruby code insertions</strong></center></td>
|
137
|
+
</tr>
|
138
|
+
<tr>
|
139
|
+
<td><tt>{ code }</tt></td>
|
140
|
+
<td>
|
141
|
+
<p>Execute the code. Generate nothing.</p>
|
142
|
+
<p><strong>Note</strong>: all code insertions inside a rule share the same scope.</p>
|
143
|
+
</td>
|
144
|
+
</tr>
|
145
|
+
<tr>
|
146
|
+
<td><tt>{= code }</tt></td>
|
147
|
+
<td>Generate a string returned by the code.</td>
|
148
|
+
</tr>
|
149
|
+
<tr>
|
150
|
+
<td><tt>{? code }</tt></td>
|
151
|
+
<td>
|
152
|
+
<p>Condition. A code which must evaluate to true.</p>
|
153
|
+
<p><strong>Note</strong>: presence of this expression turns on backtracking and output buffering and may result in enormous memory usage.</p>
|
154
|
+
</td>
|
155
|
+
</tr>
|
156
|
+
</tbody>
|
157
|
+
</table>
|
158
|
+
|
159
|
+
TODO: Capture the generated output.
|
160
|
+
|
161
|
+
Examples
|
162
|
+
--------
|
163
|
+
|
164
|
+
See them in "sample" directory.
|
165
|
+
|
166
|
+
Links
|
167
|
+
-----
|
168
|
+
|
169
|
+
- [Documentation](http://www.rubydoc.info/gems/gen-text/0.0.1)
|
170
|
+
- [Source code](https://github.com/LavirtheWhiolet/gen-text)
|
data/bin/gen-text
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
require 'gen_text/vm'
|
3
|
+
require 'gen_text/compile'
|
4
|
+
require 'io/with_dummy_pos'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
def usage
|
8
|
+
puts <<-TEXT
|
9
|
+
Usage: #{File.basename __FILE__} [options] [grammar]
|
10
|
+
|
11
|
+
Reads grammar and generates random text from it.
|
12
|
+
|
13
|
+
If the grammar file is not specified the the grammar is read from standard
|
14
|
+
input.
|
15
|
+
|
16
|
+
Options:
|
17
|
+
-h, --help Show this message and exit.
|
18
|
+
-d, --debug Turn on debug mode.
|
19
|
+
-c, --compile Compile the grammar. Do not generate text.
|
20
|
+
|
21
|
+
TEXT
|
22
|
+
end
|
23
|
+
|
24
|
+
grammar_file = :'-'
|
25
|
+
compile_only = false
|
26
|
+
until ARGV.empty?
|
27
|
+
case x = ARGV.shift
|
28
|
+
when "-h", "--help"
|
29
|
+
usage
|
30
|
+
exit
|
31
|
+
when "-d", "--debug"
|
32
|
+
$DEBUG = true
|
33
|
+
when "-c", "--compile"
|
34
|
+
compile_only = true
|
35
|
+
else
|
36
|
+
grammar_file = x
|
37
|
+
end
|
38
|
+
end
|
39
|
+
grammar =
|
40
|
+
begin
|
41
|
+
case grammar_file
|
42
|
+
when :'-' then STDIN.read
|
43
|
+
else File.read(grammar_file)
|
44
|
+
end
|
45
|
+
rescue IOError => e
|
46
|
+
abort e.message
|
47
|
+
end
|
48
|
+
program =
|
49
|
+
begin
|
50
|
+
GenText::Compile.new.(grammar, grammar_file.to_s)
|
51
|
+
rescue Parse::Error => e
|
52
|
+
abort "error: #{e.pos.file}:#{e.pos.line+1}:#{e.pos.column+1}: #{e.message}"
|
53
|
+
end
|
54
|
+
# Optimization: If the program does not cause calling out.pos=(...) then
|
55
|
+
# there is no way for GenText::VM to put the garbage after the pos.
|
56
|
+
buffered, out =
|
57
|
+
if GenText::VM.may_set_out_pos?(program) then
|
58
|
+
[true, StringIO.new]
|
59
|
+
else
|
60
|
+
[false, IO::WithDummyPos.new(STDOUT)]
|
61
|
+
end
|
62
|
+
begin
|
63
|
+
srand(Time.now.to_i)
|
64
|
+
GenText::VM.new.run(program, out, compile_only)
|
65
|
+
rescue GenText::CheckFailed => e
|
66
|
+
abort "error: #{e.pos.file}:#{e.pos.line+1}:#{e.pos.column+1}: #{e.message}"
|
67
|
+
ensure
|
68
|
+
if buffered then
|
69
|
+
n = out.pos
|
70
|
+
out.pos = 0
|
71
|
+
IO.copy_stream(out, STDOUT, n)
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,547 @@
|
|
1
|
+
require 'parse'
|
2
|
+
require 'gen_text/vm'
|
3
|
+
|
4
|
+
module GenText
|
5
|
+
|
6
|
+
class Compile < Parse
|
7
|
+
|
8
|
+
# @param (see Parse#call)
|
9
|
+
# @return the program as an Array of <code>[:method_id, *args]</code> where
|
10
|
+
# <code>method_id</code> is ID of {VM}'s method. The program may raise
|
11
|
+
# {CheckFailed}.
|
12
|
+
def call(*args)
|
13
|
+
super(*args).to_vm_code
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# ---- Utils ----
|
19
|
+
|
20
|
+
INF = Float::INFINITY
|
21
|
+
|
22
|
+
# @!visibility private
|
23
|
+
module ::ASTNode
|
24
|
+
|
25
|
+
module_function
|
26
|
+
|
27
|
+
# @return [Array<Array<(:generated_from, String)>>]
|
28
|
+
def generated_from(pos)
|
29
|
+
if $DEBUG then
|
30
|
+
[[:generated_from, "#{pos.file}:#{pos.line+1}:#{pos.column+1}"]]
|
31
|
+
else
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
# ---- AST & Code Generation ----
|
39
|
+
|
40
|
+
# @!visibility private
|
41
|
+
Context = Struct.new :rule_scope, :rule_labels
|
42
|
+
|
43
|
+
# @!visibility private
|
44
|
+
class Label
|
45
|
+
end
|
46
|
+
|
47
|
+
Program = ASTNode.new :rules do
|
48
|
+
|
49
|
+
def to_vm_code
|
50
|
+
rule_labels = {}; begin
|
51
|
+
rules.each do |rule|
|
52
|
+
raise Parse::Error.new(rule.pos, "rule `#{rule.name}' is defined twice") if rule_labels.has_key? rule.name
|
53
|
+
rule_labels[rule.name] = Label.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
code =
|
57
|
+
[
|
58
|
+
[:call, rule_labels[rules.first.name]],
|
59
|
+
[:halt],
|
60
|
+
*rules.map do |rule|
|
61
|
+
[
|
62
|
+
*generated_from(rule.pos),
|
63
|
+
rule_labels[rule.name],
|
64
|
+
*rule.body.to_vm_code(Context.new(new_binding, rule_labels)),
|
65
|
+
[:ret]
|
66
|
+
]
|
67
|
+
end.reduce(:concat)
|
68
|
+
]
|
69
|
+
return replace_labels_with_addresses(code)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# @return [Binding]
|
75
|
+
def new_binding
|
76
|
+
binding
|
77
|
+
end
|
78
|
+
|
79
|
+
def replace_labels_with_addresses(code)
|
80
|
+
# Remove labels and remember their addresses.
|
81
|
+
addresses = {}
|
82
|
+
new_code = []
|
83
|
+
code.each do |instruction|
|
84
|
+
case instruction
|
85
|
+
when Label
|
86
|
+
addresses[instruction] = new_code.size
|
87
|
+
else
|
88
|
+
new_code.push instruction
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# Replace labels in instructions' arguments.
|
92
|
+
this = lambda do |x|
|
93
|
+
case x
|
94
|
+
when Array
|
95
|
+
x.map(&this)
|
96
|
+
when Label
|
97
|
+
addresses[x]
|
98
|
+
else
|
99
|
+
x
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return this.(new_code)
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
GenString = ASTNode.new :string do
|
108
|
+
|
109
|
+
def to_vm_code(context)
|
110
|
+
[
|
111
|
+
*generated_from(pos),
|
112
|
+
[:push, string],
|
113
|
+
[:gen]
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
GenNumber = ASTNode.new :from, :to do
|
120
|
+
|
121
|
+
def to_vm_code(context)
|
122
|
+
[
|
123
|
+
*generated_from(pos),
|
124
|
+
[:push_rand, from..to],
|
125
|
+
[:gen]
|
126
|
+
]
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
Repeat = ASTNode.new :subexpr, :from_times, :to_times do
|
132
|
+
|
133
|
+
def to_vm_code(context)
|
134
|
+
raise Parse::Error.new(pos, "`from' can not be greater than `to'") if from_times > to_times
|
135
|
+
#
|
136
|
+
subexpr_code = subexpr.to_vm_code(context)
|
137
|
+
# Code.
|
138
|
+
subexpr_label = Label.new
|
139
|
+
generated_from(pos) +
|
140
|
+
# Mandatory part (0...from_times).
|
141
|
+
if from_times > 0
|
142
|
+
loop1 = Label.new
|
143
|
+
loop1_end = Label.new
|
144
|
+
[
|
145
|
+
[:push, from_times], # counter
|
146
|
+
loop1,
|
147
|
+
[:goto_if_not_0, loop1_end], # if counter == 0 then goto loop1_end
|
148
|
+
[:call, subexpr_label],
|
149
|
+
[:dec], # counter
|
150
|
+
[:goto, loop1],
|
151
|
+
loop1_end,
|
152
|
+
[:pop] # counter
|
153
|
+
]
|
154
|
+
else
|
155
|
+
[]
|
156
|
+
end +
|
157
|
+
# Optional part (from_times...to_times)
|
158
|
+
if (to_times - from_times) == 0
|
159
|
+
[]
|
160
|
+
elsif (to_times - from_times) < INF
|
161
|
+
loop2 = Label.new
|
162
|
+
loop2_end = Label.new
|
163
|
+
[
|
164
|
+
[:push_rand, (to_times - from_times + 1)], # counter
|
165
|
+
loop2,
|
166
|
+
[:goto_if_not_0, loop2_end], # if counter == 0 then goto loop2_end
|
167
|
+
[:push_rescue_point, loop2_end],
|
168
|
+
[:call, subexpr_label],
|
169
|
+
[:pop], # rescue point
|
170
|
+
[:dec], # counter
|
171
|
+
[:goto, loop2],
|
172
|
+
loop2_end,
|
173
|
+
[:pop], # counter
|
174
|
+
]
|
175
|
+
else # if (to_times - from_times) is infinite
|
176
|
+
loop2 = Label.new
|
177
|
+
loop2_end = Label.new
|
178
|
+
[
|
179
|
+
loop2,
|
180
|
+
[:goto_if_rand_gt, 0.5, loop2_end],
|
181
|
+
[:push_rescue_point],
|
182
|
+
[:call, subexpr_label],
|
183
|
+
[:pop], # rescue point
|
184
|
+
[:goto, loop2],
|
185
|
+
loop2_end
|
186
|
+
]
|
187
|
+
end +
|
188
|
+
# Subexpr as subroutine.
|
189
|
+
begin
|
190
|
+
after_subexpr = Label.new
|
191
|
+
[
|
192
|
+
[:goto, after_subexpr],
|
193
|
+
subexpr_label,
|
194
|
+
*subexpr.to_vm_code(context),
|
195
|
+
[:ret],
|
196
|
+
after_subexpr
|
197
|
+
]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
GenCode = ASTNode.new :to_s do
|
204
|
+
|
205
|
+
def to_vm_code(context)
|
206
|
+
[
|
207
|
+
*generated_from(pos),
|
208
|
+
[:eval_ruby_code, context.rule_scope, self.to_s, pos.file, pos.line+1],
|
209
|
+
[:gen]
|
210
|
+
]
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
CheckCode = ASTNode.new :to_s do
|
216
|
+
|
217
|
+
def to_vm_code(context)
|
218
|
+
passed = Label.new
|
219
|
+
[
|
220
|
+
*generated_from(pos),
|
221
|
+
[:eval_ruby_code, context.rule_scope, self.to_s, pos.file, pos.line+1],
|
222
|
+
[:goto_if, passed],
|
223
|
+
[:rescue_, lambda { raise CheckFailed.new(pos) }],
|
224
|
+
passed
|
225
|
+
]
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
ActionCode = ASTNode.new :to_s do
|
231
|
+
|
232
|
+
def to_vm_code(context)
|
233
|
+
[
|
234
|
+
*generated_from(pos),
|
235
|
+
[:eval_ruby_code, context.rule_scope, self.to_s, pos.file, pos.line+1],
|
236
|
+
[:pop]
|
237
|
+
]
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
RuleCall = ASTNode.new :name do
|
243
|
+
|
244
|
+
def to_vm_code(context)
|
245
|
+
[
|
246
|
+
*generated_from(pos),
|
247
|
+
[:call, (context.rule_labels[name] or raise Parse::Error.new(pos, "rule `#{name}' not defined"))]
|
248
|
+
]
|
249
|
+
end
|
250
|
+
|
251
|
+
end
|
252
|
+
|
253
|
+
Choice = ASTNode.new :alternatives do
|
254
|
+
|
255
|
+
def to_vm_code(context)
|
256
|
+
# Populate alternatives' weights.
|
257
|
+
if alternatives.map(&:probability).all? { |x| x == :auto } then
|
258
|
+
alternatives.each { |a| a.weight = 1 }
|
259
|
+
else
|
260
|
+
known_probabilities_sum =
|
261
|
+
alternatives.map(&:probability).reject { |x| x == :auto }.reduce(:+)
|
262
|
+
raise Parse::Error.new(pos, "probabilities sum exceed 100%") if known_probabilities_sum > 1.00 + 0.0001
|
263
|
+
auto_probability =
|
264
|
+
(1.00 - known_probabilities_sum) / alternatives.map(&:probability).select { |x| x == :auto }.size
|
265
|
+
alternatives.each do |alternative|
|
266
|
+
alternative.weight =
|
267
|
+
if alternative.probability == :auto then
|
268
|
+
auto_probability
|
269
|
+
else
|
270
|
+
alternative.probability
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
# Populate alternatives' labels.
|
275
|
+
alternatives.each { |a| a.label = Label.new }
|
276
|
+
# Generate the code.
|
277
|
+
initial_weights_and_labels = alternatives.map { |a| [a.weight, a.label] }
|
278
|
+
end_label = Label.new
|
279
|
+
[
|
280
|
+
*generated_from(pos),
|
281
|
+
[:push_dup, initial_weights_and_labels],
|
282
|
+
[:weighed_choice],
|
283
|
+
*alternatives.map do |alternative|
|
284
|
+
[
|
285
|
+
alternative.label,
|
286
|
+
*alternative.subexpr.to_vm_code(context),
|
287
|
+
[:goto, end_label],
|
288
|
+
]
|
289
|
+
end.reduce(:concat),
|
290
|
+
end_label,
|
291
|
+
[:pop], # rescue_point
|
292
|
+
[:pop], # weights_and_labels
|
293
|
+
]
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
ChoiceAlternative = ASTNode.new :probability, :subexpr do
|
299
|
+
|
300
|
+
# Used by {Choice#to_vm_code} only.
|
301
|
+
# @return [Numeric]
|
302
|
+
attr_accessor :weight
|
303
|
+
|
304
|
+
# Used by {Choice#to_vm_code} only.
|
305
|
+
# @return [Label]
|
306
|
+
attr_accessor :label
|
307
|
+
|
308
|
+
end
|
309
|
+
|
310
|
+
Seq = ASTNode.new :subexprs do
|
311
|
+
|
312
|
+
def to_vm_code(context)
|
313
|
+
generated_from(pos) +
|
314
|
+
subexprs.map { |subexpr| subexpr.to_vm_code(context) }.reduce(:concat)
|
315
|
+
end
|
316
|
+
|
317
|
+
end
|
318
|
+
|
319
|
+
RuleDefinition = ASTNode.new :name, :body
|
320
|
+
|
321
|
+
# ---- Syntax ----
|
322
|
+
|
323
|
+
rule :start do
|
324
|
+
whitespace_and_comments and
|
325
|
+
rules = many { rule_definition } and
|
326
|
+
_(Program[rules])
|
327
|
+
end
|
328
|
+
|
329
|
+
rule :expr do
|
330
|
+
choice
|
331
|
+
end
|
332
|
+
|
333
|
+
rule :choice do
|
334
|
+
first = true
|
335
|
+
as = one_or_more {
|
336
|
+
p = choice_alternative_start(first) and s = seq and
|
337
|
+
act { first = false } and
|
338
|
+
_(ChoiceAlternative[p, s])
|
339
|
+
} and
|
340
|
+
if as.size == 1 then
|
341
|
+
as.first.subexpr
|
342
|
+
else
|
343
|
+
_(Choice[as])
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Returns probability or :auto.
|
348
|
+
def choice_alternative_start(first)
|
349
|
+
_{
|
350
|
+
(_{ slash } or _{ pipe }) and
|
351
|
+
probability = (
|
352
|
+
_{
|
353
|
+
lbracket and
|
354
|
+
x = ufloat and opt { percent and act { x /= 100.0 } } and
|
355
|
+
rbracket and
|
356
|
+
x
|
357
|
+
} or
|
358
|
+
:auto
|
359
|
+
)
|
360
|
+
} or
|
361
|
+
(if first then :auto else nil end)
|
362
|
+
end
|
363
|
+
|
364
|
+
rule :seq do
|
365
|
+
e = repeat and many {
|
366
|
+
e2 = repeat and e = _(Seq[to_seq_subexprs(e) + to_seq_subexprs(e2)])
|
367
|
+
} and
|
368
|
+
e
|
369
|
+
end
|
370
|
+
|
371
|
+
def to_seq_subexprs(e)
|
372
|
+
case e
|
373
|
+
when Seq then e.subexprs
|
374
|
+
else [e]
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
rule :repeat do
|
379
|
+
e = primary and many {
|
380
|
+
_{
|
381
|
+
asterisk and
|
382
|
+
from = 0 and to = INF and
|
383
|
+
opt {
|
384
|
+
lbracket and
|
385
|
+
n = times and act { from = n and to = n } and
|
386
|
+
opt {
|
387
|
+
ellipsis and
|
388
|
+
n = times and act { to = n }
|
389
|
+
} and
|
390
|
+
rbracket
|
391
|
+
} and
|
392
|
+
e = _(Repeat[e, from, to]) } or
|
393
|
+
_{ question and e = _(Repeat[e, 0, 1]) } or
|
394
|
+
_{ plus and e = _(Repeat[e, 1, INF]) }
|
395
|
+
} and
|
396
|
+
e
|
397
|
+
end
|
398
|
+
|
399
|
+
def times
|
400
|
+
_{ uint } or
|
401
|
+
_{ inf and INF }
|
402
|
+
end
|
403
|
+
|
404
|
+
rule :primary do
|
405
|
+
_{ s = string and _(GenString[s]) } or
|
406
|
+
_{ c = code("{=") and _(GenCode[c]) } or
|
407
|
+
_{ c = code("{?") and _(CheckCode[c]) } or
|
408
|
+
_{ action_code } or
|
409
|
+
_{ n = nonterm and not_follows(:eq) and _(RuleCall[n]) } or
|
410
|
+
_{ gen_number } or
|
411
|
+
_{ lparen and e = expr and rparen and e }
|
412
|
+
end
|
413
|
+
|
414
|
+
def gen_number
|
415
|
+
n1 = number and n2 = opt { ellipsis and number } and
|
416
|
+
act { n2 = (n2.first or n1) } and
|
417
|
+
_(GenNumber[n1, n2])
|
418
|
+
end
|
419
|
+
|
420
|
+
rule :action_code do
|
421
|
+
c = code("{") and _(ActionCode[c])
|
422
|
+
end
|
423
|
+
|
424
|
+
rule :rule_definition do
|
425
|
+
n = nonterm and (_{eq} or _{larrow}) and e = choice and semicolon and
|
426
|
+
_(RuleDefinition[n, e])
|
427
|
+
end
|
428
|
+
|
429
|
+
# ---- Tokens ----
|
430
|
+
|
431
|
+
token :inf, "inf"
|
432
|
+
token :asterisk, "*"
|
433
|
+
token :question, "?"
|
434
|
+
token :plus, "+"
|
435
|
+
token :pipe, "|"
|
436
|
+
token :slash, "/"
|
437
|
+
token :eq, "="
|
438
|
+
token :semicolon, ";"
|
439
|
+
token :percent, "%"
|
440
|
+
token :ellipsis, "..."
|
441
|
+
token :lbrace, "{"
|
442
|
+
token :rbrace, "}"
|
443
|
+
token :lparen, "("
|
444
|
+
token :rparen, ")"
|
445
|
+
token :lbracket, "["
|
446
|
+
token :rbracket, "]"
|
447
|
+
token :dot, "."
|
448
|
+
token :larrow, "<-"
|
449
|
+
|
450
|
+
# Parses "#{start} #{code_part} } #{whitespace_and_comments}".
|
451
|
+
# Returns the code_part.
|
452
|
+
def code(start)
|
453
|
+
p = pos and
|
454
|
+
scan(start) and c = code_part and
|
455
|
+
(rbrace or raise Expected.new(p, "`}' at the end")) and
|
456
|
+
c
|
457
|
+
end
|
458
|
+
|
459
|
+
rule :code_part do
|
460
|
+
many {
|
461
|
+
_{ scan(/\\./) } or
|
462
|
+
_{ scan(/[^{}]+/) } or
|
463
|
+
_{
|
464
|
+
pp = pos and
|
465
|
+
p1 = scan("{") and p2 = code_part and
|
466
|
+
(p3 = scan("}") or raise Expected.new(pp, "`}' at the end")) and
|
467
|
+
p1 + p2 + p3
|
468
|
+
}
|
469
|
+
}.join
|
470
|
+
end
|
471
|
+
|
472
|
+
token :string do
|
473
|
+
_{ string0('"') } or
|
474
|
+
_{ string0("'") } or
|
475
|
+
_{ scan("U+") and c = scan(/\h+/) and [c.hex].pack("U") }
|
476
|
+
end
|
477
|
+
|
478
|
+
def string0(quote)
|
479
|
+
p = pos and scan(quote) and
|
480
|
+
s = many {
|
481
|
+
_{ scan(/\\n/) and "\n" } or
|
482
|
+
_{ scan(/\\t/) and "\t" } or
|
483
|
+
_{ scan(/\\e/) and "\e" } or
|
484
|
+
_{ scan(/\\./) } or
|
485
|
+
scan(/[^#{quote}]/)
|
486
|
+
}.join and
|
487
|
+
(scan(quote) or raise Expected.new(p, "`#{quote}' at the end")) and s
|
488
|
+
end
|
489
|
+
|
490
|
+
token :nonterm do
|
491
|
+
_{ scan(/`.*?`/) } or
|
492
|
+
_{ scan(/[[:alpha:]_:][[:alnum:]\-_:]*/) }
|
493
|
+
end
|
494
|
+
|
495
|
+
token :int, "integer number" do
|
496
|
+
n = number and n.is_a? Integer and n
|
497
|
+
end
|
498
|
+
|
499
|
+
token :uint, "non-negative integer number" do
|
500
|
+
n = int and n >= 0 and n
|
501
|
+
end
|
502
|
+
|
503
|
+
token :number do
|
504
|
+
s = scan(/[\-\+]?\d+(\.\d+)?([eE][\-\+]?\d+)?/) and
|
505
|
+
if /[\.eE]/ === s then
|
506
|
+
Float(s)
|
507
|
+
else
|
508
|
+
Integer(s)
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
token :float, "floating point number" do
|
513
|
+
number
|
514
|
+
end
|
515
|
+
|
516
|
+
token :ufloat, "non-negative floating point number" do
|
517
|
+
n = number and n >= 0 and n
|
518
|
+
end
|
519
|
+
|
520
|
+
def whitespace_and_comments
|
521
|
+
many {
|
522
|
+
_{ scan(/\s+/) } or
|
523
|
+
_{ scan("//") and scan(/[^\n]*\n/m) } or
|
524
|
+
_{
|
525
|
+
p = pos and scan("/*") and
|
526
|
+
many { not_follows { scan("*/") } and scan(/./m) } and
|
527
|
+
(scan("*/") or raise Expected.new(p, "`*/' at the end"))
|
528
|
+
}
|
529
|
+
}
|
530
|
+
end
|
531
|
+
|
532
|
+
end
|
533
|
+
|
534
|
+
class CheckFailed < Exception
|
535
|
+
|
536
|
+
# @param [Parse::Position] pos
|
537
|
+
def initialize(pos)
|
538
|
+
super("check failed")
|
539
|
+
@pos = pos
|
540
|
+
end
|
541
|
+
|
542
|
+
# @return [Parse::Position] pos
|
543
|
+
attr_reader :pos
|
544
|
+
|
545
|
+
end
|
546
|
+
|
547
|
+
end
|
data/lib/gen_text/vm.rb
ADDED
@@ -0,0 +1,269 @@
|
|
1
|
+
|
2
|
+
module GenText
|
3
|
+
|
4
|
+
class VM
|
5
|
+
|
6
|
+
# @param program Array of <code>[:method_id, *args]</code>.
|
7
|
+
# @return [Boolean] true if +program+ may result in calling
|
8
|
+
# {IO#pos=} and false otherwise.
|
9
|
+
def self.may_set_out_pos?(program)
|
10
|
+
program.any? { |instruction| instruction.first == :rescue_ }
|
11
|
+
end
|
12
|
+
|
13
|
+
# Executes +program+.
|
14
|
+
#
|
15
|
+
# After the execution the +out+ may contain garbage after its {IO#pos}.
|
16
|
+
# It is up to the caller to truncate the garbage or to copy the useful data.
|
17
|
+
#
|
18
|
+
# @param program Array of <code>[:method_id, *args]</code>.
|
19
|
+
# @param [IO] out
|
20
|
+
# @param [Boolean] do_not_run if true then +program+ will not be run
|
21
|
+
# (some checks and initializations will be performed only).
|
22
|
+
# @return [void]
|
23
|
+
def run(program, out, do_not_run = false)
|
24
|
+
#
|
25
|
+
if $DEBUG
|
26
|
+
STDERR.puts "PROGRAM:"
|
27
|
+
program.each_with_index do |instruction, addr|
|
28
|
+
STDERR.puts " #{addr}: #{inspect_instruction(instruction)}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
#
|
32
|
+
return if do_not_run
|
33
|
+
# Init.
|
34
|
+
@stack = []
|
35
|
+
@out = out
|
36
|
+
@pc = 0
|
37
|
+
@halted = false
|
38
|
+
# Run.
|
39
|
+
STDERR.puts "RUN TRACE:" if $DEBUG
|
40
|
+
until halted?
|
41
|
+
instruction = program[@pc]
|
42
|
+
method_id, *args = *instruction
|
43
|
+
STDERR.puts " #{@pc}: #{inspect_instruction(instruction)}" if $DEBUG
|
44
|
+
self.__send__(method_id, *args)
|
45
|
+
if $DEBUG then
|
46
|
+
STDERR.puts " PC: #{@pc}"
|
47
|
+
STDERR.puts " STACK: #{@stack.inspect}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Integer]
|
53
|
+
attr_reader :pc
|
54
|
+
|
55
|
+
# @return [IO]
|
56
|
+
attr_reader :out
|
57
|
+
|
58
|
+
# @return [Boolean]
|
59
|
+
def halted?
|
60
|
+
@halted
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sets {#halted?} to true.
|
64
|
+
#
|
65
|
+
# @return [void]
|
66
|
+
def halt
|
67
|
+
@halted = true
|
68
|
+
end
|
69
|
+
|
70
|
+
# NOP
|
71
|
+
#
|
72
|
+
# @return [void]
|
73
|
+
def generated_from(*args)
|
74
|
+
@pc += 1
|
75
|
+
end
|
76
|
+
|
77
|
+
# Pushes +o+ to the stack.
|
78
|
+
#
|
79
|
+
# @param [Object] o
|
80
|
+
# @return [void]
|
81
|
+
def push(o)
|
82
|
+
@stack.push o
|
83
|
+
@pc += 1
|
84
|
+
end
|
85
|
+
|
86
|
+
# {#push}(o.dup)
|
87
|
+
#
|
88
|
+
# @param [Object] o
|
89
|
+
# @return [void]
|
90
|
+
def push_dup(o)
|
91
|
+
push(o.dup)
|
92
|
+
end
|
93
|
+
|
94
|
+
# {#push}(rand(+r+) if +r+ is specified; rand() otherwise)
|
95
|
+
#
|
96
|
+
# @param [Object, nil] r
|
97
|
+
# @return [void]
|
98
|
+
def push_rand(r = nil)
|
99
|
+
push(if r then rand(r) else rand end)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Pops the value from the stack.
|
103
|
+
#
|
104
|
+
# @return [Object] the popped value.
|
105
|
+
def pop
|
106
|
+
@stack.pop
|
107
|
+
@pc += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# If {#pop} is true then {#pc} := +addr+.
|
111
|
+
#
|
112
|
+
# @param [Integer] addr
|
113
|
+
# @return [void]
|
114
|
+
def goto_if(addr)
|
115
|
+
if @stack.pop then
|
116
|
+
@pc = addr
|
117
|
+
else
|
118
|
+
@pc += 1
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [void]
|
123
|
+
def dec
|
124
|
+
@stack[-1] -= 1
|
125
|
+
@pc += 1
|
126
|
+
end
|
127
|
+
|
128
|
+
# If the value on the stack != 0 then {#goto}(+addr).
|
129
|
+
#
|
130
|
+
# @param [Integer] addr
|
131
|
+
# @return [void]
|
132
|
+
def goto_if_not_0(addr)
|
133
|
+
if @stack.last != 0 then
|
134
|
+
@pc += 1
|
135
|
+
else
|
136
|
+
@pc = addr
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# If rand > +v+ then {#goto}(addr)
|
141
|
+
#
|
142
|
+
# @param [Numeric] v
|
143
|
+
# @param [Integer] addr
|
144
|
+
# @return [void]
|
145
|
+
#
|
146
|
+
def goto_if_rand_gt(v, addr)
|
147
|
+
if rand > v then
|
148
|
+
@pc = addr
|
149
|
+
else
|
150
|
+
@pc += 1
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# @param [Integer] addr
|
155
|
+
# @return [void]
|
156
|
+
def goto(addr)
|
157
|
+
@pc = addr
|
158
|
+
end
|
159
|
+
|
160
|
+
# Writes {#pop} to {#out}.
|
161
|
+
#
|
162
|
+
# @return [void]
|
163
|
+
def gen
|
164
|
+
@out.write @stack.pop
|
165
|
+
@pc += 1
|
166
|
+
end
|
167
|
+
|
168
|
+
# {#push}(eval(+ruby_code+, +file+, +line+))
|
169
|
+
#
|
170
|
+
# @param [Binding] binding_
|
171
|
+
# @param [String] ruby_code
|
172
|
+
# @param [String] file original file of +ruby_code+.
|
173
|
+
# @param [Integer] line original line of +ruby_code+.
|
174
|
+
# @return [void]
|
175
|
+
def eval_ruby_code(binding_, ruby_code, file, line)
|
176
|
+
@stack.push binding_.eval(ruby_code, file, line)
|
177
|
+
@pc += 1
|
178
|
+
end
|
179
|
+
|
180
|
+
# {#push}({#out}'s {IO#pos} and {#pc} as {RescuePoint})
|
181
|
+
#
|
182
|
+
# @param [Integer, nil] pc if specified then it is pushed instead of {#pc}.
|
183
|
+
# @return [void]
|
184
|
+
def push_rescue_point(pc = nil)
|
185
|
+
@stack.push RescuePoint[(pc or @pc), @out.pos]
|
186
|
+
@pc += 1
|
187
|
+
end
|
188
|
+
|
189
|
+
# {#pop}s until a {RescuePoint} is found then restore {#out} and {#pc} from
|
190
|
+
# the {RescuePoint}.
|
191
|
+
#
|
192
|
+
# @param [Proc] on_failure is called if no {RescuePoint} is found
|
193
|
+
# @return [void]
|
194
|
+
def rescue_(on_failure)
|
195
|
+
@stack.pop until @stack.empty? or @stack.last.is_a? RescuePoint
|
196
|
+
if @stack.empty? then
|
197
|
+
on_failure.()
|
198
|
+
else
|
199
|
+
rescue_point = @stack.pop
|
200
|
+
@pc = rescue_point.pc
|
201
|
+
@out.pos = rescue_point.out_pos
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
# @param [Integer] addr
|
206
|
+
# @return [void]
|
207
|
+
def call(addr)
|
208
|
+
@stack.push(@pc + 1)
|
209
|
+
@pc = addr
|
210
|
+
end
|
211
|
+
|
212
|
+
# @return [void]
|
213
|
+
def ret
|
214
|
+
@pc = @stack.pop
|
215
|
+
end
|
216
|
+
|
217
|
+
# Let stack contains +wa+ = [[weight1, address1], [weight2, address2], ...].
|
218
|
+
# This function:
|
219
|
+
#
|
220
|
+
# 1. Picks a random address from +wa+ (the more weight the
|
221
|
+
# address has, the more often it is picked);
|
222
|
+
# 2. Deletes the chosen address from +wa+;
|
223
|
+
# 3. If there was the only address in +wa+ then it does {#push}(nil);
|
224
|
+
# otherwise it does {#push_rescue_point};
|
225
|
+
# 4. {#goto}(the chosen address).
|
226
|
+
#
|
227
|
+
# @return [void]
|
228
|
+
def weighed_choice
|
229
|
+
weights_and_addresses = @stack.last
|
230
|
+
# If no alternatives left...
|
231
|
+
if weights_and_addresses.size == 1 then
|
232
|
+
_, address = *weights_and_addresses.first
|
233
|
+
@stack.push nil
|
234
|
+
@pc = address
|
235
|
+
# If there are alternatives...
|
236
|
+
else
|
237
|
+
chosen_weight_and_address = sample_weighed(weights_and_addresses)
|
238
|
+
weights_and_addresses.delete chosen_weight_and_address
|
239
|
+
_, chosen_address = *chosen_weight_and_address
|
240
|
+
push_rescue_point
|
241
|
+
@pc = chosen_address
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
RescuePoint = Struct.new :pc, :out_pos
|
246
|
+
|
247
|
+
private
|
248
|
+
|
249
|
+
# @param [Array<Array<(Numeric, Object)>>] weights_and_items
|
250
|
+
# @return [Array<(Numeric, Object)>]
|
251
|
+
def sample_weighed(weights_and_items)
|
252
|
+
weight_sum = weights_and_items.map(&:first).reduce(:+)
|
253
|
+
chosen_partial_weight_sum = rand(0...weight_sum)
|
254
|
+
current_partial_weight_sum = 0
|
255
|
+
weights_and_items.find do |weight, item|
|
256
|
+
current_partial_weight_sum += weight
|
257
|
+
current_partial_weight_sum > chosen_partial_weight_sum
|
258
|
+
end or
|
259
|
+
weights_and_items.last
|
260
|
+
end
|
261
|
+
|
262
|
+
def inspect_instruction(instruction)
|
263
|
+
method_id, *args = *instruction
|
264
|
+
"#{method_id} #{args.map(&:inspect).join(", ")}"
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
end
|
data/yardopts_extra.rb
ADDED
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gen-text
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lavir the Whiolet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2016-06-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: parse-framework
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: ! 'A generator of texts from EBNF-like grammars. It features probability
|
31
|
+
management, code insertions and conditional generation with conditions written in
|
32
|
+
Ruby.
|
33
|
+
|
34
|
+
'
|
35
|
+
email: Lavir.th.Whiolet@gmail.com
|
36
|
+
executables:
|
37
|
+
- gen-text
|
38
|
+
extensions: []
|
39
|
+
extra_rdoc_files: []
|
40
|
+
files:
|
41
|
+
- lib/gen_text/vm.rb
|
42
|
+
- lib/gen_text/compile.rb
|
43
|
+
- lib/io/with_dummy_pos.rb
|
44
|
+
- README.md
|
45
|
+
- LICENSE
|
46
|
+
- .yardopts
|
47
|
+
- yardopts_extra.rb
|
48
|
+
- bin/gen-text
|
49
|
+
homepage: http://lavirthewhiolet.github.io/gen-text
|
50
|
+
licenses:
|
51
|
+
- MIT
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.9.3
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.8.23
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: A generator of texts from EBNF-like grammars.
|
74
|
+
test_files: []
|
75
|
+
has_rdoc:
|