mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-26 22:11:22 +00:00
* Berry: safely collect partially constructed native closures * Update be_func.py --------- Co-authored-by: s-hadinger <49731213+s-hadinger@users.noreply.github.com>
516 lines
16 KiB
Python
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
|