rudy4 0.2.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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +114 -0
  3. data/LICENSE +21 -0
  4. data/Makefile +137 -0
  5. data/README.md +400 -0
  6. data/bin/rudy4 +6 -0
  7. data/examples/hello_gem/README.md +91 -0
  8. data/examples/hello_gem/ext/hello_gem/extconf.rb +75 -0
  9. data/examples/hello_gem/ext/hello_gem/hello_gem.d +122 -0
  10. data/examples/hello_gem/hello_gem.gemspec +35 -0
  11. data/examples/hello_gem/lib/hello_gem/version.rb +5 -0
  12. data/examples/hello_gem/lib/hello_gem.rb +32 -0
  13. data/examples/hello_gem/test/smoke.sh +81 -0
  14. data/experiments/exp_compact.d +178 -0
  15. data/experiments/exp_convert.d +93 -0
  16. data/experiments/exp_cross.d +144 -0
  17. data/experiments/exp_defargs.d +301 -0
  18. data/experiments/exp_exc.d +265 -0
  19. data/experiments/exp_gcstorm.d +169 -0
  20. data/experiments/exp_http.d +254 -0
  21. data/experiments/exp_nonregression.d +209 -0
  22. data/experiments/exp_objproto.d +123 -0
  23. data/experiments/exp_perf.d +81 -0
  24. data/experiments/exp_wrap.d +186 -0
  25. data/lib/rudy4/cli.rb +204 -0
  26. data/lib/rudy4/doctor.rb +295 -0
  27. data/lib/rudy4/error.rb +10 -0
  28. data/lib/rudy4/generator.rb +172 -0
  29. data/lib/rudy4/templates.rb +484 -0
  30. data/lib/rudy4/vendor.rb +36 -0
  31. data/lib/rudy4/version.rb +5 -0
  32. data/lib/rudy4.rb +16 -0
  33. data/scripts/abi_audit.sh +64 -0
  34. data/scripts/memcheck.sh +77 -0
  35. data/scripts/vendor_check.sh +33 -0
  36. data/scripts/vendor_sync.sh +25 -0
  37. data/src/dexitest.d +278 -0
  38. data/src/dextest.d +119 -0
  39. data/src/dtypetest.d +159 -0
  40. data/src/rudy/args.d +253 -0
  41. data/src/rudy/bind/value.d +306 -0
  42. data/src/rudy/convert.d +221 -0
  43. data/src/rudy/defs.d +440 -0
  44. data/src/rudy/objproto.d +282 -0
  45. data/src/rudy/typeddata.d +143 -0
  46. data/test/dextest_loader.rb +6 -0
  47. data/test/exitest.rb +113 -0
  48. data/test/rudy4_test.rb +291 -0
  49. data/test/run.rb +74 -0
  50. data/test/typedtest.rb +69 -0
  51. data/vendor/rudy/args.d +253 -0
  52. data/vendor/rudy/bind/value.d +306 -0
  53. data/vendor/rudy/convert.d +221 -0
  54. data/vendor/rudy/defs.d +440 -0
  55. data/vendor/rudy/objproto.d +282 -0
  56. data/vendor/rudy/typeddata.d +143 -0
  57. metadata +106 -0
@@ -0,0 +1,221 @@
1
+ /// Two-way conversions between native D values and Ruby VALUEs.
2
+ /// D2 rewrite of the original RuDy make_object.d (2010), now calling only
3
+ /// exported Ruby functions — no struct peeking.
4
+
5
+ module rudy.convert;
6
+
7
+ import rudy.bind.value;
8
+ import std.traits;
9
+ import std.string : toStringz;
10
+ import std.exception : enforce;
11
+
12
+
13
+ // ---------------------------------------------------------------- helpers
14
+
15
+ /// Opt-in raw-VALUE passthrough for def!'d function parameters.
16
+ ///
17
+ /// VALUE identity(VALUE v) ... -> DECODED as ulong (VALUE is aliased
18
+ /// to ulong, and d_type now decodes it instead of passing the raw
19
+ /// tagged pointer through).
20
+ ///
21
+ /// Raw!VALUE identity(Raw!VALUE v) ... -> raw, untouched VALUE.
22
+ ///
23
+ /// Rationale: VALUE is `alias ulong VALUE`, so "parameter is VALUE" can't
24
+ /// be detected as a distinct type — and a plain `ulong` param used to hit
25
+ /// the old raw-passthrough branch, receiving the TAGGED value (Ruby 5
26
+ /// arrived as 11). Passthrough is now explicit: wrap in Raw!(...) to say
27
+ /// "give me the bits untouched".
28
+ struct Raw(T)
29
+ {
30
+ T value;
31
+ }
32
+
33
+ /// Compile-time check: is T the Raw!(...) passthrough wrapper?
34
+ template isRaw(T)
35
+ {
36
+ static if (is(T == Raw!R, R))
37
+ enum isRaw = true;
38
+ else
39
+ enum isRaw = false;
40
+ }
41
+
42
+ /// Type traits the old rudy/ replaced its meta/ directory for.
43
+ template isDynamicArrayType(T) { enum bool isDynamicArrayType = isDynamicArray!T; }
44
+ template isStaticArrayType(T) { enum bool isStaticArrayType = isStaticArray!T; }
45
+ template isAssocArrayType(T) { enum bool isAssocArrayType = is(T == V[K], V, K); }
46
+
47
+ /// Byte length of a Ruby String, the exported-API way. History: the old
48
+ /// code sliced rb_str_strlen (a CHARACTER count) as a byte count —
49
+ /// truncating every multibyte string (finding #12) — and the natural fix
50
+ /// target rb_str_bytes_len turned out to be inline-macro-only, NOT
51
+ /// exported (caught locally by a symbol-lookup crash; the ABI audit would
52
+ /// have caught it in CI). Ruby's own String#bytesize is the exact answer;
53
+ /// rb_intern lazily, idempotently (VM is alive inside any method call).
54
+ private __gshared ID rudy_id_bytesize;
55
+
56
+ private long rudy_bytesize(VALUE str)
57
+ {
58
+ if (!rudy_id_bytesize)
59
+ rudy_id_bytesize = rb_intern("bytesize");
60
+ return cast(long) rb_num2ll(rb_funcallv(str, rudy_id_bytesize, 0, null));
61
+ }
62
+
63
+ /// Reject negative values where the D target type is unsigned: the
64
+ /// numeric family (rb_num2ulong / rb_num2uint) would otherwise wrap
65
+ /// them 2's-complement style (-1 arrived as 2^64-1, finding #13 family).
66
+ private void rudy_check_unsigned(VALUE val)
67
+ {
68
+ // Sign via double: rb_num2long would itself raise on values above
69
+ // LONG_MAX (2^64-1 is a legitimate ulong). Sign survives the cast;
70
+ // NaN/inf fail the comparison and get caught by num2ulong below.
71
+ if (rb_num2dbl(val) < 0)
72
+ rb_raise(rb_eRangeError, "negative value not convertible to unsigned D type");
73
+ }
74
+
75
+ /// Require a Ruby feature (e.g. "complex") before using it from D.
76
+ private void rb_require_c(string name)
77
+ {
78
+ VALUE req = rb_str_new(name.ptr, cast(C_long) name.length);
79
+ VALUE[1] av = [req];
80
+ rb_funcallv(rb_cObject, rb_intern("require"), 1, av.ptr);
81
+ }
82
+
83
+ // ---------------------------------------------------------------- to_ruby_value
84
+
85
+ /// Convert a native D value into a Ruby VALUE.
86
+ VALUE to_ruby_value(T)(T t)
87
+ {
88
+ static if (isRaw!T) // Raw!VALUE -> pass bits through
89
+ return t.value;
90
+ else static if (is(T == bool))
91
+ return t ? Qtrue_ : Qfalse_;
92
+
93
+ else static if (is(T == char))
94
+ return rb_int2inum(t);
95
+
96
+ else static if (is(T == ulong) || is(T == C_ulonglong))
97
+ return rb_ull2inum(cast(C_ulonglong) t); // unsigned encode path —
98
+ // the old signed
99
+ // rb_ll2inum(cast(long))
100
+ // turned 2^64-1 into -1
101
+ // (finding #15)
102
+
103
+ else static if (is(T : C_longlong))
104
+ return rb_ll2inum(cast(long) t);
105
+
106
+ else static if (is(T : C_long))
107
+ return rb_int2inum(cast(int) t);
108
+
109
+ else static if (is(T : double)) // float, real promote
110
+ return rb_float_new(cast(double) t);
111
+
112
+ else static if (is(T == creal) || is(T == cdouble))
113
+ {
114
+ rb_require_c("complex");
115
+ VALUE c = rb_const_get(rb_cObject, rb_intern("Complex"));
116
+ VALUE[2] av = [to_ruby_value(t.re), to_ruby_value(t.im)];
117
+ return rb_funcallv(c, rb_intern("new"), 2, av.ptr);
118
+ }
119
+
120
+ else static if (is(T == string))
121
+ return rb_str_new(t.ptr, cast(C_long) t.length);
122
+
123
+ else static if (isDynamicArrayType!T)
124
+ {
125
+ VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
126
+ foreach (ref e; t)
127
+ rb_ary_push(ary, to_ruby_value(e));
128
+ return ary;
129
+ }
130
+
131
+ else static if (isStaticArrayType!T)
132
+ {
133
+ VALUE ary = rb_ary_new_capa(cast(C_long) t.length);
134
+ foreach (ref e; t)
135
+ rb_ary_push(ary, to_ruby_value(e));
136
+ return ary;
137
+ }
138
+
139
+ else static if (isAssocArrayType!T)
140
+ {
141
+ VALUE hash = rb_hash_new();
142
+ foreach (k, ref v; t)
143
+ rb_hash_aset(hash, to_ruby_value(k), to_ruby_value(v));
144
+ return hash;
145
+ }
146
+
147
+ else static assert(0, "to_ruby_value: no conversion for " ~ T.stringof);
148
+ }
149
+
150
+
151
+ // ---------------------------------------------------------------- d_type
152
+
153
+ /// Convert a Ruby VALUE into a native D type T.
154
+ ///
155
+ /// Numeric decode order matters: EXACT types must be tested before the
156
+ /// subtype branches they would otherwise fall into — is(T : C_longlong)
157
+ /// matches int too, and the old chain let `int` decode through the
158
+ /// unchecked-wide path. Each exact type now gets Ruby's own
159
+ /// range-checked converter: rb_num2int/rb_num2uint raise RangeError on
160
+ /// overflow (finding #13: silent wraparound before), fractional floats
161
+ /// truncate toward zero and NaN/inf raise FloatDomainError — exactly
162
+ /// NUM2INT/NUM2UINT/Integer() semantics (finding #14).
163
+ T d_type(T)(VALUE val)
164
+ {
165
+ static if (is(T == bool))
166
+ {
167
+ // RUBY_TEST (RTEST) semantics: nil/false are false, everything
168
+ // else — 0, 0.0, "" — is true, matching Ruby's own truthiness.
169
+ // (The old `val == Qtrue_` made Ruby truthy 0 decode as D false.)
170
+ return val != Qnil_ && val != Qfalse_;
171
+ }
172
+ else static if (is(T == string))
173
+ {
174
+ VALUE str = rb_str_to_str(val);
175
+ auto ptr = rb_string_value_ptr(&str); // may swap str in-place
176
+ return ptr[0 .. rudy_bytesize(str)].idup; // BYTE length, not chars
177
+ // (finding #12)
178
+ }
179
+
180
+ // exact unsigned machine words first (VALUE itself is a ulong alias)
181
+ else static if (is(T == ulong) || is(T == C_ulonglong))
182
+ {
183
+ rudy_check_unsigned(val); // -1 must raise, not wrap
184
+ return cast(T) rb_num2ulong(val);
185
+ } // a ulong param still means
186
+ // "Ruby Integer -> machine word",
187
+ // NOT raw passthrough
188
+ else static if (is(T == int))
189
+ return cast(T) rb_num2int(val); // range-checked (finding #13)
190
+ else static if (is(T == uint))
191
+ {
192
+ rudy_check_unsigned(val);
193
+ return cast(T) rb_num2uint(val); // range-checked (finding #13)
194
+ }
195
+
196
+ // remaining signed subtypes (long, char, short...): wide is fine
197
+ else static if (is(T : C_longlong))
198
+ return cast(T) rb_num2ll(val);
199
+
200
+ // remaining unsigned subtypes (ubyte, ushort...): range-checked
201
+ else static if (is(T : uint))
202
+ {
203
+ rudy_check_unsigned(val);
204
+ return cast(T) rb_num2uint(val);
205
+ }
206
+
207
+ else static if (is(T : double))
208
+ return cast(T) rb_num2dbl(val);
209
+
210
+ else static assert(0, "d_type: no conversion to " ~ T.stringof);
211
+ }
212
+
213
+ /// NB: D's uninitialized `double x;` is NaN (float.init), NOT 0.0 as in C.
214
+ /// Always explicitly initialize double accumulators: `double x = 0.0;` —
215
+ /// otherwise every sum silently becomes NaN (issue #3).
216
+
217
+ /// Raise a Ruby exception from D.
218
+ void rudy_raise(VALUE exc, string msg)
219
+ {
220
+ rb_raise(exc, "%s", toStringz(msg));
221
+ }
data/src/rudy/defs.d ADDED
@@ -0,0 +1,440 @@
1
+ /// `def!` — register a D function as a Ruby method, with automatic
2
+ /// argument unwrapping (VALUE -> native D) and result rewrapping
3
+ /// (native D -> VALUE). The 2010 original passed raw VALUEs through;
4
+ /// this one finally does the wrapping PyD-style.
5
+ ///
6
+ /// def!(myFunc); // global (Kernel) function
7
+ /// def!("MyClass", myFunc); // instance method of MyClass
8
+ /// def!("MyModule", myFunc); // module function
9
+ ///
10
+ /// The D function may take native D types (int, double, string, ...) as
11
+ /// parameters; each argument is converted from the incoming VALUE with
12
+ /// d_type!T, and the return value goes back through to_ruby_value.
13
+ ///
14
+ /// ---
15
+ ///
16
+ /// Special parameter types (rudy/args.d) extend the model beyond fixed
17
+ /// positional arguments:
18
+ ///
19
+ /// RubyBlock (last param) — receives the caller's block as a Proc;
20
+ /// blk.call(...) invokes it; blk.given says
21
+ /// whether a block was passed at all.
22
+ /// Rest — captures *rest into a Ruby Array.
23
+ /// KwArgs — captures **kwargs into a Ruby Hash
24
+ /// (given()==false when none passed).
25
+ /// Self (first param) — the receiver as raw VALUE.
26
+ /// Raw!T — opt-in raw VALUE passthrough.
27
+ ///
28
+ /// Any def!'d function using a special parameter (or D default args) is
29
+ /// registered with arity -1 (argc/argv style); rb_scan_args splits the
30
+ /// arguments per a compile-time-generated format string and raises the
31
+ /// standard ArgumentError on wrong arity. Plain fixed-arity functions
32
+ /// go through the same uniform trampoline (also registered -1): one
33
+ /// code path for every def!'d method.
34
+ module rudy.defs;
35
+
36
+ import rudy.bind.value;
37
+ import rudy.convert;
38
+ import rudy.args;
39
+ import std.traits;
40
+ import std.string : toStringz;
41
+ import std.conv : to;
42
+ import std.meta : AliasSeq, anySatisfy;
43
+ import std.algorithm.iteration : splitter;
44
+
45
+ private:
46
+
47
+ // ------------------------------------------------------------------
48
+ // Parameter classification
49
+ // ------------------------------------------------------------------
50
+
51
+ /// True for parameter types the trampoline fills in ITSELF (never
52
+ /// decoded from a positional VALUE): the rudy/args.d wrappers, the
53
+ /// Self receiver, and the opt-in Raw!T passthrough.
54
+ template isValueParam(T)
55
+ {
56
+ static if (is(T == RubyBlock) || is(T == Rest) || is(T == KwArgs)
57
+ || is(T == Self) || isRaw!T)
58
+ enum bool isValueParam = true;
59
+ else
60
+ enum bool isValueParam = false;
61
+ }
62
+
63
+ /// Decode one positional argument VALUE into parameter type T.
64
+ /// Raw!T passthrough wraps the bits; everything else goes through
65
+ /// d_type!T. (There is deliberately no plain-VALUE branch: VALUE is
66
+ /// aliased to ulong, and a ulong parameter means "Ruby Integer ->
67
+ /// machine word", NOT raw passthrough — see issue #1.)
68
+ T argFrom(T)(VALUE v)
69
+ {
70
+ static if (isRaw!T)
71
+ return T(v);
72
+ else
73
+ {
74
+ static assert(!isValueParam!T, "def!: " ~ T.stringof
75
+ ~ " is filled by the trampoline, not decoded from argv");
76
+ return d_type!T(v);
77
+ }
78
+ }
79
+
80
+ private template isRestParam(T) { enum bool isRestParam = is(T == Rest); }
81
+ private template isKwParam(T) { enum bool isKwParam = is(T == KwArgs); }
82
+ private template isBlockParam(T) { enum bool isBlockParam = is(T == RubyBlock); }
83
+ private template isSelfParam(T) { enum bool isSelfParam = is(T == Self); }
84
+
85
+ /// Index of the first parameter satisfying pred, or -1.
86
+ template findIdx(alias pred, P...)
87
+ {
88
+ static if (P.length == 0)
89
+ enum int findIdx = -1;
90
+ else static if (pred!(P[0]))
91
+ enum int findIdx = 0;
92
+ else
93
+ enum int findIdx = findIdx!(pred, P[1 .. $]) == -1
94
+ ? -1 : 1 + findIdx!(pred, P[1 .. $]);
95
+ }
96
+
97
+ // ------------------------------------------------------------------
98
+ // ArgInfo: compile-time layout of a def!'d function's parameters.
99
+ //
100
+ // nLead / nOpt / nTrail / hasAsterisk — pieces of the rb_scan_args
101
+ // format string. At most one of { Rest, KwArgs, RubyBlock } per
102
+ // function (static assert'ed).
103
+ // posSlots — decodable positional slots in scan order (mandatory
104
+ // first, then optional): each element is the parameter index it
105
+ // maps to.
106
+ // restIdx / kwIdx / blkIdx / selfIdx — parameter indices, -1 when
107
+ // absent; filled by the trampoline (self from the self argument).
108
+ // ------------------------------------------------------------------
109
+
110
+ struct ArgInfo(alias fn)
111
+ {
112
+ alias P = Parameters!(typeof(&fn));
113
+ alias DEFAULTS = ParameterDefaults!(fn);
114
+
115
+ enum bool hasRest = anySatisfy!(isRestParam, P);
116
+ enum bool hasKw = anySatisfy!(isKwParam, P);
117
+ enum bool hasBlock = anySatisfy!(isBlockParam, P);
118
+ enum bool hasSelf = anySatisfy!(isSelfParam, P);
119
+
120
+ enum int restIdx = findIdx!(isRestParam, P);
121
+ enum int kwIdx = findIdx!(isKwParam, P);
122
+ enum int blkIdx = findIdx!(isBlockParam, P);
123
+ enum int selfIdx = findIdx!(isSelfParam, P);
124
+
125
+ // positional classification of one parameter: is it a decodable
126
+ // argv slot, and must the caller supply it? Raw!T params are
127
+ // ordinary positional slots (raw passthrough instead of d_type).
128
+ template slotKind(size_t i)
129
+ {
130
+ static if (is(P[i] == RubyBlock) || is(P[i] == Rest)
131
+ || is(P[i] == KwArgs) || is(P[i] == Self))
132
+ enum string slotKind = "none"; // filled by trampoline
133
+ else static if (!is(DEFAULTS[i] == void))
134
+ enum string slotKind = "opt"; // optional positional
135
+ else
136
+ enum string slotKind = "req"; // mandatory positional
137
+ }
138
+
139
+ enum nLead = countLead;
140
+ enum nOpt = countOpt;
141
+ enum nTrail = 0; // trailing mandatory after splat:
142
+ // not supported yet
143
+ enum bool hasAsterisk = hasRest;
144
+
145
+ // positional slots in scan order: mandatory first, then optional
146
+ template posSlotsOf(size_t i = 0)
147
+ {
148
+ static if (i >= P.length)
149
+ alias posSlotsOf = AliasSeq!();
150
+ else static if (slotKind!i == "none")
151
+ alias posSlotsOf = posSlotsOf!(i + 1);
152
+ else static if (slotKind!i == "req")
153
+ alias posSlotsOf = AliasSeq!(i, posSlotsOf!(i + 1));
154
+ else
155
+ alias posSlotsOf = AliasSeq!(i, posSlotsOf!(i + 1));
156
+ }
157
+ alias posSlots = posSlotsOf!();
158
+
159
+ // mandatory = leading run of "req" slots in scan order
160
+ private template countLeadImpl(size_t i, int acc)
161
+ {
162
+ static if (i >= P.length || slotKind!i != "req")
163
+ enum int countLeadImpl = acc;
164
+ else
165
+ enum int countLeadImpl = countLeadImpl!(i + 1, acc + 1);
166
+ }
167
+ enum int countLead = countLeadImpl!(0, 0);
168
+
169
+ enum int countOpt = posSlots.length - countLead;
170
+
171
+ // DEBUG
172
+
173
+ static assert(!(hasRest && hasKw), "def!: Rest + KwArgs in one function not supported yet");
174
+ static assert(blkIdx == -1 || blkIdx == P.length - 1,
175
+ "def!: RubyBlock must be the LAST parameter");
176
+ static assert(countLead + countOpt + (hasRest ? 1 : 0)
177
+ + (hasKw || hasBlock ? 1 : 0) + (hasSelf ? 1 : 0)
178
+ == P.length,
179
+ "def!: unsupported parameter layout — special params (Rest/KwArgs/RubyBlock/Self) must not carry D default values");
180
+ }
181
+
182
+ // ------------------------------------------------------------------
183
+ // rb_scan_args format string, generated at compile time:
184
+ // digits: nLead, then nOpt, then '*' when hasRest, then nTrail, then
185
+ // ':' when hasKw, then '&' when hasBlock.
186
+ // Examples: "0" (no args), "1", "12&" (1 req + up to 2 opt + block),
187
+ // "1*", "2*:", "0&", "1*&" (1 req + *rest + block).
188
+ // ------------------------------------------------------------------
189
+
190
+ private string scanFmt(alias AI)()
191
+ {
192
+ string s;
193
+ static if (AI.nLead > 0)
194
+ s ~= AI.nLead.to!string;
195
+ static if (AI.nOpt > 0)
196
+ s ~= AI.nOpt.to!string;
197
+ static if (AI.hasAsterisk)
198
+ s ~= "*";
199
+ static if (AI.nTrail > 0)
200
+ s ~= AI.nTrail.to!string;
201
+ static if (AI.hasKw)
202
+ s ~= ":";
203
+ static if (AI.hasBlock)
204
+ s ~= "&";
205
+ if (s.length == 0)
206
+ s = "0"; // zero-arg functions still get the
207
+ // argc == 0 arity check
208
+ return s;
209
+ }
210
+
211
+ // ------------------------------------------------------------------
212
+ // The one uniform trampoline. Registered with arity -1 for every
213
+ // def!'d function; Ruby calls it as
214
+ // VALUE f(int argc, const(VALUE)* argv, VALUE self)
215
+ // (the classic variadic C-method ABI). rb_scan_args splits argv per
216
+ // the generated format (raising the standard ArgumentError on wrong
217
+ // arity), omitted optional slots become Qnil, and wrappers
218
+ // (Rest/KwArgs/RubyBlock) + Self/Raw slots are filled directly.
219
+ // ------------------------------------------------------------------
220
+
221
+ extern(C)
222
+ VALUE rudy_trampoline_var(alias fn)(int argc, const(VALUE)* argv, VALUE self)
223
+ {
224
+ alias AI = ArgInfo!(fn);
225
+ alias P = Parameters!(typeof(&fn));
226
+ alias DEFAULTS = ParameterDefaults!(fn);
227
+ alias SLOTS = AI.posSlots;
228
+
229
+ P args; // default-initialized
230
+
231
+ // rb_scan_args targets (positional slots in scan order); optional
232
+ // slots omitted by the caller are filled with Qnil by Ruby itself.
233
+ static if (SLOTS.length > 0)
234
+ VALUE[SLOTS.length] slotVals = Qnil_;
235
+
236
+ // NB: rb_scan_args' vararg order is FORMAT ORDER — '*' slot first,
237
+ // then ':' tail, then '&' tail. Rest+block is therefore two targets:
238
+ // restAry AND tail. KwArgs + RubyBlock can't coexist (: then & would
239
+ // be fine, but a function taking both kwargs and a block with the
240
+ // same Hash/Proc split is untestable through get!T semantics — see
241
+ // the static assert below).
242
+ static if (AI.hasRest)
243
+ {
244
+ VALUE restAry = Qnil_;
245
+ static if (AI.hasBlock)
246
+ {
247
+ VALUE tail = Qnil_; // '&' after '*'
248
+ static if (SLOTS.length == 0)
249
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &restAry, &tail);
250
+ else static if (SLOTS.length == 1)
251
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
252
+ &slotVals[0], &restAry, &tail);
253
+ else static if (SLOTS.length == 2)
254
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
255
+ &slotVals[0], &slotVals[1], &restAry, &tail);
256
+ else static if (SLOTS.length == 3)
257
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
258
+ &slotVals[0], &slotVals[1], &slotVals[2], &restAry, &tail);
259
+ else
260
+ static assert(0, "def!: max 3 positional slots + special params");
261
+ }
262
+ else static if (SLOTS.length == 0)
263
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &restAry);
264
+ else static if (SLOTS.length == 1)
265
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
266
+ &slotVals[0], &restAry);
267
+ else static if (SLOTS.length == 2)
268
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
269
+ &slotVals[0], &slotVals[1], &restAry);
270
+ else static if (SLOTS.length == 3)
271
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
272
+ &slotVals[0], &slotVals[1], &slotVals[2], &restAry);
273
+ else
274
+ static assert(0, "def!: max 3 positional slots + special params");
275
+ }
276
+ else static if (AI.hasKw || AI.hasBlock)
277
+ {
278
+ VALUE tail = Qnil_; // kwargs hash / block proc
279
+ static if (SLOTS.length == 0)
280
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &tail);
281
+ else static if (SLOTS.length == 1)
282
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
283
+ &slotVals[0], &tail);
284
+ else static if (SLOTS.length == 2)
285
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
286
+ &slotVals[0], &slotVals[1], &tail);
287
+ else static if (SLOTS.length == 3)
288
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
289
+ &slotVals[0], &slotVals[1], &slotVals[2], &tail);
290
+ else
291
+ static assert(0, "def!: max 3 positional slots + special params");
292
+ }
293
+ else static if (SLOTS.length > 0)
294
+ {
295
+ static if (SLOTS.length == 1)
296
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)), &slotVals[0]);
297
+ else static if (SLOTS.length == 2)
298
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
299
+ &slotVals[0], &slotVals[1]);
300
+ else static if (SLOTS.length == 3)
301
+ rb_scan_args(argc, argv, toStringz(scanFmt!(AI)),
302
+ &slotVals[0], &slotVals[1], &slotVals[2]);
303
+ else
304
+ static assert(0, "def!: max 3 positional slots (extend the ladder for more)");
305
+ }
306
+
307
+ // fill wrapper params from the scan results
308
+ static if (AI.hasRest)
309
+ args[AI.restIdx] = Rest(restAry);
310
+ static if (AI.hasKw)
311
+ args[AI.kwIdx] = KwArgs(tail);
312
+ static if (AI.hasBlock)
313
+ args[AI.blkIdx] = RubyBlock(tail);
314
+ static if (AI.hasSelf)
315
+ args[AI.selfIdx] = Self(self);
316
+
317
+ // decode positional slots; D default values substitute for omitted
318
+ // optional arguments (Qnil means "not supplied"). Two-var static
319
+ // foreach: si = slot position (argv order), i = parameter index.
320
+ // NB: an AliasSeq passed as an alias template arg EXPANDS into
321
+ // separate args (a slotPos!(SLOTS, i) helper died exactly that
322
+ // way) - iterating it in foreach is the safe pattern.
323
+ static foreach (si, i; SLOTS)
324
+ {{
325
+ static if (is(DEFAULTS[i] == void))
326
+ args[i] = argFrom!(P[i])(slotVals[si]);
327
+ else
328
+ {
329
+ if (slotVals[si] == Qnil_)
330
+ {
331
+ static if (is(P[i] == VALUE))
332
+ args[i] = cast(VALUE) DEFAULTS[i];
333
+ else static if (isRaw!(P[i]))
334
+ args[i] = P[i](cast(VALUE) DEFAULTS[i]);
335
+ else
336
+ args[i] = DEFAULTS[i];
337
+ }
338
+ else
339
+ args[i] = argFrom!(P[i])(slotVals[si]);
340
+ }
341
+ }}
342
+
343
+ return to_ruby_value(fn(args));
344
+ }
345
+
346
+ // ------------------------------------------------------------------
347
+ // Trampoline selection. Every def!'d function — fixed arity, defaults,
348
+ // or wrapper params — uses the uniform argc/argv trampoline, registered
349
+ // with arity -1. (The old per-arity extern(C) shims rudy_trampoline_0..
350
+ // _3 are gone: one code path, and blocks/rest/kwargs would not fit the
351
+ // fixed-arity ABI anyway.)
352
+ // ------------------------------------------------------------------
353
+
354
+ template rudy_trampoline(alias fn)
355
+ {
356
+ alias rudy_trampoline = rudy_trampoline_var!(fn);
357
+ }
358
+
359
+ /// Registered arity for Ruby: always -1 (argc/argv style).
360
+ enum int rudy_arity(alias fn) = -1;
361
+
362
+ // ------------------------------------------------------------------
363
+ // def! — the public interface
364
+ // ------------------------------------------------------------------
365
+
366
+ public:
367
+
368
+ /// Define fn as a global function (on Object).
369
+ void def(alias fn, string name = __traits(identifier, fn))()
370
+ {
371
+ // NB: alias template params must be forwarded in the bang-list;
372
+ // def!"Object"(fn, name) misparses under GDC-14 as a runtime call.
373
+ def!("Object", fn, name)();
374
+ }
375
+
376
+ /// Define fn in the scope of `modulename` (class -> method, module ->
377
+ /// module function, anything else -> global on Object).
378
+ void def(string modulename, alias fn, string name = __traits(identifier, fn))()
379
+ {
380
+ VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
381
+ alias tramp = rudy_trampoline!(fn);
382
+
383
+ // T_MODULE == 0x03 per value.d; rb_type is macro-only, classify
384
+ // via is_a?(Module)
385
+ VALUE[1] av = [rb_const_get(rb_cObject, rb_intern("Module"))];
386
+ VALUE res = rb_funcallv_public(parent, rb_intern("is_a?"), 1, av.ptr);
387
+
388
+ if (res == Qtrue_)
389
+ rb_define_module_function(parent, toStringz(name),
390
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
391
+ else
392
+ rb_define_method(parent, toStringz(name),
393
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
394
+ }
395
+
396
+ /// Define fn as a PRIVATE instance method (e.g. DSL-style callbacks).
397
+ void def_private(string modulename, alias fn,
398
+ string name = __traits(identifier, fn))()
399
+ {
400
+ VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
401
+ alias tramp = rudy_trampoline!(fn);
402
+ rb_define_private_method(parent, toStringz(name),
403
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
404
+ }
405
+
406
+ /// Define fn as a PROTECTED instance method.
407
+ void def_protected(string modulename, alias fn,
408
+ string name = __traits(identifier, fn))()
409
+ {
410
+ VALUE parent = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
411
+ alias tramp = rudy_trampoline!(fn);
412
+ rb_define_protected_method(parent, toStringz(name),
413
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
414
+ }
415
+
416
+ /// Define fn as a SINGLETON method of `modulename`'s class object
417
+ /// (i.e. a class-level method: MyClass.def!'d_thing).
418
+ void def_singleton(string modulename, alias fn,
419
+ string name = __traits(identifier, fn))()
420
+ {
421
+ VALUE klass = rb_const_get(rb_cObject, rb_intern(toStringz(modulename)));
422
+ alias tramp = rudy_trampoline!(fn);
423
+ rb_define_singleton_method(klass, toStringz(name),
424
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
425
+ }
426
+
427
+ /// Define fn as a singleton method of a specific OBJECT (per-object
428
+ /// method; the object must be reachable by name at def! time).
429
+ void def_singleton_on(string objectname, alias fn,
430
+ string name = __traits(identifier, fn))()
431
+ {
432
+ // "A::B" paths walk the scope chain (rb_const_get resolves one
433
+ // constant per call, not a path)
434
+ VALUE obj = rb_cObject;
435
+ foreach (part; objectname.splitter("::"))
436
+ obj = rb_const_get(obj, rb_intern(toStringz(part)));
437
+ alias tramp = rudy_trampoline!(fn);
438
+ rb_define_singleton_method(obj, toStringz(name),
439
+ cast(AnyMethod) &tramp, rudy_arity!(fn));
440
+ }