autotype 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.
@@ -0,0 +1,296 @@
1
+ #include "../include/stc.h"
2
+
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ stc_solver *stc_solver_new(void) {
7
+ stc_solver *solver = calloc(1, sizeof(stc_solver));
8
+ return solver;
9
+ }
10
+
11
+ static void stc_free_capabilities(stc_capability *caps, size_t count) {
12
+ if (!caps) return;
13
+ for (size_t c = 0; c < count; c++) {
14
+ free(caps[c].args);
15
+ }
16
+ free(caps);
17
+ }
18
+
19
+ void stc_solver_free(stc_solver *solver) {
20
+ if (!solver) return;
21
+ for (size_t m = 0; m < solver->method_count; m++) {
22
+ stc_free_capabilities(solver->methods[m].capabilities, solver->methods[m].capability_count);
23
+ }
24
+ free(solver->methods);
25
+ free(solver->vars);
26
+ free(solver->subs);
27
+ if (solver->type_pool) {
28
+ for (size_t i = 0; i < solver->type_pool_count; i++) {
29
+ stc_type *type = solver->type_pool[i];
30
+ if (type->kind == STC_TY_GENERIC) free(type->as.generic.args);
31
+ if (type->kind == STC_TY_UNION) free(type->as.union_.members);
32
+ free(type);
33
+ }
34
+ free(solver->type_pool);
35
+ }
36
+ free(solver);
37
+ }
38
+
39
+ void stc_solver_load_flat(stc_solver *solver, stc_capability *capabilities, size_t capability_count) {
40
+ stc_method *method = calloc(1, sizeof(stc_method));
41
+ if (!method) return;
42
+ strncpy(method->name, "flat", STC_MAX_NAME - 1);
43
+ method->capabilities = capabilities;
44
+ method->capability_count = capability_count;
45
+ solver->methods = method;
46
+ solver->method_count = 1;
47
+ }
48
+
49
+ static bool stc_is_array(stc_solver *solver, stc_type *type) {
50
+ type = stc_dereference(solver, type);
51
+ return type && type->kind == STC_TY_GENERIC && strcmp(type->as.generic.name, "Array") == 0;
52
+ }
53
+
54
+ static bool stc_is_hash(stc_solver *solver, stc_type *type) {
55
+ type = stc_dereference(solver, type);
56
+ return type && type->kind == STC_TY_GENERIC && strcmp(type->as.generic.name, "Hash") == 0 &&
57
+ type->as.generic.arg_count == 2;
58
+ }
59
+
60
+ static bool stc_is_string(stc_solver *solver, stc_type *type) {
61
+ type = stc_dereference(solver, type);
62
+ return type && type->kind == STC_TY_NAMED && strcmp(type->as.named.name, "String") == 0;
63
+ }
64
+
65
+ static stc_type *stc_array_element(stc_solver *solver, stc_type *type) {
66
+ type = stc_dereference(solver, type);
67
+ if (!stc_is_array(solver, type)) return NULL;
68
+ return type->as.generic.args[0];
69
+ }
70
+
71
+ static stc_type *stc_enumerable_element(stc_solver *solver, stc_type *type) {
72
+ type = stc_dereference(solver, type);
73
+ if (!type) return NULL;
74
+ if (stc_is_array(solver, type)) return stc_array_element(solver, type);
75
+ if (stc_is_hash(solver, type)) return type->as.generic.args[1];
76
+ return NULL;
77
+ }
78
+
79
+ static stc_type *stc_nil(stc_solver *solver) {
80
+ return stc_type_named(solver, "nil");
81
+ }
82
+
83
+ static stc_type *stc_nullable(stc_solver *solver, stc_type *type) {
84
+ stc_type *nil = stc_nil(solver);
85
+ stc_type *members[] = { type, nil };
86
+ return stc_type_union(solver, members, 2);
87
+ }
88
+
89
+ static void stc_unify_array_elements(stc_solver *solver, stc_type *receiver, stc_type *argument) {
90
+ stc_type *element = stc_array_element(solver, receiver);
91
+ if (element) stc_unify(solver, element, argument);
92
+ }
93
+
94
+ void stc_apply_builtin(stc_solver *solver, stc_capability *capability) {
95
+ stc_type *receiver = stc_dereference(solver, capability->receiver);
96
+ stc_type *result = capability->result;
97
+ const char *message = capability->message;
98
+
99
+ if (strcmp(message, "to_s") == 0 || strcmp(message, "inspect") == 0 || strcmp(message, "String") == 0) {
100
+ stc_unify(solver, result, stc_type_named(solver, "String"));
101
+ return;
102
+ }
103
+ if (strcmp(message, "to_i") == 0 || strcmp(message, "ord") == 0 || strcmp(message, "Integer") == 0 ||
104
+ strcmp(message, "size") == 0 || strcmp(message, "length") == 0 || strcmp(message, "count") == 0) {
105
+ stc_unify(solver, result, stc_type_named(solver, "Integer"));
106
+ return;
107
+ }
108
+ if (strcmp(message, "to_f") == 0 || strcmp(message, "Float") == 0 || strcmp(message, "clock_gettime") == 0) {
109
+ stc_unify(solver, result, stc_type_named(solver, "Float"));
110
+ return;
111
+ }
112
+ if (strcmp(message, "to_sym") == 0) {
113
+ stc_unify(solver, result, stc_type_named(solver, "Symbol"));
114
+ return;
115
+ }
116
+ if (strcmp(message, "!") == 0 || strcmp(message, "present?") == 0 || strcmp(message, "blank?") == 0 ||
117
+ strcmp(message, "nil?") == 0 || strcmp(message, "empty?") == 0 || strcmp(message, "any?") == 0 ||
118
+ strcmp(message, "all?") == 0 || strcmp(message, "none?") == 0 || strcmp(message, "include?") == 0 ||
119
+ strcmp(message, "key?") == 0 || strcmp(message, "has_key?") == 0) {
120
+ stc_unify(solver, result, stc_type_named(solver, "bool"));
121
+ return;
122
+ }
123
+ if (strcmp(message, "puts") == 0 || strcmp(message, "print") == 0 || strcmp(message, "p") == 0) {
124
+ stc_unify(solver, result, stc_nil(solver));
125
+ return;
126
+ }
127
+ if (strcmp(message, "tap") == 0 || strcmp(message, "itself") == 0 || strcmp(message, "deep_dup") == 0 ||
128
+ strcmp(message, "dup") == 0 || strcmp(message, "clone") == 0 || strcmp(message, "freeze") == 0 ||
129
+ strcmp(message, "with") == 0) {
130
+ stc_unify(solver, result, receiver);
131
+ return;
132
+ }
133
+ if (strcmp(message, "presence") == 0) {
134
+ stc_unify(solver, result, stc_nullable(solver, receiver));
135
+ return;
136
+ }
137
+ if (strcmp(message, "[]") == 0) {
138
+ if (stc_is_hash(solver, receiver) && capability->arg_count >= 1) {
139
+ stc_type *value = receiver->as.generic.args[1];
140
+ stc_unify(solver, result, stc_nullable(solver, value));
141
+ return;
142
+ }
143
+ if (stc_is_array(solver, receiver) && capability->arg_count >= 1) {
144
+ stc_type *element = stc_array_element(solver, receiver);
145
+ if (element) stc_unify(solver, result, stc_nullable(solver, element));
146
+ return;
147
+ }
148
+ }
149
+ if ((strcmp(message, "<<") == 0 || strcmp(message, "push") == 0 || strcmp(message, "append") == 0 ||
150
+ strcmp(message, "unshift") == 0 || strcmp(message, "prepend") == 0) &&
151
+ stc_is_array(solver, receiver)) {
152
+ for (size_t i = 0; i < capability->arg_count; i++) {
153
+ stc_unify_array_elements(solver, receiver, capability->args[i]);
154
+ }
155
+ stc_unify(solver, result, receiver);
156
+ return;
157
+ }
158
+ if (strcmp(message, "<<") == 0 && stc_is_string(solver, receiver)) {
159
+ stc_unify(solver, result, receiver);
160
+ return;
161
+ }
162
+ if (strcmp(message, "concat") == 0) {
163
+ if (stc_is_array(solver, receiver)) {
164
+ for (size_t i = 0; i < capability->arg_count; i++) {
165
+ stc_unify(solver, receiver, capability->args[i]);
166
+ }
167
+ stc_unify(solver, result, receiver);
168
+ return;
169
+ }
170
+ if (stc_is_string(solver, receiver)) {
171
+ stc_unify(solver, result, receiver);
172
+ return;
173
+ }
174
+ }
175
+ if (strcmp(message, "keys") == 0 && stc_is_hash(solver, receiver)) {
176
+ stc_type *args[] = { receiver->as.generic.args[0] };
177
+ stc_unify(solver, result, stc_type_generic(solver, "Array", args, 1));
178
+ return;
179
+ }
180
+ if (strcmp(message, "values") == 0 && stc_is_hash(solver, receiver)) {
181
+ stc_type *args[] = { receiver->as.generic.args[1] };
182
+ stc_unify(solver, result, stc_type_generic(solver, "Array", args, 1));
183
+ return;
184
+ }
185
+ if (strcmp(message, "sort") == 0 || strcmp(message, "sort_by") == 0 || strcmp(message, "reverse") == 0 ||
186
+ strcmp(message, "take") == 0 || strcmp(message, "drop") == 0 || strcmp(message, "uniq") == 0 ||
187
+ strcmp(message, "shuffle") == 0 || strcmp(message, "rotate") == 0 || strcmp(message, "to_a") == 0 ||
188
+ strcmp(message, "each") == 0 || strcmp(message, "each_value") == 0 || strcmp(message, "each_key") == 0 ||
189
+ strcmp(message, "each_pair") == 0) {
190
+ if (stc_is_array(solver, receiver) || stc_is_hash(solver, receiver)) {
191
+ stc_unify(solver, result, receiver);
192
+ return;
193
+ }
194
+ }
195
+ if ((strcmp(message, "merge") == 0 || strcmp(message, "merge!") == 0 || strcmp(message, "except") == 0 ||
196
+ strcmp(message, "slice") == 0 || strcmp(message, "deep_symbolize_keys") == 0 ||
197
+ strcmp(message, "symbolize_keys") == 0 || strcmp(message, "with_indifferent_access") == 0) &&
198
+ stc_is_hash(solver, receiver)) {
199
+ stc_unify(solver, result, receiver);
200
+ return;
201
+ }
202
+ if (strcmp(message, "deep_stringify_keys") == 0 || strcmp(message, "stringify_keys") == 0) {
203
+ if (stc_is_hash(solver, receiver)) {
204
+ stc_type *args[] = { stc_type_named(solver, "String"), receiver->as.generic.args[1] };
205
+ stc_unify(solver, result, stc_type_generic(solver, "Hash", args, 2));
206
+ return;
207
+ }
208
+ }
209
+ if (strcmp(message, "to_h") == 0 || strcmp(message, "to_hash") == 0) {
210
+ if (stc_is_hash(solver, receiver)) {
211
+ stc_unify(solver, result, receiver);
212
+ return;
213
+ }
214
+ }
215
+ if (strcmp(message, "flatten") == 0 && stc_is_array(solver, receiver)) {
216
+ stc_type *element = stc_array_element(solver, receiver);
217
+ if (element) {
218
+ stc_type *inner = stc_array_element(solver, element);
219
+ stc_type *flat = inner ? inner : element;
220
+ stc_type *args[] = { flat };
221
+ stc_unify(solver, result, stc_type_generic(solver, "Array", args, 1));
222
+ }
223
+ return;
224
+ }
225
+ if ((strcmp(message, "first") == 0 || strcmp(message, "last") == 0 || strcmp(message, "min") == 0 ||
226
+ strcmp(message, "max") == 0 || strcmp(message, "find") == 0 || strcmp(message, "detect") == 0 ||
227
+ strcmp(message, "sample") == 0)) {
228
+ stc_type *element = stc_enumerable_element(solver, receiver);
229
+ if (element) stc_unify(solver, result, stc_nullable(solver, element));
230
+ return;
231
+ }
232
+ if (strcmp(message, "delete") == 0 && stc_is_hash(solver, receiver) && capability->arg_count >= 1) {
233
+ stc_type *value = receiver->as.generic.args[1];
234
+ stc_unify(solver, result, stc_nullable(solver, value));
235
+ return;
236
+ }
237
+ if (strcmp(message, "fetch") == 0 && stc_is_hash(solver, receiver)) {
238
+ stc_type *value = receiver->as.generic.args[1];
239
+ stc_unify(solver, result, value);
240
+ return;
241
+ }
242
+ if (strcmp(message, "dig") == 0 && stc_is_hash(solver, receiver)) {
243
+ stc_unify(solver, result, stc_type_named(solver, "Object"));
244
+ return;
245
+ }
246
+ if (strcmp(message, "join") == 0 && stc_is_array(solver, receiver)) {
247
+ stc_unify(solver, result, stc_type_named(solver, "String"));
248
+ return;
249
+ }
250
+ if (strcmp(message, "split") == 0 && stc_is_string(solver, receiver)) {
251
+ stc_type *args[] = { stc_type_named(solver, "String") };
252
+ stc_unify(solver, result, stc_type_generic(solver, "Array", args, 1));
253
+ return;
254
+ }
255
+ if (strcmp(message, "strip") == 0 || strcmp(message, "downcase") == 0 || strcmp(message, "upcase") == 0 ||
256
+ strcmp(message, "capitalize") == 0 || strcmp(message, "squeeze") == 0) {
257
+ if (stc_is_string(solver, receiver)) {
258
+ stc_unify(solver, result, receiver);
259
+ return;
260
+ }
261
+ }
262
+ if ((strcmp(message, "+") == 0 || strcmp(message, "-") == 0 || strcmp(message, "*") == 0 ||
263
+ strcmp(message, "/") == 0 || strcmp(message, "%") == 0) &&
264
+ receiver && receiver->kind == STC_TY_NAMED) {
265
+ const char *name = receiver->as.named.name;
266
+ if (strcmp(name, "Integer") == 0 || strcmp(name, "Float") == 0 || strcmp(name, "String") == 0) {
267
+ stc_unify(solver, result, receiver);
268
+ return;
269
+ }
270
+ }
271
+ if ((strcmp(message, "+") == 0 || strcmp(message, "-") == 0) && stc_is_array(solver, receiver)) {
272
+ stc_unify(solver, result, receiver);
273
+ }
274
+ }
275
+
276
+ void stc_solve_to_fixed_point(stc_solver *solver) {
277
+ for (int iteration = 0; iteration < STC_MAX_ITERATIONS; iteration++) {
278
+ solver->changed = 0;
279
+ for (size_t m = 0; m < solver->method_count; m++) {
280
+ stc_method *method = &solver->methods[m];
281
+ for (size_t c = 0; c < method->capability_count; c++) {
282
+ stc_apply_builtin(solver, &method->capabilities[c]);
283
+ }
284
+ }
285
+ solver->iterations = iteration + 1;
286
+ if (!solver->changed) {
287
+ solver->converged = 1;
288
+ break;
289
+ }
290
+ }
291
+ }
292
+
293
+ int stc_solver_run(stc_solver *solver) {
294
+ stc_solve_to_fixed_point(solver);
295
+ return solver->converged;
296
+ }
@@ -0,0 +1,223 @@
1
+ #include "../include/stc.h"
2
+
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ static stc_type *stc_alloc_type(stc_solver *solver) {
7
+ stc_type *type = calloc(1, sizeof(stc_type));
8
+ if (!type) return NULL;
9
+
10
+ if (solver->type_pool_count >= solver->type_pool_cap) {
11
+ size_t new_cap = solver->type_pool_cap == 0 ? 256 : solver->type_pool_cap * 2;
12
+ stc_type **new_pool = realloc(solver->type_pool, new_cap * sizeof(stc_type *));
13
+ if (!new_pool) {
14
+ free(type);
15
+ return NULL;
16
+ }
17
+ solver->type_pool = new_pool;
18
+ solver->type_pool_cap = new_cap;
19
+ }
20
+
21
+ solver->type_pool[solver->type_pool_count++] = type;
22
+ return type;
23
+ }
24
+
25
+ stc_type *stc_type_named(stc_solver *solver, const char *name) {
26
+ stc_type *type = stc_alloc_type(solver);
27
+ if (!type) return NULL;
28
+ type->kind = STC_TY_NAMED;
29
+ strncpy(type->as.named.name, name, STC_MAX_NAME - 1);
30
+ return type;
31
+ }
32
+
33
+ stc_type *stc_type_var(stc_solver *solver, stc_var_id id, const char *hint) {
34
+ stc_type *type = stc_alloc_type(solver);
35
+ if (!type) return NULL;
36
+ type->kind = STC_TY_VAR;
37
+ type->as.var.id = id;
38
+ if (hint) strncpy(type->as.var.hint, hint, sizeof(type->as.var.hint) - 1);
39
+ return type;
40
+ }
41
+
42
+ stc_type *stc_type_generic(stc_solver *solver, const char *name, stc_type **args, size_t arg_count) {
43
+ stc_type *type = stc_alloc_type(solver);
44
+ if (!type) return NULL;
45
+ stc_type **owned_args = NULL;
46
+ if (arg_count > 0) {
47
+ owned_args = calloc(arg_count, sizeof(stc_type *));
48
+ if (!owned_args) return NULL;
49
+ memcpy(owned_args, args, arg_count * sizeof(stc_type *));
50
+ }
51
+ type->kind = STC_TY_GENERIC;
52
+ strncpy(type->as.generic.name, name, sizeof(type->as.generic.name) - 1);
53
+ type->as.generic.args = owned_args;
54
+ type->as.generic.arg_count = arg_count;
55
+ return type;
56
+ }
57
+
58
+ stc_type *stc_type_union(stc_solver *solver, stc_type **members, size_t member_count) {
59
+ stc_type *type = stc_alloc_type(solver);
60
+ if (!type) return NULL;
61
+ stc_type **owned_members = NULL;
62
+ if (member_count > 0) {
63
+ owned_members = calloc(member_count, sizeof(stc_type *));
64
+ if (!owned_members) return NULL;
65
+ memcpy(owned_members, members, member_count * sizeof(stc_type *));
66
+ }
67
+ type->kind = STC_TY_UNION;
68
+ type->as.union_.members = owned_members;
69
+ type->as.union_.member_count = member_count;
70
+ return type;
71
+ }
72
+
73
+ static stc_type *stc_find_sub(stc_solver *solver, stc_var_id var_id) {
74
+ for (size_t i = 0; i < solver->sub_count; i++) {
75
+ if (solver->subs[i].var_id == var_id) return solver->subs[i].type;
76
+ }
77
+ return NULL;
78
+ }
79
+
80
+ stc_type *stc_dereference(stc_solver *solver, stc_type *type) {
81
+ stc_var_id seen[64];
82
+ size_t seen_count = 0;
83
+
84
+ while (type && type->kind == STC_TY_VAR) {
85
+ for (size_t i = 0; i < seen_count; i++) {
86
+ if (seen[i] == type->as.var.id) return type;
87
+ }
88
+ if (seen_count < 64) seen[seen_count++] = type->as.var.id;
89
+
90
+ stc_type *next = stc_find_sub(solver, type->as.var.id);
91
+ if (!next) break;
92
+ type = next;
93
+ }
94
+ return type;
95
+ }
96
+
97
+ static bool stc_same_type(stc_type *left, stc_type *right) {
98
+ if (!left || !right) return left == right;
99
+ if (left->kind != right->kind) return false;
100
+ switch (left->kind) {
101
+ case STC_TY_NAMED:
102
+ return strcmp(left->as.named.name, right->as.named.name) == 0;
103
+ case STC_TY_VAR:
104
+ return left->as.var.id == right->as.var.id;
105
+ case STC_TY_GENERIC:
106
+ if (strcmp(left->as.generic.name, right->as.generic.name) != 0) return false;
107
+ if (left->as.generic.arg_count != right->as.generic.arg_count) return false;
108
+ for (size_t i = 0; i < left->as.generic.arg_count; i++) {
109
+ if (!stc_same_type(left->as.generic.args[i], right->as.generic.args[i])) return false;
110
+ }
111
+ return true;
112
+ case STC_TY_UNION:
113
+ if (left->as.union_.member_count != right->as.union_.member_count) return false;
114
+ for (size_t i = 0; i < left->as.union_.member_count; i++) {
115
+ if (!stc_same_type(left->as.union_.members[i], right->as.union_.members[i])) return false;
116
+ }
117
+ return true;
118
+ default:
119
+ return false;
120
+ }
121
+ }
122
+
123
+ static stc_type *stc_resolve_inner(stc_solver *solver, stc_type *type, stc_var_id *seen, size_t *seen_count, int depth);
124
+
125
+ stc_type *stc_resolve(stc_solver *solver, stc_type *type) {
126
+ stc_var_id seen[64];
127
+ size_t seen_count = 0;
128
+ return stc_resolve_inner(solver, type, seen, &seen_count, 0);
129
+ }
130
+
131
+ static stc_type *stc_resolve_inner(stc_solver *solver, stc_type *type, stc_var_id *seen, size_t *seen_count, int depth) {
132
+ if (!type || depth > 24) return type;
133
+
134
+ if (type->kind == STC_TY_VAR) {
135
+ for (size_t i = 0; i < *seen_count; i++) {
136
+ if (seen[i] == type->as.var.id) return type;
137
+ }
138
+ if (*seen_count < 64) seen[(*seen_count)++] = type->as.var.id;
139
+ }
140
+
141
+ type = stc_dereference(solver, type);
142
+ if (!type) return NULL;
143
+
144
+ if (type->kind == STC_TY_GENERIC) {
145
+ stc_type **args = calloc(type->as.generic.arg_count, sizeof(stc_type *));
146
+ if (!args) return type;
147
+ for (size_t i = 0; i < type->as.generic.arg_count; i++) {
148
+ args[i] = stc_resolve_inner(solver, type->as.generic.args[i], seen, seen_count, depth + 1);
149
+ }
150
+ return stc_type_generic(solver, type->as.generic.name, args, type->as.generic.arg_count);
151
+ }
152
+
153
+ if (type->kind == STC_TY_UNION) {
154
+ stc_type **members = calloc(type->as.union_.member_count, sizeof(stc_type *));
155
+ if (!members) return type;
156
+ size_t out = 0;
157
+ for (size_t i = 0; i < type->as.union_.member_count; i++) {
158
+ stc_type *resolved = stc_resolve_inner(solver, type->as.union_.members[i], seen, seen_count, depth + 1);
159
+ if (!resolved) continue;
160
+ bool dup = false;
161
+ for (size_t j = 0; j < out; j++) {
162
+ if (stc_same_type(members[j], resolved)) { dup = true; break; }
163
+ }
164
+ if (!dup) members[out++] = resolved;
165
+ }
166
+ if (out == 0) return type;
167
+ if (out == 1) return members[0];
168
+ return stc_type_union(solver, members, out);
169
+ }
170
+
171
+ return type;
172
+ }
173
+
174
+ void stc_bind(stc_solver *solver, stc_var_id var_id, stc_type *type) {
175
+ stc_type *existing = stc_find_sub(solver, var_id);
176
+ if (existing && stc_same_type(existing, type)) return;
177
+
178
+ if (solver->sub_count >= solver->sub_cap) {
179
+ size_t new_cap = solver->sub_cap == 0 ? 256 : solver->sub_cap * 2;
180
+ stc_substitution *new_subs = realloc(solver->subs, new_cap * sizeof(stc_substitution));
181
+ if (!new_subs) return;
182
+ solver->subs = new_subs;
183
+ solver->sub_cap = new_cap;
184
+ }
185
+
186
+ /* Update in place if already bound. */
187
+ for (size_t i = 0; i < solver->sub_count; i++) {
188
+ if (solver->subs[i].var_id == var_id) {
189
+ solver->subs[i].type = type;
190
+ solver->changed = 1;
191
+ return;
192
+ }
193
+ }
194
+
195
+ solver->subs[solver->sub_count].var_id = var_id;
196
+ solver->subs[solver->sub_count].type = type;
197
+ solver->sub_count++;
198
+ if (type->kind != STC_TY_VAR) solver->changed = 1;
199
+ }
200
+
201
+ void stc_unify(stc_solver *solver, stc_type *left, stc_type *right) {
202
+ if (!left || !right) return;
203
+ left = stc_dereference(solver, left);
204
+ right = stc_dereference(solver, right);
205
+ if (stc_same_type(left, right)) return;
206
+
207
+ if (left->kind == STC_TY_VAR) {
208
+ stc_bind(solver, left->as.var.id, right);
209
+ return;
210
+ }
211
+ if (right->kind == STC_TY_VAR) {
212
+ stc_bind(solver, right->as.var.id, left);
213
+ return;
214
+ }
215
+
216
+ if (left->kind == STC_TY_GENERIC && right->kind == STC_TY_GENERIC &&
217
+ strcmp(left->as.generic.name, right->as.generic.name) == 0 &&
218
+ left->as.generic.arg_count == right->as.generic.arg_count) {
219
+ for (size_t i = 0; i < left->as.generic.arg_count; i++) {
220
+ stc_unify(solver, left->as.generic.args[i], right->as.generic.args[i]);
221
+ }
222
+ }
223
+ }
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "fileutils"
5
+
6
+ RSpec.describe Autotype::TypeString do
7
+ it "parses named types" do
8
+ expect(described_class.parse("String")).to eq(Autotype::Named.new("String"))
9
+ end
10
+
11
+ it "parses generic types" do
12
+ type = described_class.parse("Array[String]")
13
+ expect(type).to eq(Autotype::Generic.new("Array", [Autotype::Named.new("String")]))
14
+ end
15
+
16
+ it "parses nested generics" do
17
+ type = described_class.parse("Hash[Symbol, Object]")
18
+ expect(type.name).to eq("Hash")
19
+ expect(type.arguments.map(&:name)).to eq(%w[Symbol Object])
20
+ end
21
+
22
+ it "parses unions" do
23
+ type = described_class.parse("String | nil")
24
+ expect(type).to be_a(Autotype::Union)
25
+ expect(type.members.map(&:name)).to eq(%w[String nil])
26
+ end
27
+ end
28
+
29
+ RSpec.describe Autotype::DiscoveryProfile do
30
+ let(:fixture_root) { File.expand_path("fixtures", __dir__) }
31
+
32
+ it "infers actor port wiring from handler methods" do
33
+ source = <<~RUBY
34
+ class DemoActor < Actor
35
+ input :prompts, type: Entities::Prompt
36
+
37
+ def process(prompt, from:)
38
+ prompt
39
+ end
40
+ end
41
+ RUBY
42
+
43
+ collector = Autotype::Collector.new("demo_actor.rb", profile: described_class.new)
44
+ Prism.parse(source).value.accept(collector)
45
+ profile = described_class.new
46
+ profile.finalize!([collector])
47
+
48
+ wiring = profile.port_wiring
49
+ expect(wiring.handler_method).to eq(:process)
50
+ expect(wiring.dispatch_param).to eq(:from)
51
+ end
52
+
53
+ it "locates entity files by constant name under the project tree" do
54
+ profile = described_class.new(root: fixture_root)
55
+ profile.prepare_search!([File.expand_path("fixtures/pipeline/actors/demo_actor.rb", __dir__)])
56
+
57
+ expect(profile.locate_type_file("App::Entities::ToolCall")).to eq(
58
+ File.expand_path("fixtures/entities/tool_call.rb", __dir__)
59
+ )
60
+ end
61
+
62
+ it "loads optional overrides from autotype.yml" do
63
+ Dir.mktmpdir do |dir|
64
+ config_path = File.join(dir, "autotype.yml")
65
+ File.write(config_path, <<~YAML)
66
+ skip:
67
+ - vendor/**
68
+ types:
69
+ members:
70
+ query: App::Entities::Query
71
+ YAML
72
+
73
+ profile = described_class.load_overrides(config_path)
74
+ expect(profile.skipped_files).to include("vendor/**")
75
+ expect(profile.member_type_hints[:query]).to eq(Autotype::Named.new("App::Entities::Query"))
76
+ end
77
+ end
78
+
79
+ it "discovers autotype.yml from nested directories" do
80
+ Dir.mktmpdir do |dir|
81
+ config_dir = File.join(dir, "project")
82
+ FileUtils.mkdir_p(config_dir)
83
+ File.write(File.join(config_dir, "autotype.yml"), "skip:\n - vendor/**\n")
84
+
85
+ nested = File.join(config_dir, "app", "actors")
86
+ FileUtils.mkdir_p(nested)
87
+
88
+ expect(described_class.find(nested)).to eq(File.join(config_dir, "autotype.yml"))
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe AutotypeNative do
6
+ it "loads the C extension and exposes solve_graph" do
7
+ expect(described_class.available?).to be(true)
8
+ expect(described_class).to respond_to(:solve_graph)
9
+ end
10
+
11
+ it "unifies array append capabilities via the native solver" do
12
+ described_class.load_extension!
13
+
14
+ graph = {
15
+ "capabilities" => [
16
+ {
17
+ "message" => "<<",
18
+ "receiver" => {
19
+ "k" => "generic",
20
+ "name" => "Array",
21
+ "args" => [{ "k" => "var", "id" => 100, "hint" => "element" }]
22
+ },
23
+ "result" => 200,
24
+ "args" => [{ "k" => "named", "name" => "String" }],
25
+ "line" => 1
26
+ }
27
+ ],
28
+ "bindings" => {}
29
+ }
30
+
31
+ result = described_class.solve_graph(graph)
32
+ bindings = result.fetch(:bindings).transform_keys(&:to_i).transform_values { _1.transform_keys(&:to_s) }
33
+
34
+ expect(result.fetch(:converged)).to be(true)
35
+ expect(bindings.fetch(100)).to eq("k" => "named", "name" => "String")
36
+ result_type = bindings.fetch(200)
37
+ expect(result_type.fetch("k")).to eq("generic")
38
+ expect(result_type.fetch("name")).to eq("Array")
39
+ end
40
+
41
+ it "round-trips through NativeBridge on a small inferencer graph" do
42
+ skip "NativeBridge unavailable" unless Autotype::NativeBridge.native_available?
43
+
44
+ source = <<~RUBY
45
+ class Demo
46
+ def push(item)
47
+ buffer = []
48
+ buffer << item
49
+ buffer
50
+ end
51
+ end
52
+ RUBY
53
+
54
+ result = Prism.parse(source)
55
+ collector = Autotype::Collector.new("demo.rb")
56
+ result.value.accept(collector)
57
+ inferencer = Autotype::FixedPointInferencer.new(
58
+ collector.methods,
59
+ constants: collector.constants,
60
+ includes: collector.includes,
61
+ metadata: Autotype::InferenceMetadata.empty
62
+ )
63
+ inferencer.apply_declared_annotations!
64
+ inferencer.send(:anchor_constructor_definitions)
65
+
66
+ Autotype::NativeBridge.prime!(inferencer)
67
+
68
+ renderer = Autotype::Renderer.new(inferencer.send(:resolved_method, collector.methods.first))
69
+ expect(renderer.text.lines.first).to include("-> Array[")
70
+ end
71
+ end