Snippets for the article about natural language programming with Ruby
Created
April 30, 2022 20:34
-
-
Save DmitryTsepelev/01702a27e86dd774d44998c3a3894dce to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@variables = {} | |
@unknown_token = nil | |
@current_value = nil | |
@with = nil | |
def assign(*); end | |
def variable(*) | |
@variables[@unknown_token] = @current_value | |
end | |
def value(value) | |
@current_value = value | |
end | |
def method_missing(m, *args, &block) | |
@unknown_token = m | |
end | |
def sum(*) | |
result = @variables[@unknown_token] + @with | |
print "#{result}\n" | |
end | |
def with(*) | |
@with = @variables[@unknown_token] | |
end | |
# Program | |
assign variable a value 1 | |
assign variable b value 2 | |
sum a with b |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@variables = {} | |
Value = Struct.new(:value) | |
Token = Struct.new(:name) | |
Keyword = Struct.new(:type) | |
class Stack < Array | |
def pop_if(expected_class) | |
return pop if last.is_a?(expected_class) | |
raise "Expected #{expected_class} but got #{last.class}" | |
end | |
def pop_if_keyword(keyword_type) | |
pop_if(Keyword).tap do |keyword| | |
raise "Expected #{keyword_type} but got #{keyword.type}" unless keyword.type == keyword_type | |
end | |
end | |
end | |
@stack = Stack.new | |
def assign(*) | |
@stack.pop_if_keyword(:variable) | |
token = @stack.pop_if(Token) | |
assignment = @stack.pop_if(Value) | |
@variables[token.name] = assignment.value | |
end | |
def variable(*) | |
@stack << Keyword.new(:variable) | |
end | |
def value(value) | |
@stack << Value.new(value) | |
end | |
def method_missing(token, *args, &block) | |
@stack << Token.new(token) | |
end | |
def sum(*) | |
left = @stack.pop_if(Token) | |
@stack.pop_if_keyword(:with) | |
right = @stack.pop_if(Token) | |
print @variables[left.name] + @variables[right.name] | |
end | |
def with(*) | |
@stack << Keyword.new(:with) | |
end | |
# Program | |
assign variable a value 1 | |
assign variable b value 2 | |
sum a with b |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Value = Struct.new(:value) | |
Token = Struct.new(:name) | |
Keyword = Struct.new(:type) | |
class Stack < Array | |
def pop_if(expected_class) | |
return pop if last.is_a?(expected_class) | |
raise "Expected #{expected_class} but got #{last.class}" | |
end | |
def pop_if_keyword(keyword_type) | |
pop_if(Keyword).tap do |keyword| | |
raise "Expected #{keyword_type} but got #{keyword.type}" unless keyword.type == keyword_type | |
end | |
end | |
end | |
@variables = {} | |
@stack = Stack.new | |
# DSL | |
class Command | |
attr_reader :execution_block | |
def initialize(stack, variables) | |
@stack = stack | |
@variables = variables | |
@expectations = [] | |
end | |
def build(&block) | |
self.tap { |command| command.instance_eval(&block) } | |
end | |
def args | |
@expectations.each_with_object([]) do |expectation, args| | |
if expectation.is_a?(Keyword) | |
@stack.pop_if_keyword(expectation.type) | |
else | |
args << @stack.pop_if(expectation) | |
end | |
end | |
end | |
private | |
def token | |
@expectations << Token | |
end | |
def value | |
@expectations << Value | |
end | |
def keyword(type) | |
@expectations << Keyword.new(type) | |
end | |
def execute(&block) | |
@execution_block = block | |
end | |
end | |
def command(command_name, &block) | |
command = Command.new(@stack, @variables).build(&block) | |
define_method(command_name) do |*| | |
command.execution_block.call(@variables, *command.args) | |
end | |
end | |
# Commands | |
command(:assign) do | |
keyword(:variable) | |
token | |
value | |
execute do |variables, token, value| | |
variables[token.name] = value.value | |
end | |
end | |
command(:sum) do | |
token | |
keyword(:with) | |
token | |
execute do |variables, left, right| | |
result = variables[left.name] + variables[right.name] | |
print "#{result}\n" | |
end | |
end | |
command(:deduct) do | |
token | |
keyword(:from) | |
token | |
execute do |variables, left, right| | |
result = variables[right.name] - variables[left.name] | |
print "#{result}\n" | |
end | |
end | |
# Primitives | |
def variable(*) | |
@stack << Keyword.new(:variable) | |
end | |
def value(value) | |
@stack << Value.new(value) | |
end | |
def method_missing(token, *args, &block) | |
@stack << Token.new(token) | |
end | |
def with(*) | |
@stack << Keyword.new(:with) | |
end | |
def from(*) | |
@stack << Keyword.new(:from) | |
end | |
# Program | |
assign variable a value 1 | |
assign variable b value 2 | |
sum a with b | |
assign variable x value 12 | |
assign variable y value 5 | |
deduct y from x |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Value = Struct.new(:value) | |
Token = Struct.new(:name) | |
Keyword = Struct.new(:type) | |
class Stack < Array | |
def pop_if(expected_class) | |
return pop if last.is_a?(expected_class) | |
raise "Expected #{expected_class} but got #{last.class}" | |
end | |
def pop_if_keyword(keyword_type) | |
pop_if(Keyword).tap do |keyword| | |
raise "Expected #{keyword_type} but got #{keyword.type}" unless keyword.type == keyword_type | |
end | |
end | |
end | |
class Command | |
attr_reader :execution_block | |
def self.build(&block) | |
new.build(&block) | |
end | |
def build(&block) | |
self.tap { |command| command.instance_eval(&block) } | |
end | |
def run(vm) | |
args = expectations.each_with_object([]) do |expectation, args| | |
if expectation.is_a?(Keyword) | |
vm.stack.pop_if_keyword(expectation.type) | |
else | |
args << vm.stack.pop_if(expectation) | |
end | |
end | |
execution_block.call(vm.variables, *args) | |
end | |
private | |
def token | |
expectations << Token | |
end | |
def value | |
expectations << Value | |
end | |
def keyword(type) | |
expectations << Keyword.new(type) | |
end | |
def execute(&block) | |
@execution_block = block | |
end | |
def expectations | |
@expectations ||= [] | |
end | |
end | |
class VM | |
attr_reader :variables, :stack | |
def initialize | |
@variables = {} | |
@stack = Stack.new | |
end | |
def run(&block) | |
instance_eval(&block) | |
end | |
class << self | |
def command(command_name, &block) | |
define_method(command_name) { |*| Command.build(&block).run(self) } | |
end | |
def run(&block) | |
new.run(&block) | |
end | |
end | |
# Commands | |
command(:assign) do | |
keyword(:variable) | |
token | |
value | |
execute do |variables, token, value| | |
variables[token.name] = value.value | |
end | |
end | |
command(:sum) do | |
token | |
keyword(:with) | |
token | |
execute do |variables, left, right| | |
result = variables[left.name] + variables[right.name] | |
print "#{result}\n" | |
end | |
end | |
command(:deduct) do | |
token | |
keyword(:from) | |
token | |
execute do |variables, left, right| | |
result = variables[right.name] - variables[left.name] | |
print "#{result}\n" | |
end | |
end | |
# Primitives | |
def variable(*) | |
@stack << Keyword.new(:variable) | |
end | |
def value(value) | |
@stack << Value.new(value) | |
end | |
def method_missing(token, *args, &block) | |
@stack << Token.new(token) | |
end | |
def with(*) | |
@stack << Keyword.new(:with) | |
end | |
def from(*) | |
@stack << Keyword.new(:from) | |
end | |
end | |
# Program | |
VM.run do | |
assign variable a value 1 | |
assign variable b value 2 | |
sum a with b | |
assign variable x value 12 | |
assign variable y value 5 | |
deduct y from x | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Value = Struct.new(:value) | |
Token = Struct.new(:name) | |
Keyword = Struct.new(:type) | |
class Stack < Array | |
def pop_if(expected_class) | |
return pop if last.is_a?(expected_class) | |
raise "Expected #{expected_class} but got #{last.class}" | |
end | |
def pop_if_keyword(keyword_type) | |
pop_if(Keyword).tap do |keyword| | |
raise "Expected #{keyword_type} but got #{keyword.type}" unless keyword.type == keyword_type | |
end | |
end | |
end | |
class Command | |
attr_reader :execution_block | |
def self.build(command_name, &block) | |
new(command_name).build(&block) | |
end | |
def initialize(command_name) | |
@command_name = command_name | |
end | |
def build(&block) | |
self.tap { |command| command.instance_eval(&block) } | |
end | |
def run(vm) | |
args = expectations.each_with_object([]) do |expectation, args| | |
if expectation.is_a?(Keyword) | |
vm.stack.pop_if_keyword(expectation.type) | |
else | |
args << vm.stack.pop_if(expectation) | |
end | |
end | |
raise "unexpected #{vm.stack.map(&:class).join(' ')} after #{@command_name}" if vm.stack.any? | |
execution_block.call(vm, *args) | |
end | |
def expectations | |
@expectations ||= [] | |
end | |
private | |
def token | |
expectations << Token | |
end | |
def value | |
expectations << Value | |
end | |
def keyword(type) | |
expectations << Keyword.new(type) | |
end | |
def execute(&block) | |
@execution_block = block | |
end | |
end | |
class VM | |
def self.run(lang, &block) | |
lang.commands.each do |command_name, command| | |
define_method(command_name) { |*| command.run(self) } | |
end | |
new(lang).run(&block) | |
end | |
attr_reader :variables, :stack | |
def initialize(lang) | |
@lang = lang | |
@variables = {} | |
@stack = Stack.new | |
end | |
def run(&block) | |
instance_eval(&block) | |
end | |
def assign_variable(token, value) | |
@variables[token.name] = value.value | |
end | |
def read_variable(token) | |
@variables[token.name] | |
end | |
def value(value) | |
@stack << Value.new(value) | |
end | |
def method_missing(unknown, *args, &block) | |
klass = @lang.keywords.include?(unknown) ? Keyword : Token | |
@stack << klass.new(unknown) | |
end | |
end | |
class Lang | |
def self.define(&block) | |
new.tap { |lang| lang.instance_eval(&block) } | |
end | |
def command(command_name, &block) | |
command = Command.build(command_name, &block) | |
register_keywords(command) | |
commands[command_name] = command | |
end | |
def keywords | |
@keywords ||= [] | |
end | |
def commands | |
@commands ||= {} | |
end | |
private | |
def register_keywords(command) | |
command.expectations | |
.filter { |expectation| expectation.is_a?(Keyword) } | |
.reject { |keyword| keywords.include?(keyword.type) } | |
.each { |keyword| keywords << keyword.type } | |
end | |
end | |
# Language | |
lang = Lang.define do | |
command :assign do | |
keyword :variable | |
token | |
value | |
execute { |vm, token, value| vm.assign_variable(token, value) } | |
end | |
command :sum do | |
token | |
keyword :with | |
token | |
execute do |vm, left, right| | |
result = vm.read_variable(left) + vm.read_variable(right) | |
print "#{result}\n" | |
end | |
end | |
command :deduct do | |
token | |
keyword :from | |
token | |
execute do |vm, left, right| | |
result = vm.read_variable(right) - vm.read_variable(left) | |
print "#{result}\n" | |
end | |
end | |
end | |
# Program | |
VM.run(lang) do | |
assign variable a value 1 | |
assign variable b value 2 | |
sum a with b | |
assign variable x value 12 | |
assign variable y value 5 | |
deduct y from x | |
end | |
# Language 2 | |
lang2 = Lang.define do | |
command(:set) do | |
keyword :variable | |
token | |
keyword :to | |
value | |
execute { |vm, token, value| vm.assign_variable(token, value) } | |
end | |
command(:access) do | |
keyword :variable | |
token | |
execute do |vm, token| | |
result = vm.read_variable(token) | |
print "#{result}\n" | |
end | |
end | |
end | |
# Program 2 | |
VM.run(lang2) do | |
set variable a to value 42 | |
access variable a | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Value = Struct.new(:value) | |
Token = Struct.new(:name) | |
Keyword = Struct.new(:type) | |
class Stack < Array | |
def pop_if(expected_class) | |
return pop if last.is_a?(expected_class) | |
raise "Expected #{expected_class} but got #{last.class}" | |
end | |
def pop_if_keyword(keyword_type) | |
pop_if(Keyword).tap do |keyword| | |
raise "Expected #{keyword_type} but got #{keyword.type}" unless keyword.type == keyword_type | |
end | |
end | |
end | |
class Command | |
attr_reader :execution_block, :value_method_names | |
def self.build(command_name, &block) | |
new(command_name).build(&block) | |
end | |
def initialize(command_name) | |
@command_name = command_name | |
end | |
def build(&block) | |
self.tap { |command| command.instance_eval(&block) } | |
end | |
def run(vm) | |
args = expectations.each_with_object([]) do |expectation, args| | |
if expectation.is_a?(Keyword) | |
vm.stack.pop_if_keyword(expectation.type) | |
else | |
args << vm.stack.pop_if(expectation) | |
end | |
end | |
raise "unexpected #{vm.stack.map(&:class).join(' ')} after #{@command_name}" if vm.stack.any? | |
execution_block.call(vm, *args) | |
end | |
def expectations | |
@expectations ||= [] | |
end | |
def value_method_names | |
@value_method_names ||= [] | |
end | |
private | |
def token | |
expectations << Token | |
end | |
def value(method_name) | |
value_method_names << method_name | |
expectations << Value | |
end | |
def keyword(type) | |
expectations << Keyword.new(type) | |
end | |
def execute(&block) | |
@execution_block = block | |
end | |
end | |
class VM | |
def self.run(lang, &block) | |
lang.commands.each do |command_name, command| | |
define_method(command_name) { |*| command.run(self) } | |
command.value_method_names.each do |value_method_name| | |
define_method(value_method_name) do |value| | |
@stack << Value.new(value) | |
end | |
end | |
end | |
new(lang).run(&block) | |
end | |
attr_reader :variables, :stack | |
def initialize(lang) | |
@lang = lang | |
@variables = {} | |
@stack = Stack.new | |
end | |
def run(&block) | |
instance_eval(&block) | |
end | |
def assign_variable(token, value) | |
@variables[token.name] = value.value | |
end | |
def read_variable(token) | |
@variables[token.name] | |
end | |
def method_missing(unknown, *args, &block) | |
klass = @lang.keywords.include?(unknown) ? Keyword : Token | |
@stack << klass.new(unknown) | |
end | |
end | |
class Lang | |
def self.define(&block) | |
new.tap { |lang| lang.instance_eval(&block) } | |
end | |
def command(command_name, &block) | |
command = Command.build(command_name, &block) | |
register_keywords(command) | |
commands[command_name] = command | |
end | |
def keywords | |
@keywords ||= [] | |
end | |
def commands | |
@commands ||= {} | |
end | |
private | |
def register_keywords(command) | |
command.expectations | |
.filter { |expectation| expectation.is_a?(Keyword) } | |
.reject { |keyword| keywords.include?(keyword.type) } | |
.each { |keyword| keywords << keyword.type } | |
end | |
end | |
# Language | |
lang = Lang.define do | |
command :route do | |
keyword :from | |
token | |
keyword :to | |
token | |
value :takes | |
execute do |vm, city1, city2, distance| | |
distances = vm.read_variable(:distances) || {} | |
distances[[city1, city2]] = distance | |
vm.assign_variable(:distances, Value.new(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 | |
puts "Travel from #{city1.name} to #{city2.name} takes #{distance} hours" | |
end | |
end | |
end | |
# Program | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment