natural_dsl 0.0.2 → 0.1.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
  SHA256:
3
- metadata.gz: 97856ddab791034315f5feac6ec0bb5648299d2374d2130b49f6601d315a710e
4
- data.tar.gz: 1f1fa4301a53ab7cedbad37cd68ebde01fd77df24fedbb033e30e632a8d5e6ac
3
+ metadata.gz: 358a31294950b2d1d67cf00b45adc4a519023f6d1ca529349af6850dfdcf8f90
4
+ data.tar.gz: 2d32cf63124eadf633e7808cbcc4de5cbd920ad6638c8d67cad395168d486511
5
5
  SHA512:
6
- metadata.gz: 03ead22cfb4c3d7020778bfad056a02c00fc3954a8f99450be770c4bfa79b7344459988bd62037eecac5250921be89afebe2ffb6a1095fb8459e204e846a951b
7
- data.tar.gz: 3cb2097e3e5bbf7df1e51cc508978ea5f3a313b161bb23f6aa4a543792f3651f466c0e42a91aa0debc2c22406bd20b9a9ec5710fae7105ad1c0526b115eb5ed7
6
+ metadata.gz: 64cab2794165a8bf2b2e06284bfd3d3073c76dbcd6ff330f7d6311d108e350200f68d739fd07f53b8b4416f7e7885f02e30e5f67e8a71a8df22c805b1d6de0de
7
+ data.tar.gz: b94d219a355615bf110a6d64a68d3e5340e7195be3ca1f5dcdc850b514bf67ee6d04a940d65499b4022851f97360a8c9e9c30b71524ec0c9f9b28f14fd5124f6
data/CHANGELOG.md CHANGED
@@ -2,3 +2,11 @@
2
2
 
3
3
  ## main
4
4
 
5
+ - [PR [#2](https://github.com/DmitryTsepelev/natural_dsl/pull/2)] Remove value expectation, add with_value modifier ([@DmitryTsepelev][])
6
+ - [PR [#1](https://github.com/DmitryTsepelev/natural_dsl/pull/1)] Web app example, better expectations ([@DmitryTsepelev][])
7
+
8
+ ## 0.2.0 (2022-07-26)
9
+
10
+ - Initial version. ([@DmitryTsepelev][])
11
+
12
+ [@DmitryTsepelev]: https://github.com/DmitryTsepelev
data/README.md CHANGED
@@ -9,7 +9,7 @@ lang = NaturalDSL::Lang.define do
9
9
  token
10
10
  keyword :to
11
11
  token
12
- value :takes
12
+ keyword(:takes).with_value
13
13
 
14
14
  execute do |vm, city1, city2, distance|
15
15
  distances = vm.read_variable(:distances) || {}
@@ -47,4 +47,154 @@ end
47
47
  puts result # => Travel from london to glasgow takes 22 hours
48
48
  ```
49
49
 
50
- Read more about this experiment in by [blog](https://dmitrytsepelev.dev/natural-language-programming-with-ruby).
50
+ Read more about this experiment in my [blog](https://dmitrytsepelev.dev/natural-language-programming-with-ruby).
51
+
52
+ ## Language definition
53
+
54
+ ### Command syntax
55
+
56
+ Each _language_ consists of _commands_. Command can contain _keywords_, _tokens_ and _values_:
57
+
58
+ - _keyword_ is something you want to be in the command to be semantically correct, but you don't need to have it to execute the command (e.g., `to`, `from`, etc.);
59
+ - _token_ is anything that user types, and the typed word will be passed to the execution block;
60
+ - _value_ can be read right after the last keyword or token with `with_value` modifier (e.g., `value 42`).
61
+
62
+ For instance:
63
+
64
+ ```
65
+ keyword token value
66
+ ↓ ↓ ↓
67
+ assign variable a value 1
68
+ ↑ ↑
69
+ command name keyword
70
+ ```
71
+
72
+ ### Command execution
73
+
74
+ Command makes no sense without logic it implements. We can configure it using the _execute_ method: it receives the instance of the current _Virtual Machine_ as well as all tokens and values:
75
+
76
+ ```ruby
77
+ execute do |vm, *args|
78
+ # logic goes here
79
+ end
80
+ ```
81
+
82
+ This is how we can create a very basic command that remembers values:
83
+
84
+ ```ruby
85
+ command :assign do
86
+ keyword :variable
87
+ token
88
+ keyword(:value).with_value
89
+
90
+ execute do |vm, token, value|
91
+ # how to assign?
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Shared data
97
+
98
+ We need to store the data somewhere between commands, and Virtual Machine has that storage, which can be accessed using `assign_variable` and `read_variable`. Here is the whole definition of language that can store and sum variables:
99
+
100
+ ```ruby
101
+ lang = NaturalDSL::Lang.define do
102
+ command :assign do
103
+ keyword :variable
104
+ token
105
+ keyword(:value).with_value
106
+
107
+ execute { |vm, token, value| vm.assign_variable(token, value) }
108
+ end
109
+
110
+ command :sum do
111
+ token
112
+ keyword :with
113
+ token
114
+
115
+ execute do |vm, left, right|
116
+ vm.read_variable(left).value + vm.read_variable(right).value
117
+ end
118
+ end
119
+ end
120
+ ```
121
+
122
+ ### Running languages
123
+
124
+ Finally, we can run the program written in our new DSL using the `VM` class:
125
+
126
+ ```ruby
127
+ NaturalDSL::VM.run(lang) do
128
+ assign variable a value 1
129
+ assign variable b value 2
130
+ sum a with b
131
+ end
132
+ ```
133
+
134
+ ### Multiple primitives
135
+
136
+ Need to consume the unknown amount of similar primitives? Use `zero_or_more`:
137
+
138
+ ```ruby
139
+ lang = NaturalDSL::Lang.define do
140
+ command :expose do
141
+ token.zero_or_more
142
+
143
+ execute { |_, *fields| "exposing #{fields.join(', ')}" }
144
+ end
145
+ end
146
+
147
+ result = NaturalDSL::VM.run(lang) do
148
+ expose id email
149
+ end
150
+
151
+ puts result # => exposing id, email
152
+ ```
153
+
154
+ ### Alternative name for #value
155
+
156
+ Sometimes you don't want to see the word `value` in your commands. In this case you can rename it by passing an argument:
157
+
158
+ ```ruby
159
+ lang = NaturalDSL::Lang.define do
160
+ command :john do
161
+ keyword(:takes).with_value
162
+ execute { |vm, value| vm.assign_variable(:john, value) }
163
+ end
164
+
165
+ command :jane do
166
+ keyword(:takes).with_value
167
+ execute { |vm, value| vm.assign_variable(:jane, value) }
168
+ end
169
+
170
+ command :who do
171
+ keyword :has
172
+ keyword :more
173
+
174
+ execute do |vm|
175
+ name = %i[john jane].max_by { |person| vm.read_variable(person).value }
176
+ "#{name} has more apples!"
177
+ end
178
+ end
179
+ end
180
+
181
+ result = NaturalDSL::VM.run(lang) do
182
+ john takes 2
183
+ jane takes 3
184
+ who has more
185
+ end
186
+
187
+ puts result # => jane has more
188
+ ```
189
+
190
+ ## Installation
191
+
192
+ Add this line to your application's Gemfile, and you're all set:
193
+
194
+ ```ruby
195
+ gem "natural_dsl"
196
+ ```
197
+
198
+ ## License
199
+
200
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,4 +1,5 @@
1
1
  require "natural_dsl/command_runner"
2
+ require "natural_dsl/expectations"
2
3
 
3
4
  module NaturalDSL
4
5
  class Command
@@ -13,7 +14,14 @@ module NaturalDSL
13
14
  end
14
15
 
15
16
  def build(&block)
16
- tap { |command| command.instance_eval(&block) }
17
+ tap do |command|
18
+ command.instance_eval(&block)
19
+
20
+ invalid_expectations = command.expectations[0..-2].select(&:with_value?)
21
+ if invalid_expectations.any?
22
+ raise "Command #{command.name} attempts to consume value after #{invalid_expectations.first}"
23
+ end
24
+ end
17
25
  end
18
26
 
19
27
  def run(vm)
@@ -24,23 +32,18 @@ module NaturalDSL
24
32
  @expectations ||= []
25
33
  end
26
34
 
27
- def value_method_names
28
- @value_method_names ||= []
29
- end
30
-
31
35
  private
32
36
 
33
37
  def token
34
- expectations << Primitives::Token
35
- end
36
-
37
- def value(method_name = :value)
38
- value_method_names << method_name
39
- expectations << Primitives::Value
38
+ Expectations::Token.new.tap do |expectation|
39
+ expectations << expectation
40
+ end
40
41
  end
41
42
 
42
43
  def keyword(type)
43
- expectations << Primitives::Keyword.new(type)
44
+ Expectations::Keyword.new(type).tap do |expectation|
45
+ expectations << expectation
46
+ end
44
47
  end
45
48
 
46
49
  def execute(&block)
@@ -12,7 +12,9 @@ module NaturalDSL
12
12
  end
13
13
 
14
14
  def run
15
- args = @command.expectations.each_with_object([], &method(:check_expectation))
15
+ args = @command.expectations.flat_map do |expectation|
16
+ expectation.read_arguments(@vm.stack)
17
+ end
16
18
 
17
19
  raise_stack_not_empty_error if @vm.stack.any?
18
20
 
@@ -21,14 +23,6 @@ module NaturalDSL
21
23
 
22
24
  private
23
25
 
24
- def check_expectation(expectation, args)
25
- if expectation.is_a?(Primitives::Keyword)
26
- @vm.stack.pop_if_keyword(expectation.type)
27
- else
28
- args << @vm.stack.pop_if(expectation)
29
- end
30
- end
31
-
32
26
  def raise_stack_not_empty_error
33
27
  class_names = @vm.stack.map { |primitive| primitive.class.name.demodulize }
34
28
 
@@ -0,0 +1,58 @@
1
+ module NaturalDSL
2
+ module Expectations
3
+ class Base
4
+ class << self
5
+ def modifiers
6
+ @@modifiers ||= []
7
+ end
8
+
9
+ def modifier(name, conflicts:)
10
+ modifiers << name
11
+
12
+ define_method(name) do
13
+ Array(conflicts).each do |conflict|
14
+ if public_send("#{conflict}?")
15
+ raise "#{name} cannot be configured for #{self.class.name} with #{conflict}"
16
+ end
17
+ end
18
+
19
+ instance_variable_set("@#{name}", true)
20
+ end
21
+
22
+ define_method("#{name}?") { instance_variable_get("@#{name}") }
23
+ end
24
+ end
25
+
26
+ modifier :zero_or_more, conflicts: :with_value
27
+ modifier :with_value, conflicts: :zero_or_more
28
+
29
+ def initialize
30
+ self.class.modifiers.each { |name| instance_variable_set("@#{name}", false) }
31
+ end
32
+
33
+ def read_arguments(stack)
34
+ [].tap do |args|
35
+ if zero_or_more?
36
+ loop do
37
+ arg = perform_read(stack, raise: false)
38
+ break if arg.nil?
39
+
40
+ args << arg unless arg.is_a?(Primitives::Keyword)
41
+ end
42
+ else
43
+ arg = perform_read(stack, raise: true)
44
+ args << arg unless arg.is_a?(Primitives::Keyword)
45
+
46
+ args << stack.pop_if(Primitives::Value, raise: true) if with_value?
47
+ end
48
+ end
49
+ end
50
+
51
+ protected
52
+
53
+ def perform_read(*)
54
+ raise NotImplementedError
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,22 @@
1
+ module NaturalDSL
2
+ module Expectations
3
+ class Keyword < Base
4
+ attr_reader :type
5
+
6
+ def initialize(type)
7
+ @type = type
8
+ super()
9
+ end
10
+
11
+ def to_s
12
+ "keyword :#{type}"
13
+ end
14
+
15
+ protected
16
+
17
+ def perform_read(stack, raise:)
18
+ stack.pop_if_keyword(type, raise: raise)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module NaturalDSL
2
+ module Expectations
3
+ class Token < Base
4
+ def to_s
5
+ "token"
6
+ end
7
+
8
+ protected
9
+
10
+ def perform_read(stack, raise:)
11
+ stack.pop_if(Primitives::Token, raise: raise)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ require "natural_dsl/expectations/base"
2
+ require "natural_dsl/expectations/token"
3
+ require "natural_dsl/expectations/keyword"
@@ -23,11 +23,9 @@ module NaturalDSL
23
23
  private
24
24
 
25
25
  def register_keywords(command)
26
- command.expectations.filter(&method(:keyword?)).each(&method(:register_keyword))
27
- end
28
-
29
- def keyword?(expectation)
30
- expectation.is_a?(Primitives::Keyword)
26
+ command.expectations
27
+ .filter { |expectation| expectation.is_a?(Expectations::Keyword) }
28
+ .each(&method(:register_keyword))
31
29
  end
32
30
 
33
31
  def register_keyword(keyword)
@@ -2,18 +2,21 @@ module NaturalDSL
2
2
  class Stack < Array
3
3
  using StringDemodulize
4
4
 
5
- def pop_if(expected_class)
5
+ def pop_if(expected_class, raise: true)
6
6
  return pop if last.is_a?(expected_class)
7
+ return unless raise
7
8
 
8
9
  error_reason = empty? ? "stack was empty" : "got #{last.class.name.demodulize}"
9
10
  raise "Expected #{expected_class.name.demodulize} but #{error_reason}"
10
11
  end
11
12
 
12
- def pop_if_keyword(keyword_type)
13
- pop_if(Primitives::Keyword).tap do |keyword|
14
- next if keyword.type == keyword_type
15
-
13
+ def pop_if_keyword(keyword_type, raise: true)
14
+ pop_if(Primitives::Keyword, raise: raise).tap do |keyword|
15
+ next if raise == false && keyword.nil? || keyword.type == keyword_type
16
16
  push(keyword)
17
+
18
+ next unless raise
19
+
17
20
  raise "Expected #{keyword_type} but got #{keyword.type}"
18
21
  end
19
22
  end
@@ -1,3 +1,3 @@
1
1
  module NaturalDSL
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -5,7 +5,7 @@ module NaturalDSL
5
5
  class << self
6
6
  def build(lang)
7
7
  lang.commands.each do |command_name, command|
8
- define_command(lang, command_name, command)
8
+ define_method(command_name) { |*| command.run(self) }
9
9
  end
10
10
 
11
11
  new(lang)
@@ -14,18 +14,6 @@ module NaturalDSL
14
14
  def run(lang, &block)
15
15
  build(lang).run(&block)
16
16
  end
17
-
18
- private
19
-
20
- def define_command(lang, command_name, command)
21
- define_method(command_name) { |*| command.run(self) }
22
-
23
- command.value_method_names.each do |value_method_name|
24
- define_method(value_method_name) do |value|
25
- @stack << NaturalDSL::Primitives::Value.new(value)
26
- end
27
- end
28
- end
29
17
  end
30
18
 
31
19
  attr_reader :variables, :stack
@@ -33,7 +21,7 @@ module NaturalDSL
33
21
  def initialize(lang)
34
22
  @lang = lang
35
23
  @variables = {}
36
- @stack = NaturalDSL::Stack.new
24
+ @stack = Stack.new
37
25
  end
38
26
 
39
27
  def run(&block)
@@ -48,18 +36,31 @@ module NaturalDSL
48
36
  @variables[token.name]
49
37
  end
50
38
 
51
- def method_missing(unknown, *args, &block)
39
+ def method_missing(unknown, *values, &block)
52
40
  klass = if @lang.keywords.include?(unknown)
53
- NaturalDSL::Primitives::Keyword
41
+ Primitives::Keyword
54
42
  else
55
- NaturalDSL::Primitives::Token
43
+ Primitives::Token
56
44
  end
57
45
 
46
+ lookup_value_in(values.flatten)
47
+
58
48
  @stack << klass.new(unknown)
59
49
  end
60
50
 
61
51
  def respond_to_missing?(*)
62
52
  true
63
53
  end
54
+
55
+ private
56
+
57
+ def lookup_value_in(values)
58
+ return if values.length != 1
59
+ candidate = values.first
60
+
61
+ return if candidate.is_a?(Primitives::Keyword) || candidate.is_a?(Primitives::Token)
62
+
63
+ @stack << Primitives::Value.new(candidate)
64
+ end
64
65
  end
65
66
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: natural_dsl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DmitryTsepelev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-26 00:00:00.000000000 Z
11
+ date: 2022-07-29 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -23,6 +23,10 @@ files:
23
23
  - lib/natural_dsl.rb
24
24
  - lib/natural_dsl/command.rb
25
25
  - lib/natural_dsl/command_runner.rb
26
+ - lib/natural_dsl/expectations.rb
27
+ - lib/natural_dsl/expectations/base.rb
28
+ - lib/natural_dsl/expectations/keyword.rb
29
+ - lib/natural_dsl/expectations/token.rb
26
30
  - lib/natural_dsl/lang.rb
27
31
  - lib/natural_dsl/primitives.rb
28
32
  - lib/natural_dsl/stack.rb