Skip to content

Instantly share code, notes, and snippets.

@bew
Created October 1, 2017 12:16
Show Gist options
  • Save bew/b7b6574a394f3eec09d9daf48d498e08 to your computer and use it in GitHub Desktop.
Save bew/b7b6574a394f3eec09d9daf48d498e08 to your computer and use it in GitHub Desktop.
module Contract
# We put everything in `included` macro so the constants don't clashes
# when used in multiple classes
macro included
CONTRACTS_FOR_DEF = {} of _ => _
macro __add_contract(contract)
\{% if CONTRACTS_FOR_DEF[:next_def] == nil %}
\{% CONTRACTS_FOR_DEF[:next_def] = [contract] %}
\{% else %}
\{% CONTRACTS_FOR_DEF[:next_def] << contract %}
\{% end %}
end
macro __dump_contracts
\{% p "dumping contracts for next def: #{CONTRACTS_FOR_DEF[:next_def]}" %}
end
macro requires(node)
__add_contract({:requires, \{{node}} })
end
macro ensures(node)
__add_contract({:ensures, \{{node}} })
end
# With Crystal 0.23.1, defining a def in `method_added` will call
# `method_added` on it.
#
# The workaround is to keep track of which method is defined, and
# skip the method if it's already defined.
ADDED_METHODS = [] of _
macro method_added(def_node)
\{% if !ADDED_METHODS.includes?(def_node) %}
\{% if CONTRACTS_FOR_DEF[:next_def] == nil %}
\{% ADDED_METHODS << def_node %}
\{{def_node}}
\{% else %}
\{% contracts = CONTRACTS_FOR_DEF[:next_def] %}
def \{{def_node.name}}(\{{def_node.args.splat}})
\{% for c in contracts %}
\{% type = c[0]; contract = c[1] %}
# Do your thing here!
\{% if type == :requires %}
puts "Doing 'require' \{{contract}}"
\{% elsif type == :ensures %}
puts "Doing 'ensure' \{{contract}}"
\{% end %}
\{% end %}
\{{def_node.body}}
end
# This is not useful for now, but can be handy for other later macros
# to know which contracts was applied to which method!
# Note that it is the old method, not the new generated one.
\{% CONTRACTS_FOR_DEF[def_node] = contracts %}
\{% CONTRACTS_FOR_DEF[:next_def] = nil %}
# Note: The generated method will call `method_added` recursively.
# As the contracts list will be empty, the method will simply be printed,
# and registered so that `method_added` will not re-process it.
\{% end %}
\{% end %}
end
end
end
class Foo
include Contract
def other
puts "hello from other"
end
__dump_contracts
requires var > 5
ensures result.query?
__dump_contracts
def foo
puts "hello from foo"
end
__dump_contracts
def bar
puts "hello from bar"
end
end
# make some space to distinguish the runtime output from compiletime output
{% puts "\n".id %}
Foo.new.other
puts
Foo.new.foo
puts
Foo.new.bar
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment