NaturalDSL
An experimental (and highly likely useless for real–world) DSL to build a natural–ish DSL language and write your programs using it. Right to the example:
lang = NaturalDSL::Lang.define do
command :route do
keyword :from
token
keyword :to
token
keyword(:takes).with_value
execute do |vm, city1, city2, distance|
distances = vm.read_variable(:distances) || {}
distances[[city1, city2]] = distance
vm.assign_variable(:distances, distances)
end
end
command :how do
keyword :long
keyword :will
keyword :it
keyword :take
keyword :to
keyword :get
keyword :from
token
keyword :to
token
execute do |vm, city1, city2|
distances = vm.read_variable(:distances) || {}
distance = distances[[city1, city2]].value
"Travel from #{city1.name} to #{city2.name} takes #{distance} hours"
end
end
end
result = NaturalDSL::VM.run(lang) do
route from london to glasgow takes 22
route from paris to prague takes 12
how long will it take to get from london to glasgow
end
puts result # => Travel from london to glasgow takes 22 hours
Read more about this experiment in my blog.
Language definition
Command syntax
Each language consists of commands. Command can contain keywords, tokens and values:
- 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.); - token is anything that user types, and the typed word will be passed to the execution block;
- value can be read right after the last keyword or token with
with_value
modifier (e.g.,value 42
).
For instance:
keyword token value
↓ ↓ ↓
assign variable a value 1
↑ ↑
command name keyword
Command execution
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:
execute do |vm, *args|
# logic goes here
end
This is how we can create a very basic command that remembers values:
command :assign do
keyword :variable
token
keyword(:value).with_value
execute do |vm, token, value|
# how to assign?
end
end
Shared data
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:
lang = NaturalDSL::Lang.define do
command :assign do
keyword :variable
token
keyword(:value).with_value
execute { |vm, token, value| vm.assign_variable(token, value) }
end
command :sum do
token
keyword :with
token
execute do |vm, left, right|
vm.read_variable(left).value + vm.read_variable(right).value
end
end
end
Running languages
Finally, we can run the program written in our new DSL using the VM
class:
NaturalDSL::VM.run(lang) do
assign variable a value 1
assign variable b value 2
sum a with b
end
Multiple primitives
Need to consume the unknown amount of similar primitives? Use zero_or_more
:
lang = NaturalDSL::Lang.define do
command :expose do
token.zero_or_more
execute { |_, *fields| "exposing #{fields.join(', ')}" }
end
end
result = NaturalDSL::VM.run(lang) do
expose id email
end
puts result # => exposing id, email
Alternative name for #value
Sometimes you don't want to see the word value
in your commands. In this case you can rename it by passing an argument:
lang = NaturalDSL::Lang.define do
command :john do
keyword(:takes).with_value
execute { |vm, value| vm.assign_variable(:john, value) }
end
command :jane do
keyword(:takes).with_value
execute { |vm, value| vm.assign_variable(:jane, value) }
end
command :who do
keyword :has
keyword :more
execute do |vm|
name = %i[john jane].max_by { |person| vm.read_variable(person).value }
"#{name} has more apples!"
end
end
end
result = NaturalDSL::VM.run(lang) do
john takes 2
jane takes 3
who has more
end
puts result # => jane has more
Want some fun?
Here are a couple of ideas to work on
Optional parts
Let's allow parts that can be omitted:
lang = NaturalDSL::Lang.define do
command :assign do
keyword(:variable).optional
token
keyword(:value).with_value
execute { |vm, token, value| vm.assign_variable(token, value) }
end
end
result = NaturalDSL::VM.run(lang) do
assign variable a value 1
assign b value 2
end
Subcommands
What if I want to start two commands with the same word? Example:
lang = NaturalDSL::Lang.define do
command :mov do
option do
token.with_value
execute do |vm, register, value|
# do constant assigment
end
end
option do
token
token
execute do |vm, register, register_with_value|
# copy value from register
end
end
end
end
NaturalDSL::VM.run(lang) do
mov a 9
mov a b
end
Installation
Add this line to your application's Gemfile, and you're all set:
gem "natural_dsl"
License
The gem is available as open source under the terms of the MIT License.