Created
October 1, 2017 12:16
-
-
Save bew/b7b6574a394f3eec09d9daf48d498e08 to your computer and use it in GitHub Desktop.
This file contains 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
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