bend-python 0.1.0__py3-none-any.whl

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,817 @@
1
+ // CPython and private Bend 2.0.28 runtime boundary. Every invocation exclusively
2
+ // owns a runtime instance; the generated runtime selects it through bp_current.
3
+ // The build relocates this block before the pinned runtime's first state use.
4
+ // BENDPY_RUNTIME_CONTEXT_BEGIN
5
+ #include <setjmp.h>
6
+ typedef struct BpRuntime {
7
+ Corpus heap;
8
+ u64 allocator[CUBE_T + 1][3 * ALC_WORDS] __attribute__((aligned(128)));
9
+ u32 keep_words, cube_log, bank, workers;
10
+ bool gpu;
11
+ Stk stack;
12
+ u64 heap_size;
13
+ jmp_buf escape;
14
+ bool poisoned;
15
+ char error[256];
16
+ struct BpRuntime* next;
17
+ } BpRuntime;
18
+ static _Thread_local BpRuntime* bp_current;
19
+ #define CORPUS (bp_current->heap)
20
+ #define ALC (bp_current->allocator)
21
+ #define KEEP_WORDS (bp_current->keep_words)
22
+ #define CUBE_LOG (bp_current->cube_log)
23
+ #define bank_lock (bp_current->bank)
24
+ #define pool_size (bp_current->workers)
25
+ #define io_gpu (bp_current->gpu)
26
+ #define io_stk (bp_current->stack)
27
+ #define corpus_size (bp_current->heap_size)
28
+ // BENDPY_RUNTIME_CONTEXT_END
29
+ // The build includes Python.h before the runtime's standard headers.
30
+ #include <Python.h>
31
+ #include <stdatomic.h>
32
+ #include <sys/random.h>
33
+ #include <time.h>
34
+
35
+ #if BANGS
36
+ #error "GPU entry points are not supported by this embedding."
37
+ #endif
38
+ #ifndef __STDC_IEC_559__
39
+ #error "F32 narrowing relies on IEEE 754 double-to-float conversion."
40
+ #endif
41
+
42
+ // An exported Bend function. Its fields are immutable after export, and its
43
+ // captureless Term holds no heap reference, so any runtime instance may run it.
44
+ typedef struct {
45
+ PyObject_HEAD
46
+ Term function;
47
+ bool release_gil;
48
+ PyObject* name;
49
+ PyObject* module;
50
+ } BpFunction;
51
+
52
+ typedef enum { BP_UNIT, BP_OBJECT, BP_U32, BP_F32, BP_BOOL, BP_STRING, BP_NAT, BP_BYTES } BpValue;
53
+ // A decoded Bend list: object handles, String codepoints, or raw U32 words.
54
+ typedef enum { BP_READ_OBJECTS, BP_READ_TEXT, BP_READ_WORDS } BpRead;
55
+ typedef enum { BP_INIT, BP_START, BP_RESUME, BP_DROP } BpOperation;
56
+
57
+ typedef struct {
58
+ BpRuntime* runtime;
59
+ PyObject** objects;
60
+ size_t count, capacity, nargs;
61
+ PyObject* module;
62
+ Term continuation, argument, function;
63
+ Term fields[5];
64
+ u32 effect;
65
+ u32* buffer;
66
+ size_t length;
67
+ BpValue result_kind;
68
+ u64 result;
69
+ u64 key;
70
+ bool release_gil, pending, done;
71
+ char error[256];
72
+ } BpCall;
73
+
74
+ static pthread_mutex_t bp_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
75
+ static BpRuntime* bp_idle;
76
+ static size_t bp_idle_count;
77
+ static pthread_once_t bp_secret_once = PTHREAD_ONCE_INIT;
78
+ static u64 bp_secret;
79
+ static _Atomic u64 bp_call_counter;
80
+ #define BP_IDLE_LIMIT 8
81
+ #define BP_STACK_BYTES (1ull << 31)
82
+ #define BP_STACK_GUARD 16384
83
+ // Instances whose heap grew past this are unmapped rather than cached, so idle
84
+ // instances cannot pin a large call's resident pages until process exit.
85
+ #define BP_IDLE_HEAP_BYTES (32ull << 20)
86
+
87
+ static void bp_assert_attached(void) {
88
+ // PyThreadState_Get fails fatally if this thread is detached, including on
89
+ // free-threaded CPython. Traditional builds must also own the GIL.
90
+ (void)PyThreadState_Get();
91
+ if (bp_current != NULL) Py_FatalError("Python API reached inside Bend evaluation");
92
+ #ifndef Py_GIL_DISABLED
93
+ if (!PyGILState_Check()) Py_FatalError("Bend bridge entered Python without the GIL");
94
+ #endif
95
+ }
96
+
97
+ static void bendpy_panic(const char* message) {
98
+ bp_current->poisoned = true;
99
+ snprintf(bp_current->error, sizeof(bp_current->error), "%s", message);
100
+ longjmp(bp_current->escape, 1);
101
+ }
102
+
103
+ static BpRuntime* bp_runtime_acquire(void) {
104
+ bp_assert_attached();
105
+ PyThreadState* thread = PyEval_SaveThread();
106
+ pthread_mutex_lock(&bp_pool_mutex);
107
+ BpRuntime* runtime = bp_idle;
108
+ if (runtime) {
109
+ bp_idle = runtime->next;
110
+ --bp_idle_count;
111
+ runtime->next = NULL;
112
+ }
113
+ pthread_mutex_unlock(&bp_pool_mutex);
114
+ if (!runtime) {
115
+ // The allocator cache has 128-byte alignment in the pinned runtime.
116
+ void* memory = NULL;
117
+ if (posix_memalign(&memory, _Alignof(BpRuntime), sizeof(BpRuntime)) == 0) {
118
+ runtime = memory;
119
+ memset(runtime, 0, sizeof(*runtime));
120
+ runtime->cube_log = 7;
121
+ }
122
+ }
123
+ PyEval_RestoreThread(thread);
124
+ if (!runtime) PyErr_NoMemory();
125
+ return runtime;
126
+ }
127
+
128
+ static bool bp_runtime_reusable(BpRuntime* runtime) {
129
+ if (runtime->poisoned) return false;
130
+ if (runtime->heap == NULL) return true;
131
+ u64 pages = a32_load(a32_at(runtime->heap, H_BUMP));
132
+ return (HEAP_OFF + (pages << PAGE_BITS)) * sizeof(u64) <= BP_IDLE_HEAP_BYTES;
133
+ }
134
+
135
+ static void bp_runtime_release(BpRuntime* runtime) {
136
+ bp_assert_attached();
137
+ PyThreadState* thread = PyEval_SaveThread();
138
+ bool retained = false, reusable = bp_runtime_reusable(runtime);
139
+ pthread_mutex_lock(&bp_pool_mutex);
140
+ if (reusable && bp_idle_count < BP_IDLE_LIMIT) {
141
+ runtime->next = bp_idle;
142
+ bp_idle = runtime;
143
+ ++bp_idle_count;
144
+ retained = true;
145
+ }
146
+ pthread_mutex_unlock(&bp_pool_mutex);
147
+ if (!retained) {
148
+ if (runtime->heap) munmap(runtime->heap, runtime->heap_size);
149
+ if (runtime->stack) munmap(runtime->stack, BP_STACK_BYTES + BP_STACK_GUARD);
150
+ free(runtime);
151
+ }
152
+ PyEval_RestoreThread(thread);
153
+ }
154
+
155
+ static void* bp_alloc(size_t size) {
156
+ void* p = malloc(size ? size : 1);
157
+ if (p == NULL) bendpy_panic("unable to allocate Bend bridge buffer");
158
+ return p;
159
+ }
160
+
161
+ static Term bp_apply(Env e, Term function, Term argument) {
162
+ Loc at = task_node(e, FID_CLO_APPLY, TERM_HOLE, 0, 0);
163
+ e.mem[at] = function;
164
+ e.mem[at + 1] = argument;
165
+ return corpus_eval(e.mem, term_tsk(FID_CLO_APPLY, at));
166
+ }
167
+
168
+ // Object wrappers may be packed or boxed on the heap. Anything else means the
169
+ // private runtime representation changed, so fail loudly rather than guess.
170
+ static u32 bp_unbox(Env e, Term object) {
171
+ if (term_aux(object) != CID(PyObject)) bendpy_panic("expected a Python object handle");
172
+ if (term_tag(object) == TAG_PAK) return (u32)term_loc(object);
173
+ if (term_tag(object) != TAG_CTR) bendpy_panic("unexpected Python object representation");
174
+ Term field;
175
+ spare_free(e, 0, ctr_take(e, object, 1, &field));
176
+ return (u32)field;
177
+ }
178
+
179
+ static void bp_read_list(Env e, Term list, BpCall* call, BpRead mode) {
180
+ bool string = mode == BP_READ_TEXT;
181
+ size_t length = 0, capacity = 16;
182
+ u32* data = bp_alloc(capacity * sizeof(u32));
183
+ // Store before traversal so a runtime failure still frees the host buffer.
184
+ call->buffer = data;
185
+ u32 cons = string ? CID(SCon) : CID(Con), nil = string ? CID(SNil) : CID(Nil);
186
+ while (term_tag(list) == TAG_CTR && term_aux(list) == cons) {
187
+ Term fields[2];
188
+ spare_free(e, 1, ctr_take(e, list, 2, fields));
189
+ if (length == capacity) {
190
+ capacity *= 2;
191
+ u32* grown = realloc(data, capacity * sizeof(u32));
192
+ if (grown == NULL) bendpy_panic("unable to grow Bend bridge buffer");
193
+ data = grown;
194
+ call->buffer = data;
195
+ }
196
+ if (mode == BP_READ_WORDS && (u64)fields[0] > UINT32_MAX)
197
+ bendpy_panic("unexpected Bend U32 representation");
198
+ data[length++] = mode == BP_READ_OBJECTS ? bp_unbox(e, fields[0]) : (u32)fields[0];
199
+ list = fields[1];
200
+ }
201
+ if ((term_tag(list) != TAG_PAK && term_tag(list) != TAG_CTR) || term_aux(list) != nil)
202
+ bendpy_panic("unexpected Bend list representation");
203
+ term_drop(e, list);
204
+ call->length = length;
205
+ }
206
+
207
+ // Handles are sealed: a Bend Object holds its arena index encrypted by a
208
+ // 32-bit Feistel permutation under a fresh per-invocation key. Bend code has
209
+ // no private constructors, so it could otherwise build PyObject{n} or do
210
+ // arithmetic on an id and silently alias another object of the same call.
211
+ // Accidental fabrication is usually detected, but 32-bit collisions remain
212
+ // possible. This is not authentication; the first rejected handle aborts the call.
213
+ static u64 bp_mix(u64 x) {
214
+ x ^= x >> 30; x *= 0xbf58476d1ce4e5b9ull;
215
+ x ^= x >> 27; x *= 0x94d049bb133111ebull;
216
+ return x ^ (x >> 31);
217
+ }
218
+
219
+ static void bp_seed_secret(void) {
220
+ if (getrandom(&bp_secret, sizeof(bp_secret), 0) != sizeof(bp_secret))
221
+ bp_secret = bp_mix((u64)time(NULL) ^ (u64)(uintptr_t)&bp_secret);
222
+ }
223
+
224
+ static u64 bp_call_key(void) {
225
+ pthread_once(&bp_secret_once, bp_seed_secret);
226
+ return bp_mix(bp_secret + bp_mix(atomic_fetch_add(&bp_call_counter, 1) + 1));
227
+ }
228
+
229
+ static u32 bp_round(u64 key, u32 round, u32 half) {
230
+ return (u32)bp_mix(key ^ ((u64)round << 16 | half)) & 0xffff;
231
+ }
232
+
233
+ static u32 bp_seal(u64 key, u32 index) {
234
+ u32 l = index >> 16, r = index & 0xffff;
235
+ for (u32 i = 0; i < 4; ++i) { u32 t = l ^ bp_round(key, i, r); l = r; r = t; }
236
+ return l << 16 | r;
237
+ }
238
+
239
+ static u32 bp_open(u64 key, u32 handle) {
240
+ u32 l = handle >> 16, r = handle & 0xffff;
241
+ for (u32 i = 4; i-- > 0;) { u32 t = r ^ bp_round(key, i, l); r = l; l = t; }
242
+ return l << 16 | r;
243
+ }
244
+
245
+ static Term bp_object(BpCall* call, u64 index) {
246
+ return term_pak(CID(PyObject), bp_seal(call->key, (u32)index));
247
+ }
248
+
249
+ static Term bp_pack(Env e, BpCall* call) {
250
+ switch (call->result_kind) {
251
+ case BP_UNIT: return term_pak(CID(Unit), 0);
252
+ case BP_OBJECT: return bp_object(call, call->result);
253
+ case BP_BOOL: return term_pak(call->result ? CID(True) : CID(False), 0);
254
+ case BP_STRING:
255
+ case BP_BYTES: {
256
+ bool bytes = call->result_kind == BP_BYTES;
257
+ Term text = term_pak(bytes ? CID(Nil) : CID(SNil), 0);
258
+ for (size_t i = call->length; i > 0; --i)
259
+ text = io_node(e, bytes ? CID(Con) : CID(SCon), call->buffer[i - 1], text);
260
+ return text;
261
+ }
262
+ default: return (Term)call->result;
263
+ }
264
+ }
265
+
266
+ // Decode arguments in the exclusively leased runtime. The resulting handles
267
+ // and codepoints are plain C data; Python callbacks run outside evaluation.
268
+ static void bp_decode(Env e, BpCall* call) {
269
+ Term* f = call->fields;
270
+ switch (call->effect) {
271
+ #ifdef CID(export)
272
+ case CID(export):
273
+ bp_read_list(e, f[0], call, BP_READ_TEXT);
274
+ if (term_tag(f[1]) != TAG_CLO || term_loc(f[1]) != 0)
275
+ bendpy_panic("exports require a captureless function; use a top-level wrapper");
276
+ f[2] = term_aux(f[2]) == CID(True);
277
+ break;
278
+ #endif
279
+ #ifdef CID(require_no_kwargs)
280
+ case CID(require_no_kwargs):
281
+ #endif
282
+ #ifdef CID(to_u32)
283
+ case CID(to_u32):
284
+ #endif
285
+ #ifdef CID(to_f32)
286
+ case CID(to_f32):
287
+ #endif
288
+ #ifdef CID(to_bool)
289
+ case CID(to_bool):
290
+ #endif
291
+ #ifdef CID(to_string)
292
+ case CID(to_string):
293
+ #endif
294
+ #ifdef CID(len)
295
+ case CID(len):
296
+ #endif
297
+ #ifdef CID(truthy)
298
+ case CID(truthy):
299
+ #endif
300
+ #ifdef CID(to_bytes)
301
+ case CID(to_bytes):
302
+ #endif
303
+ f[0] = bp_unbox(e, f[0]); break;
304
+ #ifdef CID(get_item)
305
+ case CID(get_item):
306
+ f[0] = bp_unbox(e, f[0]); f[1] = bp_unbox(e, f[1]); break;
307
+ #endif
308
+ #ifdef CID(set_item)
309
+ case CID(set_item):
310
+ #endif
311
+ #ifdef CID(call)
312
+ case CID(call):
313
+ #endif
314
+ for (int i = 0; i < 3; ++i) f[i] = bp_unbox(e, f[i]); break;
315
+ #ifdef CID(getattr)
316
+ case CID(getattr):
317
+ f[0] = bp_unbox(e, f[0]); bp_read_list(e, f[1], call, BP_READ_TEXT); break;
318
+ #endif
319
+ #ifdef CID(builtins)
320
+ case CID(builtins):
321
+ #endif
322
+ #ifdef CID(type_error)
323
+ case CID(type_error):
324
+ #endif
325
+ #ifdef CID(from_string)
326
+ case CID(from_string):
327
+ #endif
328
+ #ifdef CID(import_module)
329
+ case CID(import_module):
330
+ #endif
331
+ bp_read_list(e, f[0], call, BP_READ_TEXT); break;
332
+ #ifdef CID(tuple)
333
+ case CID(tuple):
334
+ #endif
335
+ #ifdef CID(list)
336
+ case CID(list):
337
+ #endif
338
+ bp_read_list(e, f[0], call, BP_READ_OBJECTS); break;
339
+ #ifdef CID(from_bytes)
340
+ case CID(from_bytes):
341
+ bp_read_list(e, f[0], call, BP_READ_WORDS); break;
342
+ #endif
343
+ #ifdef CID(from_bool)
344
+ case CID(from_bool): f[0] = term_aux(f[0]) == CID(True); break;
345
+ #endif
346
+ #ifdef CID(from_u32)
347
+ case CID(from_u32): break;
348
+ #endif
349
+ #ifdef CID(from_f32)
350
+ case CID(from_f32): break;
351
+ #endif
352
+ #ifdef CID(from_nat)
353
+ case CID(from_nat):
354
+ if ((u64)f[0] > NAT_IMM) bendpy_panic("unexpected Bend Nat representation");
355
+ break;
356
+ #endif
357
+ #ifdef CID(none)
358
+ case CID(none): break;
359
+ #endif
360
+ #ifdef CID(empty_dict)
361
+ case CID(empty_dict): break;
362
+ #endif
363
+ default: bendpy_panic("unsupported foreign effect in Python extension");
364
+ }
365
+ }
366
+
367
+ static bool bp_native(BpCall* call, BpOperation operation) {
368
+ bp_assert_attached();
369
+ #ifdef Py_GIL_DISABLED
370
+ // Without a GIL, staying attached only delays stop-the-world pauses.
371
+ PyThreadState* thread = PyEval_SaveThread();
372
+ #else
373
+ PyThreadState* thread = call->release_gil ? PyEval_SaveThread() : NULL;
374
+ #endif
375
+ bp_current = call->runtime;
376
+ // Poisoned instances are never cached, so every lease starts healthy.
377
+ if (setjmp(bp_current->escape)) {
378
+ snprintf(call->error, sizeof(call->error), "%s", bp_current->error);
379
+ } else {
380
+ if (CORPUS == NULL) {
381
+ corpus_setup(false, 1, 0);
382
+ io_stk = pool_mmap(BP_STACK_BYTES + BP_STACK_GUARD);
383
+ if (mprotect((char*)io_stk + BP_STACK_BYTES, BP_STACK_GUARD, PROT_NONE))
384
+ bendpy_panic("unable to guard Bend stack");
385
+ }
386
+ Env e = { CORPUS, ALC[0] };
387
+ switch (operation) {
388
+ case BP_INIT:
389
+ call->continuation = corpus_eval(CORPUS,
390
+ term_tsk(MAIN_FID, task_node(e, MAIN_FID, TERM_HOLE, 0, 0)));
391
+ call->argument = term_clo(FID_IO_EMIT, 0);
392
+ break;
393
+ case BP_START: {
394
+ Term args = term_pak(CID(Nil), 0);
395
+ for (size_t i = call->nargs; i > 0; --i)
396
+ args = io_node(e, CID(Con), bp_object(call, i - 1), args);
397
+ Term input = io_node(e, CID(PyCall), args, bp_object(call, call->nargs));
398
+ call->continuation = bp_apply(e, call->function, input);
399
+ call->argument = term_clo(FID_IO_EMIT, 0);
400
+ break;
401
+ }
402
+ case BP_RESUME: {
403
+ if (call->pending) {
404
+ call->argument = bp_pack(e, call);
405
+ call->pending = false;
406
+ free(call->buffer); call->buffer = NULL;
407
+ }
408
+ Term request = bp_apply(e, call->continuation, call->argument);
409
+ call->continuation = 0;
410
+ call->effect = (u32)term_aux(request);
411
+ if (call->effect == CID(Emit)) {
412
+ Term value;
413
+ spare_free(e, 0, ctr_take(e, request, 1, &value));
414
+ if (call->module) term_drop(e, value);
415
+ else call->result = bp_unbox(e, value);
416
+ call->done = true;
417
+ break;
418
+ }
419
+ u32 n = cid_arity(call->effect);
420
+ if (n == 0 || n > 5) bendpy_panic("unsupported foreign effect arity");
421
+ spare_free(e, cls_fit(n), ctr_take(e, request, n, call->fields));
422
+ call->continuation = call->fields[n - 1];
423
+ bp_decode(e, call);
424
+ break;
425
+ }
426
+ case BP_DROP:
427
+ if (call->continuation) term_drop(e, call->continuation);
428
+ break;
429
+ }
430
+ }
431
+ bp_current = NULL;
432
+ if (thread) PyEval_RestoreThread(thread);
433
+ bp_assert_attached();
434
+ return call->error[0] == 0;
435
+ }
436
+
437
+ // Each invocation owns a reference arena. Handles never contain PyObject*
438
+ // addresses. Open and check them before access, and release the arena outside
439
+ // evaluation.
440
+ static PyObject* bp_get(BpCall* call, u64 handle) {
441
+ bp_assert_attached();
442
+ u32 index = bp_open(call->key, (u32)handle);
443
+ if (index >= call->count) {
444
+ PyErr_SetString(PyExc_ValueError,
445
+ "invalid Python object handle: handles must come from the bridge in this call");
446
+ return NULL;
447
+ }
448
+ return call->objects[index];
449
+ }
450
+
451
+ static bool bp_add(BpCall* call, PyObject* object) {
452
+ bp_assert_attached();
453
+ if (object == NULL) return false;
454
+ if (call->count == UINT32_MAX) {
455
+ Py_DECREF(object); PyErr_SetString(PyExc_OverflowError, "too many object handles"); return false;
456
+ }
457
+ if (call->count == call->capacity) {
458
+ size_t capacity = call->capacity ? call->capacity * 2 : 16;
459
+ PyObject** objects = PyMem_Realloc(call->objects, capacity * sizeof(PyObject*));
460
+ if (objects == NULL) { Py_DECREF(object); PyErr_NoMemory(); return false; }
461
+ call->objects = objects; call->capacity = capacity;
462
+ }
463
+ call->result_kind = BP_OBJECT;
464
+ call->result = call->count;
465
+ call->objects[call->count++] = object;
466
+ return true;
467
+ }
468
+
469
+ static PyObject* bp_text(BpCall* call) {
470
+ bp_assert_attached();
471
+ for (size_t i = 0; i < call->length; ++i) {
472
+ if (call->buffer[i] > 0x10ffff) {
473
+ PyErr_SetString(PyExc_ValueError, "Bend String contains an invalid Unicode codepoint");
474
+ return NULL;
475
+ }
476
+ }
477
+ return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, call->buffer, call->length);
478
+ }
479
+
480
+ static PyObject* bp_call_python(PyObject*, PyObject*, PyObject*);
481
+
482
+ static void bp_function_dealloc(PyObject* self) {
483
+ BpFunction* function = (BpFunction*)self;
484
+ Py_XDECREF(function->name); Py_XDECREF(function->module);
485
+ PyObject_Free(self);
486
+ }
487
+
488
+ static PyObject* bp_function_name(PyObject* self, void* closure) {
489
+ return Py_NewRef(((BpFunction*)self)->name);
490
+ }
491
+
492
+ static PyObject* bp_function_module(PyObject* self, void* closure) {
493
+ return Py_NewRef(((BpFunction*)self)->module);
494
+ }
495
+
496
+ static PyObject* bp_function_repr(PyObject* self) {
497
+ BpFunction* function = (BpFunction*)self;
498
+ return PyUnicode_FromFormat("<bend function %U.%U>", function->module, function->name);
499
+ }
500
+
501
+ // Pickle by reference, like a module-level function: module.name.
502
+ static PyObject* bp_function_reduce(PyObject* self, PyObject* unused) {
503
+ return Py_NewRef(((BpFunction*)self)->name);
504
+ }
505
+
506
+ static PyGetSetDef bp_function_getset[] = {
507
+ {"__name__", bp_function_name, NULL, NULL, NULL},
508
+ {"__qualname__", bp_function_name, NULL, NULL, NULL},
509
+ {"__module__", bp_function_module, NULL, NULL, NULL},
510
+ {NULL},
511
+ };
512
+
513
+ static PyMethodDef bp_function_methods[] = {
514
+ {"__reduce__", bp_function_reduce, METH_NOARGS, NULL},
515
+ {NULL},
516
+ };
517
+
518
+ static PyTypeObject bp_function_type = {
519
+ PyVarObject_HEAD_INIT(NULL, 0)
520
+ .tp_name = "bend_python.function",
521
+ .tp_basicsize = sizeof(BpFunction),
522
+ .tp_dealloc = bp_function_dealloc,
523
+ .tp_repr = bp_function_repr,
524
+ .tp_call = bp_call_python,
525
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_DISALLOW_INSTANTIATION,
526
+ .tp_doc = "A native Bend function.",
527
+ .tp_methods = bp_function_methods,
528
+ .tp_getset = bp_function_getset,
529
+ };
530
+
531
+ static bool bp_export(BpCall* call) {
532
+ if (!call->module) { PyErr_SetString(PyExc_RuntimeError, "exports are only allowed during initialization"); return false; }
533
+ PyObject* name = bp_text(call);
534
+ if (name == NULL) return false;
535
+ Py_ssize_t size;
536
+ const char* text = PyUnicode_AsUTF8AndSize(name, &size);
537
+ if (!text) { Py_DECREF(name); return false; }
538
+ if (size == 0 || strlen(text) != (size_t)size || PyObject_HasAttr(call->module, name)) {
539
+ Py_DECREF(name); PyErr_SetString(PyExc_ValueError, "empty, duplicate, or NUL-containing export name"); return false;
540
+ }
541
+ PyObject* module_name = PyModule_GetNameObject(call->module);
542
+ BpFunction* function = module_name ? PyObject_New(BpFunction, &bp_function_type) : NULL;
543
+ if (!function) { Py_DECREF(name); Py_XDECREF(module_name); return false; }
544
+ function->function = call->fields[1]; function->release_gil = call->fields[2];
545
+ function->name = name; function->module = module_name;
546
+ int added = PyModule_AddObjectRef(call->module, text, (PyObject*)function);
547
+ Py_DECREF(function);
548
+ return added == 0;
549
+ }
550
+
551
+ static bool bp_effect(BpCall* call) {
552
+ bp_assert_attached();
553
+ Term* f = call->fields;
554
+ PyObject *a = NULL, *b = NULL, *c = NULL, *result = NULL;
555
+ call->result_kind = BP_UNIT;
556
+ switch (call->effect) {
557
+ #ifdef CID(export)
558
+ case CID(export): return bp_export(call);
559
+ #endif
560
+ #ifdef CID(require_no_kwargs)
561
+ case CID(require_no_kwargs):
562
+ a = bp_get(call, f[0]);
563
+ if (!a) return false;
564
+ if (!PyDict_Check(a) || PyDict_Size(a) != 0) {
565
+ PyErr_SetString(PyExc_TypeError, "this function accepts positional arguments only"); return false;
566
+ }
567
+ return true;
568
+ #endif
569
+ #ifdef CID(to_u32)
570
+ case CID(to_u32): {
571
+ a = bp_get(call, f[0]); if (!a) return false;
572
+ if (!PyLong_Check(a) || PyBool_Check(a)) {
573
+ PyErr_SetString(PyExc_TypeError, "expected an integer in the U32 range"); return false;
574
+ }
575
+ unsigned long x = PyLong_AsUnsignedLong(a);
576
+ if (PyErr_Occurred()) return false;
577
+ if (x > UINT32_MAX) { PyErr_SetString(PyExc_OverflowError, "integer does not fit U32"); return false; }
578
+ call->result_kind = BP_U32; call->result = x; return true;
579
+ }
580
+ #endif
581
+ #ifdef CID(from_u32)
582
+ case CID(from_u32): return bp_add(call, PyLong_FromUnsignedLong((u32)f[0]));
583
+ #endif
584
+ #ifdef CID(from_nat)
585
+ case CID(from_nat): return bp_add(call, PyLong_FromUnsignedLongLong((u64)f[0]));
586
+ #endif
587
+ #ifdef CID(to_f32)
588
+ case CID(to_f32): {
589
+ a = bp_get(call, f[0]); if (!a) return false;
590
+ if (!PyFloat_Check(a)) { PyErr_SetString(PyExc_TypeError, "expected a float"); return false; }
591
+ // IEEE 754 narrowing (Annex F): round to nearest, overflowing to infinity.
592
+ union { float f; u32 u; } x = { .f = (float)PyFloat_AsDouble(a) };
593
+ call->result_kind = BP_F32; call->result = x.u; return true;
594
+ }
595
+ #endif
596
+ #ifdef CID(from_f32)
597
+ case CID(from_f32): { union { u32 u; float f; } x = { .u = (u32)f[0] }; return bp_add(call, PyFloat_FromDouble(x.f)); }
598
+ #endif
599
+ #ifdef CID(to_bool)
600
+ case CID(to_bool):
601
+ a = bp_get(call, f[0]); if (!a) return false;
602
+ if (!PyBool_Check(a)) { PyErr_SetString(PyExc_TypeError, "expected a bool"); return false; }
603
+ call->result_kind = BP_BOOL; call->result = a == Py_True; return true;
604
+ #endif
605
+ #ifdef CID(from_bool)
606
+ case CID(from_bool): return bp_add(call, PyBool_FromLong(f[0] != 0));
607
+ #endif
608
+ #ifdef CID(to_string)
609
+ case CID(to_string): {
610
+ a = bp_get(call, f[0]); if (!a) return false;
611
+ if (!PyUnicode_Check(a)) { PyErr_SetString(PyExc_TypeError, "expected a string"); return false; }
612
+ Py_ssize_t n = PyUnicode_GetLength(a);
613
+ call->buffer = malloc((n ? n : 1) * sizeof(u32));
614
+ if (!call->buffer) { PyErr_NoMemory(); return false; }
615
+ for (Py_ssize_t i = 0; i < n; ++i) call->buffer[i] = PyUnicode_ReadChar(a, i);
616
+ call->length = n; call->result_kind = BP_STRING; return true;
617
+ }
618
+ #endif
619
+ #ifdef CID(from_string)
620
+ case CID(from_string): return bp_add(call, bp_text(call));
621
+ #endif
622
+ #ifdef CID(get_item)
623
+ case CID(get_item):
624
+ a = bp_get(call, f[0]); b = bp_get(call, f[1]); if (!a || !b) return false;
625
+ return bp_add(call, PyObject_GetItem(a, b));
626
+ #endif
627
+ #ifdef CID(set_item)
628
+ case CID(set_item):
629
+ a = bp_get(call, f[0]); b = bp_get(call, f[1]); c = bp_get(call, f[2]);
630
+ return a && b && c && PyObject_SetItem(a, b, c) == 0;
631
+ #endif
632
+ #ifdef CID(getattr)
633
+ case CID(getattr):
634
+ a = bp_get(call, f[0]); b = bp_text(call); if (!a || !b) { Py_XDECREF(b); return false; }
635
+ result = PyObject_GetAttr(a, b); Py_DECREF(b); return bp_add(call, result);
636
+ #endif
637
+ #ifdef CID(call)
638
+ case CID(call):
639
+ a = bp_get(call, f[0]); b = bp_get(call, f[1]); c = bp_get(call, f[2]);
640
+ if (!a || !b || !c) return false;
641
+ if (!PyTuple_Check(b) || !PyDict_Check(c)) {
642
+ PyErr_SetString(PyExc_TypeError, "call requires a tuple of arguments and a dict of keywords"); return false;
643
+ }
644
+ return bp_add(call, PyObject_Call(a, b, c));
645
+ #endif
646
+ #ifdef CID(builtins)
647
+ case CID(builtins):
648
+ a = PyImport_ImportModule("builtins"); b = bp_text(call);
649
+ if (!a || !b) { Py_XDECREF(a); Py_XDECREF(b); return false; }
650
+ result = PyObject_GetAttr(a, b); Py_DECREF(a); Py_DECREF(b); return bp_add(call, result);
651
+ #endif
652
+ #ifdef CID(none)
653
+ case CID(none): return bp_add(call, Py_NewRef(Py_None));
654
+ #endif
655
+ #ifdef CID(empty_dict)
656
+ case CID(empty_dict): return bp_add(call, PyDict_New());
657
+ #endif
658
+ #ifdef CID(tuple)
659
+ case CID(tuple):
660
+ #endif
661
+ #ifdef CID(list)
662
+ case CID(list):
663
+ #endif
664
+ result = PyTuple_New(call->length);
665
+ if (!result) return false;
666
+ for (size_t i = 0; i < call->length; ++i) {
667
+ a = bp_get(call, call->buffer[i]);
668
+ if (!a) { Py_DECREF(result); return false; }
669
+ PyTuple_SET_ITEM(result, i, Py_NewRef(a));
670
+ }
671
+ #ifdef CID(list)
672
+ if (call->effect == CID(list)) { a = PySequence_List(result); Py_DECREF(result); result = a; }
673
+ #endif
674
+ return bp_add(call, result);
675
+ #ifdef CID(truthy)
676
+ case CID(truthy): {
677
+ a = bp_get(call, f[0]); if (!a) return false;
678
+ int truth = PyObject_IsTrue(a); if (truth < 0) return false;
679
+ call->result_kind = BP_BOOL; call->result = truth; return true;
680
+ }
681
+ #endif
682
+ #ifdef CID(to_bytes)
683
+ case CID(to_bytes): {
684
+ a = bp_get(call, f[0]); if (!a) return false;
685
+ // Any C-contiguous buffer: bytes, bytearray, memoryview, array.array, ...
686
+ Py_buffer view;
687
+ if (PyObject_GetBuffer(a, &view, PyBUF_C_CONTIGUOUS) < 0) return false;
688
+ call->buffer = malloc((view.len ? view.len : 1) * sizeof(u32));
689
+ if (!call->buffer) { PyBuffer_Release(&view); PyErr_NoMemory(); return false; }
690
+ const unsigned char* bytes = view.buf;
691
+ #ifdef Py_GIL_DISABLED
692
+ // Buffer exports pin storage, but do not stop bytearray slice writes.
693
+ // Other exporters/views require caller synchronization against alias writes.
694
+ Py_BEGIN_CRITICAL_SECTION(a);
695
+ #endif
696
+ for (Py_ssize_t i = 0; i < view.len; ++i) call->buffer[i] = bytes[i];
697
+ #ifdef Py_GIL_DISABLED
698
+ Py_END_CRITICAL_SECTION();
699
+ #endif
700
+ call->length = view.len; call->result_kind = BP_BYTES;
701
+ PyBuffer_Release(&view);
702
+ return true;
703
+ }
704
+ #endif
705
+ #ifdef CID(from_bytes)
706
+ case CID(from_bytes): {
707
+ result = PyBytes_FromStringAndSize(NULL, call->length);
708
+ if (!result) return false;
709
+ char* out = PyBytes_AS_STRING(result);
710
+ for (size_t i = 0; i < call->length; ++i) {
711
+ if (call->buffer[i] > 255) {
712
+ Py_DECREF(result);
713
+ PyErr_Format(PyExc_ValueError, "byte %zu is %u, outside range(256)", i, call->buffer[i]);
714
+ return false;
715
+ }
716
+ out[i] = (char)call->buffer[i];
717
+ }
718
+ return bp_add(call, result);
719
+ }
720
+ #endif
721
+ #ifdef CID(import_module)
722
+ case CID(import_module):
723
+ a = bp_text(call); if (!a) return false;
724
+ result = PyImport_Import(a); Py_DECREF(a); return bp_add(call, result);
725
+ #endif
726
+ #ifdef CID(len)
727
+ case CID(len): {
728
+ a = bp_get(call, f[0]); if (!a) return false;
729
+ Py_ssize_t n = PyObject_Length(a); if (n < 0) return false;
730
+ if ((u64)n > NAT_IMM) { PyErr_SetString(PyExc_OverflowError, "length exceeds Bend Nat range"); return false; }
731
+ call->result_kind = BP_NAT; call->result = n; return true;
732
+ }
733
+ #endif
734
+ #ifdef CID(type_error)
735
+ case CID(type_error):
736
+ a = bp_text(call); if (!a) return false;
737
+ PyErr_SetObject(PyExc_TypeError, a); Py_DECREF(a); return false;
738
+ #endif
739
+ default: PyErr_SetString(PyExc_RuntimeError, "unsupported Python effect"); return false;
740
+ }
741
+ }
742
+
743
+ static PyObject* bp_run(BpCall* call, BpOperation start) {
744
+ call->key = bp_call_key();
745
+ call->runtime = bp_runtime_acquire();
746
+ bool ok = call->runtime != NULL && bp_native(call, start);
747
+ while (ok && !call->done) {
748
+ ok = bp_native(call, BP_RESUME);
749
+ if (!ok || call->done) break;
750
+ ok = bp_effect(call);
751
+ // Conversions returning native data retain the buffer until BP_RESUME packs it.
752
+ if (call->result_kind != BP_STRING && call->result_kind != BP_BYTES) { free(call->buffer); call->buffer = NULL; }
753
+ call->pending = ok;
754
+ }
755
+ PyObject* result = NULL;
756
+ if (ok) {
757
+ PyObject* object = call->module ? call->module : bp_get(call, call->result);
758
+ if (object) result = Py_NewRef(object);
759
+ } else {
760
+ if (!PyErr_Occurred()) PyErr_SetString(PyExc_RuntimeError, call->error);
761
+ // A Python exception only aborts its invocation. Native failures discard
762
+ // the damaged instance rather than affecting other callers.
763
+ if (call->runtime && !call->error[0]) bp_native(call, BP_DROP);
764
+ }
765
+ // Return the lease before decref: finalizers can invoke this extension too.
766
+ if (call->runtime) bp_runtime_release(call->runtime);
767
+ call->runtime = NULL;
768
+ free(call->buffer);
769
+ for (size_t i = 0; i < call->count; ++i) Py_DECREF(call->objects[i]);
770
+ PyMem_Free(call->objects);
771
+ return result;
772
+ }
773
+
774
+ static PyObject* bp_call_python(PyObject* self, PyObject* args, PyObject* kwargs) {
775
+ bp_assert_attached();
776
+ // Older CPython can reuse a single-phase module in another interpreter
777
+ // without invoking PyInit again; enforce the boundary on every call too.
778
+ if (PyInterpreterState_GetID(PyInterpreterState_Get()) != 0) {
779
+ PyErr_SetString(PyExc_RuntimeError, "Bend calls require the main Python interpreter"); return NULL;
780
+ }
781
+ BpFunction* entry = (BpFunction*)self;
782
+ BpCall call = { .function = entry->function, .release_gil = entry->release_gil,
783
+ .nargs = (size_t)PyTuple_Size(args) };
784
+ for (size_t i = 0; i < call.nargs; ++i) {
785
+ if (!bp_add(&call, Py_NewRef(PyTuple_GetItem(args, i)))) goto fail;
786
+ }
787
+ // C callers (PyObject_Call) may pass their own dict; like a Python **kwargs
788
+ // function, Bend gets a fresh one it may mutate or return.
789
+ if (!bp_add(&call, kwargs ? PyDict_Copy(kwargs) : PyDict_New())) goto fail;
790
+ return bp_run(&call, BP_START);
791
+ fail:
792
+ for (size_t i = 0; i < call.count; ++i) Py_DECREF(call.objects[i]);
793
+ PyMem_Free(call.objects);
794
+ return NULL;
795
+ }
796
+
797
+ static struct PyModuleDef bp_definition = {
798
+ PyModuleDef_HEAD_INIT, BENDPY_MODULE_NAME,
799
+ "Native Python bindings written in Bend.", -1, NULL
800
+ };
801
+
802
+ PyMODINIT_FUNC BENDPY_INIT(void) {
803
+ bp_assert_attached();
804
+ if (PyInterpreterState_GetID(PyInterpreterState_Get()) != 0) {
805
+ PyErr_SetString(PyExc_ImportError, "Bend currently supports only the main Python interpreter"); return NULL;
806
+ }
807
+ if (PyType_Ready(&bp_function_type) < 0) return NULL;
808
+ PyObject* module = PyModule_Create(&bp_definition);
809
+ if (!module) return NULL;
810
+ #ifdef Py_GIL_DISABLED
811
+ PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED);
812
+ #endif
813
+ BpCall call = { .module = module };
814
+ PyObject* result = bp_run(&call, BP_INIT);
815
+ Py_DECREF(module);
816
+ return result;
817
+ }