Repository navigation
Conversation
jow-
force-pushed
the
feat/stcall-typeof-throw
branch
from
October 23, 2026 22:35
1980f8c to
3b87b29
Compare
Add I_STCALL, which calls a standard library function with the values on the stack as arguments, without pushing a call frame: the arguments already lie where uc_fn_arg() expects them, so the callee can be entered as an ordinary C function and restores the stack itself when it calls back into managed code. Address the callee by slot rather than by name: uc_stdlib_fn_t names every slot and uc_stdlib_functions[] pins each one to its function with a designated initializer, so the order the table is written in does not move any slot and a slot can never silently name another function. Retiring a function means dropping its initializer, which leaves the slot reserved and empty; the VM reports such a slot rather than invoking something else, and the lookup by name in uc_stdlib_function() skips it, as do the helper macros registering a function list. Since the slot number is baked into compiled bytecode, the slots stay reserved and are never renumbered. Bypassing the runtime namespace makes such calls independent of user code shadowing a function's name, and leaving the call frame out lets exceptions report the position of the instruction in the calling chunk, rather than that of a synthetic frame. Keep the bytecode format versions of the program, rather than of this release: a program starts out requiring the oldest version this release accepts and records a newer one only once it is compiled to use an instruction introduced since then, which is what uc_program_write() stores. Bytecode using no such instruction therefore stays loadable by a VM predating it, while bytecode which does is rejected at load, rather than a run ending on an unknown opcode partway through. A program read from a stream keeps the version it came with. Store the version in four bits sharing closure_refs' storage unit, since trailing padding cannot be relied on across ABIs: on LP64 four padding bytes follow closure_refs, while on ILP32 - i386, armel/armhf, mips and powerpc, all of which are packaged - the struct ends exactly there and a field of its own would grow it. Size, offsets and alignment are thus unchanged on either ABI; 28 bits for closure_refs still far exceed the 25-bit reference count they are compared against. Bytecode of both versions loads now, which is why the serialized program embedded in 61_loadstring needs no change. The commits which follow use this to implement the `typeof` operator, the `throw` statement and the rest elements of destructuring patterns; the first of them records the newer version when emitting the instruction. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
Support `typeof` as prefix operator, yielding the type name of a value and null for a null value, and `throw` as statement, raising the exception described by its expression. A bare `throw` dies without a message and a caught exception object can be thrown again. Both are sugar for standard library functions and are emitted as internal calls, see I_STCALL: `typeof` calls type() and `throw` calls die(), so user code defining a `type` or `die` of its own cannot take them over. Emitting that instruction records the newer bytecode version on the program, so that only a script actually using one of these features is serialized in a form which a VM predating the instruction rejects; a script using neither is serialized as before. Add both words to the reserved ones; using them as identifiers is a syntax error from now on. Signed-off-by: Jo-Philipp Wich <jo@mein.io>
jow-
force-pushed
the
feat/stcall-typeof-throw
branch
from
October 23, 2026 22:37
3b87b29 to
a357bc0
Compare
I_STCALL encodes its callee as a slot into uc_stdlib_functions[], which the
existing listings rendered as a bare operand - the one thing that actually
identifies the call was missing from both.
Resolve the slot to the function name with a new
uc_stdlib_function_name_by_slot(), the companion of the existing
uc_stdlib_function_by_slot() used by the VM; it returns NULL for slots beyond
the table and for empty ones, i.e. functions retired since the bytecode was
compiled, which both listings render as "(?)"/"(unknown)".
- Trace output (uc_dump_insn()) annotates the instruction like it already does
for LOAD/LVAR/SVAR and the variable accesses: `STCALL {0x210001} ; type`.
- The debugger's DISASSEMBLE response gains a `stdlib_name` field beside the
existing `call_nargs` (the lower half of the operand is the argument count,
as for CALL), and the client renders it as `STCALL {0x1b0003 : splice, 3
args}`. I_STCALL never passes a this-context and is never a tail call, so
neither the mcall nor the tail marker apply to it.
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
I_STCALL, which calls a standard library function directly by slot with the values on the stack as arguments, without pushing a call frame — the arguments already lie whereuc_fn_arg()expects them, and the callee restores the stack itself when it calls back into managed code. Slots are pinned by designated initializers inuc_stdlib_functions[], never renumbered, and retiring a function leaves its slot reserved and empty. Bypassing the runtime namespace makes such calls immune to user code shadowing the function name, and leaving out the call frame lets exceptions report the position in the calling chunk instead of that of a synthetic frame.Two language features are built on it:
typeofas prefix operator, yielding the type name of a value (and null for null) — emitted as an internal call totype().throwas statement, raising the exception described by its expression; a barethrowdies without a message, and a caught exception object can be thrown again — emitted as an internal call todie().Both are sugar for standard library functions, so user code defining its own
typeordiecannot take them over. Both words are added to the reserved ones; using them as identifiers is a syntax error from now on.The bytecode version is recorded per program rather than per release: a program starts out requiring the oldest version this release accepts and records a newer one only once it is compiled to use an instruction introduced since then. Bytecode using no such instruction therefore stays loadable by a VM predating it, while bytecode that does is rejected at load instead of a run ending on an unknown opcode partway through. The version occupies four bits sharing
closure_refs' storage unit, so struct size, offsets and alignment are unchanged on LP64 and ILP32 alike.Tests:
tests/custom/00_syntax/32_typeof_and_throw; the full custom suite passes (281/281).This is the preparatory part of #500 — the destructuring feature is stacked on top of it and uses
I_STCALLfor itssplice()/rawdelete()calls instead of adding opcodes of its own.Both listings that render bytecode now resolve the callee instead of showing the raw operand: trace output (
-t) annotates the instruction (STCALL {0x210001} ; type) and the debugger's disassembly gains astdlib_namefield rendered asSTCALL {0x1b0003 : splice, 3 args}, via a newuc_stdlib_function_name_by_slot()companion to the VM's slot lookup.