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 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: