hegeltest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +264 -0
  6. data/Rakefile +19 -0
  7. data/docs/README.md +25 -0
  8. data/docs/adr/0001-bind-libhegel-through-fiddle.md +54 -0
  9. data/docs/adr/0002-ship-one-prebuilt-engine-per-platform-specific-gem.md +48 -0
  10. data/docs/adr/0003-publish-as-hegeltest-require-as-hegel.md +39 -0
  11. data/docs/adr/0004-expose-generators-through-a-mixin-with-keyword-options.md +42 -0
  12. data/docs/adr/0005-name-drawn-values-from-the-callers-source-with-prism.md +40 -0
  13. data/docs/adr/0006-verify-the-binding-in-seven-layers-with-full-coverage.md +51 -0
  14. data/docs/adr/0007-ship-a-thin-ruby-skill-shaped-for-donation.md +56 -0
  15. data/docs/adr/0008-revisit-the-binding-after-milestone-c-on-measurement.md +81 -0
  16. data/docs/adr/0009-turn-the-example-database-on-with-a-key.md +89 -0
  17. data/docs/adr/0010-declare-stateful-rules-with-a-class-macro.md +113 -0
  18. data/docs/adr/0011-let-the-test-case-own-every-pool-drawn-from-it.md +83 -0
  19. data/docs/adr/0012-build-a-failure-origin-from-the-callers-own-frame.md +72 -0
  20. data/docs/adr/0013-bind-libhegel-through-the-ffi-gem.md +102 -0
  21. data/docs/architecture.md +182 -0
  22. data/lib/hegel/draw_name.rb +109 -0
  23. data/lib/hegel/errors.rb +47 -0
  24. data/lib/hegel/generator.rb +98 -0
  25. data/lib/hegel/generators.rb +865 -0
  26. data/lib/hegel/lib_hegel/real.rb +1149 -0
  27. data/lib/hegel/lib_hegel.rb +269 -0
  28. data/lib/hegel/libhegel_version.rb +9 -0
  29. data/lib/hegel/locate.rb +188 -0
  30. data/lib/hegel/report.rb +87 -0
  31. data/lib/hegel/runner.rb +464 -0
  32. data/lib/hegel/settings.rb +164 -0
  33. data/lib/hegel/state_machine.rb +89 -0
  34. data/lib/hegel/stateful/pool.rb +111 -0
  35. data/lib/hegel/stateful.rb +120 -0
  36. data/lib/hegel/syntax/methods.rb +173 -0
  37. data/lib/hegel/test_case.rb +523 -0
  38. data/lib/hegel/version.rb +5 -0
  39. data/lib/hegel.rb +92 -0
  40. data/lib/hegeltest.rb +7 -0
  41. data/lib/tasks/libhegel.rake +112 -0
  42. data/lib/tasks/platform_gems.rake +111 -0
  43. data/sig/hegel.rbs +563 -0
  44. data/skills/hegel-ruby/SKILL.md +30 -0
  45. data/skills/hegel-ruby/references/ruby/reference.md +1210 -0
  46. metadata +113 -0
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "syntax/methods"
5
+
6
+ module Hegel
7
+ # Base class for a stateful (model-based) test's machine: a subclass
8
+ # declares its actions with the class-level #rule and #invariant macros,
9
+ # then Hegel::Stateful.run drives one instance of it inside an ordinary
10
+ # Hegel.test block. See docs/adr/0010 for the declared shape (a class with
11
+ # macros, not a method-naming convention or an instance-built list) and
12
+ # the reasons behind it.
13
+ #
14
+ # Only declaration lives here. Hegel::Stateful.run owns the loop that
15
+ # calls the declared blocks, the same split hegel-rust draws between its
16
+ # StateMachine trait (rules()/invariants()) and its own free function
17
+ # `run`.
18
+ class StateMachine
19
+ # So a rule or invariant block can call a generator method (integers,
20
+ # arrays, and so on) bare, the same way a Hegel.test block already can
21
+ # via the caller's own include -- see docs/adr/0010's own reasoning for
22
+ # why a machine needs this itself rather than inheriting its test
23
+ # class's.
24
+ include Syntax::Methods
25
+
26
+ class << self
27
+ # Declares a rule named +name+: an action the engine may pick to run
28
+ # at any step. +block+ runs via #instance_exec against the machine
29
+ # instance being tested, and is handed the running Hegel::TestCase as
30
+ # its one argument -- ignored if the block declares no parameter, the
31
+ # ordinary Ruby block rule.
32
+ def rule(name, &block)
33
+ declare(:@rules, "rule", name, block)
34
+ end
35
+
36
+ # Declares an invariant named +name+, checked once before the first
37
+ # rule runs and again after every rule that completes without its own
38
+ # assumption failing. Same block/argument contract as #rule.
39
+ def invariant(name, &block)
40
+ declare(:@invariants, "invariant", name, block)
41
+ end
42
+
43
+ # name => block, in declaration order, this class's own declarations
44
+ # merged over its ancestors'. Hegel::Stateful.run reads this directly
45
+ # to build the ordered rule-name list libhegel indexes by position.
46
+ def rule_definitions
47
+ merged_definitions(:@rules, :rule_definitions)
48
+ end
49
+
50
+ # The invariant analogue of #rule_definitions.
51
+ def invariant_definitions
52
+ merged_definitions(:@invariants, :invariant_definitions)
53
+ end
54
+
55
+ private
56
+
57
+ # Adds +name+ (stringified, so a Symbol and the same-named String
58
+ # collide) to the table at +ivar+, raising Hegel::Error when *this*
59
+ # class already declares one by that name -- the disappearing-rule
60
+ # failure docs/adr/0010 exists to turn into a raise instead. A name
61
+ # only a superclass declared is not a collision here: #declare never
62
+ # reads an ancestor's table, so a subclass re-declaring an inherited
63
+ # name is the ordinary "redefine a method" case the ADR calls out,
64
+ # not this one.
65
+ def declare(ivar, kind, name, block)
66
+ table = instance_variable_get(ivar) || instance_variable_set(ivar, {})
67
+ name = name.to_s
68
+ raise Hegel::Error, "hegel: #{kind} #{name.inspect} is already declared on #{self}" if table.key?(name)
69
+
70
+ table[name] = block
71
+ end
72
+
73
+ # Shared by #rule_definitions/#invariant_definitions: this class's own
74
+ # table layered over its superclass's already-merged one via
75
+ # Hash#merge, so a name declared on both keeps the position it first
76
+ # held (Hash#merge's own behaviour for a key present in both operands)
77
+ # while every new name from this class is appended -- together, the
78
+ # declaration order the whole ancestor chain built. Recursion stops at
79
+ # the first class that does not respond to +reader+ (Object, past
80
+ # Hegel::StateMachine's own top), rather than comparing against
81
+ # StateMachine directly, so nothing here hard-codes this one class as
82
+ # the root.
83
+ def merged_definitions(ivar, reader)
84
+ inherited = superclass.respond_to?(reader) ? superclass.public_send(reader) : {}
85
+ inherited.merge(instance_variable_get(ivar) || {})
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../generator"
4
+
5
+ module Hegel
6
+ module Stateful
7
+ # A pool of previously generated values, for a later stateful rule to
8
+ # draw one back out. Build one from the running test case, inside the
9
+ # machine's own constructor:
10
+ #
11
+ # Hegel::Stateful::Pool.new(tc)
12
+ #
13
+ # #add records a value under a fresh variable id (hegel_pool_add); #size
14
+ # and #empty? read the Ruby-side count directly. Drawing goes through the
15
+ # two generators below, not through this class's own storage, so a
16
+ # chosen id is drawn (and shrunk, and recorded in a failure report) the
17
+ # same way any other value is: #values_reusable leaves the drawn value in
18
+ # the pool, #values_consumed removes it. hegel-rust's own Pool<T>
19
+ # (src/stateful.rs) keeps the same three-way split between the engine's
20
+ # variable-id choice, this class's own id-to-value map, and the two
21
+ # generators.
22
+ #
23
+ # A caller never frees a pool. docs/adr/0011 has the reason: #initialize
24
+ # opens the native handle through Hegel::TestCase#new_pool, which records
25
+ # it on +tc+ itself, and Hegel::Runner frees every pool a test case
26
+ # recorded once that test case is done -- the same "the test case owns
27
+ # what it opened" split this library already uses for a state-machine
28
+ # handle.
29
+ class Pool
30
+ def initialize(tc)
31
+ @tc = tc
32
+ @pool = tc.new_pool
33
+ @values = {}
34
+ end
35
+
36
+ # Number of values currently in the pool.
37
+ def size
38
+ @values.size
39
+ end
40
+
41
+ # True when no values are in the pool.
42
+ def empty?
43
+ @values.empty?
44
+ end
45
+
46
+ # Records +value+ under a fresh variable id from hegel_pool_add.
47
+ # Returns self, Set#add's own contract -- hegel-rust's own Pool::add
48
+ # returns nothing instead, since Rust has no builder-chaining idiom for
49
+ # this method to match.
50
+ def add(value)
51
+ variable_id = @tc.pool_add(@pool)
52
+ @values[variable_id] = value
53
+ self
54
+ end
55
+
56
+ # A Hegel::Generator over this pool's values: drawing it leaves the
57
+ # chosen value in place, so the same value can be drawn again.
58
+ def values_reusable
59
+ ValuesReusable.new(@pool, @values)
60
+ end
61
+
62
+ # A Hegel::Generator over this pool's values: drawing it removes the
63
+ # chosen value, so it is never drawn again.
64
+ def values_consumed
65
+ ValuesConsumed.new(@pool, @values)
66
+ end
67
+
68
+ # Hegel::Generator returned by Pool#values_reusable. Left un-namespaced
69
+ # under Hegel::Generators, the same way Hegel::Generator::Mapped and
70
+ # ::Filtered are: reached only through Pool#values_reusable, not part
71
+ # of this library's own public generator vocabulary.
72
+ class ValuesReusable < Generator
73
+ def initialize(pool, values)
74
+ super()
75
+ @pool = pool
76
+ @values = values
77
+ end
78
+
79
+ # Does not check @values.empty? before drawing: hegel_pool_generate
80
+ # already answers HEGEL_E_ASSUME for an empty pool, which
81
+ # Hegel::LibHegel.check! already translates to Hegel::AssumeFailed --
82
+ # the same translation every other assumption failure gets.
83
+ # hegel-rust's own ValuesReusable calls tc.assume ahead of its own
84
+ # pool_generate call (src/stateful.rs). Here the engine's answer is
85
+ # the single path, so the empty case has one place to change.
86
+ def do_draw(tc)
87
+ variable_id = tc.pool_generate(@pool, false)
88
+ @values.fetch(variable_id)
89
+ end
90
+ end
91
+
92
+ # Hegel::Generator returned by Pool#values_consumed. Same reasoning as
93
+ # ValuesReusable above for staying un-namespaced and for not
94
+ # pre-checking emptiness.
95
+ class ValuesConsumed < Generator
96
+ def initialize(pool, values)
97
+ super()
98
+ @pool = pool
99
+ @values = values
100
+ end
101
+
102
+ def do_draw(tc)
103
+ variable_id = tc.pool_generate(@pool, true)
104
+ value = @values.fetch(variable_id)
105
+ @values.delete(variable_id)
106
+ value
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "lib_hegel"
5
+ require_relative "stateful/pool"
6
+
7
+ module Hegel
8
+ # Runs one stateful (model-based) test: drives libhegel's own state-machine
9
+ # loop (hegel_new_state_machine and the three calls that go with it) against
10
+ # a Hegel::StateMachine instance's declared rules and invariants. Call it
11
+ # from inside an ordinary Hegel.test block, the same way any other draw
12
+ # happens:
13
+ #
14
+ # Hegel.test { |tc| Hegel::Stateful.run(StackMachine.new, tc) }
15
+ #
16
+ # A module function, not a Hegel::StateMachine instance method, so that
17
+ # class stays limited to declaration -- the same split hegel-rust's own
18
+ # src/stateful.rs draws between the StateMachine trait and its free
19
+ # function `run`, which this module's own #run is ported from; see that
20
+ # file's comments for the reasoning behind the ordering below.
21
+ module Stateful
22
+ module_function
23
+
24
+ # +machine+ is a Hegel::StateMachine instance; +tc+ the running
25
+ # Hegel::TestCase.
26
+ #
27
+ # Raises Hegel::Error before making any libhegel call when +machine+
28
+ # declares no rules: hegel.h documents hegel_new_state_machine's
29
+ # rule_names as required to be non-empty, and there is nothing useful to
30
+ # run without one.
31
+ def run(machine, tc)
32
+ rules = machine.class.rule_definitions
33
+ raise Hegel::Error, "hegel: #{machine.class} has no rules; declare at least one with `rule`" if rules.empty?
34
+
35
+ invariants = machine.class.invariant_definitions
36
+ rule_names = rules.keys
37
+ state_machine = tc.new_state_machine(rule_names, invariants.keys)
38
+ begin
39
+ tc.note { "Initial invariant check." }
40
+ run_invariants(machine, invariants, tc)
41
+ drive(machine, rules, rule_names, invariants, state_machine, tc)
42
+ ensure
43
+ tc.state_machine_free(state_machine)
44
+ end
45
+ end
46
+
47
+ # Repeatedly asks +state_machine+ for the next rule to run and applies
48
+ # it, until libhegel reports the step budget for this test case spent.
49
+ #
50
+ # Measured against libhegel 0.32.5, unseeded, on the capacity-2 stack
51
+ # shrink-quality test below (test/hegel/test_stateful.rb,
52
+ # test_stateful_run_shrinks_to_the_minimal_step_count_that_breaks_the_
53
+ # invariant): closing the HEGEL_LABEL_STATEFUL_RULE span with
54
+ # stop_span(discard: false) on the DONE branch, right before breaking
55
+ # out of this loop, and leaving it open instead (matching hegel-rust's
56
+ # own `run`, which never closes that last span) both shrink the same
57
+ # failure to the same 3-step counterexample every time, 20 runs each.
58
+ # This keeps hegel-rust's own choice -- an unclosed span at DONE --
59
+ # since nothing measured favours the extra stop_span call.
60
+ def drive(machine, rules, rule_names, invariants, state_machine, tc)
61
+ steps_attempted = 0
62
+ loop do
63
+ tc.start_span(LibHegel::HEGEL_LABEL_STATEFUL_RULE)
64
+ rule_index = tc.state_machine_next_rule(state_machine)
65
+ break if rule_index == LibHegel::HEGEL_STATE_MACHINE_DONE
66
+
67
+ name = rule_names[rule_index]
68
+ steps_attempted += 1
69
+ tc.note { "Step #{steps_attempted}: #{name}" }
70
+ apply_rule(machine, rules.fetch(name), invariants, state_machine, tc)
71
+ end
72
+ end
73
+
74
+ # standard:disable Lint/RescueException -- deliberate, the same reason
75
+ # Hegel::Runner.classify's own `rescue Exception` is: Hegel::AssumeFailed
76
+ # and Hegel::StopTest both descend from Exception, not StandardError, so
77
+ # only `rescue Exception` sees every path a rule can take. Every branch
78
+ # other than AssumeFailed re-raises what it caught unchanged -- this
79
+ # never reclassifies an exception or swallows one, it only guarantees
80
+ # the span closes first, so a half-applied rule is never left mid-span
81
+ # when the exception unwinds past this method.
82
+ #
83
+ # Hegel::FATAL_EXCEPTIONS goes first and closes no span. They say the
84
+ # process is ending, so the span has no reader left to matter to, and
85
+ # answering a NoMemoryError with another native call is the wrong move.
86
+ # A rule is the one place in this library where a fatal exception is
87
+ # raised inside an open span, so this is where that ordering has to be
88
+ # written.
89
+ #
90
+ # tc.assume(false) inside a rule is not the same event as one raised
91
+ # directly inside a Hegel.test block: it rejects only this rule (told to
92
+ # libhegel via #state_machine_rule_rejected, so the rejected attempt
93
+ # does not count toward the step budget) and the loop keeps going, where
94
+ # Hegel::Runner.classify's own AssumeFailed handling discards the whole
95
+ # test case. Hegel::Runner.classify never sees this one: it is caught
96
+ # and handled right here.
97
+ def apply_rule(machine, block, invariants, state_machine, tc)
98
+ machine.instance_exec(tc, &block)
99
+ rescue *Hegel::FATAL_EXCEPTIONS
100
+ raise
101
+ rescue Hegel::AssumeFailed
102
+ tc.state_machine_rule_rejected(state_machine)
103
+ tc.stop_span(discard: true)
104
+ tc.note { "Rule stopped early due to violated assumption." }
105
+ rescue Exception
106
+ tc.stop_span(discard: false)
107
+ raise
108
+ else
109
+ tc.stop_span(discard: false)
110
+ run_invariants(machine, invariants, tc)
111
+ end
112
+ # standard:enable Lint/RescueException
113
+
114
+ # Runs every invariant, in declaration order, via #instance_exec -- same
115
+ # argument contract as a rule block (Hegel::StateMachine.invariant).
116
+ def run_invariants(machine, invariants, tc)
117
+ invariants.each_value { |block| machine.instance_exec(tc, &block) }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../generators"
4
+
5
+ module Hegel
6
+ module Syntax
7
+ # The generator-constructing methods a caller can include to call bare,
8
+ # matching FactoryBot::Syntax::Methods's shape (see docs/adr/0004):
9
+ #
10
+ # RSpec.configure { |config| config.include Hegel::Syntax::Methods }
11
+ #
12
+ # This is the one place any of these methods is defined.
13
+ # Hegel::Generators.<name> reaches the same methods without an
14
+ # include, via Hegel::Generators extending this module below.
15
+ module Methods
16
+ # A boolean, true with probability +p+.
17
+ def booleans(p: 0.5)
18
+ Generators::BooleanGenerator.new(p: p)
19
+ end
20
+
21
+ # An integer in [min_value, max_value], defaulting to the full
22
+ # 64-bit range when either bound is omitted.
23
+ def integers(min_value: nil, max_value: nil)
24
+ Generators::IntegerGenerator.new(min_value: min_value, max_value: max_value)
25
+ end
26
+
27
+ # A double in [min_value, max_value]. allow_nan and allow_infinity
28
+ # both default to false.
29
+ def floats(min_value: nil, max_value: nil, allow_nan: false, allow_infinity: false, exclude_min: false,
30
+ exclude_max: false)
31
+ Generators::FloatGenerator.new(
32
+ min_value: min_value, max_value: max_value, allow_nan: allow_nan, allow_infinity: allow_infinity,
33
+ exclude_min: exclude_min, exclude_max: exclude_max
34
+ )
35
+ end
36
+
37
+ # A Unicode string of [min_size, max_size] characters.
38
+ def text(min_size: 0, max_size: nil, codec: nil, min_codepoint: nil, max_codepoint: nil)
39
+ Generators::TextGenerator.new(
40
+ min_size: min_size, max_size: max_size, codec: codec,
41
+ min_codepoint: min_codepoint, max_codepoint: max_codepoint
42
+ )
43
+ end
44
+
45
+ # An Array of values from +elements+, with [min_size, max_size]
46
+ # entries.
47
+ def arrays(elements, min_size: 0, max_size: nil)
48
+ Generators::ArrayGenerator.new(elements, min_size: min_size, max_size: max_size)
49
+ end
50
+
51
+ # Always +value+, drawing nothing.
52
+ def just(value)
53
+ Generators::JustGenerator.new(value)
54
+ end
55
+
56
+ # One element of +collection+, picked at random.
57
+ def sampled_from(collection)
58
+ Generators::SampledFromGenerator.new(collection)
59
+ end
60
+
61
+ # A value drawn from one of +generators+, picked at random.
62
+ def one_of(*generators)
63
+ Generators::OneOfGenerator.new(generators)
64
+ end
65
+
66
+ # A value drawn from +generator+ half the time, nil the other half.
67
+ def optional(generator)
68
+ Generators::OptionalGenerator.new(generator)
69
+ end
70
+
71
+ # An Array holding one value drawn from each of +generators+, in
72
+ # order (Ruby has no tuple type; see docs/adr/0004).
73
+ def tuples(*generators)
74
+ Generators::TupleGenerator.new(generators)
75
+ end
76
+
77
+ # A Set of values from +elements+, with [min_size, max_size] entries.
78
+ def sets(elements, min_size: 0, max_size: nil)
79
+ Generators::SetGenerator.new(elements, min_size: min_size, max_size: max_size)
80
+ end
81
+
82
+ # A Hash whose keys are drawn from +keys+ and values from +values+,
83
+ # with [min_size, max_size] entries.
84
+ def hashes(keys, values, min_size: 0, max_size: nil)
85
+ Generators::HashGenerator.new(keys, values, min_size: min_size, max_size: max_size)
86
+ end
87
+
88
+ # A String of exactly one character, sharing #text's own alphabet
89
+ # options.
90
+ def characters(codec: nil, min_codepoint: nil, max_codepoint: nil)
91
+ Generators::CharactersGenerator.new(codec: codec, min_codepoint: min_codepoint, max_codepoint: max_codepoint)
92
+ end
93
+
94
+ # A byte String of [min_size, max_size] bytes.
95
+ def binary(min_size: 0, max_size: nil)
96
+ Generators::BinaryGenerator.new(min_size: min_size, max_size: max_size)
97
+ end
98
+
99
+ # A String matching +pattern+ (Python `re` syntax, not Ruby's
100
+ # Regexp syntax; see Generators::FromRegexGenerator). fullmatch
101
+ # requires the whole string to match, not just contain a match.
102
+ def from_regex(pattern, fullmatch: false)
103
+ Generators::FromRegexGenerator.new(pattern, fullmatch: fullmatch)
104
+ end
105
+
106
+ # An RFC 5321/5322 email address String.
107
+ def emails
108
+ Generators::EmailsGenerator.new
109
+ end
110
+
111
+ # An RFC 3986 http/https URL String.
112
+ def urls
113
+ Generators::UrlsGenerator.new
114
+ end
115
+
116
+ # A fully-qualified domain name String of at most +max_length+
117
+ # characters.
118
+ def domains(max_length: 255)
119
+ Generators::DomainsGenerator.new(max_length: max_length)
120
+ end
121
+
122
+ # An IPAddr, v4 or v6 depending on +v4+/+v6+ (see
123
+ # Generators::IpAddressesGenerator).
124
+ def ip_addresses(v4: true, v6: true)
125
+ Generators::IpAddressesGenerator.new(v4: v4, v6: v6)
126
+ end
127
+
128
+ # A UUID String in the standard 8-4-4-4-12 hex form. version: nil (the
129
+ # default) draws uniform random bits except the nil UUID; an explicit
130
+ # version forces the RFC 4122 version and variant nibbles (see
131
+ # Generators::UuidsGenerator).
132
+ def uuids(version: nil)
133
+ Generators::UuidsGenerator.new(version: version)
134
+ end
135
+
136
+ # A proleptic Gregorian calendar Date in [min_value, max_value],
137
+ # defaulting to year 1 through year 9999.
138
+ def dates(min_value: nil, max_value: nil)
139
+ Generators::DatesGenerator.new(min_value: min_value, max_value: max_value)
140
+ end
141
+
142
+ # A time of day String, "HH:MM:SS.ffffff", in [min_value, max_value]
143
+ # (also "HH:MM:SS.ffffff" Strings), defaulting to the full day.
144
+ def times(min_value: nil, max_value: nil)
145
+ Generators::TimesGenerator.new(min_value: min_value, max_value: max_value)
146
+ end
147
+
148
+ # A naive (no timezone) Time in [min_value, max_value], defaulting to
149
+ # year 1 through year 9999 (see Generators::DatetimesGenerator).
150
+ def datetimes(min_value: nil, max_value: nil)
151
+ Generators::DatetimesGenerator.new(min_value: min_value, max_value: max_value)
152
+ end
153
+
154
+ # A value built from imperative code: +block+ receives a draw
155
+ # surface and may call #draw on it any number of times to assemble
156
+ # one value (see Generators::CompositeGenerator).
157
+ def composite(&block)
158
+ Generators::CompositeGenerator.new(&block)
159
+ end
160
+
161
+ # A forward reference to a generator whose definition is supplied
162
+ # later via #set, enabling self-recursive and mutually recursive
163
+ # generators (see Generators::DeferredGenerator).
164
+ def deferred
165
+ Generators::DeferredGenerator.new
166
+ end
167
+ end
168
+ end
169
+
170
+ module Generators
171
+ extend Syntax::Methods
172
+ end
173
+ end