mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The compiler emits its runtime helpers by name and the VM defines them by name, so the two are one contract. It was renamed on one side only: all three engines -- the Python VM, the TypeScript VM, and the JS emitter -- already said `__printScriptValue`, while every committed snapshot still said `__printHogValue`. The snapshots had not been regenerated since. That was not only cosmetic. `catch.script` and `catch2.script` call `HogError(...)`, an STL name that became `ScriptError`, so both had been failing; and `stl.stdout` recorded `__hogInterval__` for a marker the engines stopped writing two renames ago. So: drop the language's name from the helpers, since inside its own runtime every value is a Script value and the infix distinguishes nothing. Both sides move here, together, and the snapshots are regenerated from the compiler rather than edited to agree. Two names could not simply lose the infix, and the collision is the interesting part: __toScriptDate/DateTime -> __newDate/__newDateTime, not __toDate, which already exists and is a different function -- the STL builtin that PARSES a foreign input, where these build a value from its parts. They join `__newError`, which was already named that way. ScriptError stays. Its remaining word is `Error`, also taken, and also a different function: `Error(message)` fixes the type, `ScriptError( type, message)` takes it. Leaving the families orthogonal: `__new*` builds, `__to*` converts, `__is*` tests, `__x__` marks, `__print*` renders. Verified with the two engines this ABI spans, python-VM output against compiled-JS output per test: 31 pass / 4 fail, against 29 / 6 on the commit before this one. The two it fixes are catch and catch2. The four that remain fail identically on both trees and are untouched by this. `insightsqlx.js` is deliberately NOT regenerated and still spells the old names: its source does not compile on either tree (`BytecodeCompiler has no method visit_insights_qlxtag`), and editing a snapshot by hand to agree with a rename is the exact failure this commit exists to undo. Co-authored-by: Hanzo Dev <[email protected]>
731 lines
32 KiB
Python
731 lines
32 KiB
Python
import time
|
|
import operator
|
|
from collections.abc import Callable
|
|
from copy import deepcopy
|
|
from dataclasses import dataclass
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING, Any, Optional
|
|
|
|
from common.scriptvm.python.debugger import color_bytecode, debugger
|
|
from common.scriptvm.python.objects import (
|
|
CallFrame,
|
|
ThrowFrame,
|
|
is_script_error,
|
|
is_script_upvalue,
|
|
new_script_callable,
|
|
new_script_closure,
|
|
)
|
|
from common.scriptvm.python.operation import INSIGHTSQL_BYTECODE_IDENTIFIER, INSIGHTSQL_BYTECODE_IDENTIFIER_V0, Operation
|
|
from common.scriptvm.python.stl import STL
|
|
from common.scriptvm.python.stl.bytecode import BYTECODE_STL
|
|
from common.scriptvm.python.utils import (
|
|
ScriptVMException,
|
|
ScriptVMMemoryExceededException,
|
|
ScriptVMRuntimeExceededException,
|
|
UncaughtScriptVMException,
|
|
calculate_cost,
|
|
get_nested_value,
|
|
like,
|
|
regex_match,
|
|
set_nested_value,
|
|
unify_comparison_types,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from insights.models import Team
|
|
|
|
MAX_MEMORY = 64 * 1024 * 1024 # 64 MB
|
|
MAX_FUNCTION_ARGS_LENGTH = 300
|
|
CALLSTACK_LENGTH = 1000
|
|
|
|
|
|
@dataclass
|
|
class BytecodeResult:
|
|
result: Any
|
|
bytecodes: dict[str, Any]
|
|
stdout: list[str]
|
|
|
|
|
|
def _compare_values(left: Any, right: Any, comparison: Callable[[Any, Any], bool]) -> bool:
|
|
try:
|
|
return comparison(left, right)
|
|
except TypeError as e:
|
|
raise ScriptVMException(str(e)) from e
|
|
|
|
|
|
def execute_bytecode(
|
|
input: list[Any] | dict,
|
|
globals: Optional[dict[str, Any]] = None,
|
|
functions: Optional[dict[str, Callable[..., Any]]] = None,
|
|
timeout=timedelta(seconds=5),
|
|
team: Optional["Team"] = None,
|
|
debug=False,
|
|
disallowed_functions: Optional[frozenset[str]] = None,
|
|
) -> BytecodeResult:
|
|
bytecodes: dict[str, Any] = input if isinstance(input, dict) else {"root": {"bytecode": input}}
|
|
root_bytecode = bytecodes.get("root", {}).get("bytecode", []) or []
|
|
|
|
if (
|
|
not root_bytecode
|
|
or len(root_bytecode) == 0
|
|
or (root_bytecode[0] != INSIGHTSQL_BYTECODE_IDENTIFIER and root_bytecode[0] != INSIGHTSQL_BYTECODE_IDENTIFIER_V0)
|
|
):
|
|
raise ScriptVMException(f"Invalid bytecode. Must start with '{INSIGHTSQL_BYTECODE_IDENTIFIER}'")
|
|
version = root_bytecode[1] if len(root_bytecode) >= 2 and root_bytecode[0] == INSIGHTSQL_BYTECODE_IDENTIFIER else 0
|
|
start_time = time.time()
|
|
last_op = len(root_bytecode) - 1
|
|
stack: list = []
|
|
upvalues: list[dict] = []
|
|
upvalues_by_id: dict[int, dict] = {}
|
|
mem_stack: list = []
|
|
call_stack: list[CallFrame] = []
|
|
throw_stack: list[ThrowFrame] = []
|
|
declared_functions: dict[str, tuple[int, int]] = {}
|
|
mem_used = 0
|
|
max_mem_used = 0
|
|
ops = 0
|
|
stdout: list[str] = []
|
|
debug_bytecode = []
|
|
if isinstance(timeout, int):
|
|
timeout = timedelta(seconds=timeout)
|
|
disallowed_functions = disallowed_functions or frozenset()
|
|
|
|
if len(call_stack) == 0:
|
|
call_stack.append(
|
|
CallFrame(
|
|
ip=0,
|
|
chunk="root",
|
|
stack_start=0,
|
|
arg_len=0,
|
|
closure=new_script_closure(
|
|
new_script_callable(
|
|
type="local",
|
|
arg_count=0,
|
|
upvalue_count=0,
|
|
ip=0,
|
|
chunk="root",
|
|
name="",
|
|
)
|
|
),
|
|
)
|
|
)
|
|
frame = call_stack[-1]
|
|
chunk_bytecode: list[Any] = root_bytecode
|
|
chunk_globals = globals
|
|
|
|
def set_chunk_bytecode():
|
|
nonlocal chunk_bytecode, chunk_globals, last_op, debug_bytecode
|
|
if not frame.chunk or frame.chunk == "root":
|
|
chunk_bytecode = root_bytecode
|
|
chunk_globals = globals
|
|
elif frame.chunk.startswith("stl/") and frame.chunk[4:] in BYTECODE_STL:
|
|
chunk_bytecode = BYTECODE_STL[frame.chunk[4:]][1]
|
|
chunk_globals = {}
|
|
elif bytecodes.get(frame.chunk):
|
|
chunk_bytecode = bytecodes[frame.chunk].get("bytecode", [])
|
|
chunk_globals = bytecodes[frame.chunk].get("globals", {})
|
|
else:
|
|
raise ScriptVMException(f"Unknown chunk: {frame.chunk}")
|
|
last_op = len(chunk_bytecode) - 1
|
|
if debug:
|
|
debug_bytecode = color_bytecode(chunk_bytecode)
|
|
if frame.ip == 0 and (chunk_bytecode[0] == "_H" or chunk_bytecode[0] == "_h"):
|
|
# TODO: store chunk version
|
|
frame.ip += 2 if chunk_bytecode[0] == "_H" else 1
|
|
|
|
set_chunk_bytecode()
|
|
|
|
def stack_keep_first_elements(count: int) -> list[Any]:
|
|
nonlocal stack, mem_stack, mem_used
|
|
if count < 0 or len(stack) < count:
|
|
raise ScriptVMException("Stack underflow")
|
|
for upvalue in reversed(upvalues):
|
|
if upvalue["location"] >= count:
|
|
if not upvalue["closed"]:
|
|
upvalue["closed"] = True
|
|
upvalue["value"] = stack[upvalue["location"]]
|
|
else:
|
|
break
|
|
removed = stack[count:]
|
|
stack = stack[0:count]
|
|
mem_used -= sum(mem_stack[count:])
|
|
mem_stack = mem_stack[0:count]
|
|
return removed
|
|
|
|
def next_token():
|
|
nonlocal frame, chunk_bytecode
|
|
if frame.ip >= last_op:
|
|
raise ScriptVMException("Unexpected end of bytecode")
|
|
frame.ip += 1
|
|
return chunk_bytecode[frame.ip]
|
|
|
|
def pop_stack():
|
|
if not stack:
|
|
raise ScriptVMException("Stack underflow")
|
|
nonlocal mem_used
|
|
mem_used -= mem_stack.pop()
|
|
return stack.pop()
|
|
|
|
def push_stack(value):
|
|
stack.append(value)
|
|
mem_stack.append(calculate_cost(value))
|
|
nonlocal mem_used
|
|
mem_used += mem_stack[-1]
|
|
nonlocal max_mem_used
|
|
max_mem_used = max(mem_used, max_mem_used)
|
|
if mem_used > MAX_MEMORY:
|
|
raise ScriptVMMemoryExceededException(memory_limit=MAX_MEMORY, attempted_memory=mem_used)
|
|
|
|
def check_timeout():
|
|
if time.time() - start_time > timeout.total_seconds() and not debug:
|
|
raise ScriptVMRuntimeExceededException(timeout_seconds=timeout.total_seconds(), ops_performed=ops)
|
|
|
|
def remaining_timeout() -> float:
|
|
# Budget left for this run, so blocking STL functions (e.g. sleep) can bound themselves to it.
|
|
return max(0.0, timeout.total_seconds() - (time.time() - start_time))
|
|
|
|
def check_allowed(name: str):
|
|
# Enforced at dispatch so it catches every path to an STL call (direct, expression call,
|
|
# a closure bound to a local), not just the syntactic `name(...)` a static check would see.
|
|
if name in disallowed_functions:
|
|
raise ScriptVMException(f"Function {name} is not allowed here")
|
|
|
|
def capture_upvalue(index) -> dict:
|
|
nonlocal upvalues
|
|
for upvalue in reversed(upvalues):
|
|
if upvalue["location"] < index:
|
|
break
|
|
if upvalue["location"] == index:
|
|
return upvalue
|
|
created_upvalue = {
|
|
"__upValue__": True,
|
|
"location": index,
|
|
"closed": False,
|
|
"value": None,
|
|
"id": len(upvalues) + 1,
|
|
}
|
|
upvalues.append(created_upvalue)
|
|
upvalues_by_id[created_upvalue["id"]] = created_upvalue
|
|
upvalues.sort(key=lambda x: x["location"])
|
|
return created_upvalue
|
|
|
|
symbol: Any = None
|
|
while True:
|
|
# Return or jump back to the previous call frame if ran out of bytecode to execute in this one, and return null
|
|
if frame.ip > last_op:
|
|
last_call_frame = call_stack.pop()
|
|
if len(call_stack) == 0 or last_call_frame is None:
|
|
if len(stack) > 1:
|
|
raise ScriptVMException("Invalid bytecode. More than one value left on stack")
|
|
return BytecodeResult(
|
|
result=pop_stack() if len(stack) > 0 else None, stdout=stdout, bytecodes=bytecodes
|
|
)
|
|
stack_start = last_call_frame.stack_start
|
|
stack_keep_first_elements(stack_start)
|
|
push_stack(None)
|
|
frame = call_stack[-1]
|
|
set_chunk_bytecode()
|
|
|
|
ops += 1
|
|
symbol = chunk_bytecode[frame.ip]
|
|
if (ops & 127) == 0: # every 128th operation
|
|
check_timeout()
|
|
elif debug:
|
|
debugger(symbol, chunk_bytecode, debug_bytecode, frame.ip, stack, call_stack, throw_stack)
|
|
match symbol:
|
|
case None:
|
|
break
|
|
case Operation.STRING:
|
|
push_stack(next_token())
|
|
case Operation.INTEGER:
|
|
push_stack(next_token())
|
|
case Operation.FLOAT:
|
|
push_stack(next_token())
|
|
case Operation.TRUE:
|
|
push_stack(True)
|
|
case Operation.FALSE:
|
|
push_stack(False)
|
|
case Operation.NULL:
|
|
push_stack(None)
|
|
case Operation.NOT:
|
|
push_stack(not pop_stack())
|
|
case Operation.AND:
|
|
push_stack(all([pop_stack() for _ in range(next_token())])) # noqa: C419
|
|
case Operation.OR:
|
|
push_stack(any([pop_stack() for _ in range(next_token())])) # noqa: C419
|
|
case Operation.PLUS:
|
|
push_stack(pop_stack() + pop_stack())
|
|
case Operation.MINUS:
|
|
push_stack(pop_stack() - pop_stack())
|
|
case Operation.DIVIDE:
|
|
push_stack(pop_stack() / pop_stack())
|
|
case Operation.MULTIPLY:
|
|
push_stack(pop_stack() * pop_stack())
|
|
case Operation.MOD:
|
|
push_stack(pop_stack() % pop_stack())
|
|
case Operation.EQ:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(var1 == var2)
|
|
case Operation.NOT_EQ:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(var1 != var2)
|
|
case Operation.GT:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(_compare_values(var1, var2, operator.gt))
|
|
case Operation.GT_EQ:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(_compare_values(var1, var2, operator.ge))
|
|
case Operation.LT:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(_compare_values(var1, var2, operator.lt))
|
|
case Operation.LT_EQ:
|
|
var1, var2 = unify_comparison_types(pop_stack(), pop_stack())
|
|
push_stack(_compare_values(var1, var2, operator.le))
|
|
case Operation.LIKE:
|
|
push_stack(like(pop_stack(), pop_stack()))
|
|
case Operation.ILIKE:
|
|
push_stack(like(pop_stack(), pop_stack(), case_insensitive=True))
|
|
case Operation.NOT_LIKE:
|
|
push_stack(not like(pop_stack(), pop_stack()))
|
|
case Operation.NOT_ILIKE:
|
|
push_stack(not like(pop_stack(), pop_stack(), case_insensitive=True))
|
|
case Operation.IN:
|
|
push_stack(pop_stack() in pop_stack())
|
|
case Operation.NOT_IN:
|
|
push_stack(pop_stack() not in pop_stack())
|
|
case Operation.REGEX:
|
|
args = [pop_stack(), pop_stack()]
|
|
push_stack(regex_match(args[0], args[1]))
|
|
case Operation.NOT_REGEX:
|
|
args = [pop_stack(), pop_stack()]
|
|
push_stack(not regex_match(args[0], args[1]) if args[0] and args[1] else False)
|
|
case Operation.IREGEX:
|
|
args = [pop_stack(), pop_stack()]
|
|
push_stack(regex_match(args[0], args[1], case_insensitive=True))
|
|
case Operation.NOT_IREGEX:
|
|
args = [pop_stack(), pop_stack()]
|
|
push_stack(not regex_match(args[0], args[1], case_insensitive=True) if args[0] and args[1] else False)
|
|
case Operation.GET_GLOBAL:
|
|
chain = [pop_stack() for _ in range(next_token())]
|
|
if chunk_globals and chain[0] in chunk_globals:
|
|
push_stack(deepcopy(get_nested_value(chunk_globals, chain, True)))
|
|
elif functions and chain[0] in functions:
|
|
push_stack(
|
|
new_script_closure(
|
|
new_script_callable(
|
|
type="stl",
|
|
name=chain[0],
|
|
arg_count=0,
|
|
upvalue_count=0,
|
|
ip=-1,
|
|
chunk="stl",
|
|
)
|
|
)
|
|
)
|
|
elif chain[0] in STL and len(chain) == 1:
|
|
push_stack(
|
|
new_script_closure(
|
|
new_script_callable(
|
|
type="stl",
|
|
name=chain[0],
|
|
arg_count=STL[chain[0]].maxArgs or 0,
|
|
upvalue_count=0,
|
|
ip=-1,
|
|
chunk="stl",
|
|
)
|
|
)
|
|
)
|
|
elif chain[0] in BYTECODE_STL and len(chain) == 1:
|
|
push_stack(
|
|
new_script_closure(
|
|
new_script_callable(
|
|
type="stl",
|
|
name=chain[0],
|
|
arg_count=len(BYTECODE_STL[chain[0]][0]),
|
|
upvalue_count=0,
|
|
ip=0,
|
|
chunk=f"stl/{chain[0]}",
|
|
)
|
|
)
|
|
)
|
|
else:
|
|
raise ScriptVMException(f"Global variable not found: {chain[0]}")
|
|
case Operation.POP:
|
|
pop_stack()
|
|
case Operation.CLOSE_UPVALUE:
|
|
stack_keep_first_elements(len(stack) - 1)
|
|
case Operation.RETURN:
|
|
response = pop_stack()
|
|
last_call_frame = call_stack.pop()
|
|
if len(call_stack) == 0 or last_call_frame is None:
|
|
return BytecodeResult(result=response, stdout=stdout, bytecodes=bytecodes)
|
|
stack_start = last_call_frame.stack_start
|
|
stack_keep_first_elements(stack_start)
|
|
push_stack(response)
|
|
frame = call_stack[-1]
|
|
set_chunk_bytecode()
|
|
continue # resume the loop without incrementing frame.ip
|
|
|
|
case Operation.GET_LOCAL:
|
|
stack_start = 0 if not call_stack else call_stack[-1].stack_start
|
|
push_stack(stack[next_token() + stack_start])
|
|
case Operation.SET_LOCAL:
|
|
stack_start = 0 if not call_stack else call_stack[-1].stack_start
|
|
value = pop_stack()
|
|
index = next_token() + stack_start
|
|
stack[index] = value
|
|
last_cost = mem_stack[index]
|
|
mem_stack[index] = calculate_cost(value)
|
|
mem_used += mem_stack[index] - last_cost
|
|
max_mem_used = max(mem_used, max_mem_used)
|
|
case Operation.GET_PROPERTY:
|
|
property = pop_stack()
|
|
push_stack(get_nested_value(pop_stack(), [property]))
|
|
case Operation.GET_PROPERTY_NULLISH:
|
|
property = pop_stack()
|
|
push_stack(get_nested_value(pop_stack(), [property], nullish=True))
|
|
case Operation.SET_PROPERTY:
|
|
value = pop_stack()
|
|
field = pop_stack()
|
|
set_nested_value(pop_stack(), [field], value)
|
|
case Operation.DICT:
|
|
count = next_token()
|
|
if count > 0:
|
|
elems = stack[-(count * 2) :]
|
|
stack = stack[: -(count * 2)]
|
|
mem_used -= sum(mem_stack[-(count * 2) :])
|
|
mem_stack = mem_stack[: -(count * 2)]
|
|
push_stack({elems[i]: elems[i + 1] for i in range(0, len(elems), 2)})
|
|
else:
|
|
push_stack({})
|
|
case Operation.ARRAY:
|
|
count = next_token()
|
|
if count > 0:
|
|
elems = stack[-count:]
|
|
stack = stack[:-count]
|
|
mem_used -= sum(mem_stack[-count:])
|
|
mem_stack = mem_stack[:-count]
|
|
push_stack(elems)
|
|
else:
|
|
push_stack([])
|
|
case Operation.TUPLE:
|
|
count = next_token()
|
|
if count > 0:
|
|
elems = stack[-count:]
|
|
stack = stack[:-count]
|
|
mem_used -= sum(mem_stack[-count:])
|
|
mem_stack = mem_stack[:-count]
|
|
push_stack(tuple(elems))
|
|
else:
|
|
push_stack(())
|
|
case Operation.JUMP:
|
|
count = next_token()
|
|
frame.ip += count
|
|
case Operation.JUMP_IF_FALSE:
|
|
count = next_token()
|
|
if not pop_stack():
|
|
frame.ip += count
|
|
case Operation.JUMP_IF_STACK_NOT_NULL:
|
|
count = next_token()
|
|
if len(stack) > 0 and stack[-1] is not None:
|
|
frame.ip += count
|
|
case Operation.DECLARE_FN:
|
|
# DEPRECATED
|
|
name = next_token()
|
|
arg_len = next_token()
|
|
body_len = next_token()
|
|
declared_functions[name] = (frame.ip + 1, arg_len)
|
|
frame.ip += body_len
|
|
case Operation.CALLABLE:
|
|
name = next_token() # TODO: do we need it? it could change as the variable is reassigned
|
|
arg_count = next_token()
|
|
upvalue_count = next_token()
|
|
body_length = next_token()
|
|
push_stack(
|
|
new_script_callable(
|
|
type="local",
|
|
name=name,
|
|
chunk=frame.chunk,
|
|
arg_count=arg_count,
|
|
upvalue_count=upvalue_count,
|
|
ip=frame.ip + 1,
|
|
)
|
|
)
|
|
frame.ip += body_length
|
|
case Operation.CLOSURE:
|
|
closure_callable = pop_stack()
|
|
closure = new_script_closure(closure_callable)
|
|
stack_start = frame.stack_start
|
|
upvalue_count = next_token()
|
|
if upvalue_count != closure_callable["upvalueCount"]:
|
|
raise ScriptVMException(
|
|
f"Invalid upvalue count. Expected {closure_callable['upvalueCount']}, got {upvalue_count}"
|
|
)
|
|
for _ in range(closure_callable["upvalueCount"]):
|
|
is_local, index = next_token(), next_token()
|
|
if is_local:
|
|
closure["upvalues"].append(capture_upvalue(stack_start + index)["id"])
|
|
else:
|
|
closure["upvalues"].append(frame.closure["upvalues"][index])
|
|
push_stack(closure)
|
|
case Operation.GET_UPVALUE:
|
|
index = next_token()
|
|
closure = frame.closure
|
|
if index >= len(closure["upvalues"]):
|
|
raise ScriptVMException(f"Invalid upvalue index: {index}")
|
|
upvalue = upvalues_by_id[closure["upvalues"][index]]
|
|
if not is_script_upvalue(upvalue):
|
|
raise ScriptVMException(f"Invalid upvalue: {upvalue}")
|
|
if upvalue["closed"]:
|
|
push_stack(upvalue["value"])
|
|
else:
|
|
push_stack(stack[upvalue["location"]])
|
|
case Operation.SET_UPVALUE:
|
|
index = next_token()
|
|
closure = frame.closure
|
|
if index >= len(closure["upvalues"]):
|
|
raise ScriptVMException(f"Invalid upvalue index: {index}")
|
|
upvalue = upvalues_by_id[closure["upvalues"][index]]
|
|
if not is_script_upvalue(upvalue):
|
|
raise ScriptVMException(f"Invalid upvalue: {upvalue}")
|
|
if upvalue["closed"]:
|
|
upvalue["value"] = pop_stack()
|
|
else:
|
|
stack[upvalue["location"]] = pop_stack()
|
|
case Operation.CALL_GLOBAL:
|
|
check_timeout()
|
|
name = next_token()
|
|
arg_count = next_token()
|
|
# This is for backwards compatibility. We use a closure on the stack with local functions now.
|
|
if name in declared_functions:
|
|
func_ip, arg_len = declared_functions[name]
|
|
frame.ip += 1 # advance for when we return
|
|
if arg_len > arg_count:
|
|
for _ in range(arg_len - arg_count):
|
|
push_stack(None)
|
|
frame = CallFrame(
|
|
ip=func_ip,
|
|
chunk=frame.chunk,
|
|
stack_start=len(stack) - arg_len,
|
|
arg_len=arg_len,
|
|
closure=new_script_closure(
|
|
new_script_callable(
|
|
type="local",
|
|
name=name,
|
|
arg_count=arg_len,
|
|
upvalue_count=0,
|
|
ip=func_ip,
|
|
chunk=frame.chunk,
|
|
)
|
|
),
|
|
)
|
|
set_chunk_bytecode()
|
|
call_stack.append(frame)
|
|
continue # resume the loop without incrementing frame.ip
|
|
else:
|
|
if name == "import":
|
|
if arg_count != 1:
|
|
raise ScriptVMException("Function import requires exactly 1 argument")
|
|
module_name = pop_stack()
|
|
frame.ip += 1 # advance for when we return
|
|
frame = CallFrame(
|
|
ip=0,
|
|
chunk=module_name,
|
|
stack_start=len(stack),
|
|
arg_len=0,
|
|
closure=new_script_closure(
|
|
new_script_callable(
|
|
type="local",
|
|
name=module_name,
|
|
arg_count=0,
|
|
upvalue_count=0,
|
|
ip=0,
|
|
chunk=module_name,
|
|
)
|
|
),
|
|
)
|
|
set_chunk_bytecode()
|
|
call_stack.append(frame)
|
|
continue
|
|
elif functions is not None and name in functions:
|
|
if version == 0:
|
|
args = [pop_stack() for _ in range(arg_count)]
|
|
else:
|
|
args = stack_keep_first_elements(len(stack) - arg_count)
|
|
push_stack(functions[name](*args))
|
|
elif name in STL:
|
|
check_allowed(name)
|
|
if version == 0:
|
|
args = [pop_stack() for _ in range(arg_count)]
|
|
else:
|
|
args = stack_keep_first_elements(len(stack) - arg_count)
|
|
push_stack(STL[name].fn(args, team, stdout, remaining_timeout()))
|
|
elif name in BYTECODE_STL:
|
|
arg_names = BYTECODE_STL[name][0]
|
|
if len(arg_names) != arg_count:
|
|
raise ScriptVMException(f"Function {name} requires exactly {len(arg_names)} arguments")
|
|
frame.ip += 1 # advance for when we return
|
|
frame = CallFrame(
|
|
ip=0,
|
|
chunk=f"stl/{name}",
|
|
stack_start=len(stack) - arg_count,
|
|
arg_len=arg_count,
|
|
closure=new_script_closure(
|
|
new_script_callable(
|
|
type="stl",
|
|
name=name,
|
|
arg_count=arg_count,
|
|
upvalue_count=0,
|
|
ip=0,
|
|
chunk=f"stl/{name}",
|
|
)
|
|
),
|
|
)
|
|
set_chunk_bytecode()
|
|
call_stack.append(frame)
|
|
continue # resume the loop without incrementing frame.ip
|
|
else:
|
|
raise ScriptVMException(f"Unsupported function call: {name}")
|
|
case Operation.CALL_LOCAL:
|
|
check_timeout()
|
|
closure = pop_stack()
|
|
if not isinstance(closure, dict) or closure.get("__closure__") is None:
|
|
raise ScriptVMException(f"Invalid closure: {closure}")
|
|
callable = closure.get("callable")
|
|
if not isinstance(callable, dict) or callable.get("__callable__") is None:
|
|
raise ScriptVMException(f"Invalid callable: {callable}")
|
|
args_length = next_token()
|
|
if args_length > MAX_FUNCTION_ARGS_LENGTH:
|
|
raise ScriptVMException("Too many arguments")
|
|
|
|
if callable.get("__callable__") == "local":
|
|
if callable["argCount"] > args_length:
|
|
# TODO: specify minimum required arguments somehow
|
|
for _ in range(callable["argCount"] - args_length):
|
|
push_stack(None)
|
|
elif callable["argCount"] < args_length:
|
|
raise ScriptVMException(
|
|
f"Too many arguments. Passed {args_length}, expected {callable['argCount']}"
|
|
)
|
|
frame.ip += 1 # advance for when we return
|
|
frame = CallFrame(
|
|
ip=callable["ip"],
|
|
chunk=callable["chunk"],
|
|
stack_start=len(stack) - callable["argCount"],
|
|
arg_len=callable["argCount"],
|
|
closure=closure,
|
|
)
|
|
set_chunk_bytecode()
|
|
call_stack.append(frame)
|
|
continue # resume the loop without incrementing frame.ip
|
|
|
|
elif callable.get("__callable__") == "stl":
|
|
if callable["name"] not in STL:
|
|
raise ScriptVMException(f"Unsupported function call: {callable['name']}")
|
|
check_allowed(callable["name"])
|
|
stl_fn = STL[callable["name"]]
|
|
if stl_fn.minArgs is not None and args_length < stl_fn.minArgs:
|
|
raise ScriptVMException(
|
|
f"Function {callable['name']} requires at least {stl_fn.minArgs} arguments"
|
|
)
|
|
if stl_fn.maxArgs is not None and args_length > stl_fn.maxArgs:
|
|
raise ScriptVMException(f"Function {callable['name']} requires at most {stl_fn.maxArgs} arguments")
|
|
if version == 0:
|
|
args = [pop_stack() for _ in range(args_length)]
|
|
else:
|
|
args = list(reversed([pop_stack() for _ in range(args_length)]))
|
|
if stl_fn.maxArgs is not None and len(args) < stl_fn.maxArgs:
|
|
args = [*args, *([None] * (stl_fn.maxArgs - len(args)))]
|
|
push_stack(stl_fn.fn(args, team, stdout, remaining_timeout()))
|
|
|
|
elif callable.get("__callable__") == "async":
|
|
raise ScriptVMException("Async functions are not supported")
|
|
|
|
else:
|
|
raise ScriptVMException("Invalid callable")
|
|
|
|
case Operation.TRY:
|
|
throw_stack.append(
|
|
ThrowFrame(
|
|
call_stack_len=len(call_stack), stack_len=len(stack), catch_ip=frame.ip + 1 + next_token()
|
|
)
|
|
)
|
|
case Operation.POP_TRY:
|
|
if throw_stack:
|
|
throw_stack.pop()
|
|
else:
|
|
raise ScriptVMException("Invalid operation POP_TRY: no try block to pop")
|
|
case Operation.THROW:
|
|
exception = pop_stack()
|
|
if not is_script_error(exception):
|
|
raise ScriptVMException("Can not throw: value is not of type Error")
|
|
if throw_stack:
|
|
last_throw = throw_stack.pop()
|
|
call_stack_len, stack_len, catch_ip = (
|
|
last_throw.call_stack_len,
|
|
last_throw.stack_len,
|
|
last_throw.catch_ip,
|
|
)
|
|
stack_keep_first_elements(stack_len)
|
|
call_stack = call_stack[0:call_stack_len]
|
|
push_stack(exception)
|
|
frame = call_stack[-1]
|
|
set_chunk_bytecode()
|
|
frame.ip = catch_ip
|
|
continue
|
|
else:
|
|
raise UncaughtScriptVMException(
|
|
type=exception.get("type"),
|
|
message=exception.get("message"),
|
|
payload=exception.get("payload"),
|
|
)
|
|
case _:
|
|
raise ScriptVMException(
|
|
f'Unexpected node while running bytecode in chunk "{frame.chunk}": {chunk_bytecode[frame.ip]}'
|
|
)
|
|
|
|
frame.ip += 1
|
|
|
|
return BytecodeResult(result=pop_stack() if len(stack) > 0 else None, stdout=stdout, bytecodes=bytecodes)
|
|
|
|
|
|
def validate_bytecode(bytecode: list[Any] | dict, inputs: Optional[dict] = None) -> tuple[bool, Optional[str]]:
|
|
try:
|
|
event = {
|
|
"uuid": "test-event-id",
|
|
"event": "test-event",
|
|
"distinct_id": "test-distinct-id",
|
|
"properties": {},
|
|
"timestamp": "2024-01-01T00:00:00Z",
|
|
}
|
|
test_globals = {
|
|
"event": event,
|
|
"person": {"properties": {}},
|
|
"inputs": inputs or {},
|
|
}
|
|
|
|
execute_bytecode(
|
|
bytecode,
|
|
globals=test_globals,
|
|
timeout=timedelta(milliseconds=100), # Short timeout for validation
|
|
functions={
|
|
"print": lambda *args: None, # No-op print function
|
|
"fetch": lambda *args: {"status": 200, "body": {}}, # Mock fetch
|
|
},
|
|
)
|
|
return True, None
|
|
|
|
except ScriptVMRuntimeExceededException as e:
|
|
return (
|
|
False,
|
|
f"Your function is taking too long to run (over {e.timeout_seconds} seconds). Please simplify your code.",
|
|
)
|
|
except ScriptVMMemoryExceededException as e:
|
|
memory_mb = e.memory_limit / (1024 * 1024)
|
|
attempted_mb = e.attempted_memory / (1024 * 1024)
|
|
return False, f"Your function needs too much memory ({attempted_mb:.1f}MB). The limit is {memory_mb:.1f}MB."
|
|
except ScriptVMException as e:
|
|
return False, f"Function execution error: {str(e)}"
|
|
except Exception as e:
|
|
return False, f"Unexpected error during function validation: {str(e)}"
|