gen-text 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --files sample/*
2
+ --load yardopts_extra.rb
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
@@ -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
@@ -0,0 +1,20 @@
1
+
2
+ class IO
3
+
4
+ # {IO} with {IO#pos} returning invalid value.
5
+ class WithDummyPos
6
+
7
+ def self.new(io)
8
+ def io.pos
9
+ 0
10
+ end
11
+ return io
12
+ end
13
+
14
+ end
15
+
16
+ # To disable YARD warnings:
17
+ #@!method pos
18
+ #@!method pos=(p)
19
+
20
+ end
data/yardopts_extra.rb ADDED
@@ -0,0 +1,4 @@
1
+ # encoding: UTF-8
2
+
3
+ # register ".sdl" extension using "pre" markup
4
+ YARD::Templates::Helpers::MarkupHelper::MARKUP_EXTENSIONS[:pre] = ['g']
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: