Skip to content

Instantly share code, notes, and snippets.

@DmitryTsepelev
Created April 30, 2022 20:34
Show Gist options
  • Save DmitryTsepelev/01702a27e86dd774d44998c3a3894dce to your computer and use it in GitHub Desktop.
Save DmitryTsepelev/01702a27e86dd774d44998c3a3894dce to your computer and use it in GitHub Desktop.

Snippets for the article about natural language programming with Ruby

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