calyx 0.9.4 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bde85e189408ceafef6e5e3c4904a2ee8fb8afde
4
- data.tar.gz: 6e806f06faea71b4faf8b5022179a1d4ddafbd0e
3
+ metadata.gz: c30774b8a6567532e9ee613185e1f4505665e197
4
+ data.tar.gz: fd92cdc99522713e076ae9161d855c18eb42058f
5
5
  SHA512:
6
- metadata.gz: c134603b50bdee4656209002a7f967a4e76b5c81ba300bbd36996274ae07f1c2e12bfa26a0a1d003dd1472f8dbb14d5b9ac163a4b264ef03190ebde1ab99496a
7
- data.tar.gz: 7e5f60627784213ecfe65b1ba7f3009e1ce282736ee5c2ca0f8510537825ac3c7241304738233da8a1124482c654ee954bacb0aa0e0348fcd870983891ea16cc
6
+ metadata.gz: de2c0e4685357546aeda4474c5b16be15c1461c517d4c1ca0210ce21a85e191eeb23be760ac7660a48518b764e4c12c88f8a8e61e5487440a6d5d40f848784e7
7
+ data.tar.gz: 459fcb3685caeb784df815592ad1e0da37d82bbf29def422c27e90b90d7ccf9e8168115f6e484b6a99c835fa7106268b8c4171c10bd3f7af3bc194e1c7cc0798
data/.gitignore CHANGED
@@ -8,3 +8,5 @@
8
8
  /spec/reports/
9
9
  /tmp/
10
10
  /*.gem
11
+ .DS_Store
12
+ NOTES*
data/README.md CHANGED
@@ -94,7 +94,7 @@ Nesting and hierarchy can be manipulated to balance consistency with novelty. Th
94
94
 
95
95
  ```ruby
96
96
  module HelloWorld
97
- Sentiment < Calyx::Grammar
97
+ class Sentiment < Calyx::Grammar
98
98
  start '{happy_phrase}', '{sad_phrase}'
99
99
  rule :happy_phrase, '{happy_greeting} {happy_adj} world.'
100
100
  rule :happy_greeting, 'Hello', 'Hi', 'Hey', 'Yo'
@@ -104,7 +104,7 @@ module HelloWorld
104
104
  rule :sad_adj, 'cruel', 'miserable'
105
105
  end
106
106
 
107
- Mixed < Calyx::Grammar
107
+ class Mixed < Calyx::Grammar
108
108
  start '{greeting} {adj} world.'
109
109
  rule :greeting, 'Hello', 'Hi', 'Hey', 'Yo', 'Goodbye', 'So long', 'Farewell'
110
110
  rule :adj, 'wonderful', 'amazing', 'bright', 'beautiful', 'cruel', 'miserable'
@@ -219,6 +219,36 @@ end
219
219
  # => "A pear."
220
220
  ```
221
221
 
222
+ ### Memoized Rules
223
+
224
+ Rule expansions can be ‘memoized’ so that multiple references to the same rule return the same value. This is useful for picking a noun from a list and reusing it in multiple places within a text.
225
+
226
+ The `@` symbol is used to mark memoized rules. This evaluates the rule and stores it in memory the first time it’s referenced. All subsequent references to the memoized rule use the same stored value.
227
+
228
+ ```ruby
229
+ # Without memoization
230
+ grammar = Calyx::Grammar.new do
231
+ start '{name} <{name.downcase}>'
232
+ name 'Daenerys', 'Tyrion', 'Jon'
233
+ end
234
+
235
+ 3.times { grammar.generate }
236
+ # => Daenerys <jon>
237
+ # => Tyrion <daenerys>
238
+ # => Jon <tyrion>
239
+
240
+ # With memoization
241
+ grammar = Calyx::Grammar.new do
242
+ start '{@name} <{@name.downcase}>'
243
+ name 'Daenerys', 'Tyrion', 'Jon'
244
+ end
245
+
246
+ 3.times { grammar.generate }
247
+ # => Tyrion <tyrion>
248
+ # => Daenerys <daenerys>
249
+ # => Jon <jon>
250
+ ```
251
+
222
252
  ### Dynamically Constructing Rules
223
253
 
224
254
  Template expansions can be dynamically constructed at runtime by passing a context map of rules to the `#generate` method:
@@ -0,0 +1,3 @@
1
+ module Calyx
2
+ class RuleNotFound < NoMethodError; end
3
+ end
@@ -26,10 +26,12 @@ module Calyx
26
26
  build_grammar(json)
27
27
  end
28
28
 
29
- def build_grammar(hash)
29
+ private
30
+
31
+ def build_grammar(rules)
30
32
  Calyx::Grammar.new do
31
- hash.each do |rule_name, rule_productions|
32
- rule(rule_name, *rule_productions)
33
+ rules.each do |label, productions|
34
+ rule(label, *productions)
33
35
  end
34
36
  end
35
37
  end
data/lib/calyx/grammar.rb CHANGED
@@ -5,7 +5,11 @@ module Calyx
5
5
  @registry ||= Registry.new
6
6
  end
7
7
 
8
- def rule(name, *productions, &production)
8
+ def mapping(name, pairs)
9
+ registry.mapping(name, pairs)
10
+ end
11
+
12
+ def rule(name, *productions)
9
13
  registry.rule(name, *productions)
10
14
  end
11
15
 
@@ -0,0 +1,30 @@
1
+ module Calyx
2
+ module Production
3
+ class Choices
4
+ def self.parse(productions, registry)
5
+ choices = productions.map do |choice|
6
+ if choice.is_a?(String)
7
+ Concat.parse(choice, registry)
8
+ elsif choice.is_a?(Fixnum)
9
+ Terminal.new(choice.to_s)
10
+ elsif choice.is_a?(Symbol)
11
+ if choice[0] == Memo::SIGIL
12
+ Memo.new(choice, registry)
13
+ else
14
+ NonTerminal.new(choice, registry)
15
+ end
16
+ end
17
+ end
18
+ self.new(choices)
19
+ end
20
+
21
+ def initialize(collection)
22
+ @collection = collection
23
+ end
24
+
25
+ def evaluate
26
+ [:choice, @collection.sample.evaluate]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ module Calyx
2
+ module Production
3
+ class Concat
4
+ EXPRESSION = /(\{[A-Za-z0-9_@\.]+\})/.freeze
5
+ START_TOKEN = '{'.freeze
6
+ END_TOKEN = '}'.freeze
7
+ DEREF_TOKEN = '.'.freeze
8
+
9
+ def self.parse(production, registry)
10
+ expansion = production.split(EXPRESSION).map do |atom|
11
+ if atom.is_a?(String)
12
+ if atom.chars.first == START_TOKEN && atom.chars.last == END_TOKEN
13
+ head, *tail = atom.slice(1, atom.length-2).split(DEREF_TOKEN)
14
+ if head[0] == Memo::SIGIL
15
+ rule = Memo.new(head, registry)
16
+ else
17
+ rule = NonTerminal.new(head, registry)
18
+ end
19
+ unless tail.empty?
20
+ Expression.new(rule, tail, registry)
21
+ else
22
+ rule
23
+ end
24
+ else
25
+ Terminal.new(atom)
26
+ end
27
+ end
28
+ end
29
+
30
+ self.new(expansion)
31
+ end
32
+
33
+ def initialize(expansion)
34
+ @expansion = expansion
35
+ end
36
+
37
+ def evaluate
38
+ concat = @expansion.reduce([]) do |exp, atom|
39
+ exp << atom.evaluate
40
+ end
41
+
42
+ [:concat, concat]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,20 @@
1
+ module Calyx
2
+ module Production
3
+ class Expression
4
+ def initialize(production, methods, registry)
5
+ @production = production
6
+ @methods = methods.map { |m| m.to_sym }
7
+ @registry = registry
8
+ end
9
+
10
+ def evaluate
11
+ terminal = @production.evaluate.flatten.reject { |o| o.is_a?(Symbol) }.join(''.freeze)
12
+ expression = @methods.reduce(terminal) do |value, method|
13
+ @registry.transform(method, value)
14
+ end
15
+
16
+ [:expression, expression]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,16 @@
1
+ module Calyx
2
+ module Production
3
+ class Memo
4
+ SIGIL = '@'.freeze
5
+
6
+ def initialize(symbol, registry)
7
+ @symbol = symbol.slice(1, symbol.length-1).to_sym
8
+ @registry = registry
9
+ end
10
+
11
+ def evaluate
12
+ [@symbol, @registry.memoize_expansion(@symbol)]
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ module Calyx
2
+ module Production
3
+ class NonTerminal
4
+ def initialize(symbol, registry)
5
+ @symbol = symbol.to_sym
6
+ @registry = registry
7
+ end
8
+
9
+ def evaluate
10
+ [@symbol, @registry.expand(@symbol).evaluate]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Calyx
2
+ module Production
3
+ class Terminal
4
+ def initialize(atom)
5
+ @atom = atom
6
+ end
7
+
8
+ def evaluate
9
+ [:atom, @atom]
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,35 @@
1
+ module Calyx
2
+ module Production
3
+ class WeightedChoices
4
+ def self.parse(productions, registry)
5
+ weights_sum = productions.reduce(0) do |memo, choice|
6
+ memo += choice.last
7
+ end
8
+
9
+ raise 'Weights must sum to 1' if weights_sum != 1.0
10
+
11
+ choices = productions.map do |choice, weight|
12
+ if choice.is_a?(String)
13
+ [Concat.parse(choice, registry), weight]
14
+ elsif choice.is_a?(Symbol)
15
+ [NonTerminal.new(choice, registry), weight]
16
+ end
17
+ end
18
+
19
+ self.new(choices)
20
+ end
21
+
22
+ def initialize(collection)
23
+ @collection = collection
24
+ end
25
+
26
+ def evaluate
27
+ choice = @collection.max_by do |_, weight|
28
+ rand ** (1.0 / weight)
29
+ end.first
30
+
31
+ [:weighted_choice, choice.evaluate]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,91 @@
1
+ module Calyx
2
+ class Registry
3
+ attr_reader :rules, :mappings
4
+
5
+ def initialize
6
+ @rules = {}
7
+ @mappings = {}
8
+ end
9
+
10
+ def method_missing(name, *arguments)
11
+ rule(name, *arguments)
12
+ end
13
+
14
+ def mapping(name, pairs)
15
+ mappings[name.to_sym] = construct_mapping(pairs)
16
+ end
17
+
18
+ def rule(name, *productions)
19
+ rules[name.to_sym] = construct_rule(productions)
20
+ end
21
+
22
+ def expand(symbol)
23
+ rules[symbol] || context[symbol]
24
+ end
25
+
26
+ def transform(name, value)
27
+ if value.respond_to?(name)
28
+ value.send(name)
29
+ else
30
+ mappings[name].call(value)
31
+ end
32
+ end
33
+
34
+ def memoize_expansion(symbol)
35
+ memos[symbol] ||= expand(symbol).evaluate
36
+ end
37
+
38
+ def combine(registry)
39
+ @rules = rules.merge(registry.rules)
40
+ end
41
+
42
+ def evaluate(start_symbol=:start, rules_map={})
43
+ reset_evaluation_context
44
+
45
+ rules_map.each do |key, value|
46
+ if rules.key?(key.to_sym)
47
+ raise "Rule already declared in grammar: #{key}"
48
+ end
49
+
50
+ context[key.to_sym] = if value.is_a?(Array)
51
+ Production::Choices.parse(value, self)
52
+ else
53
+ Production::Concat.parse(value.to_s, self)
54
+ end
55
+ end
56
+
57
+ expansion = expand(start_symbol)
58
+
59
+ if expansion.respond_to?(:evaluate)
60
+ [start_symbol, expansion.evaluate]
61
+ else
62
+ raise RuleNotFound.new(start_symbol)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :memos, :context
69
+
70
+ def reset_evaluation_context
71
+ @context = {}
72
+ @memos = {}
73
+ end
74
+
75
+ def construct_mapping(pairs)
76
+ mapper = -> (input) {
77
+ pairs.each { |match,target|
78
+ return input.gsub(match, target) if input =~ match
79
+ }
80
+ }
81
+ end
82
+
83
+ def construct_rule(productions)
84
+ if productions.first.is_a?(Enumerable)
85
+ Production::WeightedChoices.parse(productions, self)
86
+ else
87
+ Production::Choices.parse(productions, self)
88
+ end
89
+ end
90
+ end
91
+ end
data/lib/calyx/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Calyx
2
- VERSION = '0.9.4'.freeze
2
+ VERSION = '0.10.0'.freeze
3
3
  end
data/lib/calyx.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  require 'calyx/version'
2
2
  require 'calyx/grammar'
3
+ require 'calyx/errors'
3
4
  require 'calyx/file_converter'
4
- require 'calyx/grammar/registry'
5
- require 'calyx/grammar/production/memo'
6
- require 'calyx/grammar/production/choices'
7
- require 'calyx/grammar/production/concat'
8
- require 'calyx/grammar/production/expression'
9
- require 'calyx/grammar/production/non_terminal'
10
- require 'calyx/grammar/production/terminal'
11
- require 'calyx/grammar/production/weighted_choices'
5
+ require 'calyx/registry'
6
+ require 'calyx/production/memo'
7
+ require 'calyx/production/choices'
8
+ require 'calyx/production/concat'
9
+ require 'calyx/production/expression'
10
+ require 'calyx/production/non_terminal'
11
+ require 'calyx/production/terminal'
12
+ require 'calyx/production/weighted_choices'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: calyx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Rickerby
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-05-10 00:00:00.000000000 Z
11
+ date: 2016-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -68,16 +68,17 @@ files:
68
68
  - calyx.gemspec
69
69
  - examples/tiny_woodland.rb
70
70
  - lib/calyx.rb
71
+ - lib/calyx/errors.rb
71
72
  - lib/calyx/file_converter.rb
72
73
  - lib/calyx/grammar.rb
73
- - lib/calyx/grammar/production/choices.rb
74
- - lib/calyx/grammar/production/concat.rb
75
- - lib/calyx/grammar/production/expression.rb
76
- - lib/calyx/grammar/production/memo.rb
77
- - lib/calyx/grammar/production/non_terminal.rb
78
- - lib/calyx/grammar/production/terminal.rb
79
- - lib/calyx/grammar/production/weighted_choices.rb
80
- - lib/calyx/grammar/registry.rb
74
+ - lib/calyx/production/choices.rb
75
+ - lib/calyx/production/concat.rb
76
+ - lib/calyx/production/expression.rb
77
+ - lib/calyx/production/memo.rb
78
+ - lib/calyx/production/non_terminal.rb
79
+ - lib/calyx/production/terminal.rb
80
+ - lib/calyx/production/weighted_choices.rb
81
+ - lib/calyx/registry.rb
81
82
  - lib/calyx/version.rb
82
83
  homepage: https://github.com/maetl/calyx
83
84
  licenses:
@@ -1,32 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class Choices
5
- def self.parse(productions, registry)
6
- choices = productions.map do |choice|
7
- if choice.is_a?(String)
8
- Concat.parse(choice, registry)
9
- elsif choice.is_a?(Fixnum)
10
- Terminal.new(choice.to_s)
11
- elsif choice.is_a?(Symbol)
12
- if choice[0] == Memo::SIGIL
13
- Memo.new(choice, registry)
14
- else
15
- NonTerminal.new(choice, registry)
16
- end
17
- end
18
- end
19
- self.new(choices)
20
- end
21
-
22
- def initialize(collection)
23
- @collection = collection
24
- end
25
-
26
- def evaluate
27
- [:choice, @collection.sample.evaluate]
28
- end
29
- end
30
- end
31
- end
32
- end
@@ -1,48 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class Concat
5
- EXPRESSION = /(\{[A-Za-z0-9_@\.]+\})/.freeze
6
- START_TOKEN = '{'.freeze
7
- END_TOKEN = '}'.freeze
8
- DEREF_TOKEN = '.'.freeze
9
-
10
- def self.parse(production, registry)
11
- expansion = production.split(EXPRESSION).map do |atom|
12
- if atom.is_a?(String)
13
- if atom.chars.first == START_TOKEN && atom.chars.last == END_TOKEN
14
- head, *tail = atom.slice(1, atom.length-2).split(DEREF_TOKEN)
15
- if head[0] == Memo::SIGIL
16
- rule = Memo.new(head, registry)
17
- else
18
- rule = NonTerminal.new(head, registry)
19
- end
20
- unless tail.empty?
21
- Expression.new(rule, tail)
22
- else
23
- rule
24
- end
25
- else
26
- Terminal.new(atom)
27
- end
28
- end
29
- end
30
-
31
- self.new(expansion)
32
- end
33
-
34
- def initialize(expansion)
35
- @expansion = expansion
36
- end
37
-
38
- def evaluate
39
- concat = @expansion.reduce([]) do |exp, atom|
40
- exp << atom.evaluate
41
- end
42
-
43
- [:concat, concat]
44
- end
45
- end
46
- end
47
- end
48
- end
@@ -1,21 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class Expression
5
- def initialize(production, methods)
6
- @production = production
7
- @methods = methods.map { |m| m.to_sym }
8
- end
9
-
10
- def evaluate
11
- terminal = @production.evaluate.flatten.reject { |o| o.is_a?(Symbol) }.join(''.freeze)
12
- expression = @methods.reduce(terminal) do |value,method|
13
- value.send(method)
14
- end
15
-
16
- [:expression, expression]
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,18 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class Memo
5
- SIGIL = '@'.freeze
6
-
7
- def initialize(symbol, registry)
8
- @symbol = symbol.slice(1, symbol.length-1).to_sym
9
- @registry = registry
10
- end
11
-
12
- def evaluate
13
- [@symbol, @registry.memoize_expansion(@symbol)]
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,16 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class NonTerminal
5
- def initialize(symbol, registry)
6
- @symbol = symbol.to_sym
7
- @registry = registry
8
- end
9
-
10
- def evaluate
11
- [@symbol, @registry.expand(@symbol).evaluate]
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,15 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class Terminal
5
- def initialize(atom)
6
- @atom = atom
7
- end
8
-
9
- def evaluate
10
- [:atom, @atom]
11
- end
12
- end
13
- end
14
- end
15
- end
@@ -1,37 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- module Production
4
- class WeightedChoices
5
- def self.parse(productions, registry)
6
- weights_sum = productions.reduce(0) do |memo, choice|
7
- memo += choice.last
8
- end
9
-
10
- raise 'Weights must sum to 1' if weights_sum != 1.0
11
-
12
- choices = productions.map do |choice, weight|
13
- if choice.is_a?(String)
14
- [Concat.parse(choice, registry), weight]
15
- elsif choice.is_a?(Symbol)
16
- [NonTerminal.new(choice, registry), weight]
17
- end
18
- end
19
-
20
- self.new(choices)
21
- end
22
-
23
- def initialize(collection)
24
- @collection = collection
25
- end
26
-
27
- def evaluate
28
- choice = @collection.max_by do |_, weight|
29
- rand ** (1.0 / weight)
30
- end.first
31
-
32
- [:weighted_choice, choice.evaluate]
33
- end
34
- end
35
- end
36
- end
37
- end
@@ -1,64 +0,0 @@
1
- module Calyx
2
- class Grammar
3
- class Registry
4
- attr_reader :rules, :memos
5
-
6
- def initialize
7
- @rules = {}
8
- end
9
-
10
- def method_missing(name, *arguments)
11
- rule(name, *arguments)
12
- end
13
-
14
- def rule(name, *productions, &production)
15
- @rules[name.to_sym] = construct_rule(productions)
16
- end
17
-
18
- def expand(symbol)
19
- @rules[symbol] || @context[symbol]
20
- end
21
-
22
- def memoize_expansion(symbol)
23
- memos[symbol] ||= expand(symbol).evaluate
24
- end
25
-
26
- def combine(registry)
27
- @rules = rules.merge(registry.rules)
28
- end
29
-
30
- def evaluate(start_symbol=:start, rules_map={})
31
- reset_state
32
-
33
- rules_map.each do |key, value|
34
- if rules.key?(key.to_sym)
35
- raise "Rule already declared in grammar: #{key}"
36
- end
37
-
38
- @context[key.to_sym] = if value.is_a?(Array)
39
- Production::Choices.parse(value, self)
40
- else
41
- Production::Concat.parse(value.to_s, self)
42
- end
43
- end
44
-
45
- [start_symbol, expand(start_symbol).evaluate]
46
- end
47
-
48
- private
49
-
50
- def reset_state
51
- @context = {}
52
- @memos = {}
53
- end
54
-
55
- def construct_rule(productions)
56
- if productions.first.is_a?(Enumerable)
57
- Production::WeightedChoices.parse(productions, self)
58
- else
59
- Production::Choices.parse(productions, self)
60
- end
61
- end
62
- end
63
- end
64
- end