Files
Tasmota/lib/libesp32/berry/berry_port/be_func.py
T
SoWizardands-hadinger 1d16504489 Berry: safely collect partially constructed native closures (#25093)
* Berry: safely collect partially constructed native closures

* Update be_func.py

---------

Co-authored-by: s-hadinger <49731213+s-hadinger@users.noreply.github.com>
2026-10-03 00:23:09 +02:00

516 lines
16 KiB
Python

"""
Berry closure and upvalue management.
Mirrors: src/be_func.c / src/be_func.h
Implements closure creation, upvalue initialization and lifecycle (open/close),
prototype allocation, and debug variable name lookup. The open upvalue list
(vm.upvalist) is a singly-linked list sorted by stack level, enabling
efficient sharing of upvalues across closures that capture the same variable.
"""
# /********************************************************************
# ** Copyright (c) 2018-2020 Guan Wenliang
# ** This file is part of the Berry default interpreter.
# ** skiars@qq.com, https://github.com/Skiars/berry
# ** See Copyright Notice in the LICENSE file or at
# ** https://github.com/Skiars/berry/blob/master/LICENSE
# ********************************************************************/
from berry_port.be_object import (
BE_PROTO, BE_CLOSURE, BE_NTVCLOS,
bproto, bclosure, bntvclos, bupval, bvalue,
var_setnil, var_setntvclos, var_toobj,
cast_closure,
)
from berry_port.be_mem import be_malloc, be_free
from berry_port.berry_conf import (
BE_DEBUG_SOURCE_FILE,
BE_DEBUG_RUNTIME_INFO,
BE_DEBUG_VAR_INFO,
)
# ============================================================================
# Macros from be_func.h
# ============================================================================
# #define clousersize(n) \
# (sizeof(bclosure) + sizeof(bupval*) * ((size_t)(n) - 1))
# (Not needed in Python — bclosure.upvals is a Python list.)
# #define be_ntvclos_upval(cc, n) \
# (((bupval**)((size_t)cc + sizeof(bntvclos)))[n])
def be_ntvclos_upval(cc, n):
"""Access the n-th upvalue pointer of a native closure.
In C this is pointer arithmetic past the bntvclos struct.
In Python, upvals is a list on the bntvclos object.
"""
# #define be_ntvclos_upval(cc, n) \
# (((bupval**)((size_t)cc + sizeof(bntvclos)))[n])
return cc.upvals[n]
# ============================================================================
# Internal: findupval (static in C)
# ============================================================================
# static bupval* findupval(bvm *vm, bvalue *level)
# {
# bupval *node = vm->upvalist;
# while (node != NULL && node->value > level) {
# node = node->u.next;
# }
# if (!node || node->value != level) {
# /* not found */
# node = be_malloc(vm, sizeof(bupval));
# node->value = level;
# node->refcnt = 0;
# /* insert to list head */
# node->u.next = vm->upvalist;
# vm->upvalist = node;
# }
# return node;
# }
def _findupval(vm, level):
"""Find or create an open upvalue for the stack slot at *level*.
The open upvalue list (vm.upvalist) is a singly-linked list.
In the C implementation, upvalues are sorted by stack pointer
(descending) and the search walks until node->value <= level.
In the Python port, *level* is an integer index into vm.stack.
The list is walked via u_next links. If no matching upvalue is
found, a new one is created and inserted at the list head.
"""
node = vm.upvalist
while node is not None and node.value > level:
node = node.u_next
if node is None or node.value != level:
# Not found — create a new upvalue
new_uv = bupval()
new_uv.value = level
new_uv.refcnt = 0
# Insert at list head
new_uv.u_next = vm.upvalist
vm.upvalist = new_uv
node = new_uv
return node
# ============================================================================
# be_initupvals
# ============================================================================
# void be_initupvals(bvm *vm, bclosure *cl)
# {
# int count = cl->proto->nupvals;
# bupvaldesc *desc = cl->proto->upvals;
# bvalue *stack = vm->reg;
# bupval **uv = cl->upvals;
# bupval **superuv = cast(bclosure*, var_toobj(vm->cf->func))->upvals;
# for (; count--; desc++, uv++) {
# if (desc->instack) {
# bvalue *ref = stack + desc->idx;
# *uv = findupval(vm, ref);
# } else {
# *uv = superuv[desc->idx];
# }
# (*uv)->refcnt++;
# }
# }
def be_initupvals(vm, cl):
"""Initialize upvalues for a newly created closure.
For each upvalue descriptor in the closure's prototype:
- If instack: the upvalue captures a local in the current frame,
so find/create an open upvalue pointing to that stack slot.
- Otherwise: the upvalue is inherited from the enclosing closure's
upvalue at the given index.
Each resolved upvalue gets its refcnt incremented.
"""
count = cl.proto.nupvals
descs = cl.proto.upvals # list of bupvaldesc
reg_idx = vm.reg_idx # base register index for current frame
# The enclosing closure is the function value of the current call frame
super_cl = cast_closure(var_toobj(vm.stack[vm.cf.func]))
superuv = super_cl.upvals
# Ensure cl.upvals has enough slots
while len(cl.upvals) < count:
cl.upvals.append(None)
for i in range(count):
desc = descs[i]
if desc.instack:
# Capture a local variable from the current stack frame
ref = reg_idx + desc.idx
cl.upvals[i] = _findupval(vm, ref)
else:
# Inherit from enclosing closure
cl.upvals[i] = superuv[desc.idx]
cl.upvals[i].refcnt += 1
# ============================================================================
# be_upvals_close
# ============================================================================
# void be_upvals_close(bvm *vm, bvalue *level)
# {
# bupval *node = vm->upvalist, *next;
# bupval **prev = &vm->upvalist;
# while (node) {
# next = node->u.next;
# if (node->value >= level) {
# if (!node->refcnt) {
# be_free(vm, node, sizeof(bupval));
# } else {
# node->u.value = *node->value; /* move value to upvalue slot */
# node->value = &node->u.value;
# }
# *prev = next; /* remove from linked list */
# } else {
# prev = &node->u.next;
# }
# node = next;
# }
# }
def be_upvals_close(vm, level):
"""Close all open upvalues at or above *level*.
*level* is an integer stack index. For each upvalue whose stack
reference >= level:
- If refcnt == 0: the upvalue is unreferenced, discard it.
- Otherwise: copy the stack value into the upvalue's internal
storage (u_value) and redirect value to point to u_value
(i.e., mark it as "closed").
The upvalue is removed from the open list in either case.
"""
# We rebuild the linked list, skipping closed/freed nodes
node = vm.upvalist
# Use a sentinel approach: track the "previous node" for relinking
# Since Python doesn't have pointer-to-pointer, we handle head specially
new_head = None
tail = None
while node is not None:
next_node = node.u_next
if node.value >= level:
# This upvalue is at or above the closing level
if not node.refcnt:
# No references — just discard
pass
else:
# Close: copy stack value into internal storage, redirect
node.u_value = bvalue()
# Copy the actual value from the stack
src = vm.stack[node.value]
node.u_value.type = src.type
node.u_value.v = src.v
# Mark as closed: set value to None (no longer a stack index)
node.value = None
# Remove from linked list (don't link into new list)
# But if closed (refcnt > 0), the upvalue still exists,
# just not in the open list
else:
# Keep in the open list
if new_head is None:
new_head = node
tail = node
else:
tail.u_next = node
tail = node
tail.u_next = None
node = next_node
vm.upvalist = new_head
# ============================================================================
# be_release_upvalues
# ============================================================================
# void be_release_upvalues(bvm *vm, bclosure *cl)
# {
# int i, count = cl->nupvals;
# for (i = 0; i < count; ++i) {
# bupval *uv = cl->upvals[i];
# if (uv) {
# if (uv->refcnt) {
# --uv->refcnt;
# }
# /* delete non-referenced closed upvalue */
# if (uv->value == &uv->u.value && !uv->refcnt) {
# be_free(vm, uv, sizeof(bupval));
# }
# }
# }
# }
def be_release_upvalues(vm, cl):
"""Decrement refcnt on all upvalues held by closure *cl*.
If an upvalue is closed (value points to internal storage) and its
refcnt drops to zero, it is freed.
"""
count = cl.nupvals
for i in range(count):
if i < len(cl.upvals):
uv = cl.upvals[i]
else:
uv = None
if uv is not None:
if uv.refcnt:
uv.refcnt -= 1
# Check if closed (self-reference sentinel) and unreferenced
if _is_closed(uv) and not uv.refcnt:
# In Python, just let GC reclaim it
cl.upvals[i] = None
def _is_closed(uv):
"""Check whether an upvalue is closed.
In C: uv->value == &uv->u.value (pointer to internal storage).
In Python: closed upvalues have value set to None (no longer a stack index).
"""
return uv.value is None
def upval_value(uv):
"""Read the current value of an upvalue.
If open, reads from the VM stack (caller must provide vm).
If closed, reads from the internal u_value storage.
This is a convenience for code that needs to dereference an upvalue.
For open upvalues, the caller should use vm.stack[uv.value] directly.
For closed upvalues, use uv.u_value.
"""
if _is_closed(uv):
return uv.u_value
# For open upvalues, value is a stack index — caller needs vm.stack
return None # caller must use vm.stack[uv.value]
# ============================================================================
# be_newproto
# ============================================================================
# bproto* be_newproto(bvm *vm)
# {
# bgcobject *gco = be_gcnew(vm, BE_PROTO, bproto);
# bproto *p = cast_proto(gco);
# if (p) {
# p->upvals = NULL;
# p->ktab = NULL;
# p->ptab = NULL;
# p->code = NULL;
# p->name = NULL;
# p->gray = NULL;
# p->codesize = 0;
# p->nupvals = 0;
# p->nproto = 0;
# p->nconst = 0;
# p->nstack = 0;
# p->codesize = 0;
# p->argc = 0;
# p->varg = 0;
# #if BE_DEBUG_SOURCE_FILE
# p->source = NULL;
# #endif
# #if BE_DEBUG_RUNTIME_INFO
# p->lineinfo = NULL;
# p->nlineinfo = 0;
# #endif
# #if BE_DEBUG_VAR_INFO
# p->varinfo = NULL;
# p->nvarinfo = 0;
# #endif
# }
# return p;
# }
def be_newproto(vm):
"""Allocate and initialize a new function prototype.
All fields are zeroed/nulled.
"""
p = bproto()
p.type = BE_PROTO
p.upvals = None
p.ktab = None
p.ptab = None
p.code = None
p.name = None
p.codesize = 0
p.nupvals = 0
p.nproto = 0
p.nconst = 0
p.nstack = 0
p.argc = 0
p.varg = 0
# Conditional debug fields (always present in Python)
p.source = None
p.lineinfo = None
p.nlineinfo = 0
p.varinfo = None
p.nvarinfo = 0
return p
# ============================================================================
# be_newclosure
# ============================================================================
# bclosure* be_newclosure(bvm *vm, int nupval)
# {
# bgcobject *gco = be_newgcobj(vm, BE_CLOSURE, clousersize(nupval));
# bclosure *cl = cast_closure(gco);
# if (cl) {
# cl->proto = NULL;
# cl->nupvals = (bbyte)nupval;
# while (nupval--) {
# cl->upvals[nupval] = NULL;
# }
# }
# return cl;
# }
def be_newclosure(vm, nupval):
"""Allocate a new Berry closure with *nupval* upvalue slots.
The closure is registered on the GC list. All upvalue slots are
initialized to None, and proto is set to None.
"""
cl = bclosure()
cl.type = BE_CLOSURE
cl.proto = None
cl.nupvals = nupval
cl.upvals = [None] * nupval
return cl
# ============================================================================
# Internal: init_upvals for native closures (static in C)
# ============================================================================
# static void init_upvals(bvm *vm, bntvclos *f)
# {
# int count = f->nupvals;
# bupval **upvals = &be_ntvclos_upval(f, 0);
# while (count--) {
# bupval *uv = be_malloc(vm, sizeof(bupval)); /* was closed */
# uv->value = &uv->u.value;
# uv->refcnt = 1;
# var_setnil(uv->value);
# *upvals++ = uv;
# }
# }
def _init_upvals_ntv(vm, f):
"""Initialize the preallocated upvalue slots of a native closure.
Each upvalue is created in the "closed" state (value points to
internal storage) with refcnt=1 and a nil value. The slots are already
None before this function begins, so an allocation failure leaves a
safely destructible partially initialized closure, mirroring the C
runtime's NULL-slot behavior.
"""
for i in range(f.nupvals):
uv = bupval()
# Mark as closed: value points to internal storage (u_value)
uv.refcnt = 1
uv.u_value = bvalue()
var_setnil(uv.u_value)
uv.value = uv.u_value # closed: points to own storage
f.upvals[i] = uv
# ============================================================================
# be_newntvclosure
# ============================================================================
# bntvclos* be_newntvclosure(bvm *vm, bntvfunc cf, int nupvals)
# {
# size_t size = sizeof(bntvclos) + sizeof(bupval*) * nupvals;
# bgcobject *gco = be_newgcobj(vm, BE_NTVCLOS, size);
# bntvclos *f = cast_ntvclos(gco);
# if (f) {
# f->f = cf;
# f->nupvals = (bbyte)nupvals;
# if (nupvals) {
# var_setntvclos(vm->top, f);
# be_incrtop(vm);
# init_upvals(vm, f); /* may be GC */
# be_stackpop(vm, 1);
# }
# }
# return f;
# }
def be_newntvclosure(vm, cf, nupvals):
"""Allocate a new native closure wrapping callable *cf* with *nupvals* upvalue slots.
In C, the native closure is temporarily pushed onto the stack to
protect it from GC during upvalue initialization. In Python we
register on the GC list directly.
"""
f = bntvclos()
f.type = BE_NTVCLOS
f.f = cf
f.nupvals = nupvals
# Match the C runtime: publish every slot as None before any per-upvalue
# allocation. If construction raises, Python GC can safely reclaim the
# partially initialized closure without dereferencing a stale upvalue.
f.upvals = [None] * nupvals
if nupvals:
_init_upvals_ntv(vm, f)
return f
# ============================================================================
# be_func_varname
# ============================================================================
# #if BE_DEBUG_VAR_INFO
# bstring* be_func_varname(bproto *proto, int index, int pc)
# {
# int i, nvarinfo = proto->nvarinfo;
# bvarinfo *varinfo = proto->varinfo;
# for (i = 0; i < nvarinfo && varinfo[i].beginpc <= pc; ++i) {
# if (pc <= varinfo[i].endpc && index-- == 0) {
# return varinfo[i].name;
# }
# }
# return NULL;
# }
# #endif
def be_func_varname(proto, index, pc):
"""Look up the name of a local variable by register index at a given PC.
Walks the varinfo table (available when BE_DEBUG_VAR_INFO is enabled)
to find the variable whose scope covers *pc* and whose positional
index matches. Returns the bstring name, or None if not found.
"""
if not BE_DEBUG_VAR_INFO:
return None
if proto.varinfo is None:
return None
nvarinfo = proto.nvarinfo
varinfo = proto.varinfo
idx = index
for i in range(nvarinfo):
vi = varinfo[i]
if vi.beginpc > pc:
break
if pc <= vi.endpc:
if idx == 0:
return vi.name
idx -= 1
return None