mvm-tests/microvium.c

7719 lines
284 KiB
C

// Copyright 2020 Michael Hunter. Part of the Microvium project. Links to full code at https://microvium.com for license details.
/*
* Microvium Bytecode Interpreter
*
* Version: 0.0.21
*
* This file contains the Microvium virtual machine C implementation.
*
* The key functions are mvm_restore() and mvm_call(), which perform the
* initialization and run loop respectively.
*
* I've written Microvium in C because lots of embedded projects for small
* processors are written in pure-C, and so integration for them will be easier.
* Also, there are a surprising number of C++ compilers in the embedded world
* that deviate from the standard, and I don't want to be testing on all of them
* individually.
*
* For the moment, I'm keeping Microvium all in one file for usability. Users
* can treat this file as a black box that contains the VM, and there's only one
* file they need to have built into their project in order to have Microvium
* running. The build process also pulls in the dependent header files, so
* there's only one header file and it's the one that users of Microvium need to
* see. Certain compilers and optimization settings also do a better job when
* related functions are co-located the same compilation unit.
*
* User-facing functions and definitions are all prefixed with `mvm_` to
* namespace them separately from other functions in their project, some of
* which use the prefix `vm_` and some without a prefix. (TODO: this should be
* consolidated)
*/
#include "microvium.h"
#include <ctype.h>
#include <stdlib.h>
#include "math.h"
// See microvium.c for design notes.
#include "stdbool.h"
#include "stdint.h"
#include "assert.h"
#include "string.h"
#include "stdlib.h"
#include "microvium.h"
#include "microvium_port.h"
#include "stdint.h"
#define MVM_BYTECODE_VERSION 6
// Note: MVM_ENGINE_VERSION is at the top of `microvium_internals.h`
// These sections appear in the bytecode in the order they appear in this
// enumeration.
typedef enum mvm_TeBytecodeSection {
/**
* Import Table
*
* List of host function IDs (vm_TsImportTableEntry) which are called by the
* VM. References from the VM to host functions are represented as indexes
* into this table. These IDs are resolved to their corresponding host
* function pointers when a VM is restored.
*/
BCS_IMPORT_TABLE,
/**
* A list of immutable `vm_TsExportTableEntry` that the VM exports, mapping
* export IDs to their corresponding VM Value. Mostly these values will just
* be function pointers.
*/
// TODO: We need to test what happens if we export numbers and objects
BCS_EXPORT_TABLE,
/**
* Short Call Table. Table of vm_TsShortCallTableEntry.
*
* To make the representation of function calls in IL more compact, up to 256
* of the most frequent function calls are listed in this table, including the
* function target and the argument count.
*
* See `LBL_CALL_SHORT`
*/
BCS_SHORT_CALL_TABLE,
/**
* Builtins
*
* Table of `Value`s that need to be directly identifiable by the engine, such
* as the Array prototype.
*
* These are not copied into RAM, they are just constant values like the
* exports, but like other values in ROM they are permitted to hold mutable
* values by pointing (as BytecodeMappedPtr) to the corresponding global
* variable slot.
*
* Note: at one point, I had these as single-byte offsets into the global
* variable space, but this made the assumption that all accessible builtins
* are also mutable, which is probably not true. The new design makes the
* opposite assumption: most builtins will be immutable at runtime (e.g.
* nobody changes the array prototype), so they can be stored in ROM and
* referenced by immutable Value pointers, making them usable but not
* consuming RAM at all. It's the exception rather than the rule that some of
* these may be mutable and require indirection through the global slot table.
*/
BCS_BUILTINS,
/**
* Interned Strings Table
*
* To keep property lookup efficient, Microvium requires that strings used as
* property keys can be compared using pointer equality. This requires that
* there is only one instance of each string (see
* https://en.wikipedia.org/wiki/String_interning). This table is the
* alphabetical listing of all the strings in ROM (or at least, all those
* which are valid property keys). See also TC_REF_INTERNED_STRING.
*
* There may be two string tables: one in ROM and one in RAM. The latter is
* required in general if the program might use arbitrarily-computed strings
* as property keys. For efficiency, the ROM string table is contiguous and
* sorted, to allow for binary searching, while the RAM string table is a
* linked list for efficiency in appending (expected to be used only
* occasionally).
*/
BCS_STRING_TABLE,
/**
* Functions and other immutable data structures.
*
* While the whole bytecode is essentially "ROM", only this ROM section
* contains addressable allocations.
*/
BCS_ROM,
/**
* Globals
*
* One `Value` entry for the initial value of each global variable. The number
* of global variables is determined by the size of this section.
*
* This section will be copied into RAM at startup (restore).
*
* Note: the global slots are used both for global variables and for "handles"
* (these are different to the user-defined handles for referencing VM objects
* from user space). Handles allow ROM allocations to reference RAM
* allocations, even though the ROM can't be updated when the RAM allocation
* moves during a GC collection. A handle is a slot in the "globals" space,
* where the slot itself is pointed to by a ROM value and it points to the
* corresponding RAM value. During a GC cycle, the RAM value may move and the
* handle slot is updated, but the handle slot itself doesn't move. See
* `offsetToDynamicPtr` in `encode-snapshot.ts`.
*
* The handles appear as the *last* global slots, and will generally not be
* referenced by `LOAD_GLOBAL` instructions.
*/
BCS_GLOBALS,
/**
* Heap Section: heap allocations.
*
* This section is copied into RAM when the VM is restored. It becomes the
* initial value of the GC heap. It contains allocations that are mutable
* (like the DATA section) but also subject to garbage collection.
*
* Note: the heap must be at the end, because it is the only part that changes
* size from one snapshot to the next. There is code that depends on this
* being the last section because the size of this section is computed as
* running to the end of the bytecode image.
*/
BCS_HEAP,
BCS_SECTION_COUNT,
} mvm_TeBytecodeSection;
typedef enum mvm_TeBuiltins {
BIN_INTERNED_STRINGS,
BIN_ARRAY_PROTO,
BIN_STR_PROTOTYPE, // If the string "prototype" is interned, this builtin points to it.
BIN_BUILTIN_COUNT
} mvm_TeBuiltins;
// Minimal bytecode is 32 bytes (sizeof(mvm_TsBytecodeHeader) + BCS_SECTION_COUNT*2 + BIN_BUILTIN_COUNT*2)
typedef struct mvm_TsBytecodeHeader {
uint8_t bytecodeVersion; // MVM_BYTECODE_VERSION
uint8_t headerSize;
uint8_t requiredEngineVersion;
uint8_t reserved; // =0
uint16_t bytecodeSize; // Including header
uint16_t crc; // CCITT16 (header and data, of everything after the CRC)
uint32_t requiredFeatureFlags;
/*
Note: the sections are assumed to be in order as per mvm_TeBytecodeSection, so
that the size of a section can be computed as the difference between the
adjacent offsets. The last section runs up until the end of the bytecode.
*/
uint16_t sectionOffsets[BCS_SECTION_COUNT];
} mvm_TsBytecodeHeader;
typedef enum mvm_TeFeatureFlags {
FF_FLOAT_SUPPORT = 0,
} mvm_TeFeatureFlags;
typedef struct vm_TsExportTableEntry {
mvm_VMExportID exportID;
mvm_Value exportValue;
} vm_TsExportTableEntry;
typedef struct vm_TsShortCallTableEntry {
/* Note: the `function` field has been broken up into separate low and high
* bytes, `functionL` and `functionH` respectively, for alignment purposes,
* since this is a 3-byte structure occuring in a packed table.
*
* `functionL` and `functionH` together make an `mvm_Value` which should be a
* callable value (a pointer to a `TsBytecodeFunc`, `TsHostFunc`, or
* `TsClosure`). TODO: I don't think this currently works, and I'm not even
* sure how we would test it. */
uint8_t functionL;
uint8_t functionH;
uint8_t argCount;
} vm_TsShortCallTableEntry;
/*
Note: the instruction set documentation is in
`microvium/doc/internals/instruction-set`
Microvium categorizes operations into groups based on common features. The first
nibble of an instruction is its vm_TeOpcode. This is followed by 4 bits which
can either be interpreted as a data parameter or as another opcode (e.g.
vm_TeOpcodeEx1). I call the first nibble the "primary opcode" and the second
nibble is the "secondary opcode".
There are a number of possible secondary opcodes, and each group has common
preparation logic across the group. Preparation logic means the code that runs
before the operation. For example, many operations require popping a value off
the stack before operating on the value. The VM implementation is more compact
if the pop code is common to all instructions that do the pop.
Operations can have different "follow through" logic grouped arbitrarily, since
the implementation of all instructions requires a "jump", those that have common
follow through logic simply jump to the same follow through without additional
cost, which eventually lands up back at the loop start. So the instruction
grouping does not need to cater for follow through logic, only preparation
logic.
To keep operation commonality as seamlessly as possible, the VM implementation
use 16-bit "registers", which have overloaded meaning depending on the context:
- `reg1`
- Initially holds the zero-extended 4-bit secondary nibble
- Operations that load an 8- or 16-bit literal will overwrite `reg1` with
the literal.
- "Pure" operations use reg1 as the first popped operand (none of the pure
operations have an embedded literal). "Pure" are what I'm calling
operations whose entire effect is to pop some operands off the stack,
operate on them, and push a result back onto the stack. For example,
`ADD`.
- `reg1` is also used as the "result" value for the common push-result tail
logic
- `reg2`
- used as the second popped value of binary operations
- used as the value to store, store-like operations
- `reg3`
- can be used arbitrarily by operations and does not have a common meaning
Additionally, the number operations have variations that work on 32 or 64 bit
values. These have their own local/ephemeral registers:
- `reg1I`: the value of the reg1 register unpacked to a `uint32_t`
- `reg2I`: the value of the reg2 register unpacked to a `uint32_t`
- `reg1F`: the value of the reg1 register unpacked to a `double`
- `reg2F`: the value of the reg2 register unpacked to a `double`
Operation groups and their corresponding preparation logic
- vm_TeOpcodeEx1:
- The prep does not read a literal (all these instructions are single-byte).
- The prep pops 0, 1, or 2 values from the stack depending on the
instruction range
- vm_TeOpcodeEx2:
- Prep reads 8-bit literal into reg1
- Two separate instruction ranges specify whether to sign extend or not.
- Two instruction ranges specify whether the prep will also pop an arg into
reg2.
- vm_TeOpcodeEx3:
- Prep reads a 16-bit value from byte stream into reg1. This can be
interpreted as either signed or unsigned by the particular instruction.
- A sub-range within the instruction specifies whether an argument is popped
from the stack.
- (Edit: there are violations of this pattern because I ran out space in
vm_TeOpcodeEx1)
- vm_TeOpcodeEx4:
- Not really any common logic. Just a bucket of miscellaneous instructions.
- vm_TeNumberOp:
- These are all dual-implementation instructions which have both 32 and 64
bit implementations.
- Prep pops one or two values off the stack and reads them into reg1 and
reg2 respectively. The choice of 1 or 2 depends on the sub-range. If
popping one value, the second is left as zero.
- Prep unpacks to either int32 or float64 depending on the corresponding
data types.
- The operations can dispatch to a different tail/follow through routine
depending on whether they overflow or not.
- vm_TeBitwiseOp:
- These operations all operate on 32-bit integers and produce 32-bit integer
results.
- Prep pops one or two values off the stack and reads them into reg1 and
reg2 respectively. The choice of 1 or 2 depends on the sub-range. If
popping one value, the second is left as zero.
- Prep unpacks reg1 and reg2 to int32
Follow-through/tail routines:
- Push float (reg1F)
- Push int32 (reg1I)
- Push 16-bit result (reg1)
*/
// TODO: I think this instruction set needs an overhaul. The categorization has
// become chaotic and not that efficient.
// TODO: If we wanted to make space in the primary opcode range, we could remove
// `VM_OP_LOAD_ARG_1` and just leave `VM_OP2_LOAD_ARG_2`, since static analysis
// should be able to convert many instances of `LoadArg` into `LoadVar`
// 4-bit enum
typedef enum vm_TeOpcode {
VM_OP_LOAD_SMALL_LITERAL = 0x0, // (+ 4-bit vm_TeSmallLiteralValue)
VM_OP_LOAD_VAR_1 = 0x1, // (+ 4-bit variable index relative to stack pointer)
VM_OP_LOAD_SCOPED_1 = 0x2, // (+ 4-bit scoped variable index)
VM_OP_LOAD_ARG_1 = 0x3, // (+ 4-bit arg index)
VM_OP_CALL_1 = 0x4, // (+ 4-bit index into short-call table)
VM_OP_FIXED_ARRAY_NEW_1 = 0x5, // (+ 4-bit length)
VM_OP_EXTENDED_1 = 0x6, // (+ 4-bit vm_TeOpcodeEx1)
VM_OP_EXTENDED_2 = 0x7, // (+ 4-bit vm_TeOpcodeEx2)
VM_OP_EXTENDED_3 = 0x8, // (+ 4-bit vm_TeOpcodeEx3)
VM_OP_CALL_5 = 0x9, // (+ 4-bit arg count)
VM_OP_DIVIDER_1, // <-- ops after this point pop at least one argument (reg2)
VM_OP_STORE_VAR_1 = 0xA, // (+ 4-bit variable index relative to stack pointer)
VM_OP_STORE_SCOPED_1 = 0xB, // (+ 4-bit scoped variable index)
VM_OP_ARRAY_GET_1 = 0xC, // (+ 4-bit item index)
VM_OP_ARRAY_SET_1 = 0xD, // (+ 4-bit item index)
VM_OP_NUM_OP = 0xE, // (+ 4-bit vm_TeNumberOp)
VM_OP_BIT_OP = 0xF, // (+ 4-bit vm_TeBitwiseOp)
VM_OP_END
} vm_TeOpcode;
typedef enum vm_TeOpcodeEx1 {
VM_OP1_RETURN = 0x0,
VM_OP1_THROW = 0x1,
// (target) -> TsClosure
VM_OP1_CLOSURE_NEW = 0x2,
// (TsClass, ...args) -> object
VM_OP1_NEW = 0x3, // (+ 8-bit unsigned arg count. Target is dynamic)
// (state, type) -> TsVirtual
VM_OP1_RESERVED_VIRTUAL_NEW = 0x4, // For future use for creating TsVirtual
VM_OP1_SCOPE_PUSH = 0x5, // (+ 8-bit variable count)
// (value) -> mvm_TeType
VM_OP1_TYPE_CODE_OF = 0x6, // More efficient than VM_OP1_TYPEOF
VM_OP1_POP = 0x7, // Pop one item
VM_OP1_TYPEOF = 0x8,
VM_OP1_OBJECT_NEW = 0x9,
// boolean -> boolean
VM_OP1_LOGICAL_NOT = 0xA,
VM_OP1_DIVIDER_1, // <-- ops after this point are treated as having at least 2 stack arguments
// (object, prop) -> any
VM_OP1_OBJECT_GET_1 = 0xB, // (field ID is dynamic)
// (string, string) -> string
// (number, number) -> number
VM_OP1_ADD = 0xC,
// (any, any) -> boolean
VM_OP1_EQUAL = 0xD,
VM_OP1_NOT_EQUAL = 0xE,
// (object, prop, any) -> void
VM_OP1_OBJECT_SET_1 = 0xF, // (field ID is dynamic)
VM_OP1_END
} vm_TeOpcodeEx1;
// All of these operations are implemented with an 8-bit literal embedded into
// the instruction. The literal is stored in reg1.
typedef enum vm_TeOpcodeEx2 {
VM_OP2_BRANCH_1 = 0x0, // (+ 8-bit signed offset)
VM_OP2_STORE_ARG = 0x1, // (+ 8-bit unsigned arg index)
VM_OP2_STORE_SCOPED_2 = 0x2, // (+ 8-bit unsigned scoped variable index)
VM_OP2_STORE_VAR_2 = 0x3, // (+ 8-bit unsigned variable index relative to stack pointer)
VM_OP2_ARRAY_GET_2_RESERVED = 0x4, // (+ 8-bit unsigned field index)
VM_OP2_ARRAY_SET_2_RESERVED = 0x5, // (+ 8-bit unsigned field index)
VM_OP2_DIVIDER_1, // <-- ops before this point pop from the stack into reg2
VM_OP2_JUMP_1 = 0x6, // (+ 8-bit signed offset)
VM_OP2_CALL_HOST = 0x7, // (+ 8-bit arg count + 8-bit unsigned index into resolvedImports)
VM_OP2_CALL_3 = 0x8, // (+ 8-bit unsigned arg count. Target is dynamic)
VM_OP2_CALL_6 = 0x9, // (+ 8-bit index into short-call table)
VM_OP2_LOAD_SCOPED_2 = 0xA, // (+ 8-bit unsigned scoped variable index)
VM_OP2_LOAD_VAR_2 = 0xB, // (+ 8-bit unsigned variable index relative to stack pointer)
VM_OP2_LOAD_ARG_2 = 0xC, // (+ 8-bit unsigned arg index)
VM_OP2_EXTENDED_4 = 0xD, // (+ 8-bit unsigned vm_TeOpcodeEx4)
VM_OP2_ARRAY_NEW = 0xE, // (+ 8-bit capacity count)
VM_OP2_FIXED_ARRAY_NEW_2 = 0xF, // (+ 8-bit length count)
VM_OP2_END
} vm_TeOpcodeEx2;
// Most of these instructions all have an embedded 16-bit literal value
typedef enum vm_TeOpcodeEx3 {
VM_OP3_POP_N = 0x0, // (+ 8-bit pop count) Pops N items off the stack
VM_OP3_SCOPE_POP = 0x1,
VM_OP3_SCOPE_CLONE = 0x2,
VM_OP3_LONG_JMP_RESERVED = 0x3,
VM_OP3_DIVIDER_1, // <-- ops before this point are miscellaneous and don't automatically get any literal values or stack values
VM_OP3_SET_JMP_RESERVED = 0x6, // (+ 16-bit unsigned bytecode address)
VM_OP3_JUMP_2 = 0x7, // (+ 16-bit signed offset)
VM_OP3_LOAD_LITERAL = 0x8, // (+ 16-bit value)
VM_OP3_LOAD_GLOBAL_3 = 0x9, // (+ 16-bit global variable index)
VM_OP3_LOAD_SCOPED_3 = 0xA, // (+ 16-bit scoped variable index)
VM_OP3_DIVIDER_2, // <-- ops after this point pop an argument into reg2
VM_OP3_BRANCH_2 = 0xB, // (+ 16-bit signed offset)
VM_OP3_STORE_GLOBAL_3 = 0xC, // (+ 16-bit global variable index)
VM_OP3_STORE_SCOPED_3 = 0xD, // (+ 16-bit scoped variable index)
VM_OP3_OBJECT_GET_2 = 0xE, // (+ 16-bit property key)
VM_OP3_OBJECT_SET_2 = 0xF, // (+ 16-bit property key)
VM_OP3_END
} vm_TeOpcodeEx3;
// This is a bucket of less frequently used instructions that didn't fit into the other opcodes
typedef enum vm_TeOpcodeEx4 {
VM_OP4_START_TRY = 0x0, // (+ 16-bit label to the catch block)
VM_OP4_END_TRY = 0x1, // (No literal operands)
VM_OP4_OBJECT_KEYS = 0x2, // (No literal operands)
VM_OP4_UINT8_ARRAY_NEW = 0x3, // (No literal operands)
// (constructor, props) -> TsClass
VM_OP4_CLASS_CREATE = 0x4, // Creates TsClass (does not in instantiate a class)
VM_OP4_TYPE_CODE_OF = 0x5, // Opcode for mvm_typeOf
VM_OP4_END
} vm_TeOpcodeEx4;
// Number operations. These are operations which take one or two arguments from
// the stack and coerce them to numbers. Each of these will have two
// implementations: one for 32-bit int, and one for 64-bit float.
typedef enum vm_TeNumberOp {
// (number, number) -> boolean
VM_NUM_OP_LESS_THAN = 0x0,
VM_NUM_OP_GREATER_THAN = 0x1,
VM_NUM_OP_LESS_EQUAL = 0x2,
VM_NUM_OP_GREATER_EQUAL = 0x3,
// (number, number) -> number
VM_NUM_OP_ADD_NUM = 0x4,
VM_NUM_OP_SUBTRACT = 0x5,
VM_NUM_OP_MULTIPLY = 0x6,
VM_NUM_OP_DIVIDE = 0x7,
VM_NUM_OP_DIVIDE_AND_TRUNC = 0x8, // Represented in JS as `x / y | 0`
VM_NUM_OP_REMAINDER = 0x9,
VM_NUM_OP_POWER = 0xA,
VM_NUM_OP_DIVIDER, // <-- ops after this point are unary
// number -> number
VM_NUM_OP_NEGATE = 0xB,
VM_NUM_OP_UNARY_PLUS = 0xC,
VM_NUM_OP_END
} vm_TeNumberOp;
// Bitwise operations:
typedef enum vm_TeBitwiseOp {
// (bits, bits) -> bits
VM_BIT_OP_SHR_ARITHMETIC = 0x0, // Aka signed shift right. Aka sign-propagating right shift.
VM_BIT_OP_SHR_LOGICAL = 0x1, // Aka unsigned shift right. Aka zero-fill right shift.
VM_BIT_OP_SHL = 0x2, // Shift left
VM_BIT_OP_END_OF_SHIFT_OPERATORS, // <-- ops before this point need their operand in the 0-32 range
VM_BIT_OP_OR = 0x3,
VM_BIT_OP_AND = 0x4,
VM_BIT_OP_XOR = 0x5,
VM_BIT_OP_DIVIDER_2, // <-- ops after this point are unary
// bits -> bits
VM_BIT_OP_NOT = 0x6,
VM_BIT_OP_END
} vm_TeBitwiseOp;
// vm_TeSmallLiteralValue : 4-bit enum
//
// Note: Only up to 16 values are allowed here.
typedef enum vm_TeSmallLiteralValue {
VM_SLV_DELETED = 0x0,
VM_SLV_UNDEFINED = 0x1,
VM_SLV_NULL = 0x2,
VM_SLV_FALSE = 0x3,
VM_SLV_TRUE = 0x4,
VM_SLV_INT_MINUS_1 = 0x5,
VM_SLV_INT_0 = 0x6,
VM_SLV_INT_1 = 0x7,
VM_SLV_INT_2 = 0x8,
VM_SLV_INT_3 = 0x9,
VM_SLV_INT_4 = 0xA,
VM_SLV_INT_5 = 0xB,
} vm_TeSmallLiteralValue;
#define MVM_ENGINE_VERSION 6
#define MVM_EXPECTED_PORT_FILE_VERSION 1
// Note: MVM_BYTECODE_VERSION is at the top of `microvium_bytecode.h`
typedef mvm_VM VM;
typedef mvm_TeError TeError;
/**
* mvm_Value
*
* Hungarian prefix: v
*
* Internally, the name `Value` refers to `mvm_Value`
*
* The Microvium Value type is 16 bits with a 1 or 2 bit discriminator in the
* lowest bits:
*
* - If the lowest bit is `0`, interpret the value as a `ShortPtr`. Note that
* in a snapshot bytecode file, a ShortPtr is measured relative to the
* beginning of the RAM section of the file.
* - If the lowest bits are `11`, interpret the high 14-bits as a signed 14 bit
* integer. The Value is an `VirtualInt14`
* - If the lowest bits are `01`, interpret the high 15-bits as a
* `BytecodeMappedPtr` or a well-known value.
*/
typedef mvm_Value Value;
static inline bool Value_isShortPtr(Value value) { return (value & 1) == 0; }
static inline bool Value_isBytecodeMappedPtrOrWellKnown(Value value) { return (value & 3) == 1; }
static inline bool Value_isVirtualInt14(Value value) { return (value & 3) == 3; }
static inline bool Value_isVirtualUInt12(Value value) { return (value & 0xC003) == 3; }
static inline bool Value_isVirtualUInt8(Value value) { return (value & 0xFC03) == 3; }
/**
* ShortPtr
*
* Hungarian prefix: sp
*
* A ShortPtr is a 16-bit **non-nullable** reference which references into GC
* memory, but not to data memory or bytecode.
*
* Note: To avoid confusion of when to use different kinds of null values,
* ShortPtr should be considered non-nullable. When null is required, use
* VM_VALUE_NULL for consistency, which is not defined as a short pointer.
*
* The GC assumes that anything with a low bit 0 is a non-null pointer into GC
* memory (it does not do null checking on these, since this is a hot loop).
*
* Note: At runtime, pointers _to_ GC memory must always be encoded as
* `ShortPtr` or indirectly through a BytecodeMappedPtr to a global variable.
* This is because the GC assumes (for efficiency reasons) only values with the
* lower bit `0` need to be traced/moved.
*
* A ShortPtr is interpreted one of 3 ways depending on the context:
*
* 1. On 16-bit architectures (when MVM_NATIVE_POINTER_IS_16_BIT is set),
* while the script is running, ShortPtr can be a native pointer, allowing
* for fast access. On other architectures, ShortPtr is encoded as an
* offset from the beginning of the virtual heap.
*
* 2. On non-16-bit architectures (when MVM_NATIVE_POINTER_IS_16_BIT is not
* set), ShortPtr is an offset into the allocation buckets. Access is
* linear time to the number of buckets, but the buckets are compacted
* together during a GC cycle so the number should typically be 1 or low.
*
* 3. In the hibernating GC heap, in the snapshot, ShortPtr is treated as an
* offset into the bytecode image, but always an offset back into the
* GC-RAM section. See `loadPointers`
*
* TODO: Rather than just MVM_NATIVE_POINTER_IS_16_BIT, we could better serve
* small 32-bit devices by having a "page" #define that is added to ShortPtr to
* get the real address. This is because on ARM architectures, the RAM pointers
* are mapped to a higher address space.
*
* A ShortPtr must never exist in a ROM slot, since they need to have a
* consistent representation in all cases, and ROM slots are not visited by
* `loadPointers`. Also short pointers are used iff they point to GC memory,
* which is subject to relocation and therefore cannot be referenced from an
* immutable medium.
*
* If the lowest bit of the `ShortPtr` is 0 (i.e. points to an even boundary),
* then the `ShortPtr` is also a valid `Value`.
*
* NULL short pointers are only allowed in some special circumstances, but are
* mostly not valid.
*/
typedef uint16_t ShortPtr;
/**
* Bytecode-mapped Pointer
*
* If `b` is a BytecodeMappedPtr then `b & 0xFFFE` is treated as an offset into
* the bytecode address space, and its meaning depends on where in the bytecode
* image it points:
*
*
* 1. If the offset points to the BCS_ROM section of bytecode, it is interpreted
* as pointing to that ROM allocation or function.
*
* 2. If the offset points to the BCS_GLOBALS region of the bytecode image, the
* `BytecodeMappedPtr` is treated being a reference to the allocation
* referenced by the corresponding global variable.
*
* This allows ROM Values, such as literal, exports, and builtins, to reference
* RAM allocations. *Note*: for the moment, behavior is not defined if the
* corresponding global has non-pointer contents, such as an Int14 or well-known
* value. In future this may be explicitly allowed.
*
* A `BytecodeMappedPtr` is only a pointer type and is not defined to encode the
* well-known values or null.
*
* Note that in practice, BytecodeMappedPtr is not used anywhere except in
* decoding DynamicPtr.
*
* See `BytecodeMappedPtr_decode_long`
*/
typedef uint16_t BytecodeMappedPtr;
/**
* Dynamic Pointer
*
* Hungarian prefix: `dp`
*
* A `Value` that is a pointer. I.e. its lowest bits are not `11` and it does
* not encode a well-known value. Can be one of:
*
* - `ShortPtr`
* - `BytecodeMappedPtr`
* - `VM_VALUE_NULL`
*
* Note that the only valid representation of null for this point is
* `VM_VALUE_NULL`, not 0.
*/
typedef Value DynamicPtr;
/**
* ROM Pointer
*
* Hungarian prefix: none
*
* A `DynamicPtr` which is known to only point to ROM
*/
typedef Value RomPtr;
/**
* Int14 encoded as a Value
*
* Hungarian prefix: `vi`
*
* A 14-bit signed integer represented in the high 14 bits of a 16-bit Value,
* with the low 2 bits set to the bits `11`, as per the `Value` type.
*/
typedef Value VirtualInt14;
/**
* Hungarian prefix: `lp`
*
* A nullable-pointer that can reference bytecode and RAM in the same address
* space. Not necessarily 16-bit.
*
* The null representation for LongPtr is assumed to be 0.
*
* Values of this type are only managed through macros in the port file, never
* directly, since the exact type depends on the architecture.
*
* See description of MVM_LONG_PTR_TYPE
*/
typedef MVM_LONG_PTR_TYPE LongPtr;
#define READ_FIELD_2(longPtr, structType, fieldName) \
LongPtr_read2_aligned(LongPtr_add(longPtr, OFFSETOF(structType, fieldName)))
#define READ_FIELD_1(longPtr, structType, fieldName) \
LongPtr_read1(LongPtr_add(longPtr, OFFSETOF(structType, fieldName)))
// NOTE: In no way are assertions meant to be present in production. They're
// littered everywhere on the assumption that they consume no overhead.
#if MVM_SAFE_MODE
#define VM_ASSERT(vm, predicate) do { if (!(predicate)) MVM_FATAL_ERROR(vm, MVM_E_ASSERTION_FAILED); } while (false)
#else
#define VM_ASSERT(vm, predicate)
#endif
#ifndef __has_builtin
#define __has_builtin(x) 0
#endif
// Offset of field in a struct
#define OFFSETOF(TYPE, ELEMENT) ((uint16_t)(uintptr_t)&(((TYPE *)0)->ELEMENT))
// Maximum size of an allocation (4kB)
#define MAX_ALLOCATION_SIZE 0xFFF
// This is the only valid way of representing NaN
#define VM_IS_NAN(v) ((v) == VM_VALUE_NAN)
// This is the only valid way of representing infinity
#define VM_IS_INF(v) ((v) == VM_VALUE_INF)
// This is the only valid way of representing -infinity
#define VM_IS_NEG_INF(v) ((v) == VM_VALUE_NEG_INF)
// This is the only valid way of representing negative zero
#define VM_IS_NEG_ZERO(v) ((v) == VM_VALUE_NEG_ZERO)
#define VM_NOT_IMPLEMENTED(vm) MVM_FATAL_ERROR(vm, MVM_E_NOT_IMPLEMENTED)
#define VM_RESERVED(vm) MVM_FATAL_ERROR(vm, MVM_E_UNEXPECTED)
// An error corresponding to an internal inconsistency in the VM. Such an error
// cannot be caused by incorrect usage of the VM. In safe mode, this function
// should terminate the application. If not in safe mode, it is assumed that
// this function will never be invoked.
#define VM_UNEXPECTED_INTERNAL_ERROR(vm) (MVM_FATAL_ERROR(vm, MVM_E_UNEXPECTED), -1)
#define VM_VALUE_OF_DYNAMIC(v) ((void*)((TsAllocationHeader*)v + 1))
#define VM_DYNAMIC_TYPE(v) (((TsAllocationHeader*)v)->type)
#define VM_MAX_INT14 0x1FFF
#define VM_MIN_INT14 (-0x2000)
#if MVM_SAFE_MODE
#define VM_EXEC_SAFE_MODE(code) code
#define VM_SAFE_CHECK_NOT_NULL(v) do { if ((v) == NULL) return MVM_E_UNEXPECTED; } while (false)
#define VM_SAFE_CHECK_NOT_NULL_2(v) do { if ((v) == NULL) { MVM_FATAL_ERROR(vm, MVM_E_UNEXPECTED); return NULL; } } while (false)
#define VM_ASSERT_UNREACHABLE(vm) MVM_FATAL_ERROR(vm, MVM_E_UNEXPECTED)
#else
#define VM_EXEC_SAFE_MODE(code)
#define VM_SAFE_CHECK_NOT_NULL(v)
#define VM_SAFE_CHECK_NOT_NULL_2(v)
#define VM_ASSERT_UNREACHABLE(vm)
#endif
#if MVM_DONT_TRUST_BYTECODE || MVM_SAFE_MODE
// TODO: I think I need to do an audit of all the assertions and errors in the code, and make sure they're categorized correctly as bytecode errors or not
#define VM_INVALID_BYTECODE(vm) MVM_FATAL_ERROR(vm, MVM_E_INVALID_BYTECODE)
#define VM_BYTECODE_ASSERT(vm, condition) do { if (!(condition)) VM_INVALID_BYTECODE(vm); } while (false)
#else
#define VM_INVALID_BYTECODE(vm)
#define VM_BYTECODE_ASSERT(vm, condition)
#endif
#ifndef CODE_COVERAGE
/*
* A set of macros for manual code coverage analysis (because the off-the-shelf
* tools appear to be quite expensive). This should be overridden in the port
* file for the unit tests. Each instance of this macro should occur on its own
* line. The unit tests can dumbly scan the source text for instances of this
* macro to establish what code paths _should_ be hit. Each instance should have
* its own unique numeric ID.
*
* If the ID is omitted or a non-integer placeholder (e.g. "x"), the script `npm
* run update-coverage-markers` will fill in a valid ID.
*
* Explicit IDs are used instead of line numbers because a previous analysis
* remains roughly correct even after the code has changed.
*/
#define CODE_COVERAGE(id)
#define CODE_COVERAGE_UNTESTED(id)
#define CODE_COVERAGE_UNIMPLEMENTED(id)
#define CODE_COVERAGE_ERROR_PATH(id)
/**
* In addition to recording code coverage, it's useful to have information about
* the coverage information for table entries. Code and tables can be
* alternative representations of the same thing. For example, a lookup table
* can be represented as a switch statement. However, only the switch statement
* form typically shows up in code coverage analysis. With Microvium coverage
* analysis, tables are covered as well.
*
* If the ID is omitted or a non-integer placeholder (e.g. "x"), the script `npm
* run update-coverage-markers` will fill in a valid ID.
*
* @param indexInTable The runtime expression for the case that is actually hit.
* @param tableSize The size of the table (can be a runtime expression)
* @param id A unique numeric ID to uniquely identify the marker
*/
#define TABLE_COVERAGE(indexInTable, tableSize, id)
#endif
#ifndef MVM_SUPPORT_FLOAT
#define MVM_SUPPORT_FLOAT 1
#endif
#ifndef MVM_PORT_INT32_OVERFLOW_CHECKS
#define MVM_PORT_INT32_OVERFLOW_CHECKS 1
#endif
#ifndef MVM_SAFE_MODE
#define MVM_SAFE_MODE 0
#endif
// TODO: The example port file sets to 1 because we want it enabled in the
// tests. But really we should have a separate test port file.
#ifndef MVM_VERY_EXPENSIVE_MEMORY_CHECKS
#define MVM_VERY_EXPENSIVE_MEMORY_CHECKS 0
#endif
#ifndef MVM_DONT_TRUST_BYTECODE
#define MVM_DONT_TRUST_BYTECODE 0
#endif
#ifndef MVM_SWITCH
#define MVM_SWITCH(tag, upper) switch (tag)
#endif
#ifndef MVM_CASE
#define MVM_CASE(value) case value
#endif
/**
* Type code indicating the type of data.
*
* This enumeration is divided into reference types (TC_REF_) and value types
* (TC_VAL_). Reference type codes are used on allocations, whereas value type
* codes are never used on allocations. The space for the type code in the
* allocation header is 4 bits, so there are up to 16 reference types and these
* must be the first 16 types in the enumeration.
*
* The reference type range is subdivided into containers or non-containers. The
* GC uses this distinction to decide whether the body of the allocation should
* be interpreted as `Value`s (i.e. may contain pointers). To minimize the code,
* either ALL words in a container are `Value`s, or none.
*
* Value types are for the values that can be represented within the 16-bit
* mvm_Value without interpreting it as a pointer.
*/
typedef enum TeTypeCode {
// Note: only type code values in the range 0-15 can be used as the types for
// allocations, since the allocation header allows 4 bits for the type. Types
// 0-8 are non-container types, 0xC-F are container types (9-B reserved).
// Every word in a container must be a `Value`. No words in a non-container
// can be a `Value` (the GC uses this to distinguish whether an allocation may
// contain pointers, and the signature of each word). Note that buffer-like
// types would not count as containers by this definition.
/* --------------------------- Reference types --------------------------- */
// A type used during garbage collection. Allocations of this type have a
// single 16-bit forwarding pointer in the allocation.
TC_REF_TOMBSTONE = 0x0,
TC_REF_INT32 = 0x1, // 32-bit signed integer
TC_REF_FLOAT64 = 0x2, // 64-bit float
/**
* UTF8-encoded string that may or may not be unique.
*
* Note: If a TC_REF_STRING is in bytecode, it is because it encodes a value
* that is illegal as a property index in Microvium (i.e. it encodes an
* integer).
*/
TC_REF_STRING = 0x3,
/**
* A string whose address uniquely identifies its contents, and does not
* encode an integer in the range 0 to 0x1FFF.
*
* To keep property lookup efficient, Microvium requires that strings used as
* property keys can be compared using pointer equality. This requires that
* there is only one instance of each of those strings (see
* https://en.wikipedia.org/wiki/String_interning).
*
* A string with the type code TC_REF_INTERNED_STRING means that it exists in
* one of the interning tables (either the one in ROM or the one in RAM). Not
* all strings are interned, because it would be expensive if every string
* concatenation resulted in a search of the intern table and possibly a new
* entry (imagine if every JSON string landed up in the table!).
*
* In practice we do this:
*
* - All valid non-index property keys in ROM are interned. If a string is in
* ROM but it is not interned, the engine can conclude that it is not a
* valid property key or it is an index.
* - Strings constructed in RAM are only interned when they're used to access
* properties.
*/
TC_REF_INTERNED_STRING = 0x4,
TC_REF_FUNCTION = 0x5, // TsBytecodeFunc
TC_REF_HOST_FUNC = 0x6, // TsHostFunc
TC_REF_UINT8_ARRAY = 0x7, // Byte buffer
TC_REF_SYMBOL = 0x8, // Reserved: Symbol
/* --------------------------- Container types --------------------------- */
TC_REF_DIVIDER_CONTAINER_TYPES, // <--- Marker. Types after or including this point but less than 0x10 are container types
TC_REF_CLASS = 0x9, // TsClass
TC_REF_VIRTUAL = 0xA, // Reserved: TsVirtual
TC_REF_RESERVED_1 = 0xB, // Reserved
TC_REF_PROPERTY_LIST = 0xC, // TsPropertyList - Object represented as linked list of properties
TC_REF_ARRAY = 0xD, // TsArray
TC_REF_FIXED_LENGTH_ARRAY = 0xE, // TsFixedLengthArray
TC_REF_CLOSURE = 0xF, // TsClosure
/* ----------------------------- Value types ----------------------------- */
TC_VAL_INT14 = 0x10,
TC_VAL_UNDEFINED = 0x11,
TC_VAL_NULL = 0x12,
TC_VAL_TRUE = 0x13,
TC_VAL_FALSE = 0x14,
TC_VAL_NAN = 0x15,
TC_VAL_NEG_ZERO = 0x16,
TC_VAL_DELETED = 0x17, // Placeholder for properties and list items that have been deleted or holes in arrays
TC_VAL_STR_LENGTH = 0x18, // The string "length"
TC_VAL_STR_PROTO = 0x19, // The string "__proto__"
TC_END,
} TeTypeCode;
// Note: VM_VALUE_NAN must be used instead of a pointer to a double that has a
// NaN value (i.e. the values must be normalized to use the following table).
// Operations will assume this canonical form.
// Note: the `(... << 2) | 1` is so that these values don't overlap with the
// ShortPtr or BytecodeMappedPtr address spaces.
// Some well-known values
typedef enum vm_TeWellKnownValues {
VM_VALUE_UNDEFINED = (((int)TC_VAL_UNDEFINED - 0x11) << 2) | 1, // = 1
VM_VALUE_NULL = (((int)TC_VAL_NULL - 0x11) << 2) | 1,
VM_VALUE_TRUE = (((int)TC_VAL_TRUE - 0x11) << 2) | 1,
VM_VALUE_FALSE = (((int)TC_VAL_FALSE - 0x11) << 2) | 1,
VM_VALUE_NAN = (((int)TC_VAL_NAN - 0x11) << 2) | 1,
VM_VALUE_NEG_ZERO = (((int)TC_VAL_NEG_ZERO - 0x11) << 2) | 1,
VM_VALUE_DELETED = (((int)TC_VAL_DELETED - 0x11) << 2) | 1,
VM_VALUE_STR_LENGTH = (((int)TC_VAL_STR_LENGTH - 0x11) << 2) | 1,
VM_VALUE_STR_PROTO = (((int)TC_VAL_STR_PROTO - 0x11) << 2) | 1,
VM_VALUE_WELLKNOWN_END,
} vm_TeWellKnownValues;
#define VIRTUAL_INT14_ENCODE(i) ((uint16_t)(((unsigned int)(i) << 2) | 3))
typedef struct TsArray {
/*
* Note: the capacity of the array is the length of the TsFixedLengthArray
* pointed to by dpData, or 0 if dpData is VM_VALUE_NULL. The logical length
* of the array is determined by viLength.
*
* Note: If dpData is not null, it must be a unique pointer (it must be the
* only pointer that points to that allocation)
*
* Note: for arrays in GC memory, their dpData must point to GC memory as well
*
* Note: Values in dpData that are beyond the logical length MUST be filled
* with VM_VALUE_DELETED.
*/
DynamicPtr dpData; // Points to TsFixedLengthArray
VirtualInt14 viLength;
} TsArray;
typedef struct TsFixedLengthArray {
// Note: the length of the fixed-length-array is determined by the allocation header
Value items[1];
} TsFixedLengthArray;
typedef struct vm_TsStack vm_TsStack;
/**
* Used to represent JavaScript objects.
*
* The `proto` pointer points to the prototype of the object.
*
* Properties on object are stored in a linked list of groups. Each group has a
* `next` pointer to the next group (list). When assigning to a new property,
* rather than resizing a group, the VM will just append a new group to the list
* (a group with just the one new property).
*
* Only the `proto` field of the first group of properties in an object is used.
*
* The garbage collector compacts multiple groups into one large one, so it
* doesn't matter that appending a single property requires a whole new group on
* its own or that they have unused proto properties.
*/
typedef struct TsPropertyList {
// Note: if the property list is in GC memory, then dpNext must also point to
// GC memory, but dpProto can point to any memory (e.g. a prototype stored in
// ROM).
// Note: in the serialized form, the next pointer must be null
DynamicPtr dpNext; // TsPropertyList* or VM_VALUE_NULL, containing further appended properties
DynamicPtr dpProto; // Note: the prototype is only meaningful on the first in the list
/*
Followed by N of these pairs to the end of the allocated size:
Value key; // TC_VAL_INT14 or TC_REF_INTERNED_STRING
Value value;
*/
} TsPropertyList;
/**
* A property list with a single property. See TsPropertyList for description.
*/
typedef struct TsPropertyCell /* extends TsPropertyList */ {
TsPropertyList base;
Value key; // TC_VAL_INT14 or TC_REF_INTERNED_STRING
Value value;
} TsPropertyCell;
/**
* A closure is a function-like type that has access to an outer lexical scope
* (other than the globals, which are already accessible by any function).
*
* The `target` must reference a function, either a local function or host (it
* cannot itself be a TsClosure). This will be what is called when the closure
* is called. If it's an invalid type, the error is the same as if calling that
* type directly.
*
* The closure keeps a reference to the outer `scope`. The machine semantics for
* a `CALL` of a `TsClosure` is to set the `scope` register to the scope of the
* `TsClosure`, which is then accessible via the `VM_OP_LOAD_SCOPED_n` and
* `VM_OP_STORE_SCOPED_n` instructions. The `VM_OP1_CLOSURE_NEW` instruction
* automatically captures the current `scope` register in a new `TsClosure`.
*
* Scopes are created using `VM_OP1_SCOPE_PUSH` using the type
* `TC_REF_FIXED_LENGTH_ARRAY`, with one extra slot for the reference to the
* outer scope. An instruction like `VM_OP_LOAD_SCOPED_1` accepts an index into
* the slots in the scope chain (see `vm_findScopedVariable`)
*
* By convention, the caller passes `this` by the first argument. If the closure
* body wants to access the caller's `this` then it just access the first
* argument. If the body wants to access the outer scope's `this` then it parent
* must copy the `this` argument into the closure scope and the child can access
* it via `VM_OP_LOAD_SCOPED_1`, the same as would be done for any closed-over
* parameter.
*/
typedef struct TsClosure {
Value scope;
Value target; // Function type
} TsClosure;
/**
* This type is to provide support for a subset of the ECMAScript classes
* feature. Classes can be instantiated using `new`, but it is illegal to call
* them directly. Similarly, `new` doesn't work on arbitrary function.
*/
typedef struct TsClass {
Value constructorFunc; // Function type
Value staticProps;
} TsClass;
/**
* TsVirtual (at the time of this writing, this is just a placeholder type)
*
* This is a placeholder for an idea to have something like a "low-level proxy"
* type. See my private notes for details (if you have access to them). The
* `type` and `state` fields correspond roughly to the "handler" and "target"
* fields respectively in a normal ES `Proxy`.
*/
typedef struct TsVirtual {
Value state;
Value type;
} TsVirtual;
// External function by index in import table
typedef struct TsHostFunc {
// Note: TC_REF_HOST_FUNC is not a container type, so it's fields are not
// traced by the GC.
//
// Note: most host function reference can be optimized to not require this
// allocation -- they can use VM_OP2_CALL_HOST directly. This allocation is
// only required then the reference to host function is ambiguous or there are
// calls to more than 256 host functions.
uint16_t indexInImportTable;
} TsHostFunc;
typedef struct TsBucket {
uint16_t offsetStart; // The number of bytes in the heap before this bucket
struct TsBucket* prev;
struct TsBucket* next;
/* Note: pEndOfUsedSpace used to be on the VM struct, rather than per-bucket.
* The main reason it's useful to have it on each bucket is in the hot GC-loop
* which needs to check if it's caught up with the write cursor in to-space or
* check if it's hit the end of the bucket. Without this value being in each
* bucket, the calculation to find the end of the bucket is expensive.
*
* Note that for the last bucket, `pEndOfUsedSpace` doubles up as the write
* cursor, since it's only recording the *used* space. The *capacity* of each
* bucket is not recorded, but the capacity of the *last* bucket is recorded
* in `pLastBucketEndCapacity` (on the VM and GC structures). */
uint16_t* pEndOfUsedSpace;
/* ...data */
} TsBucket;
typedef struct TsBreakpoint {
struct TsBreakpoint* next;
uint16_t bytecodeAddress;
} TsBreakpoint;
/*
Minimum size:
- 6 pointers + 1 long pointer + 4 words
- = 24B on 16bit
- = 36B on 32bit.
Maximum size (on 64-bit machine):
- 9 pointers + 4 words
- = 80 bytes on 64-bit machine
See also the unit tests called "minimal-size"
*/
struct mvm_VM {
uint16_t* globals;
LongPtr lpBytecode;
vm_TsStack* stack;
// Last bucket of GC memory
TsBucket* pLastBucket;
// End of the capacity of the last bucket of GC memory
uint16_t* pLastBucketEndCapacity;
// Handles - values to treat as GC roots
mvm_Handle* gc_handles;
void* context;
#if MVM_INCLUDE_DEBUG_CAPABILITY
TsBreakpoint* pBreakpoints;
mvm_TfBreakpointCallback breakpointCallback;
#endif // MVM_INCLUDE_DEBUG_CAPABILITY
uint16_t heapSizeUsedAfterLastGC;
uint16_t stackHighWaterMark;
uint16_t heapHighWaterMark;
#if MVM_VERY_EXPENSIVE_MEMORY_CHECKS
// Amount to shift the heap over during each collection cycle
uint8_t gc_heap_shift;
#endif
#if MVM_SAFE_MODE
// A number that increments at every possible opportunity for a GC cycle
uint8_t gc_potentialCycleNumber;
#endif // MVM_SAFE_MODE
};
typedef struct TsInternedStringCell {
ShortPtr spNext;
Value str;
} TsInternedStringCell;
// Possible values for the `flags` machine register
typedef enum vm_TeActivationFlags {
// Note: these flags start at bit 8 because they use the same word as the argument count
// Flag to indicate if the most-recent CALL operation involved a stack-based
// function target (as opposed to a literal function target). If this is set,
// then the next RETURN instruction will also pop the function reference off
// the stack.
AF_PUSHED_FUNCTION = 1 << 9,
// Flag to indicate that returning from the current frame should return to the host
AF_CALLED_FROM_HOST = 1 << 10
} vm_TeActivationFlags;
/**
* This struct is malloc'd from the host when the host calls into the VM
*/
typedef struct vm_TsRegisters { // 24 B on 32-bit machine
uint16_t* pFrameBase;
uint16_t* pStackPointer;
LongPtr lpProgramCounter;
// Note: I previously used to infer the location of the arguments based on the
// number of values PUSHed by a CALL instruction to preserve the activation
// state (i.e. 3 words). But now that distance is dynamic, so we need and
// explicit register.
Value* pArgs;
uint16_t argCountAndFlags; // Lower 8 bits are argument count, upper 8 bits are vm_TeActivationFlags
Value scope; // Closure scope
uint16_t catchTarget; // 0 if no catch block
#if MVM_SAFE_MODE
// This will be true if the VM is operating on the local variables rather
// than the shared vm_TsRegisters structure.
uint8_t usingCachedRegisters;
uint8_t _reserved; // My compiler seems to pad this out anyway
#endif
} vm_TsRegisters;
/**
* This struct is malloc'd from the host when the host calls into the VM and
* freed when the VM finally returns to the host. This struct embeds both the
* working registers and the call stack in the same allocation since they are
* needed at the same time and it's more efficient to do a single malloc where
* possible.
*/
struct vm_TsStack {
// Allocate registers along with the stack, because these are needed at the same time (i.e. while the VM is active)
vm_TsRegisters reg;
// Note: the stack grows upwards (towards higher addresses)
// ... (stack memory) ...
};
typedef struct TsAllocationHeader {
/* 4 least-significant-bits are the type code (TeTypeCode). Remaining 12 bits
are the allocation size, excluding the size of the header itself, in bytes
(measured in bytes so that we can represent the length of strings exactly).
See also `vm_getAllocationSizeExcludingHeaderFromHeaderWord` */
uint16_t headerData;
} TsAllocationHeader;
typedef struct TsBytecodeFunc {
uint8_t maxStackDepth;
/* Followed by the bytecode bytes */
} TsBytecodeFunc;
typedef struct vm_TsImportTableEntry {
mvm_HostFunctionID hostFunctionID;
/*
Note: I considered having a `paramCount` field in the header since a common
scenario would be copying the arguments into the parameter slots. However,
most parameters are not actually mutated in a function, so the LOAD_ARG
instruction could just be used directly to get the parameter value (if the
optimizer can detect such cases).
*/
} vm_TsImportTableEntry;
#define GC_TRACE_STACK_COUNT 20
typedef struct gc_TsGCCollectionState {
VM* vm;
TsBucket* firstBucket;
TsBucket* lastBucket;
uint16_t* lastBucketEndCapacity;
} gc_TsGCCollectionState;
#define TOMBSTONE_HEADER ((TC_REF_TOMBSTONE << 12) | 2)
// A CALL instruction saves the current registers to the stack. I'm calling this
// the "frame boundary" since it is a fixed-size sequence of words that marks
// the boundary between stack frames. The shape of this saved state is coupled
// to a few different places in the engine, so I'm versioning it here in case I
// need to make changes
#define VM_FRAME_BOUNDARY_VERSION 2
// The number of words between one call stack frame and the next (i.e. the
// number of saved registers during a CALL)
#define VM_FRAME_BOUNDARY_SAVE_SIZE_WORDS 4
static inline mvm_HostFunctionID vm_getHostFunctionId(VM*vm, uint16_t hostFunctionIndex);
static TeError vm_createStackAndRegisters(VM* vm);
static TeError vm_requireStackSpace(VM* vm, uint16_t* pStackPointer, uint16_t sizeRequiredInWords);
static Value vm_convertToString(VM* vm, Value value);
static Value vm_concat(VM* vm, Value* left, Value* right);
static TeTypeCode deepTypeOf(VM* vm, Value value);
static bool vm_isString(VM* vm, Value value);
static int32_t vm_readInt32(VM* vm, TeTypeCode type, Value value);
static TeError vm_resolveExport(VM* vm, mvm_VMExportID id, Value* result);
static inline mvm_TfHostFunction* vm_getResolvedImports(VM* vm);
static void gc_createNextBucket(VM* vm, uint16_t bucketSize, uint16_t minBucketSize);
static void* gc_allocateWithHeader(VM* vm, uint16_t sizeBytes, TeTypeCode typeCode);
static void gc_freeGCMemory(VM* vm);
static Value vm_allocString(VM* vm, size_t sizeBytes, void** data);
static TeError getProperty(VM* vm, Value* pObjectValue, Value* pPropertyName, Value* out_propertyValue);
static TeError setProperty(VM* vm, Value* pOperands);
static TeError toPropertyName(VM* vm, Value* value);
static void toInternedString(VM* vm, Value* pValue);
static uint16_t vm_stringSizeUtf8(VM* vm, Value str);
static bool vm_ramStringIsNonNegativeInteger(VM* vm, Value str);
static TeError toInt32Internal(mvm_VM* vm, mvm_Value value, int32_t* out_result);
static inline uint16_t vm_getAllocationSizeExcludingHeaderFromHeaderWord(uint16_t headerWord);
static inline LongPtr LongPtr_add(LongPtr lp, int16_t offset);
static inline uint16_t LongPtr_read2_aligned(LongPtr lp);
static inline uint16_t LongPtr_read2_unaligned(LongPtr lp);
static void memcpy_long(void* target, LongPtr source, size_t size);
static void loadPointers(VM* vm, uint8_t* heapStart);
static inline ShortPtr ShortPtr_encode(VM* vm, void* ptr);
static inline uint8_t LongPtr_read1(LongPtr lp);
static LongPtr DynamicPtr_decode_long(VM* vm, DynamicPtr ptr);
static inline int16_t LongPtr_sub(LongPtr lp1, LongPtr lp2);
static inline uint16_t readAllocationHeaderWord(void* pAllocation);
static inline uint16_t readAllocationHeaderWord_long(LongPtr pAllocation);
static inline void* gc_allocateWithConstantHeader(VM* vm, uint16_t header, uint16_t sizeIncludingHeader);
static inline uint16_t vm_makeHeaderWord(VM* vm, TeTypeCode tc, uint16_t size);
static int memcmp_long(LongPtr p1, LongPtr p2, size_t size);
static LongPtr getBytecodeSection(VM* vm, mvm_TeBytecodeSection id, LongPtr* out_end);
static inline void* LongPtr_truncate(LongPtr lp);
static inline LongPtr LongPtr_new(void* p);
static inline uint16_t* getBottomOfStack(vm_TsStack* stack);
static inline uint16_t* getTopOfStackSpace(vm_TsStack* stack);
static inline void* getBucketDataBegin(TsBucket* bucket);
static uint16_t getBucketOffsetEnd(TsBucket* bucket);
static uint16_t getSectionSize(VM* vm, mvm_TeBytecodeSection section);
static Value vm_intToStr(VM* vm, int32_t i);
static Value vm_newStringFromCStrNT(VM* vm, const char* s);
static TeError vm_validatePortFileMacros(MVM_LONG_PTR_TYPE lpBytecode, mvm_TsBytecodeHeader* pHeader);
static LongPtr vm_toStringUtf8_long(VM* vm, Value value, size_t* out_sizeBytes);
static LongPtr vm_findScopedVariable(VM* vm, uint16_t index);
static Value vm_cloneFixedLengthArray(VM* vm, Value* pArr);
static Value vm_safePop(VM* vm, Value* pStackPointerAfterDecr);
static LongPtr vm_getStringData(VM* vm, Value value);
static inline VirtualInt14 VirtualInt14_encode(VM* vm, int16_t i);
static inline TeTypeCode vm_getTypeCodeFromHeaderWord(uint16_t headerWord);
static bool DynamicPtr_isRomPtr(VM* vm, DynamicPtr dp);
static inline void vm_checkValueAccess(VM* vm, uint8_t potentialCycleNumber);
static inline uint16_t vm_getAllocationSize(void* pAllocation);
static inline uint16_t vm_getAllocationSize_long(LongPtr lpAllocation);
static inline mvm_TeBytecodeSection vm_sectionAfter(VM* vm, mvm_TeBytecodeSection section);
static void* ShortPtr_decode(VM* vm, ShortPtr shortPtr);
static TeError vm_newError(VM* vm, TeError err);
static void* vm_malloc(VM* vm, size_t size);
static void vm_free(VM* vm, void* ptr);
static inline uint16_t* getTopOfStackSpace(vm_TsStack* stack);
static inline Value* getHandleTargetOrNull(VM* vm, Value value);
static TeError vm_objectKeys(VM* vm, Value* pObject);
static mvm_TeError vm_uint8ArrayNew(VM* vm, Value* slot);
static Value getBuiltin(VM* vm, mvm_TeBuiltins builtinID);
#if MVM_SAFE_MODE
static inline uint16_t vm_getResolvedImportCount(VM* vm);
#endif // MVM_SAFE_MODE
static const Value smallLiterals[] = {
/* VM_SLV_UNDEFINED */ VM_VALUE_DELETED,
/* VM_SLV_UNDEFINED */ VM_VALUE_UNDEFINED,
/* VM_SLV_NULL */ VM_VALUE_NULL,
/* VM_SLV_FALSE */ VM_VALUE_FALSE,
/* VM_SLV_TRUE */ VM_VALUE_TRUE,
/* VM_SLV_INT_MINUS_1 */ VIRTUAL_INT14_ENCODE(-1),
/* VM_SLV_INT_0 */ VIRTUAL_INT14_ENCODE(0),
/* VM_SLV_INT_1 */ VIRTUAL_INT14_ENCODE(1),
/* VM_SLV_INT_2 */ VIRTUAL_INT14_ENCODE(2),
/* VM_SLV_INT_3 */ VIRTUAL_INT14_ENCODE(3),
/* VM_SLV_INT_4 */ VIRTUAL_INT14_ENCODE(4),
/* VM_SLV_INT_5 */ VIRTUAL_INT14_ENCODE(5),
};
#define smallLiteralsSize (sizeof smallLiterals / sizeof smallLiterals[0])
static const char PROTO_STR[] = "__proto__";
static const char LENGTH_STR[] = "length";
static const char TYPE_STRINGS[] =
"undefined\0boolean\0number\0string\0function\0object\0symbol\0bigint";
// 0 10 18 25 32 41 48 55
// Character offsets into TYPE_STRINGS
static const uint8_t typeStringOffsetByType[VM_T_END] = {
0 , /* VM_T_UNDEFINED */
41, /* VM_T_NULL */
10, /* VM_T_BOOLEAN */
18, /* VM_T_NUMBER */
25, /* VM_T_STRING */
32, /* VM_T_FUNCTION */
41, /* VM_T_OBJECT */
41, /* VM_T_ARRAY */
41, /* VM_T_UINT8_ARRAY */
32, /* VM_T_CLASS */
48, /* VM_T_SYMBOL */
55, /* VM_T_BIG_INT */
};
// TeTypeCode -> mvm_TeType
static const uint8_t typeByTC[TC_END] = {
VM_T_END, /* TC_REF_TOMBSTONE */
VM_T_NUMBER, /* TC_REF_INT32 */
VM_T_NUMBER, /* TC_REF_FLOAT64 */
VM_T_STRING, /* TC_REF_STRING */
VM_T_STRING, /* TC_REF_INTERNED_STRING */
VM_T_FUNCTION, /* TC_REF_FUNCTION */
VM_T_FUNCTION, /* TC_REF_HOST_FUNC */
VM_T_UINT8_ARRAY, /* TC_REF_UINT8_ARRAY */
VM_T_SYMBOL, /* TC_REF_SYMBOL */
VM_T_CLASS, /* TC_REF_CLASS */
VM_T_END, /* TC_REF_VIRTUAL */
VM_T_END, /* TC_REF_RESERVED_1 */
VM_T_OBJECT, /* TC_REF_PROPERTY_LIST */
VM_T_ARRAY, /* TC_REF_ARRAY */
VM_T_ARRAY, /* TC_REF_FIXED_LENGTH_ARRAY */
VM_T_FUNCTION, /* TC_REF_CLOSURE */
VM_T_NUMBER, /* TC_VAL_INT14 */
VM_T_UNDEFINED, /* TC_VAL_UNDEFINED */
VM_T_NULL, /* TC_VAL_NULL */
VM_T_BOOLEAN, /* TC_VAL_TRUE */
VM_T_BOOLEAN, /* TC_VAL_FALSE */
VM_T_NUMBER, /* TC_VAL_NAN */
VM_T_NUMBER, /* TC_VAL_NEG_ZERO */
VM_T_UNDEFINED, /* TC_VAL_DELETED */
VM_T_STRING, /* TC_VAL_STR_LENGTH */
VM_T_STRING, /* TC_VAL_STR_PROTO */
};
#define GC_ALLOCATE_TYPE(vm, type, typeCode) \
(type*)gc_allocateWithConstantHeader(vm, vm_makeHeaderWord(vm, typeCode, sizeof (type)), 2 + sizeof (type))
#if MVM_SUPPORT_FLOAT
static int32_t mvm_float64ToInt32(MVM_FLOAT64 value);
#endif
// MVM_LOCAL declares a local variable whose value would become invalidated if
// the GC performs a cycle. All access to the local should use MVM_GET_LOCAL AND
// MVM_SET_LOCAL. This only needs to be used for pointer values or values that
// might hold a pointer.
#if MVM_SAFE_MODE
#define MVM_LOCAL(type, varName, initial) type varName ## Value = initial; uint8_t _ ## varName ## PotentialCycleNumber = vm->gc_potentialCycleNumber
#define MVM_GET_LOCAL(varName) (vm_checkValueAccess(vm, _ ## varName ## PotentialCycleNumber), varName ## Value)
#define MVM_SET_LOCAL(varName, value) varName ## Value = value; _ ## varName ## PotentialCycleNumber = vm->gc_potentialCycleNumber
#else
#define MVM_LOCAL(type, varName, initial) type varName = initial
#define MVM_GET_LOCAL(varName) (varName)
#define MVM_SET_LOCAL(varName, value) varName = value
#endif // MVM_SAFE_MODE
// Various things require the registers (vm->stack->reg) to be up to date
#define VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm) \
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters)
/**
* Public API to call into the VM to run the given function with the given
* arguments (also contains the run loop).
*
* Control returns from `mvm_call` either when it hits an error or when it
* executes a RETURN instruction within the called function.
*
* If the return code is MVM_E_UNCAUGHT_EXCEPTION then `out_result` points to the exception.
*/
TeError mvm_call(VM* vm, Value targetFunc, Value* out_result, Value* args, uint8_t argCount) {
/*
Note: when microvium calls the host, only `mvm_call` is on the call stack.
This is for the objective of being lightweight. Each stack frame in an
embedded environment can be quite expensive in terms of memory because of all
the general-purpose registers that need to be preserved.
*/
// -------------------------------- Definitions -----------------------------
#define CACHE_REGISTERS() do { \
VM_ASSERT(vm, reg->usingCachedRegisters == false); \
VM_EXEC_SAFE_MODE(reg->usingCachedRegisters = true;) \
lpProgramCounter = reg->lpProgramCounter; \
pFrameBase = reg->pFrameBase; \
pStackPointer = reg->pStackPointer; \
} while (false)
#define FLUSH_REGISTER_CACHE() do { \
VM_ASSERT(vm, reg->usingCachedRegisters == true); \
VM_EXEC_SAFE_MODE(reg->usingCachedRegisters = false;) \
reg->lpProgramCounter = lpProgramCounter; \
reg->pFrameBase = pFrameBase; \
reg->pStackPointer = pStackPointer; \
} while (false)
#define READ_PGM_1(target) do { \
VM_ASSERT(vm, reg->usingCachedRegisters == true); \
target = LongPtr_read1(lpProgramCounter);\
lpProgramCounter = LongPtr_add(lpProgramCounter, 1); \
} while (false)
#define READ_PGM_2(target) do { \
VM_ASSERT(vm, reg->usingCachedRegisters == true); \
target = LongPtr_read2_unaligned(lpProgramCounter); \
lpProgramCounter = LongPtr_add(lpProgramCounter, 2); \
} while (false)
#define PUSH(v) do { \
VM_ASSERT(vm, reg->usingCachedRegisters == true); \
VM_ASSERT(vm, pStackPointer < getTopOfStackSpace(vm->stack)); \
*pStackPointer = v; \
pStackPointer++; \
} while (false)
#if MVM_SAFE_MODE
#define POP() vm_safePop(vm, --pStackPointer)
#else
#define POP() (*(--pStackPointer))
#endif
// Push the current registers onto the call stack
#define PUSH_REGISTERS(lpReturnAddress) do { \
VM_ASSERT(vm, VM_FRAME_BOUNDARY_VERSION == 2); \
PUSH((uint16_t)(uintptr_t)pStackPointer - (uint16_t)(uintptr_t)pFrameBase); \
PUSH(reg->scope); \
PUSH(reg->argCountAndFlags); \
PUSH((uint16_t)LongPtr_sub(lpReturnAddress, vm->lpBytecode)); \
} while (false)
// Inverse of PUSH_REGISTERS
#define POP_REGISTERS() do { \
VM_ASSERT(vm, VM_FRAME_BOUNDARY_VERSION == 2); \
lpProgramCounter = LongPtr_add(vm->lpBytecode, POP()); \
reg->argCountAndFlags = POP(); \
reg->scope = POP(); \
pStackPointer--; \
pFrameBase = (uint16_t*)((uint8_t*)pStackPointer - *pStackPointer); \
reg->pArgs = pFrameBase - VM_FRAME_BOUNDARY_SAVE_SIZE_WORDS - (uint8_t)reg->argCountAndFlags; \
} while (false)
// Reinterpret reg1 as 8-bit signed
#define SIGN_EXTEND_REG_1() reg1 = (uint16_t)((int16_t)((int8_t)reg1))
#define INSTRUCTION_RESERVED() VM_ASSERT(vm, false)
// ------------------------------ Common Variables --------------------------
VM_SAFE_CHECK_NOT_NULL(vm);
if (argCount) VM_SAFE_CHECK_NOT_NULL(args);
TeError err = MVM_E_SUCCESS;
// These are cached values of `vm->stack->reg`, for quick access. Note: I've
// chosen only the most important registers to be cached here, in the hope
// that the C compiler will promote these eagerly to the CPU registers,
// although it may choose not to.
register uint16_t* pFrameBase;
register uint16_t* pStackPointer;
register LongPtr lpProgramCounter;
// These are general-purpose scratch "registers". Note: probably the compiler
// would be fine at performing register allocation if we didn't have specific
// register variables, but having them explicit forces us to think about what
// state is being used and designing the code to minimize it.
register uint16_t reg1;
register uint16_t reg2;
register uint16_t reg3;
uint16_t* regP1 = 0;
LongPtr regLP1 = 0;
uint16_t* globals;
vm_TsRegisters* reg;
vm_TsRegisters registerValuesAtEntry;
#if MVM_DONT_TRUST_BYTECODE
LongPtr maxProgramCounter;
LongPtr minProgramCounter = getBytecodeSection(vm, BCS_ROM, &maxProgramCounter);
#endif
// Note: these initial values are not actually used, but some compilers give a
// warning if you omit them.
pFrameBase = 0;
pStackPointer = 0;
lpProgramCounter = 0;
reg1 = 0;
reg2 = 0;
reg3 = 0;
// ------------------------------ Initialization ---------------------------
CODE_COVERAGE(4); // Hit
// Create the call stack if it doesn't exist
if (!vm->stack) {
CODE_COVERAGE(230); // Hit
err = vm_createStackAndRegisters(vm);
if (err != MVM_E_SUCCESS) {
return err;
}
} else {
CODE_COVERAGE_UNTESTED(232); // Not hit
}
globals = vm->globals;
reg = &vm->stack->reg;
registerValuesAtEntry = *reg;
// Because we're coming from C-land, any exceptions that happen during
// mvm_call should register as host errors
reg->catchTarget = VM_VALUE_UNDEFINED;
// Copy the state of the VM registers into the logical variables for quick access
CACHE_REGISTERS();
// ---------------------- Push host arguments to the stack ------------------
// 254 is the maximum because we also push the `this` value implicitly
if (argCount > 254) {
CODE_COVERAGE_ERROR_PATH(220); // Not hit
return MVM_E_TOO_MANY_ARGUMENTS;
} else {
CODE_COVERAGE(15); // Hit
}
vm_requireStackSpace(vm, pStackPointer, argCount + 1);
PUSH(VM_VALUE_UNDEFINED); // Push `this` pointer of undefined
TABLE_COVERAGE(argCount ? 1 : 0, 2, 513); // Hit 2/2
reg1 = argCount;
while (reg1--) {
PUSH(*args++);
}
// ---------------------------- Call target function ------------------------
reg1 /* argCountAndFlags */ = (argCount + 1) | AF_CALLED_FROM_HOST; // +1 for the `this` value
reg2 /* target */ = targetFunc;
goto LBL_CALL;
// --------------------------------- Run Loop ------------------------------
// This forms the start of the run loop
//
// Some useful debug watches:
//
// - Program counter: /* pc */ (uint8_t*)lpProgramCounter - (uint8_t*)vm->lpBytecode
// /* pc */ (uint8_t*)vm->stack->reg.lpProgramCounter - (uint8_t*)vm->lpBytecode
//
// - Frame height (in words): /* fh */ (uint16_t*)pStackPointer - (uint16_t*)pFrameBase
// /* fh */ (uint16_t*)vm->stack->reg.pStackPointer - (uint16_t*)vm->stack->reg.pFrameBase
//
// - Frame: /* frame */ (uint16_t*)pFrameBase,10
// /* frame */ (uint16_t*)vm->stack->reg.pFrameBase,10
//
// - Stack height (in words): /* sp */ (uint16_t*)pStackPointer - (uint16_t*)(vm->stack + 1)
// /* sp */ (uint16_t*)vm->stack->reg.pStackPointer - (uint16_t*)(vm->stack + 1)
//
// - Frame base (in words): /* bp */ (uint16_t*)pFrameBase - (uint16_t*)(vm->stack + 1)
// /* bp */ (uint16_t*)vm->stack->reg.pFrameBase - (uint16_t*)(vm->stack + 1)
//
// - Arg count: /* argc */ (uint8_t)vm->stack->reg.argCountAndFlags
// - First 4 arg values: /* args */ vm->stack->reg.pArgs,4
//
// Notes:
//
// - The value of VM_VALUE_UNDEFINED is 0x001
// - If a value is _odd_, interpret it as a bytecode address by dividing by 2
//
LBL_DO_NEXT_INSTRUCTION:
CODE_COVERAGE(59); // Hit
// This is not required for execution but is intended for diagnostics,
// required by mvm_getCurrentAddress.
// TODO: If MVM_INCLUDE_DEBUG_CAPABILITY is not included, maybe this shouldn't be here, and `mvm_getCurrentAddress` should also not be available.
reg->lpProgramCounter = lpProgramCounter;
// Check we're within range
#if MVM_DONT_TRUST_BYTECODE
if ((lpProgramCounter < minProgramCounter) || (lpProgramCounter >= maxProgramCounter)) {
VM_INVALID_BYTECODE(vm);
}
#endif
// Check breakpoints
#if MVM_INCLUDE_DEBUG_CAPABILITY
if (vm->pBreakpoints) {
TsBreakpoint* pBreakpoint = vm->pBreakpoints;
uint16_t currentBytecodeAddress = LongPtr_sub(lpProgramCounter, vm->lpBytecode);
do {
if (pBreakpoint->bytecodeAddress == currentBytecodeAddress) {
FLUSH_REGISTER_CACHE();
mvm_TfBreakpointCallback breakpointCallback = vm->breakpointCallback;
if (breakpointCallback)
breakpointCallback(vm, currentBytecodeAddress);
CACHE_REGISTERS();
break;
}
pBreakpoint = pBreakpoint->next;
} while (pBreakpoint);
}
#endif // MVM_INCLUDE_DEBUG_CAPABILITY
// Instruction bytes are divided into two nibbles
READ_PGM_1(reg3);
reg1 = reg3 & 0xF;
reg3 = reg3 >> 4;
if (reg3 >= VM_OP_DIVIDER_1) {
CODE_COVERAGE(428); // Hit
reg2 = POP();
} else {
CODE_COVERAGE(429); // Hit
}
VM_ASSERT(vm, reg3 < VM_OP_END);
MVM_SWITCH(reg3, (VM_OP_END - 1)) {
/* ------------------------------------------------------------------------- */
/* VM_OP_LOAD_SMALL_LITERAL */
/* Expects: */
/* reg1: small literal ID */
/* ------------------------------------------------------------------------- */
MVM_CASE(VM_OP_LOAD_SMALL_LITERAL): {
CODE_COVERAGE(60); // Hit
TABLE_COVERAGE(reg1, smallLiteralsSize, 448); // Hit 11/12
#if MVM_DONT_TRUST_BYTECODE
if (reg1 >= smallLiteralsSize) {
err = vm_newError(vm, MVM_E_INVALID_BYTECODE);
goto LBL_EXIT;
}
#endif
reg1 = smallLiterals[reg1];
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_LOAD_VAR_1 */
/* Expects: */
/* reg1: variable index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_LOAD_VAR_1):
CODE_COVERAGE(61); // Hit
LBL_OP_LOAD_VAR:
reg1 = pStackPointer[-reg1 - 1];
if (reg1 == VM_VALUE_DELETED) {
err = vm_newError(vm, MVM_E_TDZ_ERROR);
goto LBL_EXIT;
}
goto LBL_TAIL_POP_0_PUSH_REG1;
/* ------------------------------------------------------------------------- */
/* VM_OP_LOAD_SCOPED_1 */
/* Expects: */
/* reg1: variable index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_LOAD_SCOPED_1):
CODE_COVERAGE(62); // Hit
LongPtr lpVar;
LBL_OP_LOAD_SCOPED:
lpVar = vm_findScopedVariable(vm, reg1);
reg1 = LongPtr_read2_aligned(lpVar);
goto LBL_TAIL_POP_0_PUSH_REG1;
/* ------------------------------------------------------------------------- */
/* VM_OP_LOAD_ARG_1 */
/* Expects: */
/* reg1: argument index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_LOAD_ARG_1):
CODE_COVERAGE(63); // Hit
goto LBL_OP_LOAD_ARG;
/* ------------------------------------------------------------------------- */
/* VM_OP_CALL_1 */
/* Expects: */
/* reg1: index into short-call table */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_CALL_1): {
CODE_COVERAGE_UNTESTED(66); // Not hit
goto LBL_CALL_SHORT;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_FIXED_ARRAY_NEW_1 */
/* Expects: */
/* reg1: length of new fixed-length-array */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_FIXED_ARRAY_NEW_1): {
CODE_COVERAGE_UNTESTED(134); // Not hit
goto LBL_FIXED_ARRAY_NEW;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_EXTENDED_1 */
/* Expects: */
/* reg1: vm_TeOpcodeEx1 */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_EXTENDED_1):
CODE_COVERAGE(69); // Hit
goto LBL_OP_EXTENDED_1;
/* ------------------------------------------------------------------------- */
/* VM_OP_EXTENDED_2 */
/* Expects: */
/* reg1: vm_TeOpcodeEx2 */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_EXTENDED_2):
CODE_COVERAGE(70); // Hit
goto LBL_OP_EXTENDED_2;
/* ------------------------------------------------------------------------- */
/* VM_OP_EXTENDED_3 */
/* Expects: */
/* reg1: vm_TeOpcodeEx3 */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_EXTENDED_3):
CODE_COVERAGE(71); // Hit
goto LBL_OP_EXTENDED_3;
/* ------------------------------------------------------------------------- */
/* VM_OP_CALL_5 */
/* Expects: */
/* reg1: argCount */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_CALL_5): {
CODE_COVERAGE_UNTESTED(72); // Not hit
// Uses 16 bit literal for function offset
READ_PGM_2(reg2);
reg3 /* scope */ = VM_VALUE_UNDEFINED;
goto LBL_CALL_BYTECODE_FUNC;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_STORE_VAR_1 */
/* Expects: */
/* reg1: variable index relative to stack pointer */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_STORE_VAR_1): {
CODE_COVERAGE(73); // Hit
LBL_OP_STORE_VAR:
// Note: the value to store has already been popped off the stack at this
// point. The index 0 refers to the slot currently at the top of the
// stack.
pStackPointer[-reg1 - 1] = reg2;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_STORE_SCOPED_1 */
/* Expects: */
/* reg1: variable index */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_STORE_SCOPED_1): {
CODE_COVERAGE(74); // Hit
LongPtr lpVar;
LBL_OP_STORE_SCOPED:
lpVar = vm_findScopedVariable(vm, reg1);
Value* pVar = (Value*)LongPtr_truncate(lpVar);
// It would be an illegal operation to write to a closure variable stored in ROM
VM_BYTECODE_ASSERT(vm, lpVar == LongPtr_new(pVar));
*pVar = reg2;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_ARRAY_GET_1 */
/* Expects: */
/* reg1: item index (4-bit) */
/* reg2: reference to array */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_ARRAY_GET_1): {
CODE_COVERAGE_UNTESTED(75); // Not hit
// I think it makes sense for this instruction only to be an optimization for fixed-length arrays
VM_ASSERT(vm, deepTypeOf(vm, reg2) == TC_REF_FIXED_LENGTH_ARRAY);
regLP1 = DynamicPtr_decode_long(vm, reg2);
// These indexes should be compiler-generated, so they should never be out of range
VM_ASSERT(vm, reg1 < (vm_getAllocationSize_long(regLP1) >> 1));
regLP1 = LongPtr_add(regLP1, reg2 << 1);
reg1 = LongPtr_read2_aligned(regLP1);
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_ARRAY_SET_1 */
/* Expects: */
/* reg1: item index (4-bit) */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_ARRAY_SET_1): {
CODE_COVERAGE_UNTESTED(76); // Not hit
reg2 = POP(); // array reference
// I think it makes sense for this instruction only to be an optimization for fixed-length arrays
VM_ASSERT(vm, deepTypeOf(vm, reg3) == TC_REF_FIXED_LENGTH_ARRAY);
// We can only write to it if it's in RAM, so it must be a short-pointer
regP1 = (Value*)ShortPtr_decode(vm, reg3);
// These indexes should be compiler-generated, so they should never be out of range
VM_ASSERT(vm, reg1 < (vm_getAllocationSize(regP1) >> 1));
regP1[reg1] = reg2;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP_NUM_OP */
/* Expects: */
/* reg1: vm_TeNumberOp */
/* reg2: first popped operand */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_NUM_OP): {
CODE_COVERAGE(77); // Hit
goto LBL_OP_NUM_OP;
} // End of case VM_OP_NUM_OP
/* ------------------------------------------------------------------------- */
/* VM_OP_BIT_OP */
/* Expects: */
/* reg1: vm_TeBitwiseOp */
/* reg2: first popped operand */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP_BIT_OP): {
CODE_COVERAGE(92); // Hit
goto LBL_OP_BIT_OP;
}
} // End of primary switch
// All cases should loop explicitly back
VM_ASSERT_UNREACHABLE(vm);
/* ------------------------------------------------------------------------- */
/* LBL_OP_LOAD_ARG */
/* Expects: */
/* reg1: argument index */
/* ------------------------------------------------------------------------- */
LBL_OP_LOAD_ARG: {
CODE_COVERAGE(32); // Hit
reg2 /* argCountAndFlags */ = reg->argCountAndFlags;
if (reg1 /* argIndex */ < (uint8_t)reg2 /* argCount */) {
CODE_COVERAGE(64); // Hit
reg1 /* result */ = reg->pArgs[reg1 /* argIndex */];
} else {
CODE_COVERAGE_UNTESTED(65); // Not hit
reg1 = VM_VALUE_UNDEFINED;
}
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* LBL_CALL_SHORT */
/* Expects: */
/* reg1: index into short-call table */
/* ------------------------------------------------------------------------- */
LBL_CALL_SHORT: {
CODE_COVERAGE_UNTESTED(173); // Not hit
LongPtr lpShortCallTable = getBytecodeSection(vm, BCS_SHORT_CALL_TABLE, NULL);
LongPtr lpShortCallTableEntry = LongPtr_add(lpShortCallTable, reg1 * sizeof (vm_TsShortCallTableEntry));
#if MVM_SAFE_MODE
LongPtr lpShortCallTableEnd;
getBytecodeSection(vm, BCS_SHORT_CALL_TABLE, &lpShortCallTableEnd);
VM_ASSERT(vm, lpShortCallTableEntry < lpShortCallTableEnd);
#endif
reg2 /* target */ = LongPtr_read2_aligned(lpShortCallTableEntry);
lpShortCallTableEntry = LongPtr_add(lpShortCallTableEntry, 2);
// Note: reg1 holds the new argCountAndFlags, but the flags are zero in this situation
reg1 /* argCountAndFlags */ = LongPtr_read1(lpShortCallTableEntry);
reg3 /* scope */ = VM_VALUE_UNDEFINED;
// The high bit of function indicates if this is a call to the host
bool isHostCall = reg2 & 1;
if (isHostCall) {
CODE_COVERAGE_UNTESTED(67); // Not hit
goto LBL_CALL_HOST_COMMON;
} else {
CODE_COVERAGE_UNTESTED(68); // Not hit
reg2 >>= 1;
goto LBL_CALL_BYTECODE_FUNC;
}
} // LBL_CALL_SHORT
/* ------------------------------------------------------------------------- */
/* LBL_OP_BIT_OP */
/* Expects: */
/* reg1: vm_TeBitwiseOp */
/* reg2: first popped operand */
/* ------------------------------------------------------------------------- */
LBL_OP_BIT_OP: {
int32_t reg1I = 0;
int32_t reg2I = 0;
int8_t reg2B = 0;
reg3 = reg1;
// Convert second operand to an int32
reg2I = mvm_toInt32(vm, reg2);
// If it's a binary operator, then we pop a second operand
if (reg3 < VM_BIT_OP_DIVIDER_2) {
CODE_COVERAGE(117); // Hit
reg1 = POP();
reg1I = mvm_toInt32(vm, reg1);
// If we're doing a shift operation, the operand is in the 0-32 range
if (reg3 < VM_BIT_OP_END_OF_SHIFT_OPERATORS) {
reg2B = reg2I & 0x1F;
}
} else {
CODE_COVERAGE(118); // Hit
}
VM_ASSERT(vm, reg3 < VM_BIT_OP_END);
MVM_SWITCH (reg3, (VM_BIT_OP_END - 1)) {
MVM_CASE(VM_BIT_OP_SHR_ARITHMETIC): {
CODE_COVERAGE(93); // Hit
reg1I = reg1I >> reg2B;
break;
}
MVM_CASE(VM_BIT_OP_SHR_LOGICAL): {
CODE_COVERAGE(94); // Hit
// Cast the number to unsigned int so that the C interprets the shift
// as unsigned/logical rather than signed/arithmetic.
reg1I = (int32_t)((uint32_t)reg1I >> reg2B);
#if MVM_SUPPORT_FLOAT && MVM_PORT_INT32_OVERFLOW_CHECKS
// This is a rather annoying edge case if you ask me, since all
// other bitwise operations yield signed int32 results every time.
// If the shift is by exactly zero units, then negative numbers
// become positive and overflow the signed-32 bit type. Since we
// don't have an unsigned 32 bit type, this means they need to be
// extended to floats.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Signed_32-bit_integers
if ((reg2B == 0) & (reg1I < 0)) {
FLUSH_REGISTER_CACHE();
reg1 = mvm_newNumber(vm, (MVM_FLOAT64)((uint32_t)reg1I));
CACHE_REGISTERS();
goto LBL_TAIL_POP_0_PUSH_REG1;
}
#endif // MVM_PORT_INT32_OVERFLOW_CHECKS
break;
}
MVM_CASE(VM_BIT_OP_SHL): {
CODE_COVERAGE(95); // Hit
reg1I = reg1I << reg2B;
break;
}
MVM_CASE(VM_BIT_OP_OR): {
CODE_COVERAGE(96); // Hit
reg1I = reg1I | reg2I;
break;
}
MVM_CASE(VM_BIT_OP_AND): {
CODE_COVERAGE(97); // Hit
reg1I = reg1I & reg2I;
break;
}
MVM_CASE(VM_BIT_OP_XOR): {
CODE_COVERAGE(98); // Hit
reg1I = reg1I ^ reg2I;
break;
}
MVM_CASE(VM_BIT_OP_NOT): {
CODE_COVERAGE(99); // Hit
reg1I = ~reg2I;
break;
}
}
CODE_COVERAGE(101); // Hit
// Convert the result from a 32-bit integer
if ((reg1I >= VM_MIN_INT14) && (reg1I <= VM_MAX_INT14)) {
CODE_COVERAGE(34); // Hit
reg1 = VirtualInt14_encode(vm, (uint16_t)reg1I);
} else {
CODE_COVERAGE(35); // Hit
FLUSH_REGISTER_CACHE();
reg1 = mvm_newInt32(vm, reg1I);
CACHE_REGISTERS();
}
goto LBL_TAIL_POP_0_PUSH_REG1;
} // End of LBL_OP_BIT_OP
/* ------------------------------------------------------------------------- */
/* LBL_OP_EXTENDED_1 */
/* Expects: */
/* reg1: vm_TeOpcodeEx1 */
/* ------------------------------------------------------------------------- */
LBL_OP_EXTENDED_1: {
CODE_COVERAGE(102); // Hit
reg3 = reg1;
VM_ASSERT(vm, reg3 <= VM_OP1_END);
MVM_SWITCH (reg3, VM_OP1_END - 1) {
/* ------------------------------------------------------------------------- */
/* VM_OP1_RETURN_x */
/* Expects: */
/* reg1: vm_TeOpcodeEx1 */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_RETURN): {
CODE_COVERAGE(107); // Hit
reg1 = POP();
goto LBL_RETURN;
}
MVM_CASE (VM_OP1_THROW): {
CODE_COVERAGE(106); // Hit
reg1 = POP(); // The exception value
// Find the closest catch block
reg2 = reg->catchTarget;
// If none, it's an uncaught exception
if (reg2 == VM_VALUE_UNDEFINED) {
CODE_COVERAGE(208); // Hit
*out_result = reg1;
err = MVM_E_UNCAUGHT_EXCEPTION;
goto LBL_EXIT;
} else {
CODE_COVERAGE(209); // Hit
}
VM_ASSERT(vm, ((intptr_t)reg2 & 1) == 1);
// Unwind the stack. regP1 is the stack pointer address we want to land up at
regP1 = (uint16_t*)(((intptr_t)getBottomOfStack(vm->stack) + (intptr_t)reg2) & ~1);
VM_ASSERT(vm, pStackPointer >= getBottomOfStack(vm->stack));
VM_ASSERT(vm, pStackPointer < getTopOfStackSpace(vm->stack));
while (pFrameBase > regP1) {
CODE_COVERAGE(211); // Hit
// Near the beginning of mvm_call, we set `catchTarget` to undefined
// (and then restore at the end), which should direct exceptions through
// the path of "uncaught exception" above, so no frame here should ever
// be a host frame.
VM_ASSERT(vm, !(reg->argCountAndFlags & AF_CALLED_FROM_HOST));
// In the current frame structure, the size of the preceding frame is
// saved 4 words ahead of the frame base
pStackPointer = pFrameBase;
POP_REGISTERS();
}
pStackPointer = regP1;
// The next catch target is the outer one
reg->catchTarget = pStackPointer[0];
// Jump to the catch block
reg2 = pStackPointer[1];
VM_ASSERT(vm, (reg2 & 1) == 1);
lpProgramCounter = LongPtr_add(vm->lpBytecode, reg2 & ~1);
// Push the exception to the stack for the catch block to use
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_CLOSURE_NEW */
/* Expects: */
/* reg3: vm_TeOpcodeEx1 */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_CLOSURE_NEW): {
CODE_COVERAGE(599); // Hit
FLUSH_REGISTER_CACHE();
TsClosure* pClosure = gc_allocateWithHeader(vm, sizeof (TsClosure), TC_REF_CLOSURE);
CACHE_REGISTERS();
pClosure->scope = reg->scope; // Capture the current scope
pClosure->target = POP();
reg1 = ShortPtr_encode(vm, pClosure);
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_NEW */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_NEW): {
CODE_COVERAGE(347); // Hit
READ_PGM_1(reg1); // arg count
regP1 = &pStackPointer[-reg1 - 1]; // Pointer to class
reg1 /*argCountAndFlags*/ |= AF_PUSHED_FUNCTION;
reg2 /*class*/ = regP1[0];
// Can only `new` classes in Microvium
if (deepTypeOf(vm, reg2) != TC_REF_CLASS) {
err = MVM_E_USING_NEW_ON_NON_CLASS;
goto LBL_EXIT;
}
regLP1 = DynamicPtr_decode_long(vm, reg2);
// Note: using the stack as a temporary store because things can shift
// during a GC collection and we these temporaries to be GC-visible. It's
// safe to trash these particular slots. The regP1[1] slot holds the
// `this` value passed by the caller, which will always be undefined
// because `new` doesn't allows passing a `this`, and `regP1[0]` holds the
// class, which we've already read.
regP1[1] /*props*/ = READ_FIELD_2(regLP1, TsClass, staticProps);
regP1[0] /*func*/ = READ_FIELD_2(regLP1, TsClass, constructorFunc);
// Using the stack just to root this in the GC graph
PUSH(getBuiltin(vm, BIN_STR_PROTOTYPE));
// We've already checked that the target of the `new` operation is a
// class. A class cannot existed without a `prototype` property. If the
// class was created at compile time, the "prototype" string will be
// embedded in the bytecode because the class definition uses it. If the
// class was created at runtime, the "prototype" string will *also* be
// embedded in the bytecode because classes at runtime are only created by
// sequences of instructions that also includes reference to the
// "prototype" string. So either way, the fact that we're at this point in
// the code means that the "prototype" string must exist as a builtin.
VM_ASSERT(vm, pStackPointer[-1] != VM_VALUE_UNDEFINED);
FLUSH_REGISTER_CACHE();
TsPropertyList* pObject = GC_ALLOCATE_TYPE(vm, TsPropertyList, TC_REF_PROPERTY_LIST);
pObject->dpNext = VM_VALUE_NULL;
getProperty(vm, &regP1[1], &pStackPointer[-1], &pObject->dpProto);
TeTypeCode tc = deepTypeOf(vm, pObject->dpProto);
if ((tc != TC_REF_PROPERTY_LIST) && (tc != TC_REF_CLASS) && (tc != TC_REF_ARRAY)) {
pObject->dpProto = VM_VALUE_NULL;
}
CACHE_REGISTERS();
POP(); // BIN_STR_PROTOTYPE
if (err != MVM_E_SUCCESS) goto LBL_EXIT;
// The first argument is the `this` value
regP1[1] = ShortPtr_encode(vm, pObject);
reg2 = regP1[0];
goto LBL_CALL;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_SCOPE_PUSH */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_SCOPE_PUSH): {
CODE_COVERAGE(605); // Hit
READ_PGM_1(reg1); // Scope variable count
reg2 = (reg1 + 1) * 2; // Scope array size, including 1 slot for parent reference
FLUSH_REGISTER_CACHE();
uint16_t* newScope = gc_allocateWithHeader(vm, reg2, TC_REF_FIXED_LENGTH_ARRAY);
CACHE_REGISTERS();
uint16_t* p = newScope;
*p++ = reg->scope; // Reference to parent
while (reg1--)
*p++ = VM_VALUE_UNDEFINED; // Initial variable values
// Add to the scope chain
reg->scope = ShortPtr_encode(vm, newScope);
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_TYPE_CODE_OF */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_TYPE_CODE_OF): {
CODE_COVERAGE_UNTESTED(607); // Not hit
reg1 = POP();
reg1 = mvm_typeOf(vm, reg1);
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_POP */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_POP): {
CODE_COVERAGE(138); // Hit
pStackPointer--;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_TYPEOF */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_TYPEOF): {
CODE_COVERAGE(167); // Hit
// TODO: This is should really be done using some kind of built-in helper
// function, but we don't support those yet. The trouble with this
// implementation is that it's doing a string allocation every time. Also
// the new string is not an interned string so it's expensive to compare
// `typeof x === y`. Basically this is just a stop-gap.
reg1 = mvm_typeOf(vm, pStackPointer[-1]);
VM_ASSERT(vm, reg1 < sizeof typeStringOffsetByType);
reg1 = typeStringOffsetByType[reg1];
VM_ASSERT(vm, reg1 < sizeof(TYPE_STRINGS) - 1);
const char* str = &TYPE_STRINGS[reg1];
FLUSH_REGISTER_CACHE();
reg1 = vm_newStringFromCStrNT(vm, str);
CACHE_REGISTERS();
goto LBL_TAIL_POP_1_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_OBJECT_NEW */
/* Expects: */
/* (nothing) */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_OBJECT_NEW): {
CODE_COVERAGE(112); // Hit
FLUSH_REGISTER_CACHE();
TsPropertyList* pObject = GC_ALLOCATE_TYPE(vm, TsPropertyList, TC_REF_PROPERTY_LIST);
CACHE_REGISTERS();
reg1 = ShortPtr_encode(vm, pObject);
pObject->dpNext = VM_VALUE_NULL;
pObject->dpProto = VM_VALUE_NULL;
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_LOGICAL_NOT */
/* Expects: */
/* (nothing) */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_LOGICAL_NOT): {
CODE_COVERAGE(113); // Hit
reg2 = POP(); // value to negate
reg1 = mvm_toBool(vm, reg2) ? VM_VALUE_FALSE : VM_VALUE_TRUE;
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_OBJECT_GET_1 */
/* Expects: */
/* reg1: objectValue */
/* reg2: propertyName */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_OBJECT_GET_1): {
CODE_COVERAGE(114); // Hit
FLUSH_REGISTER_CACHE();
err = getProperty(vm, pStackPointer - 2, pStackPointer - 1, pStackPointer - 2);
CACHE_REGISTERS();
if (err != MVM_E_SUCCESS) goto LBL_EXIT;
goto LBL_TAIL_POP_1_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_ADD */
/* Expects: */
/* reg1: left operand */
/* reg2: right operand */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_ADD): {
CODE_COVERAGE(115); // Hit
reg1 = pStackPointer[-2];
reg2 = pStackPointer[-1];
// Special case for adding unsigned 12 bit numbers, for example in most
// loops. 12 bit unsigned addition does not require any overflow checks
if (Value_isVirtualUInt12(reg1) && Value_isVirtualUInt12(reg2)) {
CODE_COVERAGE(116); // Hit
reg1 = reg1 + reg2 - VirtualInt14_encode(vm, 0);
goto LBL_TAIL_POP_2_PUSH_REG1;
} else {
CODE_COVERAGE(119); // Hit
}
if (vm_isString(vm, reg1) || vm_isString(vm, reg2)) {
CODE_COVERAGE(120); // Hit
FLUSH_REGISTER_CACHE();
// Note: the intermediate values are saved back to the stack so that
// they're preserved if there is a GC collection. Even these conversions
// can trigger a GC collection
pStackPointer[-2] = vm_convertToString(vm, pStackPointer[-2]);
pStackPointer[-1] = vm_convertToString(vm, pStackPointer[-1]);
reg1 = vm_concat(vm, &pStackPointer[-2], &pStackPointer[-1]);
CACHE_REGISTERS();
goto LBL_TAIL_POP_2_PUSH_REG1;
} else {
CODE_COVERAGE(121); // Hit
// Interpret like any of the other numeric operations
// TODO: If VM_NUM_OP_ADD_NUM might cause a GC collection, then we shouldn't be popping here
POP();
reg1 = VM_NUM_OP_ADD_NUM;
goto LBL_OP_NUM_OP;
}
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_EQUAL */
/* Expects: */
/* reg1: left operand */
/* reg2: right operand */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_EQUAL): {
CODE_COVERAGE(122); // Hit
// TODO: This popping should be done on the egress rather than the ingress
reg2 = POP();
reg1 = POP();
FLUSH_REGISTER_CACHE();
bool eq = mvm_equal(vm, reg1, reg2);
CACHE_REGISTERS();
if (eq) {
CODE_COVERAGE(483); // Hit
reg1 = VM_VALUE_TRUE;
} else {
CODE_COVERAGE(484); // Hit
reg1 = VM_VALUE_FALSE;
}
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_NOT_EQUAL */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_NOT_EQUAL): {
reg1 = pStackPointer[-2];
reg2 = pStackPointer[-1];
// TODO: there seem to be so many places where we have to flush the
// register cache, that I'm wondering if it's actually a net benefit. It
// would be worth doing an experiment to see if the code size is smaller
// without the register cache. Also, is it strictly necessary to flush all
// the registers or can we maybe define a lightweight flush that just
// flushes the stack pointer?
FLUSH_REGISTER_CACHE();
bool eq = mvm_equal(vm, reg1, reg2);
CACHE_REGISTERS();
if(eq) {
CODE_COVERAGE(123); // Hit
reg1 = VM_VALUE_FALSE;
} else {
CODE_COVERAGE(485); // Hit
reg1 = VM_VALUE_TRUE;
}
goto LBL_TAIL_POP_2_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_OBJECT_SET_1 */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP1_OBJECT_SET_1): {
CODE_COVERAGE(124); // Hit
FLUSH_REGISTER_CACHE();
err = setProperty(vm, pStackPointer - 3);
CACHE_REGISTERS();
if (err != MVM_E_SUCCESS) {
CODE_COVERAGE_UNTESTED(265); // Not hit
goto LBL_EXIT;
} else {
CODE_COVERAGE(322); // Hit
}
goto LBL_TAIL_POP_3_PUSH_0;
}
} // End of VM_OP_EXTENDED_1 switch
// All cases should jump to whatever tail they intend. Nothing should get here
VM_ASSERT_UNREACHABLE(vm);
} // End of LBL_OP_EXTENDED_1
/* ------------------------------------------------------------------------- */
/* VM_OP_NUM_OP */
/* Expects: */
/* reg1: vm_TeNumberOp */
/* reg2: first popped operand */
/* ------------------------------------------------------------------------- */
LBL_OP_NUM_OP: {
CODE_COVERAGE(25); // Hit
int32_t reg1I = 0;
int32_t reg2I = 0;
reg3 = reg1;
// If it's a binary operator, then we pop a second operand
if (reg3 < VM_NUM_OP_DIVIDER) {
CODE_COVERAGE(440); // Hit
reg1 = POP();
if (toInt32Internal(vm, reg1, &reg1I) != MVM_E_SUCCESS) {
CODE_COVERAGE(444); // Hit
#if MVM_SUPPORT_FLOAT
goto LBL_NUM_OP_FLOAT64;
#endif // MVM_SUPPORT_FLOAT
} else {
CODE_COVERAGE(445); // Hit
}
} else {
CODE_COVERAGE(441); // Hit
reg1 = 0;
}
// Convert second operand to a int32 (or the only operand if it's a unary op)
if (toInt32Internal(vm, reg2, &reg2I) != MVM_E_SUCCESS) {
CODE_COVERAGE(442); // Hit
// If we failed to convert to int32, then we need to process the operation as a float
#if MVM_SUPPORT_FLOAT
goto LBL_NUM_OP_FLOAT64;
#endif // MVM_SUPPORT_FLOAT
} else {
CODE_COVERAGE(443); // Hit
}
VM_ASSERT(vm, reg3 < VM_NUM_OP_END);
MVM_SWITCH (reg3, (VM_NUM_OP_END - 1)) {
MVM_CASE(VM_NUM_OP_LESS_THAN): {
CODE_COVERAGE(78); // Hit
reg1 = reg1I < reg2I;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_GREATER_THAN): {
CODE_COVERAGE(79); // Hit
reg1 = reg1I > reg2I;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_LESS_EQUAL): {
CODE_COVERAGE(80); // Hit
reg1 = reg1I <= reg2I;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_GREATER_EQUAL): {
CODE_COVERAGE(81); // Hit
reg1 = reg1I >= reg2I;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_ADD_NUM): {
CODE_COVERAGE(82); // Hit
#if MVM_SUPPORT_FLOAT && MVM_PORT_INT32_OVERFLOW_CHECKS
#if __has_builtin(__builtin_add_overflow)
if (__builtin_add_overflow(reg1I, reg2I, &reg1I)) {
goto LBL_NUM_OP_FLOAT64;
}
#else // No builtin overflow
int32_t result = reg1I + reg2I;
// Check overflow https://blog.regehr.org/archives/1139
if (((reg1I ^ result) & (reg2I ^ result)) < 0) goto LBL_NUM_OP_FLOAT64;
reg1I = result;
#endif // No builtin overflow
#else // No overflow checks
reg1I = reg1I + reg2I;
#endif
break;
}
MVM_CASE(VM_NUM_OP_SUBTRACT): {
CODE_COVERAGE(83); // Hit
#if MVM_SUPPORT_FLOAT && MVM_PORT_INT32_OVERFLOW_CHECKS
#if __has_builtin(__builtin_sub_overflow)
if (__builtin_sub_overflow(reg1I, reg2I, &reg1I)) {
goto LBL_NUM_OP_FLOAT64;
}
#else // No builtin overflow
reg2I = -reg2I;
int32_t result = reg1I + reg2I;
// Check overflow https://blog.regehr.org/archives/1139
if (((reg1I ^ result) & (reg2I ^ result)) < 0) goto LBL_NUM_OP_FLOAT64;
reg1I = result;
#endif // No builtin overflow
#else // No overflow checks
reg1I = reg1I - reg2I;
#endif
break;
}
MVM_CASE(VM_NUM_OP_MULTIPLY): {
CODE_COVERAGE(84); // Hit
#if MVM_SUPPORT_FLOAT && MVM_PORT_INT32_OVERFLOW_CHECKS
#if __has_builtin(__builtin_mul_overflow)
if (__builtin_mul_overflow(reg1I, reg2I, &reg1I)) {
goto LBL_NUM_OP_FLOAT64;
}
#else // No builtin overflow
// There isn't really an efficient way to determine multiplied
// overflow on embedded devices without accessing the hardware
// status registers. The fast shortcut here is to just assume that
// anything more than 14-bit multiplication could overflow a 32-bit
// integer.
if (Value_isVirtualInt14(reg1) && Value_isVirtualInt14(reg2)) {
reg1I = reg1I * reg2I;
} else {
goto LBL_NUM_OP_FLOAT64;
}
#endif // No builtin overflow
#else // No overflow checks
reg1I = reg1I * reg2I;
#endif
break;
}
MVM_CASE(VM_NUM_OP_DIVIDE): {
CODE_COVERAGE(85); // Hit
#if MVM_SUPPORT_FLOAT
// With division, we leave it up to the user to write code that
// performs integer division instead of floating point division, so
// this instruction is always the case where they're doing floating
// point division.
goto LBL_NUM_OP_FLOAT64;
#else // !MVM_SUPPORT_FLOAT
err = vm_newError(vm, MVM_E_OPERATION_REQUIRES_FLOAT_SUPPORT);
goto LBL_EXIT;
#endif
}
MVM_CASE(VM_NUM_OP_DIVIDE_AND_TRUNC): {
CODE_COVERAGE(86); // Hit
if (reg2I == 0) {
reg1I = 0;
break;
}
reg1I = reg1I / reg2I;
break;
}
MVM_CASE(VM_NUM_OP_REMAINDER): {
CODE_COVERAGE(87); // Hit
if (reg2I == 0) {
CODE_COVERAGE(26); // Hit
reg1 = VM_VALUE_NAN;
goto LBL_TAIL_POP_0_PUSH_REG1;
}
CODE_COVERAGE(90); // Hit
reg1I = reg1I % reg2I;
break;
}
MVM_CASE(VM_NUM_OP_POWER): {
CODE_COVERAGE(88); // Hit
#if MVM_SUPPORT_FLOAT
// Maybe in future we can we implement an integer version.
goto LBL_NUM_OP_FLOAT64;
#else // !MVM_SUPPORT_FLOAT
err = vm_newError(vm, MVM_E_OPERATION_REQUIRES_FLOAT_SUPPORT);
goto LBL_EXIT;
#endif
}
MVM_CASE(VM_NUM_OP_NEGATE): {
CODE_COVERAGE(89); // Hit
#if MVM_SUPPORT_FLOAT && MVM_PORT_INT32_OVERFLOW_CHECKS
// Note: Zero negates to negative zero, which is not representable as an int32
if ((reg2I == INT32_MIN) || (reg2I == 0)) goto LBL_NUM_OP_FLOAT64;
#endif
reg1I = -reg2I;
break;
}
MVM_CASE(VM_NUM_OP_UNARY_PLUS): {
reg1I = reg2I;
break;
}
} // End of switch vm_TeNumberOp for int32
// Convert the result from a 32-bit integer
if ((reg1I >= VM_MIN_INT14) && (reg1I <= VM_MAX_INT14)) {
CODE_COVERAGE(103); // Hit
reg1 = VirtualInt14_encode(vm, (uint16_t)reg1I);
} else {
CODE_COVERAGE(104); // Hit
FLUSH_REGISTER_CACHE();
reg1 = mvm_newInt32(vm, reg1I);
CACHE_REGISTERS();
}
goto LBL_TAIL_POP_0_PUSH_REG1;
} // End of case LBL_OP_NUM_OP
/* ------------------------------------------------------------------------- */
/* LBL_OP_EXTENDED_2 */
/* Expects: */
/* reg1: vm_TeOpcodeEx2 */
/* ------------------------------------------------------------------------- */
LBL_OP_EXTENDED_2: {
CODE_COVERAGE(127); // Hit
reg3 = reg1;
// All the ex-2 instructions have an 8-bit parameter. This is stored in
// reg1 for consistency with 4-bit and 16-bit literal modes
READ_PGM_1(reg1);
// Some operations pop an operand off the stack. This goes into reg2
if (reg3 < VM_OP2_DIVIDER_1) {
CODE_COVERAGE(128); // Hit
reg2 = POP();
} else {
CODE_COVERAGE(129); // Hit
}
VM_ASSERT(vm, reg3 < VM_OP2_END);
MVM_SWITCH (reg3, (VM_OP2_END - 1)) {
/* ------------------------------------------------------------------------- */
/* VM_OP2_BRANCH_1 */
/* Expects: */
/* reg1: signed 8-bit offset to branch to, encoded in 16-bit unsigned */
/* reg2: condition to branch on */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_BRANCH_1): {
CODE_COVERAGE(130); // Hit
SIGN_EXTEND_REG_1();
goto LBL_BRANCH_COMMON;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_STORE_ARG */
/* Expects: */
/* reg1: unsigned index of argument in which to store */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_STORE_ARG): {
CODE_COVERAGE_UNTESTED(131); // Not hit
#if MVM_DONT_TRUST_BYTECODE
// The ability to write to argument slots is intended as an optimization
// feature to elide the parameter variable slots and instead use the
// argument slots directly. But this only works if the optimizer can
// prove that unprovided parameters are never written to (or that all
// parameters are satisfied by arguments). If you don't trust the
// optimizer, it's possible the callee attempts to write to the
// caller-provided argument slots that don't exist.
if (reg1 >= (uint8_t)reg->argCountAndFlags) {
err = vm_newError(vm, MVM_E_INVALID_BYTECODE);
goto LBL_EXIT;
}
#endif
reg->pArgs[reg1] = reg2;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_STORE_SCOPED_2 */
/* Expects: */
/* reg1: unsigned index of global in which to store */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_STORE_SCOPED_2): {
CODE_COVERAGE(132); // Hit
goto LBL_OP_STORE_SCOPED;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_STORE_VAR_2 */
/* Expects: */
/* reg1: unsigned index of variable in which to store, relative to SP */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_STORE_VAR_2): {
CODE_COVERAGE_UNTESTED(133); // Not hit
goto LBL_OP_STORE_VAR;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_JUMP_1 */
/* Expects: */
/* reg1: signed 8-bit offset to branch to, encoded in 16-bit unsigned */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_JUMP_1): {
CODE_COVERAGE(136); // Hit
SIGN_EXTEND_REG_1();
goto LBL_JUMP_COMMON;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_CALL_HOST */
/* Expects: */
/* reg1: arg count */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_CALL_HOST): {
CODE_COVERAGE_UNTESTED(137); // Not hit
// TODO: Unit tests for the host calling itself etc.
// Put function index into reg2
READ_PGM_1(reg2);
// Note: reg1 is the argCount and also argCountAndFlags, because the flags
// are all zero in this case. In particular, the target is specified as an
// instruction literal, so `AF_PUSHED_FUNCTION` is false.
goto LBL_CALL_HOST_COMMON;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_CALL_3 */
/* Expects: */
/* reg1: arg count */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_CALL_3): {
CODE_COVERAGE(142); // Hit
reg1 /* argCountAndFlags */ |= AF_PUSHED_FUNCTION;
reg2 /* target */ = pStackPointer[-(int16_t)(uint8_t)reg1 - 1]; // The function was pushed before the arguments
goto LBL_CALL;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_CALL_6 */
/* Expects: */
/* reg1: index into short-call table */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_CALL_6): {
CODE_COVERAGE_UNTESTED(145); // Not hit
goto LBL_CALL_SHORT;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_LOAD_SCOPED_2 */
/* Expects: */
/* reg1: unsigned closure scoped variable index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_LOAD_SCOPED_2): {
CODE_COVERAGE(146); // Hit
goto LBL_OP_LOAD_SCOPED;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_LOAD_VAR_2 */
/* Expects: */
/* reg1: unsigned variable index relative to stack pointer */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_LOAD_VAR_2): {
CODE_COVERAGE_UNTESTED(147); // Not hit
goto LBL_OP_LOAD_VAR;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_LOAD_ARG_2 */
/* Expects: */
/* reg1: unsigned variable index relative to stack pointer */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_LOAD_ARG_2): {
CODE_COVERAGE_UNTESTED(148); // Not hit
VM_NOT_IMPLEMENTED(vm);
err = MVM_E_FATAL_ERROR_MUST_KILL_VM;
goto LBL_EXIT;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_EXTENDED_4 */
/* Expects: */
/* reg1: The Ex-4 instruction opcode */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_EXTENDED_4): {
CODE_COVERAGE(149); // Hit
goto LBL_OP_EXTENDED_4;
}
/* ------------------------------------------------------------------------- */
/* VM_OP2_ARRAY_NEW */
/* reg1: Array capacity */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_ARRAY_NEW): {
CODE_COVERAGE(100); // Hit
// Allocation size excluding header
uint16_t capacity = reg1;
TABLE_COVERAGE(capacity ? 1 : 0, 2, 371); // Hit 2/2
FLUSH_REGISTER_CACHE();
MVM_LOCAL(TsArray*, arr, GC_ALLOCATE_TYPE(vm, TsArray, TC_REF_ARRAY));
CACHE_REGISTERS();
reg1 = ShortPtr_encode(vm, MVM_GET_LOCAL(arr));
PUSH(reg1); // We need to push early to avoid the GC collecting it
MVM_GET_LOCAL(arr)->viLength = VirtualInt14_encode(vm, 0);
MVM_GET_LOCAL(arr)->dpData = VM_VALUE_NULL;
if (capacity) {
FLUSH_REGISTER_CACHE();
uint16_t* pData = gc_allocateWithHeader(vm, capacity * 2, TC_REF_FIXED_LENGTH_ARRAY);
CACHE_REGISTERS();
MVM_SET_LOCAL(arr, ShortPtr_decode(vm, pStackPointer[-1])); // arr may have moved during the collection
MVM_GET_LOCAL(arr)->dpData = ShortPtr_encode(vm, pData);
uint16_t* p = pData;
uint16_t n = capacity;
while (n--)
*p++ = VM_VALUE_DELETED;
}
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP1_FIXED_ARRAY_NEW_2 */
/* Expects: */
/* reg1: Fixed-array length (8-bit) */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP2_FIXED_ARRAY_NEW_2): {
CODE_COVERAGE_UNTESTED(135); // Not hit
goto LBL_FIXED_ARRAY_NEW;
}
} // End of vm_TeOpcodeEx2 switch
// All cases should jump to whatever tail they intend. Nothing should get here
VM_ASSERT_UNREACHABLE(vm);
} // End of LBL_OP_EXTENDED_2
/* ------------------------------------------------------------------------- */
/* LBL_FIXED_ARRAY_NEW */
/* Expects: */
/* reg1: length of fixed-array to create */
/* ------------------------------------------------------------------------- */
LBL_FIXED_ARRAY_NEW: {
FLUSH_REGISTER_CACHE();
uint16_t* arr = gc_allocateWithHeader(vm, reg1 * 2, TC_REF_FIXED_LENGTH_ARRAY);
CACHE_REGISTERS();
uint16_t* p = arr;
// Note: when reading a DELETED value from the array, it will read as
// `undefined`. When fixed-length arrays are used to hold closure values, the
// `DELETED` value can be used to represent the TDZ.
while (reg1--)
*p++ = VM_VALUE_DELETED;
reg1 = ShortPtr_encode(vm, arr);
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* LBL_OP_EXTENDED_3 */
/* Expects: */
/* reg1: vm_TeOpcodeEx3 */
/* ------------------------------------------------------------------------- */
LBL_OP_EXTENDED_3: {
CODE_COVERAGE(150); // Hit
reg3 = reg1;
// Most Ex-3 instructions have a 16-bit parameter
if (reg3 >= VM_OP3_DIVIDER_1) {
CODE_COVERAGE(603); // Hit
READ_PGM_2(reg1);
} else {
CODE_COVERAGE(606); // Hit
}
if (reg3 >= VM_OP3_DIVIDER_2) {
CODE_COVERAGE(151); // Hit
reg2 = POP();
} else {
CODE_COVERAGE(152); // Hit
}
VM_ASSERT(vm, reg3 < VM_OP3_END);
MVM_SWITCH (reg3, (VM_OP3_END - 1)) {
/* ------------------------------------------------------------------------- */
/* VM_OP3_POP_N */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_POP_N): {
CODE_COVERAGE(602); // Hit
READ_PGM_1(reg1);
while (reg1--)
(void)POP();
goto LBL_TAIL_POP_0_PUSH_0;
}
/* -------------------------------------------------------------------------*/
/* VM_OP3_SCOPE_POP */
/* Pops the top closure scope off the scope stack */
/* */
/* Expects: */
/* Nothing */
/* -------------------------------------------------------------------------*/
MVM_CASE (VM_OP3_SCOPE_POP): {
CODE_COVERAGE(634); // Hit
reg1 = reg->scope;
VM_ASSERT(vm, reg1 != VM_VALUE_UNDEFINED);
LongPtr lpArr = DynamicPtr_decode_long(vm, reg1);
#if MVM_SAFE_MODE
uint16_t headerWord = readAllocationHeaderWord_long(lpArr);
VM_ASSERT(vm, vm_getTypeCodeFromHeaderWord(headerWord) == TC_REF_FIXED_LENGTH_ARRAY);
uint16_t arrayLength = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord) / 2;
VM_ASSERT(vm, arrayLength >= 1);
#endif
reg1 = LongPtr_read2_aligned(lpArr);
reg->scope = reg1;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_SCOPE_CLONE */
/* */
/* Clones the top closure scope (which must exist) and sets it as the */
/* new scope */
/* */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_SCOPE_CLONE): {
CODE_COVERAGE(635); // Hit
VM_ASSERT(vm, reg->scope != VM_VALUE_UNDEFINED);
FLUSH_REGISTER_CACHE();
Value newScope = vm_cloneFixedLengthArray(vm, &reg->scope);
CACHE_REGISTERS();
reg->scope = newScope;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_JUMP_2 */
/* Expects: */
/* reg1: signed offset */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_JUMP_2): {
CODE_COVERAGE(153); // Hit
goto LBL_JUMP_COMMON;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_LOAD_LITERAL */
/* Expects: */
/* reg1: literal value */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_LOAD_LITERAL): {
CODE_COVERAGE(154); // Hit
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_LOAD_GLOBAL_3 */
/* Expects: */
/* reg1: global variable index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_LOAD_GLOBAL_3): {
CODE_COVERAGE(155); // Hit
reg1 = globals[reg1];
goto LBL_TAIL_POP_0_PUSH_REG1;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_LOAD_SCOPED_3 */
/* Expects: */
/* reg1: scoped variable index */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_LOAD_SCOPED_3): {
CODE_COVERAGE_UNTESTED(600); // Not hit
goto LBL_OP_LOAD_SCOPED;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_BRANCH_2 */
/* Expects: */
/* reg1: signed offset */
/* reg2: condition */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_BRANCH_2): {
CODE_COVERAGE(156); // Hit
goto LBL_BRANCH_COMMON;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_STORE_GLOBAL_3 */
/* Expects: */
/* reg1: global variable index */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_STORE_GLOBAL_3): {
CODE_COVERAGE(157); // Hit
globals[reg1] = reg2;
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_STORE_SCOPED_3 */
/* Expects: */
/* reg1: scoped variable index */
/* reg2: value to store */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_STORE_SCOPED_3): {
CODE_COVERAGE_UNTESTED(601); // Not hit
goto LBL_OP_STORE_SCOPED;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_OBJECT_GET_2 */
/* Expects: */
/* reg1: property key value */
/* reg2: object value */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_OBJECT_GET_2): {
CODE_COVERAGE_UNTESTED(158); // Not hit
VM_NOT_IMPLEMENTED(vm);
err = MVM_E_FATAL_ERROR_MUST_KILL_VM;
goto LBL_EXIT;
}
/* ------------------------------------------------------------------------- */
/* VM_OP3_OBJECT_SET_2 */
/* Expects: */
/* reg1: property key value */
/* reg2: value */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP3_OBJECT_SET_2): {
CODE_COVERAGE_UNTESTED(159); // Not hit
VM_NOT_IMPLEMENTED(vm);
err = MVM_E_FATAL_ERROR_MUST_KILL_VM;
goto LBL_EXIT;
}
} // End of vm_TeOpcodeEx3 switch
// All cases should jump to whatever tail they intend. Nothing should get here
VM_ASSERT_UNREACHABLE(vm);
} // End of LBL_OP_EXTENDED_3
/* ------------------------------------------------------------------------- */
/* VM_OP3_OBJECT_SET_2 */
/* Expects: */
/* reg1: The Ex-4 instruction opcode */
/* ------------------------------------------------------------------------- */
LBL_OP_EXTENDED_4: {
MVM_SWITCH(reg1, (VM_NUM_OP4_END - 1)) {
/* ------------------------------------------------------------------------- */
/* VM_OP4_START_TRY */
/* Expects: nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE(VM_OP4_START_TRY): {
CODE_COVERAGE(206); // Hit
// Capture the stack pointer value *before* pushing the catch target
reg1 = (uint16_t)((intptr_t)pStackPointer - (intptr_t)getBottomOfStack(vm->stack));
// Assert it didn't overflow
VM_ASSERT(vm, (intptr_t)reg1 == ((intptr_t)pStackPointer - (intptr_t)getBottomOfStack(vm->stack)));
// Set lower bit to `1` so that this value will be ignored by the GC. The
// GC doesn't look at the `catchTarget` register, but catch targets are
// pushed to the stack if there is a nested `try`, and the GC scans the
// stack.
VM_ASSERT(vm, (reg1 & 1) == 0); // Expect stack to be 2-byte aligned
reg1 |= 1;
// Location of previous catch target
PUSH(reg->catchTarget);
// Location to jump to if there's an exception
READ_PGM_2(reg2);
VM_ASSERT(vm, (reg2 & 1) == 1); // The compiler should add the 1 LSb so the GC ignores this value
PUSH(reg2);
reg->catchTarget = reg1;
goto LBL_TAIL_POP_0_PUSH_0;
} // End of VM_OP4_START_TRY
MVM_CASE(VM_OP4_END_TRY): {
CODE_COVERAGE(207); // Hit
// Note: EndTry can be invoked either at the normal ending of a `try`
// block, or during a `return` out of a try block. In the former case, the
// stack will already be at the level it was after the StartTry, but in
// the latter case the stack level could be anything since `return` won't
// go to the effort of popping intermediate variables off the stack.
regP1 = (uint16_t*)((intptr_t)getBottomOfStack(vm->stack) + (intptr_t)reg->catchTarget - 1);
VM_ASSERT(vm, ((intptr_t)regP1 & 1) == 0); // Expect to be 2-byte aligned
VM_ASSERT(vm, ((intptr_t)regP1 >= (intptr_t)pFrameBase)); // EndTry can only end a try within the current frame
pStackPointer = regP1; // Pop the stack
reg->catchTarget = pStackPointer[0];
goto LBL_TAIL_POP_0_PUSH_0;
} // End of VM_OP4_END_TRY
MVM_CASE(VM_OP4_OBJECT_KEYS): {
CODE_COVERAGE(223); // Hit
// Note: leave object on the stack in case a GC cycle is triggered by the array allocation
FLUSH_REGISTER_CACHE();
err = vm_objectKeys(vm, &pStackPointer[-1]);
// TODO: We could maybe eliminate the common CACHE_REGISTERS operation if
// the exit path checked the flag and cached for us.
CACHE_REGISTERS();
goto LBL_TAIL_POP_0_PUSH_0; // Pop the object and push the keys
} // End of VM_OP4_OBJECT_KEYS
MVM_CASE(VM_OP4_UINT8_ARRAY_NEW): {
CODE_COVERAGE(324); // Hit
FLUSH_REGISTER_CACHE();
err = vm_uint8ArrayNew(vm, &pStackPointer[-1]);
CACHE_REGISTERS();
goto LBL_TAIL_POP_0_PUSH_0;
} // End of VM_OP4_OBJECT_KEYS
/* ------------------------------------------------------------------------- */
/* VM_OP4_CLASS_CREATE */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP4_CLASS_CREATE): {
CODE_COVERAGE(614); // Hit
// TODO: I think we could save some flash space if we grouped all the
// opcodes together according to whether they flush the register cache.
// Also maybe they could be dispatched through a lookup table.
FLUSH_REGISTER_CACHE();
TsClass* pClass = gc_allocateWithHeader(vm, sizeof (TsClass), TC_REF_CLASS);
CACHE_REGISTERS();
pClass->constructorFunc = pStackPointer[-2];
pClass->staticProps = pStackPointer[-1];
pStackPointer[-2] = ShortPtr_encode(vm, pClass);
goto LBL_TAIL_POP_1_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* VM_OP4_TYPE_CODE_OF */
/* Expects: */
/* Nothing */
/* ------------------------------------------------------------------------- */
MVM_CASE (VM_OP4_TYPE_CODE_OF): {
CODE_COVERAGE(631); // Hit
reg1 = mvm_typeOf(vm, pStackPointer[-1]);
reg1 = VirtualInt14_encode(vm, reg1);
goto LBL_TAIL_POP_1_PUSH_REG1;
}
}
} // End of LBL_OP_EXTENDED_4
/* ------------------------------------------------------------------------- */
/* LBL_BRANCH_COMMON */
/* Expects: */
/* reg1: signed 16-bit amount to jump by if the condition is truthy */
/* reg2: condition to branch on */
/* ------------------------------------------------------------------------- */
LBL_BRANCH_COMMON: {
CODE_COVERAGE(160); // Hit
if (mvm_toBool(vm, reg2)) {
lpProgramCounter = LongPtr_add(lpProgramCounter, (int16_t)reg1);
}
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* LBL_JUMP_COMMON */
/* Expects: */
/* reg1: signed 16-bit amount to jump by */
/* ------------------------------------------------------------------------- */
LBL_JUMP_COMMON: {
CODE_COVERAGE(161); // Hit
lpProgramCounter = LongPtr_add(lpProgramCounter, (int16_t)reg1);
goto LBL_TAIL_POP_0_PUSH_0;
}
/* ------------------------------------------------------------------------- */
/* */
/* LBL_RETURN */
/* */
/* Return from the current frame */
/* */
/* Expects: */
/* reg1: the return value */
/* ------------------------------------------------------------------------- */
LBL_RETURN: {
CODE_COVERAGE(105); // Hit
// Pop variables
pStackPointer = pFrameBase;
// Save argCountAndFlags from this frame
reg3 = reg->argCountAndFlags;
// Restore caller state
POP_REGISTERS();
goto LBL_POP_ARGS;
}
/* ------------------------------------------------------------------------- */
/* */
/* LBL_POP_ARGS */
/* */
/* The second part of a "RETURN". Assumes that we're already in the */
/* caller stack frame by this point. */
/* */
/* Expects: */
/* reg1: returning result */
/* reg3: argCountAndFlags for callee frame */
/* ------------------------------------------------------------------------- */
LBL_POP_ARGS: {
// Pop arguments
pStackPointer -= (uint8_t)reg3;
// Pop function reference
if (reg3 & AF_PUSHED_FUNCTION) {
CODE_COVERAGE(108); // Hit
(void)POP();
} else {
CODE_COVERAGE(109); // Hit
}
// Called from the host?
if (reg3 & AF_CALLED_FROM_HOST) {
CODE_COVERAGE(221); // Hit
goto LBL_RETURN_TO_HOST;
} else {
CODE_COVERAGE(111); // Hit
goto LBL_TAIL_POP_0_PUSH_REG1;
}
}
/* ------------------------------------------------------------------------- */
/* */
/* LBL_RETURN_TO_HOST */
/* */
/* Return control to the host */
/* */
/* This is after popping the arguments */
/* */
/* Expects: */
/* reg1: the return value */
/* ------------------------------------------------------------------------- */
LBL_RETURN_TO_HOST: {
CODE_COVERAGE(110); // Hit
// Provide the return value to the host
if (out_result) {
*out_result = reg1;
}
goto LBL_EXIT;
}
/* ------------------------------------------------------------------------- */
/* */
/* LBL_CALL */
/* */
/* Performs a dynamic call to a given function value */
/* */
/* Expects: */
/* reg1: argCountAndFlags for the new frame */
/* reg2: target function value to call */
/* ------------------------------------------------------------------------- */
LBL_CALL: {
CODE_COVERAGE(224); // Hit
reg3 /* scope */ = VM_VALUE_UNDEFINED;
while (true) {
TeTypeCode tc = deepTypeOf(vm, reg2 /* target */);
if (tc == TC_REF_FUNCTION) {
CODE_COVERAGE(141); // Hit
// The following trick of assuming the function offset is just
// `target >>= 1` is only true if the function is in ROM.
VM_ASSERT(vm, DynamicPtr_isRomPtr(vm, reg2 /* target */));
reg2 &= 0xFFFE;
goto LBL_CALL_BYTECODE_FUNC;
} else if (tc == TC_REF_HOST_FUNC) {
CODE_COVERAGE(143); // Hit
LongPtr lpHostFunc = DynamicPtr_decode_long(vm, reg2 /* target */);
reg2 = READ_FIELD_2(lpHostFunc, TsHostFunc, indexInImportTable);
goto LBL_CALL_HOST_COMMON;
} else if (tc == TC_REF_CLOSURE) {
CODE_COVERAGE(598); // Hit
LongPtr lpClosure = DynamicPtr_decode_long(vm, reg2 /* target */);
reg2 /* target */ = READ_FIELD_2(lpClosure, TsClosure, target);
// Scope
reg3 /* scope */ = READ_FIELD_2(lpClosure, TsClosure, scope);
// Redirect the call to closure target
continue;
} else {
CODE_COVERAGE_UNTESTED(264); // Not hit
// Other value types are not callable
err = vm_newError(vm, MVM_E_TYPE_ERROR_TARGET_IS_NOT_CALLABLE);
goto LBL_EXIT;
}
}
}
/* ------------------------------------------------------------------------- */
/* LBL_CALL_HOST_COMMON */
/* Expects: */
/* reg1: argCountAndFlags */
/* reg2: index in import table */
/* ------------------------------------------------------------------------- */
LBL_CALL_HOST_COMMON: {
CODE_COVERAGE(162); // Hit
// Note: the interface with the host doesn't include the `this` pointer as the
// first argument, so `args` points to the *next* argument.
reg3 /* argCount */ = (uint8_t)reg1 - 1;
// Note: I'm not calling `FLUSH_REGISTER_CACHE` here, even though control is
// leaving the `run` function. One reason is that control is _also_ leaving
// the current function activation, and the local registers states have no use
// to the callee. The other reason is that it's "safer" and cheaper to keep
// the activation state local, rather than flushing it to the shared space
// `vm->stack->reg` where it could be trashed indirectly by the callee (see
// the earlier comment in this block).
//
// The only the exception to this is the stack pointer, which is obviously
// shared between the caller and callee, and the base pointer which is required
// if the host function triggers a garbage collection
reg->pStackPointer = pStackPointer;
reg->pFrameBase = pFrameBase;
VM_ASSERT(vm, reg2 < vm_getResolvedImportCount(vm));
mvm_TfHostFunction hostFunction = vm_getResolvedImports(vm)[reg2];
mvm_HostFunctionID hostFunctionID = vm_getHostFunctionId(vm, reg2);
Value result = VM_VALUE_UNDEFINED;
/*
Note: this subroutine does not call PUSH_REGISTERS to save the frame boundary.
Calls to the host can be thought of more like machine instructions than
distinct CALL operations in this sense, since they operate within the frame of
the caller.
This needs to work even if the host in turn calls the VM again during the call
out to the host. When the host calls the VM again, it will push a new stack
frame.
*/
#if (MVM_SAFE_MODE)
vm_TsRegisters regCopy = *reg;
// Saving the stack pointer here is "flushing the cache registers" since it's
// the only one we need to preserve.
reg->usingCachedRegisters = false;
#endif
regP1 /* pArgs */ = pStackPointer - reg3;
// Call the host function
err = hostFunction(vm, hostFunctionID, &result, regP1, (uint8_t)reg3);
if (err != MVM_E_SUCCESS) goto LBL_EXIT;
// The host function should not have left the stack unbalanced. A failure here
// is not really a problem with the host since the Microvium C API doesn't
// give the host access to the stack anyway.
VM_ASSERT(vm, pStackPointer == reg->pStackPointer);
VM_ASSERT(vm, pFrameBase == reg->pFrameBase);
#if (MVM_SAFE_MODE)
reg->usingCachedRegisters = true;
/*
The host function should leave the VM registers in the same state.
`pStackPointer` can be modified temporarily because the host may call back
into the VM, but it should be restored again by the time the host returns,
otherwise the stack is unbalanced.
The other registers (e.g. lpProgramCounter) should only be modified by
bytecode instructions, which can be if the host calls back into the VM. But
if the host calls back into the VM, it will go through LBL_CALL which
performs a PUSH_REGISTERS to save the previous machine state, and then will
restore the machine state when it returns.
This check is also what confirms that we don't need a FLUSH_REGISTER_CACHE
and CACHE_REGISTERS around the host call, since the host doesn't modify or
use these registers, even if it calls back into the VM (with the exception
of the stack pointer which is used but restored again afterward).
Why do I care about this optimization? In part because typical Microvium
scripts that I've seen tend to make a _lot_ of host calls, treating host
functions roughly like a special-purpose instruction set and the script
decides the sequence of instructions.
*/
VM_ASSERT(vm, memcmp(&regCopy, reg, sizeof regCopy) == 0);
#endif
reg3 = reg1; // Callee argCountAndFlags
reg1 = result;
goto LBL_POP_ARGS;
}
/* ------------------------------------------------------------------------- */
/* LBL_CALL_BYTECODE_FUNC */
/* */
/* Calls a bytecode function */
/* */
/* Expects: */
/* reg1: new argCountAndFlags */
/* reg2: offset of target function in bytecode */
/* reg3: scope, if reg1 & AF_SCOPE, else unused */
/* ------------------------------------------------------------------------- */
LBL_CALL_BYTECODE_FUNC: {
CODE_COVERAGE(163); // Hit
regP1 /* pArgs */ = pStackPointer - (uint8_t)reg1;
regLP1 /* lpReturnAddress */ = lpProgramCounter;
// Move PC to point to new function code
lpProgramCounter = LongPtr_add(vm->lpBytecode, reg2);
// Check the stack space required (before we PUSH_REGISTERS)
READ_PGM_1(reg2 /* requiredFrameSizeWords */);
reg2 /* requiredFrameSizeWords */ += VM_FRAME_BOUNDARY_SAVE_SIZE_WORDS;
err = vm_requireStackSpace(vm, pStackPointer, reg2 /* requiredFrameSizeWords */);
if (err != MVM_E_SUCCESS) {
CODE_COVERAGE_ERROR_PATH(226); // Not hit
goto LBL_EXIT;
}
// Save old registers to the stack
PUSH_REGISTERS(regLP1);
// Set up new frame
pFrameBase = pStackPointer;
reg->argCountAndFlags = reg1;
reg->scope = reg3;
reg->pArgs = regP1;
goto LBL_TAIL_POP_0_PUSH_0;
} // End of LBL_CALL_BYTECODE_FUNC
/* ------------------------------------------------------------------------- */
/* LBL_NUM_OP_FLOAT64 */
/* Expects: */
/* reg1: left operand (second pop), or zero for unary ops */
/* reg2: right operand (first pop), or single operand for unary ops */
/* reg3: vm_TeNumberOp */
/* ------------------------------------------------------------------------- */
#if MVM_SUPPORT_FLOAT
LBL_NUM_OP_FLOAT64: {
CODE_COVERAGE_UNIMPLEMENTED(447); // Hit
MVM_FLOAT64 reg1F = 0;
if (reg1) reg1F = mvm_toFloat64(vm, reg1);
MVM_FLOAT64 reg2F = mvm_toFloat64(vm, reg2);
VM_ASSERT(vm, reg3 < VM_NUM_OP_END);
MVM_SWITCH (reg3, (VM_NUM_OP_END - 1)) {
MVM_CASE(VM_NUM_OP_LESS_THAN): {
CODE_COVERAGE(449); // Hit
reg1 = reg1F < reg2F;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_GREATER_THAN): {
CODE_COVERAGE(450); // Hit
reg1 = reg1F > reg2F;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_LESS_EQUAL): {
CODE_COVERAGE(451); // Hit
reg1 = reg1F <= reg2F;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_GREATER_EQUAL): {
CODE_COVERAGE(452); // Hit
reg1 = reg1F >= reg2F;
goto LBL_TAIL_PUSH_REG1_BOOL;
}
MVM_CASE(VM_NUM_OP_ADD_NUM): {
CODE_COVERAGE(453); // Hit
reg1F = reg1F + reg2F;
break;
}
MVM_CASE(VM_NUM_OP_SUBTRACT): {
CODE_COVERAGE(454); // Hit
reg1F = reg1F - reg2F;
break;
}
MVM_CASE(VM_NUM_OP_MULTIPLY): {
CODE_COVERAGE(455); // Hit
reg1F = reg1F * reg2F;
break;
}
MVM_CASE(VM_NUM_OP_DIVIDE): {
CODE_COVERAGE(456); // Hit
reg1F = reg1F / reg2F;
break;
}
MVM_CASE(VM_NUM_OP_DIVIDE_AND_TRUNC): {
CODE_COVERAGE(457); // Hit
reg1F = mvm_float64ToInt32((reg1F / reg2F));
break;
}
MVM_CASE(VM_NUM_OP_REMAINDER): {
CODE_COVERAGE(458); // Hit
reg1F = fmod(reg1F, reg2F);
break;
}
MVM_CASE(VM_NUM_OP_POWER): {
CODE_COVERAGE(459); // Hit
if (!isfinite(reg2F) && ((reg1F == 1.0) || (reg1F == -1.0))) {
reg1 = VM_VALUE_NAN;
goto LBL_TAIL_POP_0_PUSH_REG1;
}
reg1F = pow(reg1F, reg2F);
break;
}
MVM_CASE(VM_NUM_OP_NEGATE): {
CODE_COVERAGE(460); // Hit
reg1F = -reg2F;
break;
}
MVM_CASE(VM_NUM_OP_UNARY_PLUS): {
CODE_COVERAGE(461); // Hit
reg1F = reg2F;
break;
}
} // End of switch vm_TeNumberOp for float64
// Convert the result from a float
FLUSH_REGISTER_CACHE();
reg1 = mvm_newNumber(vm, reg1F);
CACHE_REGISTERS();
goto LBL_TAIL_POP_0_PUSH_REG1;
} // End of LBL_NUM_OP_FLOAT64
#endif // MVM_SUPPORT_FLOAT
/* --------------------------------------------------------------------------
TAILS
These "tails" are the common epilogues to various instructions. Instructions in
general must keep their arguments on the stack right until the end, to prevent
any pointer arguments from becoming dangling if the instruction triggers a GC
collection. So popping the arguments is done at the end of the instruction, and
the number of pops is common to many different instructions.
* -------------------------------------------------------------------------- */
LBL_TAIL_PUSH_REG1_BOOL:
CODE_COVERAGE(489); // Hit
reg1 = reg1 ? VM_VALUE_TRUE : VM_VALUE_FALSE;
goto LBL_TAIL_POP_0_PUSH_REG1;
LBL_TAIL_POP_2_PUSH_REG1:
CODE_COVERAGE(227); // Hit
pStackPointer -= 1;
goto LBL_TAIL_POP_1_PUSH_REG1;
LBL_TAIL_POP_0_PUSH_REG1:
CODE_COVERAGE(164); // Hit
PUSH(reg1);
goto LBL_TAIL_POP_0_PUSH_0;
LBL_TAIL_POP_3_PUSH_0:
CODE_COVERAGE(611); // Hit
pStackPointer -= 3;
goto LBL_TAIL_POP_0_PUSH_0;
LBL_TAIL_POP_1_PUSH_0:
CODE_COVERAGE(617); // Hit
pStackPointer -= 1;
goto LBL_TAIL_POP_0_PUSH_0;
LBL_TAIL_POP_1_PUSH_REG1:
CODE_COVERAGE(126); // Hit
pStackPointer[-1] = reg1;
goto LBL_TAIL_POP_0_PUSH_0;
LBL_TAIL_POP_0_PUSH_0:
CODE_COVERAGE(125); // Hit
if (err != MVM_E_SUCCESS) goto LBL_EXIT;
goto LBL_DO_NEXT_INSTRUCTION;
LBL_EXIT:
CODE_COVERAGE(165); // Hit
#if MVM_SAFE_MODE
FLUSH_REGISTER_CACHE();
VM_ASSERT(vm, registerValuesAtEntry.pStackPointer <= reg->pStackPointer);
VM_ASSERT(vm, registerValuesAtEntry.pFrameBase <= reg->pFrameBase);
#endif
// I don't think there's anything that can happen during mvm_call that can
// justify the values of the registers at exit needing being different to
// those at entry. Restoring the entry registers here means that if we have an
// error or uncaught exception at any time during the call (including the case
// where it's within nested calls) then at least we unwind the stack and
// restore the original program counter, catchTarget, stackPointer etc.
// `registerValuesAtEntry` was also captured before we pushed the mvm_call
// arguments to the stack, so this also effectively pops the arguments off the
// stack.
*reg = registerValuesAtEntry;
// If the stack is empty, we can free it. It may not be empty if this is a
// reentrant call, in which case there would be other frames below this one.
if (reg->pStackPointer == getBottomOfStack(vm->stack)) {
CODE_COVERAGE(222); // Hit
vm_free(vm, vm->stack);
vm->stack = NULL;
}
return err;
} // End of mvm_call
const Value mvm_undefined = VM_VALUE_UNDEFINED;
const Value vm_null = VM_VALUE_NULL;
static inline uint16_t vm_getAllocationSize(void* pAllocation) {
CODE_COVERAGE(12); // Hit
return vm_getAllocationSizeExcludingHeaderFromHeaderWord(((uint16_t*)pAllocation)[-1]);
}
static inline uint16_t vm_getAllocationSize_long(LongPtr lpAllocation) {
CODE_COVERAGE(514); // Hit
uint16_t headerWord = LongPtr_read2_aligned(LongPtr_add(lpAllocation, -2));
return vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
}
static inline mvm_TeBytecodeSection vm_sectionAfter(VM* vm, mvm_TeBytecodeSection section) {
CODE_COVERAGE(13); // Hit
VM_ASSERT(vm, section < BCS_SECTION_COUNT - 1);
return (mvm_TeBytecodeSection)((uint8_t)section + 1);
}
static inline TeTypeCode vm_getTypeCodeFromHeaderWord(uint16_t headerWord) {
CODE_COVERAGE(1); // Hit
// The type code is in the high byte because it's the byte that occurs closest
// to the allocation itself, potentially allowing us in future to omit the
// size in the allocation header for some kinds of allocations.
return (TeTypeCode)(headerWord >> 12);
}
static inline uint16_t vm_makeHeaderWord(VM* vm, TeTypeCode tc, uint16_t size) {
CODE_COVERAGE(210); // Hit
VM_ASSERT(vm, size <= MAX_ALLOCATION_SIZE);
VM_ASSERT(vm, tc <= 0xF);
return ((tc << 12) | size);
}
static inline VirtualInt14 VirtualInt14_encode(VM* vm, int16_t i) {
CODE_COVERAGE(14); // Hit
VM_ASSERT(vm, (i >= VM_MIN_INT14) && (i <= VM_MAX_INT14));
return VIRTUAL_INT14_ENCODE(i);
}
static inline int16_t VirtualInt14_decode(VM* vm, VirtualInt14 viInt) {
CODE_COVERAGE(16); // Hit
VM_ASSERT(vm, Value_isVirtualInt14(viInt));
return (int16_t)viInt >> 2;
}
static void setHeaderWord(VM* vm, void* pAllocation, TeTypeCode tc, uint16_t size) {
CODE_COVERAGE(36); // Hit
((uint16_t*)pAllocation)[-1] = vm_makeHeaderWord(vm, tc, size);
}
// Returns the allocation size, excluding the header itself
static inline uint16_t vm_getAllocationSizeExcludingHeaderFromHeaderWord(uint16_t headerWord) {
CODE_COVERAGE(2); // Hit
// Note: The header size is measured in bytes and not words mainly to account
// for string allocations, which would be inconvenient to align to word
// boundaries.
return headerWord & 0xFFF;
}
#if MVM_SAFE_MODE
static bool Value_encodesBytecodeMappedPtr(Value value) {
CODE_COVERAGE(37); // Hit
return ((value & 3) == 1) && value >= VM_VALUE_WELLKNOWN_END;
}
#endif // MVM_SAFE_MODE
static inline uint16_t getSectionOffset(LongPtr lpBytecode, mvm_TeBytecodeSection section) {
CODE_COVERAGE(38); // Hit
LongPtr lpSection = LongPtr_add(lpBytecode, OFFSETOF(mvm_TsBytecodeHeader, sectionOffsets) + section * 2);
uint16_t offset = LongPtr_read2_aligned(lpSection);
return offset;
}
#if MVM_SAFE_MODE
static inline uint16_t vm_getResolvedImportCount(VM* vm) {
CODE_COVERAGE(41); // Hit
uint16_t importTableSize = getSectionSize(vm, BCS_IMPORT_TABLE);
uint16_t importCount = importTableSize / sizeof(vm_TsImportTableEntry);
return importCount;
}
#endif // MVM_SAFE_MODE
#if MVM_SAFE_MODE
/**
* Returns true if the value is a pointer which points to ROM. Null is not a
* value that points to ROM.
*/
static bool DynamicPtr_isRomPtr(VM* vm, DynamicPtr dp) {
CODE_COVERAGE(39); // Hit
VM_ASSERT(vm, !Value_isVirtualInt14(dp));
if (dp == VM_VALUE_NULL) {
CODE_COVERAGE_UNTESTED(47); // Not hit
return false;
}
if (Value_isShortPtr(dp)) {
CODE_COVERAGE_UNTESTED(52); // Not hit
return false;
}
CODE_COVERAGE(91); // Hit
VM_ASSERT(vm, Value_encodesBytecodeMappedPtr(dp));
VM_ASSERT(vm, vm_sectionAfter(vm, BCS_ROM) < BCS_SECTION_COUNT);
uint16_t offset = dp & 0xFFFE;
return (offset >= getSectionOffset(vm->lpBytecode, BCS_ROM))
& (offset < getSectionOffset(vm->lpBytecode, vm_sectionAfter(vm, BCS_ROM)));
}
#endif // MVM_SAFE_MODE
TeError mvm_restore(mvm_VM** result, MVM_LONG_PTR_TYPE lpBytecode, size_t bytecodeSize_, void* context, mvm_TfResolveImport resolveImport) {
// Note: these are declared here because some compilers give warnings when "goto" bypasses some variable declarations
mvm_TfHostFunction* resolvedImports;
uint16_t importTableOffset;
LongPtr lpImportTableStart;
LongPtr lpImportTableEnd;
mvm_TfHostFunction* resolvedImport;
LongPtr lpImportTableEntry;
uint16_t initialHeapOffset;
uint16_t initialHeapSize;
CODE_COVERAGE(3); // Hit
if (MVM_PORT_VERSION != MVM_EXPECTED_PORT_FILE_VERSION) {
return MVM_E_PORT_FILE_VERSION_MISMATCH;
}
#if MVM_SAFE_MODE
uint16_t x = 0x4243;
bool isLittleEndian = ((uint8_t*)&x)[0] == 0x43;
VM_ASSERT(NULL, isLittleEndian);
VM_ASSERT(NULL, sizeof (ShortPtr) == 2);
#endif
TeError err = MVM_E_SUCCESS;
VM* vm = NULL;
// Bytecode size field is located at the second word
if (bytecodeSize_ < sizeof (mvm_TsBytecodeHeader)) {
CODE_COVERAGE_ERROR_PATH(21); // Not hit
return MVM_E_INVALID_BYTECODE;
}
mvm_TsBytecodeHeader header;
memcpy_long(&header, lpBytecode, sizeof header);
// Note: the restore function takes an explicit bytecode size because there
// may be a size inherent to the medium from which the bytecode image comes,
// and we don't want to accidentally read past the end of this space just
// because the header apparently told us we could (since we could be reading a
// corrupt header).
uint16_t bytecodeSize = header.bytecodeSize;
if (bytecodeSize != bytecodeSize_) {
CODE_COVERAGE_ERROR_PATH(240); // Not hit
return MVM_E_INVALID_BYTECODE;
}
uint16_t expectedCRC = header.crc;
if (!MVM_CHECK_CRC16_CCITT(LongPtr_add(lpBytecode, 8), (uint16_t)bytecodeSize - 8, expectedCRC)) {
CODE_COVERAGE_ERROR_PATH(54); // Not hit
return MVM_E_BYTECODE_CRC_FAIL;
}
if (bytecodeSize < header.headerSize) {
CODE_COVERAGE_ERROR_PATH(241); // Not hit
return MVM_E_INVALID_BYTECODE;
}
if (header.bytecodeVersion != MVM_BYTECODE_VERSION) {
CODE_COVERAGE_ERROR_PATH(430); // Not hit
return MVM_E_WRONG_BYTECODE_VERSION;
}
if (MVM_ENGINE_VERSION < header.requiredEngineVersion) {
CODE_COVERAGE_ERROR_PATH(247); // Not hit
return MVM_E_REQUIRES_LATER_ENGINE;
}
uint32_t featureFlags = header.requiredFeatureFlags;;
if (MVM_SUPPORT_FLOAT && !(featureFlags & (1 << FF_FLOAT_SUPPORT))) {
CODE_COVERAGE_ERROR_PATH(180); // Not hit
return MVM_E_BYTECODE_REQUIRES_FLOAT_SUPPORT;
}
err = vm_validatePortFileMacros(lpBytecode, &header);
if (err) return err;
uint16_t importTableSize = header.sectionOffsets[vm_sectionAfter(vm, BCS_IMPORT_TABLE)] - header.sectionOffsets[BCS_IMPORT_TABLE];
uint16_t importCount = importTableSize / sizeof (vm_TsImportTableEntry);
uint16_t globalsSize = header.sectionOffsets[vm_sectionAfter(vm, BCS_GLOBALS)] - header.sectionOffsets[BCS_GLOBALS];
size_t allocationSize = sizeof(mvm_VM) +
sizeof(mvm_TfHostFunction) * importCount + // Import table
globalsSize; // Globals
vm = (VM*)vm_malloc(vm, allocationSize);
if (!vm) {
CODE_COVERAGE_ERROR_PATH(139); // Not hit
err = MVM_E_MALLOC_FAIL;
goto LBL_EXIT;
}
#if MVM_SAFE_MODE
memset(vm, 0xCC, allocationSize);
#endif
memset(vm, 0, sizeof (mvm_VM));
resolvedImports = vm_getResolvedImports(vm);
vm->context = context;
vm->lpBytecode = lpBytecode;
vm->globals = (void*)(resolvedImports + importCount);
importTableOffset = header.sectionOffsets[BCS_IMPORT_TABLE];
lpImportTableStart = LongPtr_add(lpBytecode, importTableOffset);
lpImportTableEnd = LongPtr_add(lpImportTableStart, importTableSize);
// Resolve imports (linking)
resolvedImport = resolvedImports;
lpImportTableEntry = lpImportTableStart;
while (lpImportTableEntry < lpImportTableEnd) {
CODE_COVERAGE(431); // Hit
mvm_HostFunctionID hostFunctionID = READ_FIELD_2(lpImportTableEntry, vm_TsImportTableEntry, hostFunctionID);
lpImportTableEntry = LongPtr_add(lpImportTableEntry, sizeof (vm_TsImportTableEntry));
mvm_TfHostFunction handler = NULL;
err = resolveImport(hostFunctionID, context, &handler);
if (err != MVM_E_SUCCESS) {
CODE_COVERAGE_ERROR_PATH(432); // Not hit
goto LBL_EXIT;
}
if (!handler) {
CODE_COVERAGE_ERROR_PATH(433); // Not hit
err = MVM_E_UNRESOLVED_IMPORT;
goto LBL_EXIT;
} else {
CODE_COVERAGE(434); // Hit
}
*resolvedImport++ = handler;
}
// The GC is empty to start
gc_freeGCMemory(vm);
// Initialize data
memcpy_long(vm->globals, getBytecodeSection(vm, BCS_GLOBALS, NULL), globalsSize);
// Initialize heap
initialHeapOffset = header.sectionOffsets[BCS_HEAP];
initialHeapSize = bytecodeSize - initialHeapOffset;
vm->heapSizeUsedAfterLastGC = initialHeapSize;
vm->heapHighWaterMark = initialHeapSize;
if (initialHeapSize) {
CODE_COVERAGE(435); // Hit
// The initial heap needs to be 2-byte aligned because we start appending
// new allocations to the end of it directly.
VM_ASSERT(vm, initialHeapSize % 2 == 0);
gc_createNextBucket(vm, initialHeapSize, initialHeapSize);
VM_ASSERT(vm, !vm->pLastBucket->prev); // Only one bucket
uint16_t* heapStart = getBucketDataBegin(vm->pLastBucket);
memcpy_long(heapStart, LongPtr_add(lpBytecode, initialHeapOffset), initialHeapSize);
vm->pLastBucket->pEndOfUsedSpace = (uint16_t*)((intptr_t)vm->pLastBucket->pEndOfUsedSpace + initialHeapSize);
// The running VM assumes the invariant that all pointers to the heap are
// represented as ShortPtr (and no others). We only need to call
// `loadPointers` if there is an initial heap at all, otherwise there
// will be no pointers to it.
loadPointers(vm, (uint8_t*)heapStart);
} else {
CODE_COVERAGE(436); // Hit
}
LBL_EXIT:
if (err != MVM_E_SUCCESS) {
CODE_COVERAGE_ERROR_PATH(437); // Not hit
*result = NULL;
if (vm) {
vm_free(vm, vm);
vm = NULL;
} else {
CODE_COVERAGE_ERROR_PATH(438); // Not hit
}
} else {
CODE_COVERAGE(439); // Hit
}
*result = vm;
return err;
}
static inline uint16_t getBytecodeSize(VM* vm) {
CODE_COVERAGE_UNTESTED(168); // Not hit
LongPtr lpBytecodeSize = LongPtr_add(vm->lpBytecode, OFFSETOF(mvm_TsBytecodeHeader, bytecodeSize));
return LongPtr_read2_aligned(lpBytecodeSize);
}
static LongPtr getBytecodeSection(VM* vm, mvm_TeBytecodeSection id, LongPtr* out_end) {
CODE_COVERAGE(170); // Hit
LongPtr lpBytecode = vm->lpBytecode;
LongPtr lpSections = LongPtr_add(lpBytecode, OFFSETOF(mvm_TsBytecodeHeader, sectionOffsets));
LongPtr lpSection = LongPtr_add(lpSections, id * 2);
uint16_t offset = LongPtr_read2_aligned(lpSection);
LongPtr result = LongPtr_add(lpBytecode, offset);
if (out_end) {
CODE_COVERAGE(171); // Hit
uint16_t endOffset;
if (id == BCS_SECTION_COUNT - 1) {
endOffset = getBytecodeSize(vm);
} else {
LongPtr lpNextSection = LongPtr_add(lpSection, 2);
endOffset = LongPtr_read2_aligned(lpNextSection);
}
*out_end = LongPtr_add(lpBytecode, endOffset);
} else {
CODE_COVERAGE(172); // Hit
}
return result;
}
static uint16_t getSectionSize(VM* vm, mvm_TeBytecodeSection section) {
CODE_COVERAGE(174); // Hit
uint16_t sectionStart = getSectionOffset(vm->lpBytecode, section);
uint16_t sectionEnd;
if (section == BCS_SECTION_COUNT - 1) {
CODE_COVERAGE_UNTESTED(175); // Not hit
sectionEnd = getBytecodeSize(vm);
} else {
CODE_COVERAGE(177); // Hit
VM_ASSERT(vm, section < BCS_SECTION_COUNT);
sectionEnd = getSectionOffset(vm->lpBytecode, vm_sectionAfter(vm, section));
}
VM_ASSERT(vm, sectionEnd >= sectionStart);
return sectionEnd - sectionStart;
}
/**
* Called at startup to translate all the pointers that point to GC memory into
* ShortPtr for efficiency and to maintain invariants assumed in other places in
* the code.
*/
static void loadPointers(VM* vm, uint8_t* heapStart) {
CODE_COVERAGE(178); // Hit
uint16_t n;
uint16_t v;
uint16_t* p;
// Roots in global variables
uint16_t globalsSize = getSectionSize(vm, BCS_GLOBALS);
p = vm->globals;
n = globalsSize / 2;
TABLE_COVERAGE(n ? 1 : 0, 2, 179); // Hit 1/2
while (n--) {
v = *p;
if (Value_isShortPtr(v)) {
*p = ShortPtr_encode(vm, heapStart + v);
}
p++;
}
// Pointers in heap memory
p = (uint16_t*)heapStart;
VM_ASSERT(vm, vm->pLastBucketEndCapacity == vm->pLastBucket->pEndOfUsedSpace);
uint16_t* heapEnd = vm->pLastBucketEndCapacity;
while (p < heapEnd) {
CODE_COVERAGE(181); // Hit
uint16_t header = *p++;
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
uint16_t words = (size + 1) / 2;
TeTypeCode tc = vm_getTypeCodeFromHeaderWord(header);
if (tc < TC_REF_DIVIDER_CONTAINER_TYPES) { // Non-container types
CODE_COVERAGE(182); // Hit
p += words;
continue;
} // Else, container types
CODE_COVERAGE(183); // Hit
while (words--) {
v = *p;
if (Value_isShortPtr(v)) {
*p = ShortPtr_encode(vm, heapStart + v);
}
p++;
}
}
}
void* mvm_getContext(VM* vm) {
return vm->context;
}
// Note: mvm_free frees the VM, while vm_free is the counterpart to vm_malloc
void mvm_free(VM* vm) {
CODE_COVERAGE(166); // Hit
gc_freeGCMemory(vm);
VM_EXEC_SAFE_MODE(memset(vm, 0, sizeof(*vm)));
vm_free(vm, vm);
}
/**
* @param sizeBytes Size in bytes of the allocation, *excluding* the header
* @param typeCode The type code to insert into the header
*/
static void* gc_allocateWithHeader(VM* vm, uint16_t sizeBytes, TeTypeCode typeCode) {
uint16_t* p;
uint16_t* end;
if (sizeBytes >= (MAX_ALLOCATION_SIZE + 1)) {
CODE_COVERAGE_ERROR_PATH(353); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_ALLOCATION_TOO_LARGE);
} else {
CODE_COVERAGE(354); // Hit
}
// If we happened to trigger a GC collection, we need to know that the
// registers are flushed, if they're allocated at all
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
CODE_COVERAGE(184); // Hit
TsBucket* pBucket;
const uint16_t sizeIncludingHeader = (sizeBytes + 3) & 0xFFFE;
// + 2 bytes header, round up to 2-byte boundary
VM_ASSERT(vm, (sizeIncludingHeader & 1) == 0);
// Minimum allocation size is 4 bytes, because that's the size of a
// tombstone. Note that nothing in code will attempt to allocate less,
// since even a 1-char string (+null terminator) is a 4-byte allocation.
VM_ASSERT(vm, sizeIncludingHeader >= 4);
#if MVM_VERY_EXPENSIVE_MEMORY_CHECKS
// Each time a GC collection _could_ occur, we do it. This is to catch
// insidious bugs where the only reference to something is a native
// reference and so the GC sees it as unreachable, but the native pointer
// appears to work fine until once in a blue moon a GC collection is
// triggered at exactly the right time.
mvm_runGC(vm, false);
#endif
#if MVM_SAFE_MODE
vm->gc_potentialCycleNumber++;
#endif
RETRY:
pBucket = vm->pLastBucket;
if (!pBucket) {
CODE_COVERAGE_UNTESTED(185); // Not hit
goto GROW_HEAP_AND_RETRY;
}
p = pBucket->pEndOfUsedSpace;
end = (uint16_t*)((intptr_t)p + sizeIncludingHeader);
if (end > vm->pLastBucketEndCapacity) {
CODE_COVERAGE(186); // Hit
goto GROW_HEAP_AND_RETRY;
}
pBucket->pEndOfUsedSpace = end;
// Write header
*p++ = vm_makeHeaderWord(vm, typeCode, sizeBytes);
return p;
GROW_HEAP_AND_RETRY:
CODE_COVERAGE(187); // Hit
gc_createNextBucket(vm, MVM_ALLOCATION_BUCKET_SIZE, sizeIncludingHeader);
goto RETRY;
}
// Slow fallback for gc_allocateWithConstantHeader
static void* gc_allocateWithConstantHeaderSlow(VM* vm, uint16_t header) {
CODE_COVERAGE(188); // Hit
// If we happened to trigger a GC collection, we need to know that the
// registers are flushed, if they're allocated at all
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
TeTypeCode tc = vm_getTypeCodeFromHeaderWord(header);
return gc_allocateWithHeader(vm, size, tc);
}
/*
* This function is like gc_allocateWithHeader except that it's optimized for
* situations where:
*
* 1. The header can be precomputed to a C constant, rather than assembling it
* from the size and type
* 2. The size is known to be a multiple of 2 and at least 2 bytes
*
* This is more efficient in some cases because it has fewer checks and
* preprocessing to do. This function can probably be inlined in some cases.
*
* Note: the size is passed separately rather than computed from the header
* because this function is optimized for cases where the size is known at
* compile time (and even better if this function is inlined).
*/
static inline void* gc_allocateWithConstantHeader(VM* vm, uint16_t header, uint16_t sizeIncludingHeader) {
CODE_COVERAGE(189); // Hit
uint16_t* p;
uint16_t* end;
// If we happened to trigger a GC collection, we need to know that the
// registers are flushed, if they're allocated at all
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
VM_ASSERT(vm, sizeIncludingHeader % 2 == 0);
VM_ASSERT(vm, sizeIncludingHeader >= 4);
VM_ASSERT(vm, vm_getAllocationSizeExcludingHeaderFromHeaderWord(header) == sizeIncludingHeader - 2);
#if MVM_VERY_EXPENSIVE_MEMORY_CHECKS
// Each time a GC collection _could_ occur, we do it. This is to catch
// insidious bugs where the only reference to something is a native
// reference and so the GC sees it as unreachable, but the native pointer
// appears to work fine until once in a blue moon a GC collection is
// triggered at exactly the right time.
mvm_runGC(vm, false);
#endif
#if MVM_SAFE_MODE
vm->gc_potentialCycleNumber++;
#endif
TsBucket* pBucket = vm->pLastBucket;
if (!pBucket) {
CODE_COVERAGE_UNTESTED(190); // Not hit
goto SLOW;
}
p = pBucket->pEndOfUsedSpace;
end = (uint16_t*)((intptr_t)p + sizeIncludingHeader);
if (end > vm->pLastBucketEndCapacity) {
CODE_COVERAGE(191); // Hit
goto SLOW;
}
pBucket->pEndOfUsedSpace = end;
*p++ = header;
return p;
SLOW:
CODE_COVERAGE(192); // Hit
return gc_allocateWithConstantHeaderSlow(vm, header);
}
// Looks for a variable in the closure scope chain based on its index. Scope
// records can be stored in ROM in some optimized cases, so this returns a long
// pointer.
static LongPtr vm_findScopedVariable(VM* vm, uint16_t varIndex) {
// Slots are 2 bytes
uint16_t offset = varIndex << 1;
/*
Closure scopes are arrays, with the first slot in the array being a
reference to the outer scope
*/
Value scope = vm->stack->reg.scope;
while (true)
{
// The bytecode is corrupt or the compiler has a bug if we hit the bottom of
// the scope chain without finding the variable.
VM_ASSERT(vm, scope != VM_VALUE_UNDEFINED);
LongPtr lpArr = DynamicPtr_decode_long(vm, scope);
uint16_t headerWord = readAllocationHeaderWord_long(lpArr);
VM_ASSERT(vm, vm_getTypeCodeFromHeaderWord(headerWord) == TC_REF_FIXED_LENGTH_ARRAY);
uint16_t arraySize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
// The first slot of each scope is the link to its parent
VM_ASSERT(vm, offset != 0);
if (offset < arraySize) {
return LongPtr_add(lpArr, offset);
} else {
offset -= arraySize;
scope = LongPtr_read2_aligned(lpArr);
}
}
}
static inline void* getBucketDataBegin(TsBucket* bucket) {
CODE_COVERAGE(193); // Hit
return (void*)(bucket + 1);
}
/** The used heap size, excluding spare capacity in the last block, but
* including any uncollected garbage. */
static uint16_t getHeapSize(VM* vm) {
TsBucket* lastBucket = vm->pLastBucket;
if (lastBucket) {
CODE_COVERAGE(194); // Hit
return getBucketOffsetEnd(lastBucket);
} else {
CODE_COVERAGE(195); // Hit
return 0;
}
}
void mvm_getMemoryStats(VM* vm, mvm_TsMemoryStats* r) {
CODE_COVERAGE(627); // Hit
VM_ASSERT(NULL, vm != NULL);
VM_ASSERT(vm, r != NULL);
memset(r, 0, sizeof *r);
// Core size
r->coreSize = sizeof(VM);
r->fragmentCount++;
// Import table size
r->importTableSize = getSectionSize(vm, BCS_IMPORT_TABLE) / sizeof (vm_TsImportTableEntry) * sizeof(mvm_TfHostFunction);
// Global variables size
r->globalVariablesSize = getSectionSize(vm, BCS_IMPORT_TABLE);
r->stackHighWaterMark = vm->stackHighWaterMark;
r->virtualHeapHighWaterMark = vm->heapHighWaterMark;
// Running Parameters
vm_TsStack* stack = vm->stack;
if (stack) {
CODE_COVERAGE(628); // Hit
r->fragmentCount++;
vm_TsRegisters* reg = &stack->reg;
r->registersSize = sizeof *reg;
r->stackHeight = (uint8_t*)reg->pStackPointer - (uint8_t*)getBottomOfStack(vm->stack);
r->stackAllocatedCapacity = MVM_STACK_SIZE;
}
// Heap Stats
TsBucket* pLastBucket = vm->pLastBucket;
size_t heapOverheadSize = 0;
if (pLastBucket) {
CODE_COVERAGE(629); // Hit
TsBucket* b;
for (b = pLastBucket; b; b = b->prev) {
r->fragmentCount++;
heapOverheadSize += sizeof (TsBucket); // Extra space for bucket header
}
r->virtualHeapUsed = getHeapSize(vm);
if (r->virtualHeapUsed > r->virtualHeapHighWaterMark)
r->virtualHeapHighWaterMark = r->virtualHeapUsed;
r->virtualHeapAllocatedCapacity = pLastBucket->offsetStart + (uint16_t)(uintptr_t)vm->pLastBucketEndCapacity - (uint16_t)(uintptr_t)getBucketDataBegin(pLastBucket);
}
// Total size
r->totalSize =
r->coreSize +
r->importTableSize +
r->globalVariablesSize +
r->registersSize +
r->stackAllocatedCapacity +
r->virtualHeapAllocatedCapacity +
heapOverheadSize;
}
/**
* Expand the VM heap by allocating a new "bucket" of memory from the host.
*
* @param bucketSize The ideal size of the contents of the new bucket
* @param minBucketSize The smallest the bucketSize can be reduced and still be valid
*/
static void gc_createNextBucket(VM* vm, uint16_t bucketSize, uint16_t minBucketSize) {
CODE_COVERAGE(7); // Hit
uint16_t heapSize = getHeapSize(vm);
if (bucketSize < minBucketSize) {
CODE_COVERAGE_UNTESTED(196); // Not hit
bucketSize = minBucketSize;
}
VM_ASSERT(vm, minBucketSize <= bucketSize);
// If this tips us over the top of the heap, then we run a collection
if (heapSize + bucketSize > MVM_MAX_HEAP_SIZE) {
CODE_COVERAGE(197); // Hit
mvm_runGC(vm, false);
heapSize = getHeapSize(vm);
}
// Can't fit?
if (heapSize + minBucketSize > MVM_MAX_HEAP_SIZE) {
CODE_COVERAGE_ERROR_PATH(5); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_OUT_OF_MEMORY);
}
// Can fit, but only by chopping the end off the new bucket?
if (heapSize + bucketSize > MVM_MAX_HEAP_SIZE) {
CODE_COVERAGE_UNTESTED(6); // Not hit
bucketSize = MVM_MAX_HEAP_SIZE - heapSize;
}
size_t allocSize = sizeof (TsBucket) + bucketSize;
TsBucket* bucket = vm_malloc(vm, allocSize);
if (!bucket) {
CODE_COVERAGE_ERROR_PATH(198); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_MALLOC_FAIL);
}
#if MVM_SAFE_MODE
memset(bucket, 0x7E, allocSize);
#endif
bucket->prev = vm->pLastBucket;
bucket->next = NULL;
bucket->pEndOfUsedSpace = getBucketDataBegin(bucket);
TABLE_COVERAGE(bucket->prev ? 1 : 0, 2, 11); // Hit 2/2
// Note: we start the next bucket at the allocation cursor, not at what we
// previously called the end of the previous bucket
bucket->offsetStart = heapSize;
vm->pLastBucketEndCapacity = (uint16_t*)((intptr_t)bucket->pEndOfUsedSpace + bucketSize);
if (vm->pLastBucket) {
CODE_COVERAGE(199); // Hit
vm->pLastBucket->next = bucket;
} else {
CODE_COVERAGE(200); // Hit
}
vm->pLastBucket = bucket;
}
static void gc_freeGCMemory(VM* vm) {
CODE_COVERAGE(10); // Hit
TABLE_COVERAGE(vm->pLastBucket ? 1 : 0, 2, 201); // Hit 2/2
while (vm->pLastBucket) {
CODE_COVERAGE(169); // Hit
TsBucket* prev = vm->pLastBucket->prev;
vm_free(vm, vm->pLastBucket);
TABLE_COVERAGE(prev ? 1 : 0, 2, 202); // Hit 1/2
vm->pLastBucket = prev;
}
vm->pLastBucketEndCapacity = NULL;
}
#if MVM_INCLUDE_SNAPSHOT_CAPABILITY || (!MVM_NATIVE_POINTER_IS_16_BIT && !MVM_USE_SINGLE_RAM_PAGE)
/**
* Given a pointer `ptr` into the heap, this returns the equivalent offset from
* the start of the heap (0 meaning that `ptr` points to the beginning of the
* heap).
*
* This is used in 2 places:
*
* 1. On a 32-bit machine, this is used to get a 16-bit equivalent encoding for ShortPtr
* 2. On any machine, this is used in serializePtr for creating snapshots
*/
static uint16_t pointerOffsetInHeap(VM* vm, TsBucket* pLastBucket, void* ptr) {
CODE_COVERAGE(203); // Hit
/*
* This algorithm iterates through the buckets in the heap backwards. Although
* this is technically linear cost, in reality I expect that the pointer will
* be found in the very first searched bucket almost all the time. This is
* because the GC compacts everything into a single bucket, and because the
* most recently bucket is also likely to be the most frequently accessed.
*
* See ShortPtr_decode for more description
*/
TsBucket* bucket = pLastBucket;
while (bucket) {
// Note: using `<=` here because the pointer is permitted to point to the
// end of the heap.
if ((ptr >= (void*)bucket) && (ptr <= (void*)bucket->pEndOfUsedSpace)) {
CODE_COVERAGE(204); // Hit
uint16_t offsetInBucket = (uint16_t)((intptr_t)ptr - (intptr_t)getBucketDataBegin(bucket));
VM_ASSERT(vm, offsetInBucket < 0x8000);
uint16_t offsetInHeap = bucket->offsetStart + offsetInBucket;
// It isn't strictly necessary that all short pointers are 2-byte aligned,
// but it probably indicates a mistake somewhere if a short pointer is not
// 2-byte aligned, since `Value` cannot be a `ShortPtr` unless it's 2-byte
// aligned.
VM_ASSERT(vm, (offsetInHeap & 1) == 0);
VM_ASSERT(vm, offsetInHeap < getHeapSize(vm));
return offsetInHeap;
} else {
CODE_COVERAGE(205); // Hit
}
bucket = bucket->prev;
}
// A failure here means we're trying to encode a pointer that doesn't map
// to something in GC memory, which is a mistake.
MVM_FATAL_ERROR(vm, MVM_E_UNEXPECTED);
return 0;
}
#endif // MVM_INCLUDE_SNAPSHOT_CAPABILITY || (!MVM_NATIVE_POINTER_IS_16_BIT && !MVM_USE_SINGLE_RAM_PAGE)
#if MVM_NATIVE_POINTER_IS_16_BIT
static inline void* ShortPtr_decode(VM* vm, ShortPtr ptr) {
return (void*)ptr;
}
static inline ShortPtr ShortPtr_encode(VM* vm, void* ptr) {
return (ShortPtr)ptr;
}
static inline ShortPtr ShortPtr_encodeInToSpace(gc_TsGCCollectionState* gc, void* ptr) {
return (ShortPtr)ptr;
}
#elif MVM_USE_SINGLE_RAM_PAGE
static inline void* ShortPtr_decode(VM* vm, ShortPtr ptr) {
/**
* Minor performance note:
*
* I think I recall that the ARM instruction set can inline 16-bit literal
* values but not 32-bit values. This is one of the reasons why this uses
* the "high bits" and not just some arbitrary pointer addition. Basically,
* I'm trying to make this as efficient as possible, since pointers are used
* everywhere
*/
return (void*)(((intptr_t)MVM_RAM_PAGE_ADDR) | ptr);
}
static inline ShortPtr ShortPtr_encode(VM* vm, void* ptr) {
VM_ASSERT(vm, ((intptr_t)ptr - (intptr_t)MVM_RAM_PAGE_ADDR) <= 0xFFFF);
return (ShortPtr)(uintptr_t)ptr;
}
static inline ShortPtr ShortPtr_encodeInToSpace(gc_TsGCCollectionState* gc, void* ptr) {
VM_ASSERT(gc->vm, ((intptr_t)ptr - (intptr_t)MVM_RAM_PAGE_ADDR) <= 0xFFFF);
return (ShortPtr)(uintptr_t)ptr;
}
#else // !MVM_NATIVE_POINTER_IS_16_BIT && !MVM_USE_SINGLE_RAM_PAGE
static void* ShortPtr_decode(VM* vm, ShortPtr shortPtr) {
// It isn't strictly necessary that all short pointers are 2-byte aligned,
// but it probably indicates a mistake somewhere if a short pointer is not
// 2-byte aligned, since `Value` cannot be a `ShortPtr` unless it's 2-byte
// aligned. Among other things, this catches VM_VALUE_NULL.
VM_ASSERT(vm, (shortPtr & 1) == 0);
// The shortPtr is treated as an offset into the heap
uint16_t offsetInHeap = shortPtr;
VM_ASSERT(vm, offsetInHeap < getHeapSize(vm));
/*
Note: this is a linear search through the buckets, but a redeeming factor is
that GC compacts the heap into a single bucket, so the number of buckets is
small at any one time. Also, most-recently-allocated data are likely to be
in the last bucket and accessed fastest. Also, the representation of the
function is only needed on more powerful platforms. For 16-bit platforms,
the implementation of ShortPtr_decode is a no-op.
*/
TsBucket* bucket = vm->pLastBucket;
while (true) {
// All short pointers must map to some memory in a bucket, otherwise the pointer is corrupt
VM_ASSERT(vm, bucket != NULL);
if (offsetInHeap >= bucket->offsetStart) {
uint16_t offsetInBucket = offsetInHeap - bucket->offsetStart;
void* result = (void*)((intptr_t)getBucketDataBegin(bucket) + offsetInBucket);
return result;
}
bucket = bucket->prev;
}
}
/**
* Like ShortPtr_encode except conducted against an arbitrary bucket list.
*
* Used internally by ShortPtr_encode and ShortPtr_encodeInToSpace.
*/
static inline ShortPtr ShortPtr_encode_generic(VM* vm, TsBucket* pLastBucket, void* ptr) {
return pointerOffsetInHeap(vm, pLastBucket, ptr);
}
// Encodes a pointer as pointing to a value in the current heap
static inline ShortPtr ShortPtr_encode(VM* vm, void* ptr) {
return ShortPtr_encode_generic(vm, vm->pLastBucket, ptr);
}
// Encodes a pointer as pointing to a value in the _new_ heap (tospace) during
// an ongoing garbage collection.
static inline ShortPtr ShortPtr_encodeInToSpace(gc_TsGCCollectionState* gc, void* ptr) {
return ShortPtr_encode_generic(gc->vm, gc->lastBucket, ptr);
}
#endif
static LongPtr BytecodeMappedPtr_decode_long(VM* vm, BytecodeMappedPtr ptr) {
CODE_COVERAGE(214); // Hit
// BytecodeMappedPtr values are treated as offsets into a bytecode image if
// you zero the lowest 2 bits
uint16_t offsetInBytecode = ptr & 0xFFFC;
LongPtr lpBytecode = vm->lpBytecode;
// A BytecodeMappedPtr can either point to ROM or via a global variable to
// RAM. Here to discriminate the two, we're assuming the handles section comes
// first
VM_ASSERT(vm, BCS_ROM < BCS_GLOBALS);
uint16_t globalsOffset = getSectionOffset(lpBytecode, BCS_GLOBALS);
if (offsetInBytecode < globalsOffset) { // Points to ROM section?
CODE_COVERAGE(215); // Hit
VM_ASSERT(vm, offsetInBytecode >= getSectionOffset(lpBytecode, BCS_ROM));
VM_ASSERT(vm, offsetInBytecode < getSectionOffset(lpBytecode, vm_sectionAfter(vm, BCS_ROM)));
VM_ASSERT(vm, (offsetInBytecode & 3) == 0);
// The pointer just references ROM
return LongPtr_add(lpBytecode, offsetInBytecode);
} else { // Else, must point to RAM via a global variable
CODE_COVERAGE(216); // Hit
VM_ASSERT(vm, offsetInBytecode >= getSectionOffset(lpBytecode, BCS_GLOBALS));
VM_ASSERT(vm, offsetInBytecode < getSectionOffset(lpBytecode, vm_sectionAfter(vm, BCS_GLOBALS)));
VM_ASSERT(vm, (offsetInBytecode & 3) == 0);
uint16_t offsetInGlobals = offsetInBytecode - globalsOffset;
Value handleValue = *(Value*)((intptr_t)vm->globals + offsetInGlobals);
// Note: handle values can't be null, because handles are used to point from
// ROM to RAM and ROM will never change. So if the value in ROM was null
// then it will always be null and not need a handle. And if the value in
// ROM points to an allocation in RAM then that allocation is permanently
// reachable.
VM_ASSERT(vm, Value_isShortPtr(handleValue));
return LongPtr_new(ShortPtr_decode(vm, handleValue));
}
}
static LongPtr DynamicPtr_decode_long(VM* vm, DynamicPtr ptr) {
CODE_COVERAGE(217); // Hit
if (Value_isShortPtr(ptr)) {
CODE_COVERAGE(218); // Hit
return LongPtr_new(ShortPtr_decode(vm, ptr));
}
if (ptr == VM_VALUE_NULL || ptr == VM_VALUE_UNDEFINED) {
CODE_COVERAGE(219); // Hit
return LongPtr_new(NULL);
}
CODE_COVERAGE(242); // Hit
// This function is for decoding pointers, so if this isn't a pointer then
// there's a problem.
VM_ASSERT(vm, !Value_isVirtualInt14(ptr));
// At this point, it's not a short pointer, so it must be a bytecode-mapped
// pointer
VM_ASSERT(vm, Value_encodesBytecodeMappedPtr(ptr));
// I'm expecting this to be inlined by the compiler
return BytecodeMappedPtr_decode_long(vm, ptr);
}
/*
* Decode a DynamicPtr when the target is known to live in natively-addressable
* memory (i.e. heap memory). If the target might be in ROM, use
* DynamicPtr_decode_long.
*/
static void* DynamicPtr_decode_native(VM* vm, DynamicPtr ptr) {
CODE_COVERAGE(253); // Hit
LongPtr lp = DynamicPtr_decode_long(vm, ptr);
void* p = LongPtr_truncate(lp);
// Assert that the resulting native pointer is equivalent to the long pointer.
// I.e. that we didn't lose anything in the truncation (i.e. that it doesn't
// point to ROM).
VM_ASSERT(vm, LongPtr_new(p) == lp);
return p;
}
// I'm using inline wrappers around the port macros because I want to add a
// layer of type safety.
static inline LongPtr LongPtr_new(void* p) {
CODE_COVERAGE(284); // Hit
return MVM_LONG_PTR_NEW(p);
}
static inline void* LongPtr_truncate(LongPtr lp) {
CODE_COVERAGE(332); // Hit
return MVM_LONG_PTR_TRUNCATE(lp);
}
static inline LongPtr LongPtr_add(LongPtr lp, int16_t offset) {
CODE_COVERAGE(333); // Hit
return MVM_LONG_PTR_ADD(lp, offset);
}
static inline int16_t LongPtr_sub(LongPtr lp1, LongPtr lp2) {
CODE_COVERAGE(334); // Hit
return (int16_t)(MVM_LONG_PTR_SUB(lp1, lp2));
}
static inline uint8_t LongPtr_read1(LongPtr lp) {
CODE_COVERAGE(335); // Hit
return (uint8_t)(MVM_READ_LONG_PTR_1(lp));
}
// Read a 16-bit value from a long pointer, if the target is 16-bit aligned
static inline uint16_t LongPtr_read2_aligned(LongPtr lp) {
CODE_COVERAGE(336); // Hit
// Expect an even boundary. Weird things happen on some platforms if you try
// to read unaligned memory through aligned instructions.
VM_ASSERT(0, ((uint16_t)(uintptr_t)lp & 1) == 0);
return (uint16_t)(MVM_READ_LONG_PTR_2(lp));
}
// Read a 16-bit value from a long pointer, if the target is not 16-bit aligned
static inline uint16_t LongPtr_read2_unaligned(LongPtr lp) {
CODE_COVERAGE(626); // Hit
return (uint32_t)(MVM_READ_LONG_PTR_1(lp)) |
((uint32_t)(MVM_READ_LONG_PTR_1((MVM_LONG_PTR_ADD(lp, 1)))) << 8);
}
static inline uint32_t LongPtr_read4(LongPtr lp) {
// We don't often read 4 bytes, since the word size for microvium is 2 bytes.
// When we do need to, I think it's safer to just read it as 2 separate words
// since we don't know for sure that we're not executing on a 32 bit machine
// that can't do unaligned access. All memory in microvium is at least 16-bit
// aligned, with the exception of bytecode instructions, but those do not
// contain 32-bit literals.
CODE_COVERAGE(337); // Hit
return (uint32_t)(MVM_READ_LONG_PTR_2(lp)) |
((uint32_t)(MVM_READ_LONG_PTR_2((MVM_LONG_PTR_ADD(lp, 2)))) << 16);
}
static uint16_t getBucketOffsetEnd(TsBucket* bucket) {
CODE_COVERAGE(338); // Hit
return bucket->offsetStart + (uint16_t)(uintptr_t)bucket->pEndOfUsedSpace - (uint16_t)(uintptr_t)getBucketDataBegin(bucket);
}
static uint16_t gc_getHeapSize(gc_TsGCCollectionState* gc) {
CODE_COVERAGE(351); // Hit
TsBucket* pLastBucket = gc->lastBucket;
if (pLastBucket) {
CODE_COVERAGE(352); // Hit
return getBucketOffsetEnd(pLastBucket);
} else {
CODE_COVERAGE(355); // Hit
return 0;
}
}
static void gc_newBucket(gc_TsGCCollectionState* gc, uint16_t newSpaceSize, uint16_t minNewSpaceSize) {
CODE_COVERAGE(356); // Hit
uint16_t heapSize = gc_getHeapSize(gc);
if (newSpaceSize < minNewSpaceSize) {
CODE_COVERAGE_UNTESTED(357); // Not hit
newSpaceSize = minNewSpaceSize;
} else {
CODE_COVERAGE(358); // Hit
}
// Since this is during a GC, it should be impossible for us to need more heap
// than is allowed, since the original heap should never have exceeded the
// MVM_MAX_HEAP_SIZE.
VM_ASSERT(NULL, heapSize + minNewSpaceSize <= MVM_MAX_HEAP_SIZE);
// Can fit, but only by chopping the end off the new bucket?
if (heapSize + newSpaceSize > MVM_MAX_HEAP_SIZE) {
CODE_COVERAGE_UNTESTED(8); // Not hit
newSpaceSize = MVM_MAX_HEAP_SIZE - heapSize;
} else {
CODE_COVERAGE(360); // Hit
}
TsBucket* pBucket = (TsBucket*)vm_malloc(gc->vm, sizeof (TsBucket) + newSpaceSize);
if (!pBucket) {
CODE_COVERAGE_ERROR_PATH(376); // Not hit
MVM_FATAL_ERROR(NULL, MVM_E_MALLOC_FAIL);
return;
}
pBucket->next = NULL;
uint16_t* pDataInBucket = (uint16_t*)(pBucket + 1);
if (((intptr_t)pDataInBucket) & 1) {
CODE_COVERAGE_ERROR_PATH(377); // Not hit
MVM_FATAL_ERROR(NULL, MVM_E_MALLOC_MUST_RETURN_POINTER_TO_EVEN_BOUNDARY);
return;
}
pBucket->offsetStart = heapSize;
pBucket->prev = gc->lastBucket;
pBucket->pEndOfUsedSpace = getBucketDataBegin(pBucket);
if (!gc->firstBucket) {
CODE_COVERAGE(392); // Hit
gc->firstBucket = pBucket;
} else {
CODE_COVERAGE(393); // Hit
}
if (gc->lastBucket) {
CODE_COVERAGE(394); // Hit
gc->lastBucket->next = pBucket;
} else {
CODE_COVERAGE(395); // Hit
}
gc->lastBucket = pBucket;
gc->lastBucketEndCapacity = (uint16_t*)((intptr_t)pDataInBucket + newSpaceSize);
}
static void gc_processShortPtrValue(gc_TsGCCollectionState* gc, Value* pValue) {
CODE_COVERAGE(407); // Hit
uint16_t* writePtr;
const Value spSrc = *pValue;
VM* const vm = gc->vm;
uint16_t* const pSrc = (uint16_t*)ShortPtr_decode(vm, spSrc);
// ShortPtr is defined as not encoding null
VM_ASSERT(vm, pSrc != NULL);
const uint16_t headerWord = pSrc[-1];
// If there's a tombstone, then we've already collected this allocation
if (headerWord == TOMBSTONE_HEADER) {
CODE_COVERAGE(464); // Hit
*pValue = pSrc[0];
return;
} else {
CODE_COVERAGE(465); // Hit
}
// Otherwise, we need to move the allocation
LBL_MOVE_ALLOCATION:
// Note: the variables before this point are `const` because an allocation
// movement can be aborted half way and tried again (in particular, see the
// property list compaction). It's only right at the end of this function
// where the writePtr is "committed" to the gc structure.
VM_ASSERT(vm, gc->lastBucket != NULL);
writePtr = gc->lastBucket->pEndOfUsedSpace;
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t words = (size + 3) / 2; // Rounded up, including header
// Check we have space
if (writePtr + words > gc->lastBucketEndCapacity) {
CODE_COVERAGE(466); // Hit
uint16_t minRequiredSpace = words * 2;
gc_newBucket(gc, MVM_ALLOCATION_BUCKET_SIZE, minRequiredSpace);
goto LBL_MOVE_ALLOCATION;
} else {
CODE_COVERAGE(467); // Hit
}
// Write the header
*writePtr++ = headerWord;
words--;
uint16_t* pOld = pSrc;
uint16_t* pNew = writePtr;
// Copy the allocation body
uint16_t* readPtr = pSrc;
while (words--)
*writePtr++ = *readPtr++;
// Dynamic arrays and property lists are compacted here
TeTypeCode tc = vm_getTypeCodeFromHeaderWord(headerWord);
if (tc == TC_REF_ARRAY) {
CODE_COVERAGE(468); // Hit
TsArray* arr = (TsArray*)pNew;
DynamicPtr dpData = arr->dpData;
if (dpData != VM_VALUE_NULL) {
CODE_COVERAGE(469); // Hit
VM_ASSERT(vm, Value_isShortPtr(dpData));
// Note: this decodes the pointer against fromspace
TsFixedLengthArray* pData = ShortPtr_decode(vm, dpData);
uint16_t len = VirtualInt14_decode(vm, arr->viLength);
#if MVM_SAFE_MODE
uint16_t headerWord = readAllocationHeaderWord(pData);
uint16_t dataTC = vm_getTypeCodeFromHeaderWord(headerWord);
// Note: because dpData is a unique pointer, we can be sure that it
// hasn't already been moved in response to some other reference to
// it (it's not a tombstone yet).
VM_ASSERT(vm, dataTC == TC_REF_FIXED_LENGTH_ARRAY);
uint16_t dataSize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t capacity = dataSize / 2;
VM_ASSERT(vm, len <= capacity);
#endif
if (len > 0) {
CODE_COVERAGE(470); // Hit
// We just truncate the fixed-length-array to match the programmed
// length of the dynamic array, which is necessarily equal or less than
// its previous value. The GC will copy the data later and update the
// data pointer as it would normally do when following pointers.
setHeaderWord(vm, pData, TC_REF_FIXED_LENGTH_ARRAY, len * 2);
} else {
CODE_COVERAGE_UNTESTED(472); // Not hit
// Or if there's no length, we can remove the data altogether.
arr->dpData = VM_VALUE_NULL;
}
} else {
CODE_COVERAGE(473); // Hit
}
} else if (tc == TC_REF_PROPERTY_LIST) {
CODE_COVERAGE(474); // Hit
TsPropertyList* props = (TsPropertyList*)pNew;
Value dpNext = props->dpNext;
// If the object has children (detached extensions to the main
// allocation), we take this opportunity to compact them into the parent
// allocation to save space and improve access performance.
if (dpNext != VM_VALUE_NULL) {
CODE_COVERAGE(478); // Hit
// Note: The "root" property list counts towards the total but its
// fields do not need to be copied because it's already copied, above
uint16_t headerWord = readAllocationHeaderWord(props);
uint16_t allocationSize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t totalPropCount = (allocationSize - sizeof(TsPropertyList)) / 4;
do {
// Note: while `next` is not strictly a ShortPtr in general, when used
// within GC allocations it will never point to an allocation in ROM
// or data memory, since it's only used to extend objects with new
// properties.
VM_ASSERT(vm, Value_isShortPtr(dpNext));
TsPropertyList* child = (TsPropertyList*)ShortPtr_decode(vm, dpNext);
uint16_t headerWord = readAllocationHeaderWord(child);
uint16_t allocationSize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t childPropCount = (allocationSize - sizeof(TsPropertyList)) / 4;
totalPropCount += childPropCount;
uint16_t* end = writePtr + childPropCount;
// Check we have space for the new properties
if (end > gc->lastBucketEndCapacity) {
CODE_COVERAGE_UNTESTED(479); // Not hit
// If we don't have space, we need to revert and try again. The
// "revert" isn't explict. It depends on the fact that the gc.writePtr
// hasn't been committed yet, and no mutations have been applied to
// the source memory (i.e. the tombstone hasn't been written yet).
uint16_t minRequiredSpace = sizeof (TsPropertyList) + totalPropCount * 4;
gc_newBucket(gc, MVM_ALLOCATION_BUCKET_SIZE, minRequiredSpace);
goto LBL_MOVE_ALLOCATION;
} else {
CODE_COVERAGE(480); // Hit
}
uint16_t* pField = (uint16_t*)(child + 1);
// Copy the child fields directly into the parent
while (childPropCount--) {
*writePtr++ = *pField++; // key
*writePtr++ = *pField++; // value
}
dpNext = child->dpNext;
TABLE_COVERAGE(dpNext ? 1 : 0, 2, 490); // Hit 1/2
} while (dpNext != VM_VALUE_NULL);
// We've collapsed all the lists into one, so let's adjust the header
uint16_t newSize = sizeof (TsPropertyList) + totalPropCount * 4;
if (newSize > MAX_ALLOCATION_SIZE) {
CODE_COVERAGE_ERROR_PATH(491); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_ALLOCATION_TOO_LARGE);
return;
}
setHeaderWord(vm, props, TC_REF_PROPERTY_LIST, newSize);
props->dpNext = VM_VALUE_NULL;
}
} else {
CODE_COVERAGE(492); // Hit
}
// Commit the move (grow the target heap and add the tombstone)
gc->lastBucket->pEndOfUsedSpace = writePtr;
ShortPtr spNew = ShortPtr_encodeInToSpace(gc, pNew);
pOld[-1] = TOMBSTONE_HEADER;
pOld[0] = spNew; // Forwarding pointer
*pValue = spNew;
}
static inline void gc_processValue(gc_TsGCCollectionState* gc, Value* pValue) {
// Note: only short pointer values are allowed to point to GC memory,
// and we only need to follow references that go to GC memory.
if (Value_isShortPtr(*pValue)) {
CODE_COVERAGE(446); // Hit
gc_processShortPtrValue(gc, pValue);
} else {
CODE_COVERAGE(463); // Hit
}
}
void mvm_runGC(VM* vm, bool squeeze) {
CODE_COVERAGE(593); // Hit
/*
This is a semispace collection model based on Cheney's algorithm
https://en.wikipedia.org/wiki/Cheney%27s_algorithm. It collects by moving
reachable allocations from the fromspace to the tospace and then releasing the
fromspace. It starts by moving allocations reachable by the roots, and then
iterates through moved allocations, checking the pointers therein, moving the
allocations they reference.
When an object is moved, the space it occupied is changed to a tombstone
(TC_REF_TOMBSTONE) which contains a forwarding pointer. When a pointer in
tospace is seen to point to an allocation in fromspace, if the fromspace
allocation is a tombstone then the pointer can be updated to the forwarding
pointer.
This algorithm relies on allocations in tospace each have a header. Some
allocations, such as property cells, don't have a header, but will only be
found in fromspace. When copying objects into tospace, the detached property
cells are merged into the object's head allocation.
Note: all pointer _values_ are only processed once each (since their
corresponding container is only processed once). This means that fromspace and
tospace can be treated as distinct spaces. An unprocessed pointer is
interpreted in terms of _fromspace_. Forwarding pointers and pointers in
processed allocations always reference _tospace_.
*/
uint16_t n;
uint16_t* p;
uint16_t heapSize = getHeapSize(vm);
if (heapSize > vm->heapHighWaterMark)
vm->heapHighWaterMark = heapSize;
// A collection of variables shared by GC routines
gc_TsGCCollectionState gc;
memset(&gc, 0, sizeof gc);
gc.vm = vm;
// We don't know how big the heap needs to be, so we just allocate the same
// amount of space as used last time and then expand as-needed
uint16_t estimatedSize = vm->heapSizeUsedAfterLastGC;
#if MVM_VERY_EXPENSIVE_MEMORY_CHECKS
// Move the heap address space by 2 bytes on each cycle.
vm->gc_heap_shift += 2;
if (vm->gc_heap_shift == 0) {
// Minimum of 2 bytes just so we have consistency when it overflows
vm->gc_heap_shift = 2;
}
// We shift up the address space by `gc_heap_shift` amount by just
// allocating a bucket of that size at the beginning and marking it full.
gc_newBucket(&gc, vm->gc_heap_shift, 0);
// The heap must be parsable, so we need to have an allocation header to
// mark the space. In general, we do not allow allocations to be smaller
// than 4 bytes because a tombstone is 4 bytes. However, there can be no
// references to this "allocation" so no tombstone is required, so it can
// be as small as 2 bytes. I'm using a string here because it's a
// "non-container" type, so the GC will not interpret its contents.
VM_ASSERT(vm, vm->gc_heap_shift >= 2);
*gc.lastBucket->pEndOfUsedSpace = vm_makeHeaderWord(vm, TC_REF_STRING, vm->gc_heap_shift - 2);
#endif // MVM_VERY_EXPENSIVE_MEMORY_CHECKS
if (estimatedSize) {
CODE_COVERAGE(493); // Hit
gc_newBucket(&gc, estimatedSize, 0);
} else {
CODE_COVERAGE_UNTESTED(494); // Not hit
}
// Roots in global variables (including indirection handles)
// Note: Interned strings are referenced from a handle and so will be GC'd here
// TODO: It would actually be good to have a test case showing that the string interning table is handled properly during GC
uint16_t globalsSize = getSectionSize(vm, BCS_GLOBALS);
p = vm->globals;
n = globalsSize / 2;
TABLE_COVERAGE(n ? 1 : 0, 2, 495); // Hit 1/2
while (n--)
gc_processValue(&gc, p++);
// Roots in gc_handles
mvm_Handle* handle = vm->gc_handles;
TABLE_COVERAGE(handle ? 1 : 0, 2, 496); // Hit 2/2
while (handle) {
gc_processValue(&gc, &handle->_value);
TABLE_COVERAGE(handle->_next ? 1 : 0, 2, 497); // Hit 2/2
handle = handle->_next;
}
// Roots on the stack or registers
vm_TsStack* stack = vm->stack;
if (stack) {
CODE_COVERAGE(498); // Hit
vm_TsRegisters* reg = &stack->reg;
VM_ASSERT(vm, reg->usingCachedRegisters == false);
// Roots in scope
gc_processValue(&gc, &reg->scope);
// Roots on call stack
uint16_t* beginningOfStack = getBottomOfStack(stack);
uint16_t* beginningOfFrame = reg->pFrameBase;
uint16_t* endOfFrame = reg->pStackPointer;
while (true) {
VM_ASSERT(vm, beginningOfFrame >= beginningOfStack);
// Loop through words in frame
p = beginningOfFrame;
while (p != endOfFrame) {
VM_ASSERT(vm, p < endOfFrame);
// TODO: It would be an interesting exercise to see if the GC can be written into a single function so that we don't need to pass around the &gc struct everywhere
gc_processValue(&gc, p++);
}
if (beginningOfFrame == beginningOfStack) {
break;
}
VM_ASSERT(vm, beginningOfFrame >= beginningOfStack);
// The following statements assume a particular stack shape
VM_ASSERT(vm, VM_FRAME_BOUNDARY_VERSION == 2);
// Skip over the registers that are saved during a CALL instruction
endOfFrame = beginningOfFrame - 4;
// The saved scope pointer
Value* pScope = endOfFrame + 1;
gc_processValue(&gc, pScope);
// The first thing saved during a CALL is the size of the preceding frame
beginningOfFrame = (uint16_t*)((uint8_t*)endOfFrame - *endOfFrame);
TABLE_COVERAGE(beginningOfFrame == beginningOfStack ? 1 : 0, 2, 499); // Hit 2/2
}
} else {
CODE_COVERAGE(500); // Hit
}
// Now we process moved allocations to make sure objects they point to are
// also moved, and to update pointers to reference the new space
TsBucket* bucket = gc.firstBucket;
TABLE_COVERAGE(bucket ? 1 : 0, 2, 501); // Hit 1/2
// Loop through buckets
while (bucket) {
uint16_t* p = (uint16_t*)getBucketDataBegin(bucket);
// Loop through allocations in bucket. Note that this loop will hit exactly
// the end of the bucket even when there are multiple buckets, because empty
// space in a bucket is truncated when a new one is created (in
// gc_processValue)
while (p != bucket->pEndOfUsedSpace) { // Hot loop
VM_ASSERT(vm, p < bucket->pEndOfUsedSpace);
uint16_t header = *p++;
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
uint16_t words = (size + 1) >> 1;
// Note: we're comparing the header words here to compare the type code.
// The RHS here is constant
if (header < (uint16_t)(TC_REF_DIVIDER_CONTAINER_TYPES << 12)) { // Non-container types
CODE_COVERAGE(502); // Hit
p += words;
continue;
} else {
// Else, container types
CODE_COVERAGE(505); // Hit
}
while (words--) { // Hot loop
if (Value_isShortPtr(*p))
gc_processValue(&gc, p);
p++;
}
}
// Go to next bucket
bucket = bucket->next;
TABLE_COVERAGE(bucket ? 1 : 0, 2, 506); // Hit 2/2
}
// Release old heap
TsBucket* oldBucket = vm->pLastBucket;
TABLE_COVERAGE(oldBucket ? 1 : 0, 2, 507); // Hit 1/2
while (oldBucket) {
TsBucket* prev = oldBucket->prev;
vm_free(vm, oldBucket);
oldBucket = prev;
}
// Adopt new heap
vm->pLastBucket = gc.lastBucket;
vm->pLastBucketEndCapacity = gc.lastBucketEndCapacity;
uint16_t finalUsedSize = getHeapSize(vm);
vm->heapSizeUsedAfterLastGC = finalUsedSize;
if (squeeze && (finalUsedSize != estimatedSize)) {
CODE_COVERAGE(508); // Hit
/*
Note: The most efficient way to calculate the exact size needed for the heap
is actually to run a collection twice. The collection algorithm itself is
almost as efficient as any size-counting algorithm in terms of running time
since it needs to iterate the whole reachability graph and all the pointers
contained therein. But having a distinct size-counting algorithm is less
efficient in terms of the amount of code-space (ROM) used, since it must
duplicate much of the logic to parse the heap. It also needs to keep
separate flags to know what it's already counted or not, and these flags
would presumably take up space in the headers that isn't otherwise needed.
Furthermore, it's suspected that a common case is where the VM is repeatedly
used to perform the same calculation, such as a "tick" or "check" function,
that does basically the same thing every time and so lands up in the same
equilibrium size each time. With this squeeze implementation we would only
run the GC once each time, since the estimated size would be correct most of
the time.
In conclusion, I decided that the best way to "squeeze" the heap is to just
run the collection twice. The first time will tell us the exact size, and
then if that's different to what we estimated then we perform the collection
again, now with the exact target size, so that there is no unused space
malloc'd from the host, and no unnecessary mallocs from the host.
Note: especially for small programs, the squeeze could make a significant
difference to the idle memory usage. A program that goes from 18 bytes to 20
bytes will cause a whole new bucket to be allocated for the additional 2B,
leaving 254B unused (if the bucket size is 256B). The "squeeze" pass will
compact everything into a single 20B allocation.
*/
mvm_runGC(vm, false);
} else {
CODE_COVERAGE(509); // Hit
}
}
/**
* Create the call VM call stack and registers
*/
TeError vm_createStackAndRegisters(VM* vm) {
CODE_COVERAGE(225); // Hit
// This is freed again at the end of mvm_call. Note: the allocated
// memory includes the registers, which are part of the vm_TsStack
// structure
vm_TsStack* stack = vm_malloc(vm, sizeof (vm_TsStack) + MVM_STACK_SIZE);
if (!stack) {
CODE_COVERAGE_ERROR_PATH(231); // Not hit
return vm_newError(vm, MVM_E_MALLOC_FAIL);
}
vm->stack = stack;
vm_TsRegisters* reg = &stack->reg;
memset(reg, 0, sizeof *reg);
// The stack grows upward. The bottom is the lowest address.
uint16_t* bottomOfStack = getBottomOfStack(stack);
reg->pFrameBase = bottomOfStack;
reg->pStackPointer = bottomOfStack;
reg->lpProgramCounter = vm->lpBytecode; // This is essentially treated as a null value
reg->argCountAndFlags = 0;
reg->scope = VM_VALUE_UNDEFINED;
reg->catchTarget = VM_VALUE_UNDEFINED;
VM_ASSERT(vm, reg->pArgs == 0);
return MVM_E_SUCCESS;
}
// Lowest address on stack
static inline uint16_t* getBottomOfStack(vm_TsStack* stack) {
CODE_COVERAGE(510); // Hit
return (uint16_t*)(stack + 1);
}
// Highest possible address on stack (+1) before overflow
static inline uint16_t* getTopOfStackSpace(vm_TsStack* stack) {
CODE_COVERAGE(511); // Hit
return getBottomOfStack(stack) + MVM_STACK_SIZE / 2;
}
#if MVM_DEBUG
// Some utility functions, mainly to execute in the debugger (could also be copy-pasted as expressions in some cases)
uint16_t dbgStackDepth(VM* vm) {
return (uint16_t)((uint16_t*)vm->stack->reg.pStackPointer - (uint16_t*)(vm->stack + 1));
}
uint16_t* dbgStack(VM* vm) {
return (uint16_t*)(vm->stack + 1);
}
uint16_t dbgPC(VM* vm) {
return (uint16_t)((intptr_t)vm->stack->reg.lpProgramCounter - (intptr_t)vm->lpBytecode);
}
#endif // MVM_DEBUG
/**
* Checks that we have enough stack space for the given size, and updates the
* high water mark.
*/
static TeError vm_requireStackSpace(VM* vm, uint16_t* pStackPointer, uint16_t sizeRequiredInWords) {
uint16_t* pStackHighWaterMark = pStackPointer + ((intptr_t)sizeRequiredInWords);
if (pStackHighWaterMark > getTopOfStackSpace(vm->stack)) {
CODE_COVERAGE_ERROR_PATH(233); // Not hit
// TODO(low): Since we know the max stack depth for the function, we could
// actually grow the stack dynamically rather than allocate it fixed size.
// Actually, it seems likely that we could allocate the VM stack on the C
// stack, since it's a fixed-size structure anyway.
//
// (A way to do the allocation on the stack would be to perform a nested
// call to mvm_call, and the allocation can be at the begining of mvm_call).
// Otherwise we could just malloc, which has the advantage of simplicity and
// we can grow the stack at any time.
//
// Rather than a segmented stack, it might also be simpler to just grow the
// stack size and copy across old data. This has the advantage of keeping
// the GC simple.
return vm_newError(vm, MVM_E_STACK_OVERFLOW);
}
// Stack high-water mark
uint16_t stackHighWaterMark = (uint16_t)((intptr_t)pStackHighWaterMark - (intptr_t)getBottomOfStack(vm->stack));
if (stackHighWaterMark > vm->stackHighWaterMark) {
vm->stackHighWaterMark = stackHighWaterMark;
}
return MVM_E_SUCCESS;
}
TeError vm_resolveExport(VM* vm, mvm_VMExportID id, Value* result) {
CODE_COVERAGE(17); // Hit
LongPtr exportTableEnd;
LongPtr exportTable = getBytecodeSection(vm, BCS_EXPORT_TABLE, &exportTableEnd);
// See vm_TsExportTableEntry
LongPtr exportTableEntry = exportTable;
while (exportTableEntry < exportTableEnd) {
CODE_COVERAGE(234); // Hit
mvm_VMExportID exportID = LongPtr_read2_aligned(exportTableEntry);
if (exportID == id) {
CODE_COVERAGE(235); // Hit
LongPtr pExportvalue = LongPtr_add(exportTableEntry, 2);
mvm_VMExportID exportValue = LongPtr_read2_aligned(pExportvalue);
*result = exportValue;
return MVM_E_SUCCESS;
} else {
CODE_COVERAGE(236); // Hit
}
exportTableEntry = LongPtr_add(exportTableEntry, sizeof (vm_TsExportTableEntry));
}
*result = VM_VALUE_UNDEFINED;
return vm_newError(vm, MVM_E_UNRESOLVED_EXPORT);
}
TeError mvm_resolveExports(VM* vm, const mvm_VMExportID* idTable, Value* resultTable, uint8_t count) {
CODE_COVERAGE(18); // Hit
TeError err = MVM_E_SUCCESS;
while (count--) {
CODE_COVERAGE(237); // Hit
TeError tempErr = vm_resolveExport(vm, *idTable++, resultTable++);
if (tempErr != MVM_E_SUCCESS) {
CODE_COVERAGE_ERROR_PATH(238); // Not hit
err = tempErr;
} else {
CODE_COVERAGE(239); // Hit
}
}
return err;
}
#if MVM_SAFE_MODE
static bool vm_isHandleInitialized(VM* vm, const mvm_Handle* handle) {
CODE_COVERAGE(22); // Hit
mvm_Handle* h = vm->gc_handles;
while (h) {
CODE_COVERAGE(243); // Hit
if (h == handle) {
CODE_COVERAGE_UNTESTED(244); // Not hit
return true;
}
else {
CODE_COVERAGE(245); // Hit
}
h = h->_next;
}
return false;
}
#endif // MVM_SAFE_MODE
void mvm_initializeHandle(VM* vm, mvm_Handle* handle) {
CODE_COVERAGE(19); // Hit
VM_ASSERT(vm, !vm_isHandleInitialized(vm, handle));
handle->_next = vm->gc_handles;
vm->gc_handles = handle;
handle->_value = VM_VALUE_UNDEFINED;
}
void vm_cloneHandle(VM* vm, mvm_Handle* target, const mvm_Handle* source) {
CODE_COVERAGE_UNTESTED(20); // Not hit
VM_ASSERT(vm, !vm_isHandleInitialized(vm, source));
mvm_initializeHandle(vm, target);
target->_value = source->_value;
}
TeError mvm_releaseHandle(VM* vm, mvm_Handle* handle) {
// This function doesn't contain coverage markers because node hits this path
// non-deterministically.
mvm_Handle** h = &vm->gc_handles;
while (*h) {
if (*h == handle) {
*h = handle->_next;
handle->_value = VM_VALUE_UNDEFINED;
handle->_next = NULL;
return MVM_E_SUCCESS;
}
h = &((*h)->_next);
}
handle->_value = VM_VALUE_UNDEFINED;
handle->_next = NULL;
return vm_newError(vm, MVM_E_INVALID_HANDLE);
}
static Value vm_convertToString(VM* vm, Value value) {
CODE_COVERAGE(23); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
TeTypeCode type = deepTypeOf(vm, value);
const char* constStr;
switch (type) {
case TC_VAL_INT14:
case TC_REF_INT32: {
CODE_COVERAGE(246); // Hit
int32_t i = vm_readInt32(vm, type, value);
return vm_intToStr(vm, i);
}
case TC_REF_FLOAT64: {
CODE_COVERAGE_UNTESTED(248); // Not hit
return 0xFFFF;
}
case TC_REF_STRING: {
CODE_COVERAGE(249); // Hit
return value;
}
case TC_REF_INTERNED_STRING: {
CODE_COVERAGE(250); // Hit
return value;
}
case TC_REF_PROPERTY_LIST: {
CODE_COVERAGE_UNTESTED(251); // Not hit
constStr = "[Object]";
break;
}
case TC_REF_CLOSURE: {
CODE_COVERAGE_UNTESTED(365); // Not hit
constStr = "[Function]";
break;
}
case TC_REF_FIXED_LENGTH_ARRAY:
case TC_REF_ARRAY: {
CODE_COVERAGE_UNTESTED(252); // Not hit
constStr = "[Object]";
break;
}
case TC_REF_FUNCTION: {
CODE_COVERAGE_UNTESTED(254); // Not hit
constStr = "[Function]";
break;
}
case TC_REF_HOST_FUNC: {
CODE_COVERAGE_UNTESTED(255); // Not hit
constStr = "[Function]";
break;
}
case TC_REF_UINT8_ARRAY: {
CODE_COVERAGE_UNTESTED(256); // Not hit
constStr = "[Object]";
break;
}
case TC_REF_CLASS: {
CODE_COVERAGE_UNTESTED(596); // Not hit
constStr = "[Function]";
break;
}
case TC_REF_VIRTUAL: {
CODE_COVERAGE_UNTESTED(597); // Not hit
VM_NOT_IMPLEMENTED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
case TC_REF_SYMBOL: {
CODE_COVERAGE_UNTESTED(257); // Not hit
VM_NOT_IMPLEMENTED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
case TC_VAL_UNDEFINED: {
CODE_COVERAGE(258); // Hit
constStr = "undefined";
break;
}
case TC_VAL_NULL: {
CODE_COVERAGE(259); // Hit
constStr = "null";
break;
}
case TC_VAL_TRUE: {
CODE_COVERAGE(260); // Hit
constStr = "true";
break;
}
case TC_VAL_FALSE: {
CODE_COVERAGE(261); // Hit
constStr = "false";
break;
}
case TC_VAL_NAN: {
CODE_COVERAGE_UNTESTED(262); // Not hit
constStr = "NaN";
break;
}
case TC_VAL_NEG_ZERO: {
CODE_COVERAGE(263); // Hit
constStr = "0";
break;
}
case TC_VAL_STR_LENGTH: {
CODE_COVERAGE(266); // Hit
return value;
}
case TC_VAL_STR_PROTO: {
CODE_COVERAGE_UNTESTED(267); // Not hit
return value;
}
case TC_VAL_DELETED: {
return VM_UNEXPECTED_INTERNAL_ERROR(vm);
}
default: return VM_UNEXPECTED_INTERNAL_ERROR(vm);
}
return vm_newStringFromCStrNT(vm, constStr);
}
static Value vm_intToStr(VM* vm, int32_t i) {
CODE_COVERAGE(618); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
// TODO: Is this really logic we can't just assume in the C standard library?
// What if we made it a port entry? Maybe all uses of the standard library
// should be port entries anyway.
static const char strMinInt[] = "-2147483648";
char buf[12]; // Up to 11 digits plus a minus sign
char* cur = &buf[sizeof buf];
bool negative = false;
if (i < 0) {
CODE_COVERAGE(619); // Hit
// Special case for this value because `-i` overflows.
if (i == (int32_t)0x80000000) {
CODE_COVERAGE(621); // Hit
return vm_newStringFromCStrNT(vm, strMinInt);
} else {
CODE_COVERAGE(622); // Hit
}
negative = true;
i = -i;
}
else {
CODE_COVERAGE(620); // Hit
negative = false;
}
do {
*--cur = '0' + i % 10;
i /= 10;
} while (i);
if (negative) {
*--cur = '-';
}
return mvm_newString(vm, cur, &buf[sizeof buf] - cur);
}
static Value vm_concat(VM* vm, Value* left, Value* right) {
CODE_COVERAGE(553); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
uint16_t leftSize = vm_stringSizeUtf8(vm, *left);
uint16_t rightSize = vm_stringSizeUtf8(vm, *right);
uint8_t* data;
// Note: this allocation can cause a GC collection which could cause the
// strings to move in memory
Value value = vm_allocString(vm, leftSize + rightSize, (void**)&data);
LongPtr lpLeftStr = vm_getStringData(vm, *left);
LongPtr lpRightStr = vm_getStringData(vm, *right);
memcpy_long(data, lpLeftStr, leftSize);
memcpy_long(data + leftSize, lpRightStr, rightSize);
return value;
}
/* Returns the deep type code of the value, looking through pointers and boxing */
static TeTypeCode deepTypeOf(VM* vm, Value value) {
CODE_COVERAGE(27); // Hit
if (Value_isShortPtr(value)) {
CODE_COVERAGE(0); // Hit
void* p = ShortPtr_decode(vm, value);
uint16_t headerWord = readAllocationHeaderWord(p);
TeTypeCode typeCode = vm_getTypeCodeFromHeaderWord(headerWord);
return typeCode;
} else {
CODE_COVERAGE(515); // Hit
}
if (Value_isVirtualInt14(value)) {
CODE_COVERAGE(295); // Hit
return TC_VAL_INT14;
} else {
CODE_COVERAGE(516); // Hit
}
VM_ASSERT(vm, Value_isBytecodeMappedPtrOrWellKnown(value));
// Check for "well known" values such as TC_VAL_UNDEFINED
if (value < VM_VALUE_WELLKNOWN_END) {
CODE_COVERAGE(296); // Hit
return (TeTypeCode)((value >> 2) + 0x11);
} else {
CODE_COVERAGE(297); // Hit
}
LongPtr p = DynamicPtr_decode_long(vm, value);
uint16_t headerWord = readAllocationHeaderWord_long(p);
TeTypeCode typeCode = vm_getTypeCodeFromHeaderWord(headerWord);
return typeCode;
}
#if MVM_SUPPORT_FLOAT
int32_t mvm_float64ToInt32(MVM_FLOAT64 value) {
CODE_COVERAGE(486); // Hit
if (isfinite(value)) {
CODE_COVERAGE(487); // Hit
return (int32_t)value;
} else {
CODE_COVERAGE(488); // Hit
return 0;
}
}
Value mvm_newNumber(VM* vm, MVM_FLOAT64 value) {
CODE_COVERAGE(28); // Hit
if (isnan(value)) {
CODE_COVERAGE(298); // Hit
return VM_VALUE_NAN;
} else {
CODE_COVERAGE(517); // Hit
}
// Note: VisualC++ (and maybe other compilers) seem to have `0.0==-0.0` evaluate to true, which is why there's the second check here
if ((value == -0.0) && (signbit(value) != 0)) {
CODE_COVERAGE_UNTESTED(299); // Not hit
return VM_VALUE_NEG_ZERO;
} else {
CODE_COVERAGE(518); // Hit
}
// Doubles are very expensive to compute, so at every opportunity, we'll check
// if we can coerce back to an integer
int32_t valueAsInt = mvm_float64ToInt32(value);
if (value == (MVM_FLOAT64)valueAsInt) {
CODE_COVERAGE(300); // Hit
return mvm_newInt32(vm, valueAsInt);
} else {
CODE_COVERAGE(301); // Hit
}
MVM_FLOAT64* pResult = GC_ALLOCATE_TYPE(vm, MVM_FLOAT64, TC_REF_FLOAT64);
*pResult = value;
return ShortPtr_encode(vm, pResult);
}
#endif // MVM_SUPPORT_FLOAT
Value mvm_newInt32(VM* vm, int32_t value) {
CODE_COVERAGE(29); // Hit
if ((value >= VM_MIN_INT14) && (value <= VM_MAX_INT14)) {
CODE_COVERAGE(302); // Hit
return VirtualInt14_encode(vm, value);
} else {
CODE_COVERAGE(303); // Hit
}
// Int32
int32_t* pResult = GC_ALLOCATE_TYPE(vm, int32_t, TC_REF_INT32);
*pResult = value;
return ShortPtr_encode(vm, pResult);
}
bool mvm_toBool(VM* vm, Value value) {
CODE_COVERAGE(30); // Hit
TeTypeCode type = deepTypeOf(vm, value);
switch (type) {
case TC_VAL_INT14: {
CODE_COVERAGE(304); // Hit
return value != VirtualInt14_encode(vm, 0);
}
case TC_REF_INT32: {
CODE_COVERAGE_UNTESTED(305); // Not hit
// Int32 can't be zero, otherwise it would be encoded as an int14
VM_ASSERT(vm, vm_readInt32(vm, type, value) != 0);
return false;
}
case TC_REF_FLOAT64: {
CODE_COVERAGE_UNTESTED(306); // Not hit
#if MVM_SUPPORT_FLOAT
// Double can't be zero, otherwise it would be encoded as an int14
VM_ASSERT(vm, mvm_toFloat64(vm, value) != 0);
#endif
return false;
}
case TC_REF_INTERNED_STRING:
case TC_REF_STRING: {
CODE_COVERAGE(307); // Hit
return vm_stringSizeUtf8(vm, value) != 0;
}
case TC_REF_PROPERTY_LIST: {
CODE_COVERAGE(308); // Hit
return true;
}
case TC_REF_CLOSURE: {
CODE_COVERAGE_UNTESTED(372); // Not hit
return true;
}
case TC_REF_ARRAY: {
CODE_COVERAGE(309); // Hit
return true;
}
case TC_REF_FUNCTION: {
CODE_COVERAGE_UNTESTED(311); // Not hit
return true;
}
case TC_REF_HOST_FUNC: {
CODE_COVERAGE_UNTESTED(312); // Not hit
return true;
}
case TC_REF_UINT8_ARRAY: {
CODE_COVERAGE_UNTESTED(313); // Not hit
return true;
}
case TC_REF_SYMBOL: {
CODE_COVERAGE_UNTESTED(314); // Not hit
return true;
}
case TC_REF_CLASS: {
CODE_COVERAGE(604); // Hit
return true;
}
case TC_REF_VIRTUAL: {
CODE_COVERAGE_UNTESTED(609); // Not hit
VM_RESERVED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
case TC_REF_RESERVED_1: {
CODE_COVERAGE_UNTESTED(610); // Not hit
VM_RESERVED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
case TC_VAL_UNDEFINED: {
CODE_COVERAGE(315); // Hit
return false;
}
case TC_VAL_NULL: {
CODE_COVERAGE(316); // Hit
return false;
}
case TC_VAL_TRUE: {
CODE_COVERAGE(317); // Hit
return true;
}
case TC_VAL_FALSE: {
CODE_COVERAGE(318); // Hit
return false;
}
case TC_VAL_NAN: {
CODE_COVERAGE_UNTESTED(319); // Not hit
return false;
}
case TC_VAL_NEG_ZERO: {
CODE_COVERAGE_UNTESTED(320); // Not hit
return false;
}
case TC_VAL_DELETED: {
CODE_COVERAGE_UNTESTED(321); // Not hit
return false;
}
case TC_VAL_STR_LENGTH: {
CODE_COVERAGE_UNTESTED(268); // Not hit
return true;
}
case TC_VAL_STR_PROTO: {
CODE_COVERAGE_UNTESTED(269); // Not hit
return true;
}
default: return VM_UNEXPECTED_INTERNAL_ERROR(vm);
}
}
static bool vm_isString(VM* vm, Value value) {
CODE_COVERAGE(31); // Hit
return mvm_typeOf(vm, value) == VM_T_STRING;
}
/** Reads a numeric value that is a subset of a 32-bit integer */
static int32_t vm_readInt32(VM* vm, TeTypeCode type, Value value) {
CODE_COVERAGE(33); // Hit
if (type == TC_VAL_INT14) {
CODE_COVERAGE(330); // Hit
return VirtualInt14_decode(vm, value);
} else if (type == TC_REF_INT32) {
CODE_COVERAGE(331); // Hit
LongPtr target = DynamicPtr_decode_long(vm, value);
int32_t result = (int32_t)LongPtr_read4(target);
return result;
} else {
return VM_UNEXPECTED_INTERNAL_ERROR(vm);
}
}
static inline uint16_t readAllocationHeaderWord_long(LongPtr pAllocation) {
CODE_COVERAGE(519); // Hit
return LongPtr_read2_aligned(LongPtr_add(pAllocation, -2));
}
static inline uint16_t readAllocationHeaderWord(void* pAllocation) {
CODE_COVERAGE(520); // Hit
return ((uint16_t*)pAllocation)[-1];
}
static inline mvm_TfHostFunction* vm_getResolvedImports(VM* vm) {
CODE_COVERAGE(40); // Hit
return (mvm_TfHostFunction*)(vm + 1); // Starts right after the header
}
static inline mvm_HostFunctionID vm_getHostFunctionId(VM* vm, uint16_t hostFunctionIndex) {
LongPtr lpImportTable = getBytecodeSection(vm, BCS_IMPORT_TABLE, NULL);
LongPtr lpImportTableEntry = LongPtr_add(lpImportTable, hostFunctionIndex * sizeof (vm_TsImportTableEntry));
return LongPtr_read2_aligned(lpImportTableEntry);
}
mvm_TeType mvm_typeOf(VM* vm, Value value) {
TeTypeCode tc = deepTypeOf(vm, value);
VM_ASSERT(vm, tc < sizeof typeByTC);
TABLE_COVERAGE(tc, TC_END, 42); // Hit 16/26
return (mvm_TeType)typeByTC[tc];
}
LongPtr vm_toStringUtf8_long(VM* vm, Value value, size_t* out_sizeBytes) {
CODE_COVERAGE(43); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
value = vm_convertToString(vm, value);
TeTypeCode typeCode = deepTypeOf(vm, value);
if (typeCode == TC_VAL_STR_PROTO) {
CODE_COVERAGE_UNTESTED(521); // Not hit
*out_sizeBytes = sizeof PROTO_STR - 1;
return LongPtr_new((void*)&PROTO_STR);
} else {
CODE_COVERAGE(522); // Hit
}
if (typeCode == TC_VAL_STR_LENGTH) {
CODE_COVERAGE_UNTESTED(523); // Not hit
*out_sizeBytes = sizeof LENGTH_STR - 1;
return LongPtr_new((void*)&LENGTH_STR);
} else {
CODE_COVERAGE(524); // Hit
}
VM_ASSERT(vm, (typeCode == TC_REF_STRING) || (typeCode == TC_REF_INTERNED_STRING));
LongPtr lpTarget = DynamicPtr_decode_long(vm, value);
uint16_t headerWord = readAllocationHeaderWord_long(lpTarget);
uint16_t sourceSize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
if (out_sizeBytes) {
CODE_COVERAGE(349); // Hit
*out_sizeBytes = sourceSize - 1; // Without the extra safety null-terminator
} else {
CODE_COVERAGE_UNTESTED(350); // Not hit
}
return lpTarget;
}
/**
* Gets a pointer to the string bytes of the string represented by `value`.
*
* `value` must be a string
*
* Warning: the result is a native pointer and becomes invalid if a GC
* collection occurs.
*/
LongPtr vm_getStringData(VM* vm, Value value) {
CODE_COVERAGE(228); // Hit
TeTypeCode typeCode = deepTypeOf(vm, value);
switch (typeCode) {
case TC_VAL_STR_PROTO:
CODE_COVERAGE_UNTESTED(229); // Not hit
return LongPtr_new((void*)&PROTO_STR);
case TC_VAL_STR_LENGTH:
CODE_COVERAGE(512); // Hit
return LongPtr_new((void*)&LENGTH_STR);
case TC_REF_STRING:
case TC_REF_INTERNED_STRING:
return DynamicPtr_decode_long(vm, value);
default:
VM_ASSERT_UNREACHABLE(vm);
return LongPtr_new(0);
}
}
const char* mvm_toStringUtf8(VM* vm, Value value, size_t* out_sizeBytes) {
CODE_COVERAGE(623); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
/*
* Note: I previously had this function returning a long pointer, but this
* tripped someone up because they passed the result directly to printf, which
* on MSP430 apparently doesn't support arbitrary long pointers (data20
* pointers). Now I just copy it locally.
*/
size_t size; // Size excluding a null terminator
LongPtr lpTarget = vm_toStringUtf8_long(vm, value, &size);
if (out_sizeBytes)
*out_sizeBytes = size;
void* pTarget = LongPtr_truncate(lpTarget);
// Is the string in local memory?
if (LongPtr_new(pTarget) == lpTarget) {
CODE_COVERAGE(624); // Hit
return (const char*)pTarget;
} else {
CODE_COVERAGE_UNTESTED(625); // Not hit
// Allocate a new string in local memory (with additional null terminator)
vm_allocString(vm, size, &pTarget);
memcpy_long(pTarget, lpTarget, size);
return (const char*)pTarget;
}
}
Value mvm_newBoolean(bool source) {
CODE_COVERAGE_UNTESTED(44); // Not hit
return source ? VM_VALUE_TRUE : VM_VALUE_FALSE;
}
Value vm_allocString(VM* vm, size_t sizeBytes, void** out_pData) {
CODE_COVERAGE(45); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
if (sizeBytes < 3) {
TABLE_COVERAGE(sizeBytes, 3, 525); // Hit 2/3
}
// Note: allocating 1 extra byte for the extra null terminator
char* pData = gc_allocateWithHeader(vm, (uint16_t)sizeBytes + 1, TC_REF_STRING);
*out_pData = pData;
// Null terminator
pData[sizeBytes] = '\0';
return ShortPtr_encode(vm, pData);
}
// New string from null-terminated
static Value vm_newStringFromCStrNT(VM* vm, const char* s) {
size_t len = strlen(s);
return mvm_newString(vm, s, len);
}
Value mvm_newString(VM* vm, const char* sourceUtf8, size_t sizeBytes) {
CODE_COVERAGE(46); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
void* data;
Value value = vm_allocString(vm, sizeBytes, &data);
memcpy(data, sourceUtf8, sizeBytes);
return value;
}
static Value getBuiltin(VM* vm, mvm_TeBuiltins builtinID) {
CODE_COVERAGE(526); // Hit
LongPtr lpBuiltins = getBytecodeSection(vm, BCS_BUILTINS, NULL);
LongPtr lpBuiltin = LongPtr_add(lpBuiltins, (int16_t)(builtinID * sizeof (Value)));
Value value = LongPtr_read2_aligned(lpBuiltin);
// Check if the builtin accesses a RAM value via a handle
Value* target = getHandleTargetOrNull(vm, value);
if (target) {
CODE_COVERAGE(212); // Hit
return *target;
} else {
CODE_COVERAGE(213); // Hit
return value;
}
}
/**
* If the value is a handle, this returns a pointer to the global variable
* referenced by the handle. Otherwise, this returns NULL.
*/
static inline Value* getHandleTargetOrNull(VM* vm, Value value) {
CODE_COVERAGE(527); // Hit
if (!Value_isBytecodeMappedPtrOrWellKnown(value)) {
CODE_COVERAGE_UNTESTED(528); // Not hit
return NULL;
} else {
CODE_COVERAGE(529); // Hit
}
uint16_t globalsOffset = getSectionOffset(vm->lpBytecode, BCS_GLOBALS);
uint16_t globalsEndOffset = getSectionOffset(vm->lpBytecode, vm_sectionAfter(vm, BCS_GLOBALS));
if ((value < globalsOffset) || (value >= globalsEndOffset)) {
CODE_COVERAGE(530); // Hit
return NULL;
} else {
CODE_COVERAGE(531); // Hit
}
uint16_t globalIndex = (value - globalsOffset) / 2;
return &vm->globals[globalIndex];
}
/**
* Assigns to the slot pointed to by lpTarget
*
* If lpTarget points to a handle, then the corresponding global variable is
* mutated. Otherwise, the target is directly mutated.
*
* This is used to synthesize mutation of slots in ROM, such as exports,
* builtins, and properties of ROM objects. Such logically-mutable slots *must*
* hold a value that is a BytecodeMappedPtr to a global variable that holds the
* mutable reference.
*
* The function works transparently on RAM or ROM slots.
*/
// TODO: probably SetProperty should use this, so it works on ROM-allocated
// objects/arrays. Probably a good candidate for TDD.
static void setSlot_long(VM* vm, LongPtr lpSlot, Value value) {
CODE_COVERAGE(532); // Hit
Value slotContents = LongPtr_read2_aligned(lpSlot);
// Work out if the target slot is actually a handle.
Value* handleTarget = getHandleTargetOrNull(vm, slotContents);
if (handleTarget) {
CODE_COVERAGE(533); // Hit
// Set the corresponding global variable
*handleTarget = value;
return;
} else {
CODE_COVERAGE_UNTESTED(534); // Not hit
}
// Otherwise, for the mutation must be valid, the slot must be in RAM.
// We never mutate through a long pointer, because anything mutable must be in
// RAM and anything in RAM must be addressable by a short pointer
Value* pSlot = LongPtr_truncate(lpSlot);
// Check the truncation hasn't lost anything. If this fails, the slot could be
// in ROM. If this passes, the slot
VM_ASSERT(vm, LongPtr_new(pSlot) == lpSlot);
// The compiler must never produce bytecode that is able to attempt to write
// to the bytecode image itself, but just to catch mistakes, here's an
// assertion to make sure it doesn't write to bytecode. In a properly working
// system (compiler + engine), this assertion isn't needed
VM_ASSERT(vm, (lpSlot < vm->lpBytecode) ||
(lpSlot >= LongPtr_add(vm->lpBytecode, getBytecodeSize(vm))));
*pSlot = value;
}
static void setBuiltin(VM* vm, mvm_TeBuiltins builtinID, Value value) {
CODE_COVERAGE(535); // Hit
LongPtr lpBuiltins = getBytecodeSection(vm, BCS_BUILTINS, NULL);
LongPtr lpBuiltin = LongPtr_add(lpBuiltins, (int16_t)(builtinID * sizeof (Value)));
setSlot_long(vm, lpBuiltin, value);
}
// Warning: this function trashes the word at pObjectValue.
// Note: out_propertyValue may point to the same address as pObjectValue
static TeError getProperty(VM* vm, Value* pObjectValue, Value* pPropertyName, Value* out_propertyValue) {
CODE_COVERAGE(48); // Hit
mvm_TeError err;
LongPtr lpArr;
LongPtr lpClass;
uint16_t length;
TeTypeCode type;
Value objectValue;
Value propertyName;
// This function may trigger a GC cycle because it may add a cell to the string intern table
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
// Note: toPropertyName can trigger a GC cycle
err = toPropertyName(vm, pPropertyName);
if (err != MVM_E_SUCCESS) return err;
LBL_GET_PROPERTY:
propertyName = *pPropertyName;
objectValue = *pObjectValue;
type = deepTypeOf(vm, objectValue);
switch (type) {
case TC_REF_UINT8_ARRAY: {
CODE_COVERAGE(339); // Hit
lpArr = DynamicPtr_decode_long(vm, objectValue);
uint16_t header = readAllocationHeaderWord_long(lpArr);
length = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
if (propertyName == VM_VALUE_STR_LENGTH) {
CODE_COVERAGE(340); // Hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VirtualInt14_encode(vm, length);
return MVM_E_SUCCESS;
} else {
CODE_COVERAGE(341); // Hit
}
if (!Value_isVirtualInt14(propertyName)) {
CODE_COVERAGE_ERROR_PATH(342); // Not hit
return MVM_E_INVALID_ARRAY_INDEX;
}
int16_t index = VirtualInt14_decode(vm, propertyName);
if ((index < 0) || (index >= length)) {
CODE_COVERAGE_ERROR_PATH(343); // Not hit
return MVM_E_INVALID_ARRAY_INDEX;
}
uint8_t byteValue = LongPtr_read1(LongPtr_add(lpArr, (uint16_t)index));
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VirtualInt14_encode(vm, byteValue);
return MVM_E_SUCCESS;
}
case TC_REF_PROPERTY_LIST: {
CODE_COVERAGE(359); // Hit
LongPtr lpPropertyList = DynamicPtr_decode_long(vm, objectValue);
DynamicPtr dpProto = READ_FIELD_2(lpPropertyList, TsPropertyList, dpProto);
if (propertyName == VM_VALUE_STR_PROTO) {
CODE_COVERAGE_UNIMPLEMENTED(326); // Hit
*out_propertyValue = dpProto;
return MVM_E_SUCCESS;
}
while (lpPropertyList) {
uint16_t headerWord = readAllocationHeaderWord_long(lpPropertyList);
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t propCount = (size - sizeof (TsPropertyList)) / 4;
LongPtr p = LongPtr_add(lpPropertyList, sizeof (TsPropertyList));
while (propCount--) {
Value key = LongPtr_read2_aligned(p);
p = LongPtr_add(p, 2);
Value value = LongPtr_read2_aligned(p);
p = LongPtr_add(p, 2);
if (key == propertyName) {
CODE_COVERAGE(361); // Hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = value;
return MVM_E_SUCCESS;
} else {
CODE_COVERAGE(362); // Hit
}
}
DynamicPtr dpNext = READ_FIELD_2(lpPropertyList, TsPropertyList, dpNext);
// Move to next group, if there is one
if (dpNext != VM_VALUE_NULL) {
CODE_COVERAGE(536); // Hit
lpPropertyList = DynamicPtr_decode_long(vm, dpNext);
} else { // Otherwise try read from the prototype
CODE_COVERAGE(537); // Hit
lpPropertyList = DynamicPtr_decode_long(vm, dpProto);
if (lpPropertyList) {
CODE_COVERAGE(538); // Hit
dpProto = READ_FIELD_2(lpPropertyList, TsPropertyList, dpProto);
} else {
CODE_COVERAGE(539); // Hit
}
}
}
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VM_VALUE_UNDEFINED;
return MVM_E_SUCCESS;
}
case TC_REF_ARRAY: {
CODE_COVERAGE(363); // Hit
lpArr = DynamicPtr_decode_long(vm, objectValue);
Value viLength = READ_FIELD_2(lpArr, TsArray, viLength);
length = VirtualInt14_decode(vm, viLength);
// Drill in to fixed-length array inside the array
DynamicPtr dpData = READ_FIELD_2(lpArr, TsArray, dpData);
lpArr = DynamicPtr_decode_long(vm, dpData);
goto LBL_GET_PROP_FIXED_LENGTH_ARRAY;
}
case TC_REF_FIXED_LENGTH_ARRAY: {
CODE_COVERAGE(286); // Hit
lpArr = DynamicPtr_decode_long(vm, objectValue);
uint16_t header = readAllocationHeaderWord_long(lpArr);
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
length = size >> 1;
goto LBL_GET_PROP_FIXED_LENGTH_ARRAY;
}
case TC_REF_CLASS: {
CODE_COVERAGE(615); // Hit
lpClass = DynamicPtr_decode_long(vm, objectValue);
// Delegate to the `staticProps` of the class
*pObjectValue = READ_FIELD_2(lpClass, TsClass, staticProps);
goto LBL_GET_PROPERTY;
}
default: return vm_newError(vm, MVM_E_TYPE_ERROR);
}
LBL_GET_PROP_FIXED_LENGTH_ARRAY:
CODE_COVERAGE(323); // Hit
if (propertyName == VM_VALUE_STR_LENGTH) {
CODE_COVERAGE(274); // Hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VirtualInt14_encode(vm, length);
return MVM_E_SUCCESS;
} else if (propertyName == VM_VALUE_STR_PROTO) {
CODE_COVERAGE(275); // Hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = getBuiltin(vm, BIN_ARRAY_PROTO);
return MVM_E_SUCCESS;
} else {
CODE_COVERAGE(276); // Hit
}
// Array index
if (Value_isVirtualInt14(propertyName)) {
CODE_COVERAGE(277); // Hit
int16_t index = VirtualInt14_decode(vm, propertyName);
if (index < 0) {
CODE_COVERAGE_ERROR_PATH(144); // Not hit
return vm_newError(vm, MVM_E_INVALID_ARRAY_INDEX);
}
if ((uint16_t)index >= length) {
CODE_COVERAGE(283); // Hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VM_VALUE_UNDEFINED;
return MVM_E_SUCCESS;
} else {
CODE_COVERAGE(328); // Hit
}
// We've already checked if the value exceeds the length, so lpData
// cannot be null and the capacity must be at least as large as the
// length of the array.
VM_ASSERT(vm, lpArr);
VM_ASSERT(vm, length * 2 <= vm_getAllocationSizeExcludingHeaderFromHeaderWord(readAllocationHeaderWord_long(lpArr)));
Value value = LongPtr_read2_aligned(LongPtr_add(lpArr, (uint16_t)index * 2));
if (value == VM_VALUE_DELETED) {
CODE_COVERAGE(329); // Hit
value = VM_VALUE_UNDEFINED;
} else {
CODE_COVERAGE(364); // Hit
}
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = value;
return MVM_E_SUCCESS;
}
CODE_COVERAGE(278); // Hit
*pObjectValue = getBuiltin(vm, BIN_ARRAY_PROTO);
if (*pObjectValue != VM_VALUE_NULL) {
CODE_COVERAGE(396); // Hit
goto LBL_GET_PROPERTY;
} else {
CODE_COVERAGE_UNTESTED(397); // Not hit
VM_EXEC_SAFE_MODE(*pObjectValue = VM_VALUE_NULL);
*out_propertyValue = VM_VALUE_UNDEFINED;
return MVM_E_SUCCESS;
}
}
static void growArray(VM* vm, Value* pvArr, uint16_t newLength, uint16_t newCapacity) {
CODE_COVERAGE(293); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
VM_ASSERT(vm, newCapacity >= newLength);
if (newCapacity > MAX_ALLOCATION_SIZE / 2) {
CODE_COVERAGE_ERROR_PATH(540); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_ARRAY_TOO_LONG);
}
VM_ASSERT(vm, newCapacity != 0);
uint16_t* pNewData = gc_allocateWithHeader(vm, newCapacity * 2, TC_REF_FIXED_LENGTH_ARRAY);
// Copy values from the old array. Note that the above allocation can trigger
// a GC collection which moves the array, so we need to decode the value again
TsArray* arr = DynamicPtr_decode_native(vm, *pvArr);
DynamicPtr dpOldData = arr->dpData;
uint16_t oldCapacity = 0;
if (dpOldData != VM_VALUE_NULL) {
CODE_COVERAGE(294); // Hit
LongPtr lpOldData = DynamicPtr_decode_long(vm, dpOldData);
uint16_t oldDataHeader = readAllocationHeaderWord_long(lpOldData);
uint16_t oldSize = vm_getAllocationSizeExcludingHeaderFromHeaderWord(oldDataHeader);
VM_ASSERT(vm, (oldSize & 1) == 0);
oldCapacity = oldSize / 2;
memcpy_long(pNewData, lpOldData, oldSize);
} else {
CODE_COVERAGE(310); // Hit
}
CODE_COVERAGE(325); // Hit
VM_ASSERT(vm, newCapacity >= oldCapacity);
// Fill in the rest of the memory as holes
uint16_t* p = &pNewData[oldCapacity];
uint16_t* end = &pNewData[newCapacity];
while (p != end) {
*p++ = VM_VALUE_DELETED;
}
arr->dpData = ShortPtr_encode(vm, pNewData);
arr->viLength = VirtualInt14_encode(vm, newLength);
}
static TeError vm_objectKeys(VM* vm, Value* inout_slot) {
CODE_COVERAGE(636); // Hit
Value obj;
LongPtr lpClass;
LBL_OBJECT_KEYS:
obj = *inout_slot;
TeTypeCode tc = deepTypeOf(vm, obj);
if (tc == TC_REF_CLASS) {
CODE_COVERAGE_UNTESTED(637); // Not hit
lpClass = DynamicPtr_decode_long(vm, obj);
// Delegate to the `staticProps` of the class
*inout_slot = READ_FIELD_2(lpClass, TsClass, staticProps);
goto LBL_OBJECT_KEYS;
}
CODE_COVERAGE(638); // Hit
if (tc != TC_REF_PROPERTY_LIST) {
CODE_COVERAGE_ERROR_PATH(639); // Not hit
return MVM_E_OBJECT_KEYS_ON_NON_OBJECT;
}
// Count the number of properties (first add up the sizes)
uint16_t propsSize = 0;
Value propList = obj;
// Note: the GC packs an object into a single allocation, so this should
// frequently be O(1) and only loop once
do {
LongPtr lpPropList = DynamicPtr_decode_long(vm, propList);
propsSize += vm_getAllocationSize_long(lpPropList) - sizeof(TsPropertyList);
propList = LongPtr_read2_aligned(lpPropList) /* dpNext */;
TABLE_COVERAGE(propList != VM_VALUE_NULL ? 1 : 0, 2, 640); // Hit 2/2
} while (propList != VM_VALUE_NULL);
// Each prop is 4 bytes, and each entry in the array is 2 bytes
uint16_t arrSize = propsSize >> 1;
// If the array is empty, an empty allocation is illegal. A 1-byte allocation
// will be rounded down when asking the size, but rounded up in the allocation
// unit.
if (!arrSize) {
CODE_COVERAGE(641); // Hit
arrSize = 1;
}
// Allocate the new array.
uint16_t* p = gc_allocateWithHeader(vm, arrSize, TC_REF_FIXED_LENGTH_ARRAY);
obj = *inout_slot; // Invalidated by potential GC collection
// Populate the array
propList = obj;
*inout_slot = ShortPtr_encode(vm, p);
do {
LongPtr lpPropList = DynamicPtr_decode_long(vm, propList);
propList = LongPtr_read2_aligned(lpPropList) /* dpNext */;
uint16_t propsSize = vm_getAllocationSize_long(lpPropList) - sizeof(TsPropertyList);
LongPtr lpProp = LongPtr_add(lpPropList, sizeof(TsPropertyList));
TABLE_COVERAGE(propsSize != 0 ? 1 : 0, 2, 642); // Hit 2/2
while (propsSize) {
*p = LongPtr_read2_aligned(lpProp);
p++; // Move to next entry in array
// Each property cell is 4 bytes
lpProp /* prop */ = LongPtr_add(lpProp /* prop */, 4);
propsSize -= 4;
}
TABLE_COVERAGE(propList != VM_VALUE_NULL ? 1 : 0, 2, 643); // Hit 2/2
} while (propList != VM_VALUE_NULL);
return MVM_E_SUCCESS;
}
/**
* Note: the operands are passed by pointer to make sure they're anchored in the
* stack and that if the GC moves their targets, we will be using the latest
* values. The operands are:
*
* - pOperands[0]: object
* - pOperands[1]: propertyName
* - pOperands[2]: propertyValue
*/
static TeError setProperty(VM* vm, Value* pOperands) {
CODE_COVERAGE(49); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
mvm_TeError err;
LongPtr lpClass;
TeTypeCode type;
// This function may trigger a GC cycle because it may add a cell to the string intern table
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
err = toPropertyName(vm, &pOperands[1]);
if (err != MVM_E_SUCCESS) return err;
MVM_LOCAL(Value, vObjectValue, 0);
MVM_LOCAL(Value, vPropertyName, pOperands[1]);
MVM_LOCAL(Value, vPropertyValue, pOperands[2]);
LBL_SET_PROPERTY:
MVM_SET_LOCAL(vObjectValue, pOperands[0]);
type = deepTypeOf(vm, MVM_GET_LOCAL(vObjectValue));
switch (type) {
case TC_REF_UINT8_ARRAY: {
CODE_COVERAGE(594); // Hit
// It's not valid for the optimizer to move a buffer into ROM if it's
// ever written to, so it must be in RAM.
VM_ASSERT(vm, Value_isShortPtr(MVM_GET_LOCAL(vObjectValue)));
uint8_t* p = ShortPtr_decode(vm, MVM_GET_LOCAL(vObjectValue));
uint16_t header = readAllocationHeaderWord(p);
uint16_t length = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
if (!Value_isVirtualInt14(MVM_GET_LOCAL(vPropertyName))) {
CODE_COVERAGE_ERROR_PATH(595); // Not hit
return MVM_E_INVALID_ARRAY_INDEX;
}
int16_t index = VirtualInt14_decode(vm, MVM_GET_LOCAL(vPropertyName));
if ((index < 0) || (index >= length)) {
CODE_COVERAGE_ERROR_PATH(612); // Not hit
return MVM_E_INVALID_ARRAY_INDEX;
}
Value byteValue = MVM_GET_LOCAL(vPropertyValue);
if (!Value_isVirtualUInt8(byteValue)) {
// For performance reasons, Microvium does not automatically coerce
// values to bytes.
CODE_COVERAGE_ERROR_PATH(613); // Not hit
return MVM_E_CAN_ONLY_ASSIGN_BYTES_TO_UINT8_ARRAY;
}
p[index] = (uint8_t)VirtualInt14_decode(vm, byteValue);
return MVM_E_SUCCESS;
}
case TC_REF_PROPERTY_LIST: {
CODE_COVERAGE(366); // Hit
if (MVM_GET_LOCAL(vPropertyName) == VM_VALUE_STR_PROTO) {
CODE_COVERAGE_UNIMPLEMENTED(327); // Not hit
VM_NOT_IMPLEMENTED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
} else {
CODE_COVERAGE(541); // Hit
}
// Note: while objects in general can be in ROM, objects which are
// writable must always be in RAM.
MVM_LOCAL(TsPropertyList*, pPropertyList, DynamicPtr_decode_native(vm, MVM_GET_LOCAL(vObjectValue)));
while (true) {
CODE_COVERAGE(367); // Hit
uint16_t headerWord = readAllocationHeaderWord(MVM_GET_LOCAL(pPropertyList));
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t propCount = (size - sizeof (TsPropertyList)) / 4;
uint16_t* p = (uint16_t*)(MVM_GET_LOCAL(pPropertyList) + 1);
while (propCount--) {
Value key = *p++;
// We can do direct comparison because the strings have been interned,
// and numbers are represented in a normalized way.
if (key == MVM_GET_LOCAL(vPropertyName)) {
CODE_COVERAGE(368); // Hit
*p = MVM_GET_LOCAL(vPropertyValue);
return MVM_E_SUCCESS;
} else {
// Skip to next property
p++;
CODE_COVERAGE(369); // Hit
}
}
DynamicPtr dpNext = MVM_GET_LOCAL(pPropertyList)->dpNext;
// Move to next group, if there is one
if (dpNext != VM_VALUE_NULL) {
CODE_COVERAGE(542); // Hit
MVM_SET_LOCAL(pPropertyList, DynamicPtr_decode_native(vm, dpNext));
} else {
CODE_COVERAGE(543); // Hit
break;
}
}
// If we reach the end, then this is a new property. We add new properties
// by just appending a new TsPropertyList onto the linked list. The GC
// will compact these into the head later.
TsPropertyCell* pNewCell = GC_ALLOCATE_TYPE(vm, TsPropertyCell, TC_REF_PROPERTY_LIST);
// GC collection invalidates the following values so we need to refresh
// them from the stack slots.
MVM_SET_LOCAL(vPropertyName, pOperands[1]);
MVM_SET_LOCAL(vPropertyValue, pOperands[2]);
MVM_SET_LOCAL(pPropertyList, DynamicPtr_decode_native(vm, pOperands[0]));
/*
Note: This is a bit of a pain. When we allocate the new cell, it may or
may not trigger a GC collection cycle. If it does, then the object may be
moved AND COMPACTED, so the linked list chain of properties is different
to before (or may not be different, if there was no GC cycle), so we need
to re-iterate the linked list to find the last node, where we append the
property.
*/
while (true) {
DynamicPtr dpNext = MVM_GET_LOCAL(pPropertyList)->dpNext;
if (dpNext != VM_VALUE_NULL) {
MVM_SET_LOCAL(pPropertyList, DynamicPtr_decode_native(vm, dpNext));
} else {
break;
}
}
ShortPtr spNewCell = ShortPtr_encode(vm, pNewCell);
pNewCell->base.dpNext = VM_VALUE_NULL;
pNewCell->base.dpProto = VM_VALUE_NULL; // Not used because this is a child cell, but still needs a value because the GC sees it.
pNewCell->key = MVM_GET_LOCAL(vPropertyName);
pNewCell->value = MVM_GET_LOCAL(vPropertyValue);
// Attach to linked list. This needs to be a long-pointer write because we
// don't know if the original property list was in data memory.
//
// Note: `pPropertyList` currently points to the last property list in
// the chain.
MVM_GET_LOCAL(pPropertyList)->dpNext = spNewCell;
return MVM_E_SUCCESS;
}
case TC_REF_ARRAY: {
CODE_COVERAGE(370); // Hit
// Note: while objects in general can be in ROM, objects which are
// writable must always be in RAM.
MVM_LOCAL(TsArray*, arr, DynamicPtr_decode_native(vm, MVM_GET_LOCAL(vObjectValue)));
VirtualInt14 viLength = MVM_GET_LOCAL(arr)->viLength;
VM_ASSERT(vm, Value_isVirtualInt14(viLength));
uint16_t oldLength = VirtualInt14_decode(vm, viLength);
MVM_LOCAL(DynamicPtr, dpData, MVM_GET_LOCAL(arr)->dpData);
MVM_LOCAL(uint16_t*, pData, NULL);
uint16_t oldCapacity = 0;
if (MVM_GET_LOCAL(dpData) != VM_VALUE_NULL) {
CODE_COVERAGE(544); // Hit
VM_ASSERT(vm, Value_isShortPtr(MVM_GET_LOCAL(dpData)));
MVM_SET_LOCAL(pData, DynamicPtr_decode_native(vm, MVM_GET_LOCAL(dpData)));
uint16_t dataSize = vm_getAllocationSize(MVM_GET_LOCAL(pData));
oldCapacity = dataSize / 2;
} else {
CODE_COVERAGE(545); // Hit
}
// If the property name is "length" then we'll be changing the length
if (MVM_GET_LOCAL(vPropertyName) == VM_VALUE_STR_LENGTH) {
CODE_COVERAGE(282); // Hit
if (!Value_isVirtualInt14(MVM_GET_LOCAL(vPropertyValue)))
MVM_FATAL_ERROR(vm, MVM_E_TYPE_ERROR);
uint16_t newLength = VirtualInt14_decode(vm, MVM_GET_LOCAL(vPropertyValue));
if (newLength < oldLength) { // Making array smaller
CODE_COVERAGE(176); // Hit
// pData will not be null because oldLength must be more than 1 for it to get here
VM_ASSERT(vm, MVM_GET_LOCAL(pData));
// Wipe array items that aren't reachable
uint16_t count = oldLength - newLength;
uint16_t* p = &MVM_GET_LOCAL(pData)[newLength];
while (count--)
*p++ = VM_VALUE_DELETED;
MVM_GET_LOCAL(arr)->viLength = VirtualInt14_encode(vm, newLength);
return MVM_E_SUCCESS;
} else if (newLength == oldLength) {
CODE_COVERAGE_UNTESTED(546); // Not hit
/* Do nothing */
} else if (newLength <= oldCapacity) { // Array is getting bigger, but still less than capacity
CODE_COVERAGE(287); // Hit
// We can just overwrite the length field. Note that the newly
// uncovered memory is already filled with VM_VALUE_DELETED
MVM_GET_LOCAL(arr)->viLength = VirtualInt14_encode(vm, newLength);
return MVM_E_SUCCESS;
} else { // Make array bigger
CODE_COVERAGE(288); // Hit
// I'll assume that direct assignments to the length mean that people
// know exactly how big the array should be, so we don't add any
// extra capacity
uint16_t newCapacity = newLength;
growArray(vm, &pOperands[0], newLength, newCapacity);
return MVM_E_SUCCESS;
}
} else if (MVM_GET_LOCAL(vPropertyName) == VM_VALUE_STR_PROTO) { // Writing to the __proto__ property
CODE_COVERAGE_UNTESTED(289); // Not hit
// We could make this read/write in future
return vm_newError(vm, MVM_E_PROTO_IS_READONLY);
} else if (Value_isVirtualInt14(MVM_GET_LOCAL(vPropertyName))) { // Array index
CODE_COVERAGE(285); // Hit
int16_t index = VirtualInt14_decode(vm, MVM_GET_LOCAL(vPropertyName) );
if (index < 0) {
CODE_COVERAGE_ERROR_PATH(24); // Not hit
return vm_newError(vm, MVM_E_INVALID_ARRAY_INDEX);
}
// Need to expand the array?
if ((uint16_t)index >= oldLength) {
CODE_COVERAGE(290); // Hit
uint16_t newLength = (uint16_t)index + 1;
if ((uint16_t)index < oldCapacity) {
CODE_COVERAGE(291); // Hit
// The length changes to include the value. The extra slots are
// already filled in with holes from the original allocation.
MVM_GET_LOCAL(arr)->viLength = VirtualInt14_encode(vm, newLength);
} else {
CODE_COVERAGE(292); // Hit
// We expand the capacity more aggressively here because this is the
// path used when we push into arrays or just assign values to an
// array in a loop.
uint16_t newCapacity = oldCapacity * 2;
if (newCapacity < 4) newCapacity = 4;
if (newCapacity < newLength) newCapacity = newLength;
growArray(vm, &pOperands[0], newLength, newCapacity);
MVM_SET_LOCAL(vPropertyValue, pOperands[2]); // Value could have changed due to GC collection
MVM_SET_LOCAL(vObjectValue, pOperands[0]); // Value could have changed due to GC collection
MVM_SET_LOCAL(arr, DynamicPtr_decode_native(vm, MVM_GET_LOCAL(vObjectValue))); // Value could have changed due to GC collection
}
} // End of array expansion
// By this point, the array should have expanded as necessary
MVM_SET_LOCAL(dpData, MVM_GET_LOCAL(arr)->dpData);
VM_ASSERT(vm, MVM_GET_LOCAL(dpData) != VM_VALUE_NULL);
VM_ASSERT(vm, Value_isShortPtr(MVM_GET_LOCAL(dpData)));
MVM_SET_LOCAL(pData, DynamicPtr_decode_native(vm, MVM_GET_LOCAL(dpData)));
VM_ASSERT(vm, !!MVM_GET_LOCAL(pData));
// Write the item to memory
MVM_GET_LOCAL(pData)[(uint16_t)index] = MVM_GET_LOCAL(vPropertyValue);
return MVM_E_SUCCESS;
}
// Else not a valid array index
CODE_COVERAGE_ERROR_PATH(140); // Not hit
return vm_newError(vm, MVM_E_INVALID_ARRAY_INDEX);
}
case TC_REF_CLASS: {
CODE_COVERAGE(630); // Hit
lpClass = DynamicPtr_decode_long(vm, MVM_GET_LOCAL(vObjectValue));
// Delegate to the `staticProps` of the class
pOperands[0] = READ_FIELD_2(lpClass, TsClass, staticProps);
goto LBL_SET_PROPERTY;
}
default: return vm_newError(vm, MVM_E_TYPE_ERROR);
}
}
/** Converts the argument to either an TC_VAL_INT14 or a TC_REF_INTERNED_STRING, or gives an error */
static TeError toPropertyName(VM* vm, Value* value) {
CODE_COVERAGE(50); // Hit
// This function may trigger a GC cycle because it may add a cell to the string intern table
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
// Property names in microvium are either integer indexes or non-integer interned strings
TeTypeCode type = deepTypeOf(vm, *value);
switch (type) {
// These are already valid property names
case TC_VAL_INT14: {
CODE_COVERAGE(279); // Hit
if (VirtualInt14_decode(vm, *value) < 0) {
CODE_COVERAGE_UNTESTED(280); // Not hit
return vm_newError(vm, MVM_E_RANGE_ERROR);
}
CODE_COVERAGE(281); // Hit
return MVM_E_SUCCESS;
}
case TC_REF_INTERNED_STRING: {
CODE_COVERAGE(373); // Hit
return MVM_E_SUCCESS;
}
case TC_REF_INT32: {
CODE_COVERAGE_ERROR_PATH(374); // Not hit
// 32-bit numbers are out of the range of supported array indexes
return vm_newError(vm, MVM_E_RANGE_ERROR);
}
case TC_REF_STRING: {
CODE_COVERAGE(375); // Hit
// Note: In Microvium at the moment, it's illegal to use an integer-valued
// string as a property name. If the string is in bytecode, it will only
// have the type TC_REF_STRING if it's a number and is illegal.
if (!Value_isShortPtr(*value)) {
return vm_newError(vm, MVM_E_TYPE_ERROR);
}
if (vm_ramStringIsNonNegativeInteger(vm, *value)) {
CODE_COVERAGE_ERROR_PATH(378); // Not hit
return vm_newError(vm, MVM_E_TYPE_ERROR);
} else {
CODE_COVERAGE(379); // Hit
}
// Strings need to be converted to interned strings in order to be valid
// property names. This is because properties are searched by reference
// equality.
toInternedString(vm, value);
return MVM_E_SUCCESS;
}
case TC_VAL_STR_LENGTH: {
CODE_COVERAGE(272); // Hit
return MVM_E_SUCCESS;
}
case TC_VAL_STR_PROTO: {
CODE_COVERAGE(273); // Hit
return MVM_E_SUCCESS;
}
default: {
CODE_COVERAGE_ERROR_PATH(380); // Not hit
return vm_newError(vm, MVM_E_TYPE_ERROR);
}
}
}
// Converts a TC_REF_STRING to a TC_REF_INTERNED_STRING
// TODO: Test cases for this function
static void toInternedString(VM* vm, Value* pValue) {
CODE_COVERAGE(51); // Hit
Value value = *pValue;
VM_ASSERT(vm, deepTypeOf(vm, value) == TC_REF_STRING);
// This function may trigger a GC cycle because it may add a cell to the intern table
VM_ASSERT(vm, !vm->stack || !vm->stack->reg.usingCachedRegisters);
// TC_REF_STRING values are always in GC memory. If they were in flash, they'd
// already be TC_REF_INTERNED_STRING.
char* pStr1 = DynamicPtr_decode_native(vm, value);
uint16_t str1Size = vm_getAllocationSize(pStr1);
LongPtr lpStr1 = LongPtr_new(pStr1);
// Note: the sizes here include the null terminator
if ((str1Size == sizeof PROTO_STR) && (memcmp_long(lpStr1, LongPtr_new((void*)&PROTO_STR), sizeof PROTO_STR) == 0)) {
CODE_COVERAGE_UNTESTED(547); // Not hit
*pValue = VM_VALUE_STR_PROTO;
} else if ((str1Size == sizeof LENGTH_STR) && (memcmp_long(lpStr1, LongPtr_new((void*)&LENGTH_STR), sizeof LENGTH_STR) == 0)) {
CODE_COVERAGE(548); // Hit
*pValue = VM_VALUE_STR_LENGTH;
} else {
CODE_COVERAGE(549); // Hit
}
LongPtr lpBytecode = vm->lpBytecode;
// We start by searching the string table for interned strings that are baked
// into the ROM. These are stored alphabetically, so we can perform a binary
// search.
uint16_t stringTableOffset = getSectionOffset(vm->lpBytecode, BCS_STRING_TABLE);
uint16_t stringTableSize = getSectionOffset(vm->lpBytecode, vm_sectionAfter(vm, BCS_STRING_TABLE)) - stringTableOffset;
int strCount = stringTableSize / sizeof (Value);
int first = 0;
int last = strCount - 1;
while (first <= last) {
CODE_COVERAGE(381); // Hit
int middle = (first + last) / 2;
uint16_t str2Offset = stringTableOffset + middle * 2;
Value vStr2 = LongPtr_read2_aligned(LongPtr_add(lpBytecode, str2Offset));
LongPtr lpStr2 = DynamicPtr_decode_long(vm, vStr2);
uint16_t header = readAllocationHeaderWord_long(lpStr2);
VM_ASSERT(vm, vm_getTypeCodeFromHeaderWord(header) == TC_REF_INTERNED_STRING);
uint16_t str2Size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
int compareSize = str1Size < str2Size ? str1Size : str2Size;
int c = memcmp_long(lpStr1, lpStr2, compareSize);
// If they compare equal for the range that they have in common, we check the length
if (c == 0) {
CODE_COVERAGE(382); // Hit
if (str1Size < str2Size) {
CODE_COVERAGE_UNTESTED(383); // Not hit
c = -1;
} else if (str1Size > str2Size) {
CODE_COVERAGE_UNTESTED(384); // Not hit
c = 1;
} else {
CODE_COVERAGE(385); // Hit
// Exact match
*pValue = vStr2;
return;
}
}
// c is > 0 if the string we're searching for comes after the middle point
if (c > 0) {
CODE_COVERAGE(386); // Hit
first = middle + 1;
} else {
CODE_COVERAGE(387); // Hit
last = middle - 1;
}
}
// At this point, we haven't found the interned string in the bytecode. We
// need to check in RAM. Now we're comparing an in-RAM string against other
// in-RAM strings. We're looking for an exact match, not performing a binary
// search with inequality comparison, since the linked list of interned
// strings in RAM is not sorted.
Value vInternedStrings = getBuiltin(vm, BIN_INTERNED_STRINGS);
Value spCell = vInternedStrings;
while (spCell != VM_VALUE_UNDEFINED) {
CODE_COVERAGE(388); // Hit
VM_ASSERT(vm, Value_isShortPtr(spCell));
TsInternedStringCell* pCell = ShortPtr_decode(vm, spCell);
Value vStr2 = pCell->str;
char* pStr2 = ShortPtr_decode(vm, vStr2);
uint16_t str2Header = readAllocationHeaderWord(pStr2);
uint16_t str2Size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(str2Header);
// The sizes have to match for the strings to be equal
if (str2Size == str1Size) {
CODE_COVERAGE(389); // Hit
// Note: we use memcmp instead of strcmp because strings are allowed to
// have embedded null terminators.
int c = memcmp(pStr1, pStr2, str1Size);
// Equal?
if (c == 0) {
CODE_COVERAGE(390); // Hit
*pValue = vStr2;
return;
} else {
CODE_COVERAGE(391); // Hit
}
} else {
CODE_COVERAGE(550); // Hit
}
spCell = pCell->spNext;
TABLE_COVERAGE(spCell ? 1 : 0, 2, 551); // Hit 1/2
}
CODE_COVERAGE(616); // Hit
// If we get here, it means there was no matching interned string already
// existing in ROM or RAM. We upgrade the current string to a
// TC_REF_INTERNED_STRING, since we now know it doesn't conflict with any existing
// existing interned strings.
setHeaderWord(vm, pStr1, TC_REF_INTERNED_STRING, str1Size);
// Add the string to the linked list of interned strings
TsInternedStringCell* pCell = GC_ALLOCATE_TYPE(vm, TsInternedStringCell, TC_REF_FIXED_LENGTH_ARRAY);
value = *pValue; // Invalidated by potential GC collection
// Push onto linked list2
pCell->spNext = vInternedStrings;
pCell->str = value;
setBuiltin(vm, BIN_INTERNED_STRINGS, ShortPtr_encode(vm, pCell));
}
static int memcmp_long(LongPtr p1, LongPtr p2, size_t size) {
CODE_COVERAGE(471); // Hit
return MVM_LONG_MEM_CMP(p1, p2, size);
}
static void memcpy_long(void* target, LongPtr source, size_t size) {
CODE_COVERAGE(9); // Hit
MVM_LONG_MEM_CPY(target, source, size);
}
/** Size of string excluding bonus null terminator */
static uint16_t vm_stringSizeUtf8(VM* vm, Value value) {
CODE_COVERAGE(53); // Hit
TeTypeCode typeCode = deepTypeOf(vm, value);
switch (typeCode) {
case TC_REF_STRING:
case TC_REF_INTERNED_STRING: {
LongPtr lpStr = DynamicPtr_decode_long(vm, value);
uint16_t headerWord = readAllocationHeaderWord_long(lpStr);
// Less 1 because of the bonus null terminator
return vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord) - 1;
}
case TC_VAL_STR_PROTO: {
CODE_COVERAGE_UNTESTED(552); // Not hit
return sizeof PROTO_STR - 1;
}
case TC_VAL_STR_LENGTH: {
CODE_COVERAGE(608); // Hit
return sizeof LENGTH_STR - 1;
}
default:
VM_ASSERT_UNREACHABLE(vm);
return 0;
}
}
/**
* Checks if a string contains only decimal digits (and is not empty). May only
* be called on TC_REF_STRING and only those in GC memory.
*/
static bool vm_ramStringIsNonNegativeInteger(VM* vm, Value str) {
CODE_COVERAGE(55); // Hit
VM_ASSERT(vm, deepTypeOf(vm, str) == TC_REF_STRING);
char* pStr = ShortPtr_decode(vm, str);
// Length excluding bonus null terminator
uint16_t len = vm_getAllocationSize(pStr) - 1;
char* p = pStr;
if (!len) {
CODE_COVERAGE_UNTESTED(554); // Not hit
return false;
} else {
CODE_COVERAGE(555); // Hit
}
while (len--) {
CODE_COVERAGE(398); // Hit
if (!isdigit(*p++)) {
CODE_COVERAGE(399); // Hit
return false;
} else {
CODE_COVERAGE_UNTESTED(400); // Not hit
}
}
return true;
}
TeError toInt32Internal(mvm_VM* vm, mvm_Value value, int32_t* out_result) {
CODE_COVERAGE(56); // Hit
// TODO: when the type codes are more stable, we should convert these to a table.
*out_result = 0;
TeTypeCode type = deepTypeOf(vm, value);
MVM_SWITCH(type, TC_END - 1) {
MVM_CASE(TC_VAL_INT14):
MVM_CASE(TC_REF_INT32): {
CODE_COVERAGE(401); // Hit
*out_result = vm_readInt32(vm, type, value);
return MVM_E_SUCCESS;
}
MVM_CASE(TC_REF_FLOAT64): {
CODE_COVERAGE(402); // Hit
return MVM_E_FLOAT64;
}
MVM_CASE(TC_REF_STRING): {
CODE_COVERAGE_UNIMPLEMENTED(403); // Not hit
VM_NOT_IMPLEMENTED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
MVM_CASE(TC_REF_INTERNED_STRING): {
CODE_COVERAGE_UNIMPLEMENTED(404); // Not hit
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
MVM_CASE(TC_VAL_STR_LENGTH): {
CODE_COVERAGE_UNIMPLEMENTED(270); // Not hit
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
MVM_CASE(TC_VAL_STR_PROTO): {
CODE_COVERAGE_UNIMPLEMENTED(271); // Not hit
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
MVM_CASE(TC_REF_PROPERTY_LIST): {
CODE_COVERAGE(405); // Hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_ARRAY): {
CODE_COVERAGE_UNTESTED(406); // Not hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_FUNCTION): {
CODE_COVERAGE(408); // Hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_HOST_FUNC): {
CODE_COVERAGE_UNTESTED(409); // Not hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_CLOSURE): {
CODE_COVERAGE_UNTESTED(410); // Not hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_UINT8_ARRAY): {
CODE_COVERAGE_UNTESTED(411); // Not hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_VIRTUAL): {
CODE_COVERAGE_UNTESTED(632); // Not hit
VM_RESERVED(vm);
return MVM_E_FATAL_ERROR_MUST_KILL_VM;
}
MVM_CASE(TC_REF_CLASS): {
CODE_COVERAGE(633); // Hit
return MVM_E_NAN;
}
MVM_CASE(TC_REF_SYMBOL): {
CODE_COVERAGE_UNTESTED(412); // Not hit
return MVM_E_NAN;
}
MVM_CASE(TC_VAL_UNDEFINED): {
CODE_COVERAGE(413); // Hit
return MVM_E_NAN;
}
MVM_CASE(TC_VAL_NULL): {
CODE_COVERAGE(414); // Hit
break;
}
MVM_CASE(TC_VAL_TRUE): {
CODE_COVERAGE_UNTESTED(415); // Not hit
*out_result = 1; break;
}
MVM_CASE(TC_VAL_FALSE): {
CODE_COVERAGE_UNTESTED(416); // Not hit
break;
}
MVM_CASE(TC_VAL_NAN): {
CODE_COVERAGE(417); // Hit
return MVM_E_NAN;
}
MVM_CASE(TC_VAL_NEG_ZERO): {
CODE_COVERAGE(418); // Hit
return MVM_E_NEG_ZERO;
}
MVM_CASE(TC_VAL_DELETED): {
CODE_COVERAGE_UNTESTED(419); // Not hit
return MVM_E_NAN;
}
default:
VM_ASSERT_UNREACHABLE(vm);
}
return MVM_E_SUCCESS;
}
int32_t mvm_toInt32(mvm_VM* vm, mvm_Value value) {
CODE_COVERAGE(57); // Hit
int32_t result;
TeError err = toInt32Internal(vm, value, &result);
if (err == MVM_E_SUCCESS) {
CODE_COVERAGE(420); // Hit
return result;
} else if (err == MVM_E_NAN) {
CODE_COVERAGE(421); // Hit
return 0;
} else if (err == MVM_E_NEG_ZERO) {
CODE_COVERAGE_UNTESTED(422); // Not hit
return 0;
} else {
CODE_COVERAGE_UNTESTED(423); // Not hit
}
VM_ASSERT(vm, deepTypeOf(vm, value) == TC_REF_FLOAT64);
#if MVM_SUPPORT_FLOAT
return (int32_t)mvm_toFloat64(vm, value);
#else // !MVM_SUPPORT_FLOAT
// If things were compiled correctly, there shouldn't be any floats in the
// system at all
return 0;
#endif
}
#if MVM_SUPPORT_FLOAT
MVM_FLOAT64 mvm_toFloat64(mvm_VM* vm, mvm_Value value) {
CODE_COVERAGE(58); // Hit
int32_t result;
TeError err = toInt32Internal(vm, value, &result);
if (err == MVM_E_SUCCESS) {
CODE_COVERAGE(424); // Hit
return result;
} else if (err == MVM_E_NAN) {
CODE_COVERAGE(425); // Hit
return MVM_FLOAT64_NAN;
} else if (err == MVM_E_NEG_ZERO) {
CODE_COVERAGE(426); // Hit
return -0.0;
} else {
CODE_COVERAGE(427); // Hit
}
VM_ASSERT(vm, deepTypeOf(vm, value) == TC_REF_FLOAT64);
LongPtr lpFloat = DynamicPtr_decode_long(vm, value);
MVM_FLOAT64 f;
memcpy_long(&f, lpFloat, sizeof f);
return f;
}
#endif // MVM_SUPPORT_FLOAT
// See implementation of mvm_equal for the meaning of each
typedef enum TeEqualityAlgorithm {
EA_NONE,
EA_COMPARE_PTR_VALUE_AND_TYPE,
EA_COMPARE_NON_PTR_TYPE,
EA_COMPARE_REFERENCE,
EA_NOT_EQUAL,
EA_COMPARE_STRING,
} TeEqualityAlgorithm;
static const TeEqualityAlgorithm equalityAlgorithmByTypeCode[TC_END] = {
EA_NONE, // TC_REF_TOMBSTONE = 0x0
EA_COMPARE_PTR_VALUE_AND_TYPE, // TC_REF_INT32 = 0x1
EA_COMPARE_PTR_VALUE_AND_TYPE, // TC_REF_FLOAT64 = 0x2
EA_COMPARE_STRING, // TC_REF_STRING = 0x3
EA_COMPARE_STRING, // TC_REF_INTERNED_STRING = 0x4
EA_COMPARE_REFERENCE, // TC_REF_FUNCTION = 0x5
EA_COMPARE_PTR_VALUE_AND_TYPE, // TC_REF_HOST_FUNC = 0x6
EA_COMPARE_PTR_VALUE_AND_TYPE, // TC_REF_BIG_INT = 0x7
EA_COMPARE_REFERENCE, // TC_REF_SYMBOL = 0x8
EA_NONE, // TC_REF_CLASS = 0x9
EA_NONE, // TC_REF_VIRTUAL = 0xA
EA_NONE, // TC_REF_RESERVED_1 = 0xB
EA_COMPARE_REFERENCE, // TC_REF_PROPERTY_LIST = 0xC
EA_COMPARE_REFERENCE, // TC_REF_ARRAY = 0xD
EA_COMPARE_REFERENCE, // TC_REF_FIXED_LENGTH_ARRAY = 0xE
EA_COMPARE_REFERENCE, // TC_REF_CLOSURE = 0xF
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_INT14 = 0x10
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_UNDEFINED = 0x11
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_NULL = 0x12
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_TRUE = 0x13
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_FALSE = 0x14
EA_NOT_EQUAL, // TC_VAL_NAN = 0x15
EA_COMPARE_NON_PTR_TYPE, // TC_VAL_NEG_ZERO = 0x16
EA_NONE, // TC_VAL_DELETED = 0x17
EA_COMPARE_STRING, // TC_VAL_STR_LENGTH = 0x18
EA_COMPARE_STRING, // TC_VAL_STR_PROTO = 0x19
};
bool mvm_equal(mvm_VM* vm, mvm_Value a, mvm_Value b) {
CODE_COVERAGE(462); // Hit
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
TeTypeCode aType = deepTypeOf(vm, a);
TeTypeCode bType = deepTypeOf(vm, b);
TeEqualityAlgorithm algorithmA = equalityAlgorithmByTypeCode[aType];
TeEqualityAlgorithm algorithmB = equalityAlgorithmByTypeCode[bType];
TABLE_COVERAGE(algorithmA, 6, 556); // Hit 4/6
TABLE_COVERAGE(algorithmB, 6, 557); // Hit 4/6
TABLE_COVERAGE(aType, TC_END, 558); // Hit 6/26
TABLE_COVERAGE(bType, TC_END, 559); // Hit 8/26
// If the values aren't even in the same class of comparison, they're not
// equal. In particular, strings will not be equal to non-strings.
if (algorithmA != algorithmB) {
CODE_COVERAGE(560); // Hit
return false;
} else {
CODE_COVERAGE(561); // Hit
}
if (algorithmA == EA_NOT_EQUAL) {
CODE_COVERAGE(562); // Hit
return false; // E.g. comparing NaN
} else {
CODE_COVERAGE(563); // Hit
}
if (a == b) {
CODE_COVERAGE(564); // Hit
return true;
} else {
CODE_COVERAGE(565); // Hit
}
switch (algorithmA) {
case EA_COMPARE_REFERENCE: {
// Reference equality comparison assumes that two values with different
// locations in memory must be different values, since their identity is
// their address. Since we've already checked `a == b`, this must be false.
return false;
}
case EA_COMPARE_NON_PTR_TYPE: {
// Non-pointer types are those like Int14 and the well-known values
// (except NaN). These can just be compared with `a == b`, which we've
// already done.
return false;
}
case EA_COMPARE_STRING: {
// Strings are a pain to compare because there are edge cases like the
// fact that the string "length" _may_ be represented by
// VM_VALUE_STR_LENGTH rather than a pointer to a string (or it may be a
// TC_REF_STRING). To keep the code concise, I'm fetching a pointer to the
// string data itself and then comparing that. This is the only equality
// algorithm that doesn't check the type. It makes use of the check for
// `algorithmA != algorithmB` from earlier and the fact that only strings
// compare with this algorithm, which means we won't get to this point
// unless both `a` and `b` are strings.
if (a == b) {
CODE_COVERAGE_UNTESTED(566); // Not hit
return true;
} else {
CODE_COVERAGE(567); // Hit
}
size_t sizeA;
size_t sizeB;
LongPtr lpStrA = vm_toStringUtf8_long(vm, a, &sizeA);
LongPtr lpStrB = vm_toStringUtf8_long(vm, b, &sizeB);
bool result = (sizeA == sizeB) && (memcmp_long(lpStrA, lpStrB, (uint16_t)sizeA) == 0);
TABLE_COVERAGE(result ? 1 : 0, 2, 568); // Hit 2/2
return result;
}
/*
Compares two values that are both pointer values that point to non-reference
types (e.g. int32). These will be equal if the value pointed to has the same
type, the same size, and the raw data pointed to is the same.
*/
case EA_COMPARE_PTR_VALUE_AND_TYPE: {
CODE_COVERAGE_UNTESTED(475); // Not hit
if (a == b) {
CODE_COVERAGE_UNTESTED(569); // Not hit
return true;
} else {
CODE_COVERAGE_UNTESTED(570); // Not hit
}
if (aType != bType) {
CODE_COVERAGE_UNTESTED(571); // Not hit
return false;
} else {
CODE_COVERAGE_UNTESTED(572); // Not hit
}
LongPtr lpA = DynamicPtr_decode_long(vm, a);
LongPtr lpB = DynamicPtr_decode_long(vm, b);
uint16_t aHeaderWord = readAllocationHeaderWord_long(lpA);
uint16_t bHeaderWord = readAllocationHeaderWord_long(lpB);
// If the header words are different, the sizes or types are different
if (aHeaderWord != bHeaderWord) {
CODE_COVERAGE_UNTESTED(476); // Not hit
return false;
} else {
CODE_COVERAGE_UNTESTED(477); // Not hit
}
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(aHeaderWord);
if (memcmp_long(lpA, lpB, size) == 0) {
CODE_COVERAGE_UNTESTED(481); // Not hit
return true;
} else {
CODE_COVERAGE_UNTESTED(482); // Not hit
return false;
}
}
default: {
VM_ASSERT_UNREACHABLE(vm);
return false;
}
}
}
bool mvm_isNaN(mvm_Value value) {
CODE_COVERAGE_UNTESTED(573); // Not hit
return value == VM_VALUE_NAN;
}
#if MVM_INCLUDE_SNAPSHOT_CAPABILITY
// Called during snapshotting to convert native pointers to their position-independent form
static void serializePtr(VM* vm, Value* pv) {
CODE_COVERAGE(576); // Hit
Value v = *pv;
if (!Value_isShortPtr(v)) {
CODE_COVERAGE(577); // Hit
return;
} else {
CODE_COVERAGE(578); // Hit
}
void* p = ShortPtr_decode(vm, v);
// Pointers are encoded as an offset in the heap
uint16_t offsetInHeap = pointerOffsetInHeap(vm, vm->pLastBucket, p);
// The lowest bit must be zero so that this is tagged as a "ShortPtr".
VM_ASSERT(vm, (offsetInHeap & 1) == 0);
*pv = offsetInHeap;
}
// The opposite of `loadPointers`
static void serializePointers(VM* vm, mvm_TsBytecodeHeader* bc) {
CODE_COVERAGE(579); // Hit
// CAREFUL! This function mutates `bc`, not `vm`.
uint16_t n;
uint16_t* p;
uint16_t heapOffset = bc->sectionOffsets[BCS_HEAP];
uint16_t heapSize = bc->bytecodeSize - heapOffset;
uint16_t* pGlobals = (uint16_t*)((uint8_t*)bc + bc->sectionOffsets[BCS_GLOBALS]);
uint16_t* heapMemory = (uint16_t*)((uint8_t*)bc + heapOffset);
// Roots in global variables
uint16_t globalsSize = bc->sectionOffsets[BCS_GLOBALS + 1] - bc->sectionOffsets[BCS_GLOBALS];
p = pGlobals;
n = globalsSize / 2;
TABLE_COVERAGE(n ? 1 : 0, 2, 580); // Hit 1/2
while (n--) {
serializePtr(vm, p++);
}
// Pointers in heap memory
p = heapMemory;
uint16_t* heapEnd = (uint16_t*)((uint8_t*)heapMemory + heapSize);
while (p < heapEnd) {
CODE_COVERAGE(581); // Hit
uint16_t header = *p++;
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(header);
uint16_t words = (size + 1) / 2;
TeTypeCode tc = vm_getTypeCodeFromHeaderWord(header);
if (tc < TC_REF_DIVIDER_CONTAINER_TYPES) { // Non-container types
CODE_COVERAGE(582); // Hit
p += words;
continue;
} else {
// Else, container types
CODE_COVERAGE(583); // Hit
}
while (words--) {
if (Value_isShortPtr(*p))
serializePtr(vm, p);
p++;
}
}
}
void* mvm_createSnapshot(mvm_VM* vm, size_t* out_size) {
CODE_COVERAGE(503); // Hit
if (out_size)
*out_size = 0;
uint16_t heapOffset = getSectionOffset(vm->lpBytecode, BCS_HEAP);
uint16_t heapSize = getHeapSize(vm);
// This assumes that the heap is the last section in the bytecode. Since the
// heap is the only part of the bytecode image that changes size, we can just
// calculate the new bytecode size as follows
VM_ASSERT(vm, BCS_HEAP == BCS_SECTION_COUNT - 1);
uint32_t bytecodeSize = (uint32_t)heapOffset + heapSize;
if (bytecodeSize > 0xFFFF) {
CODE_COVERAGE_ERROR_PATH(584); // Not hit
MVM_FATAL_ERROR(vm, MVM_E_SNAPSHOT_TOO_LARGE);
} else {
CODE_COVERAGE(585); // Hit
}
mvm_TsBytecodeHeader* pNewBytecode = vm_malloc(vm, bytecodeSize);
if (!pNewBytecode) return NULL;
// The globals and heap are the last parts of the image because they're the
// only mutable sections
VM_ASSERT(vm, BCS_GLOBALS == BCS_SECTION_COUNT - 2);
uint16_t sizeOfConstantPart = getSectionOffset(vm->lpBytecode, BCS_GLOBALS);
// The first part of the snapshot doesn't change between executions (except
// some header fields, which we'll update later).
memcpy_long(pNewBytecode, vm->lpBytecode, sizeOfConstantPart);
// Snapshot the globals memory
uint16_t sizeOfGlobals = getSectionSize(vm, BCS_GLOBALS);
memcpy((uint8_t*)pNewBytecode + pNewBytecode->sectionOffsets[BCS_GLOBALS], vm->globals, sizeOfGlobals);
// Snapshot heap memory
TsBucket* pBucket = vm->pLastBucket;
// Start at the end of the heap and work backwards, because buckets are linked
// in reverse order. (Edit: actually, they're also linked forwards now, but I
// might retract that at some point so I'll leave this with the backwards
// iteration).
uint8_t* pHeapStart = (uint8_t*)pNewBytecode + pNewBytecode->sectionOffsets[BCS_HEAP];
uint8_t* pTarget = pHeapStart + heapSize;
uint16_t cursor = heapSize;
TABLE_COVERAGE(pBucket ? 1 : 0, 2, 586); // Hit 1/2
while (pBucket) {
CODE_COVERAGE(504); // Hit
uint16_t offsetStart = pBucket->offsetStart;
uint16_t bucketSize = cursor - offsetStart;
uint8_t* pBucketData = getBucketDataBegin(pBucket);
pTarget -= bucketSize;
memcpy(pTarget, pBucketData, bucketSize);
cursor = offsetStart;
pBucket = pBucket->prev;
}
// Update header fields
pNewBytecode->bytecodeSize = bytecodeSize;
// Convert pointers-to-RAM into their corresponding serialized form
serializePointers(vm, pNewBytecode);
uint16_t crcStartOffset = OFFSETOF(mvm_TsBytecodeHeader, crc) + sizeof pNewBytecode->crc;
uint16_t crcSize = bytecodeSize - crcStartOffset;
void* pCrcStart = (uint8_t*)pNewBytecode + crcStartOffset;
pNewBytecode->crc = MVM_CALC_CRC16_CCITT(pCrcStart, crcSize);
if (out_size) {
CODE_COVERAGE(587); // Hit
*out_size = bytecodeSize;
}
return (void*)pNewBytecode;
}
#endif // MVM_INCLUDE_SNAPSHOT_CAPABILITY
#if MVM_INCLUDE_DEBUG_CAPABILITY
void mvm_dbg_setBreakpoint(VM* vm, uint16_t bytecodeAddress) {
CODE_COVERAGE_UNTESTED(588); // Not hit
// These checks on the bytecode address are assertions rather than user faults
// because the address is probably not manually computed by a user, it's
// derived from some kind of debug symbol file. In a production environment,
// setting a breakpoint on an address that's never executed (e.g. because it's
// not executable) is not a VM failure.
VM_ASSERT(vm, bytecodeAddress >= getSectionOffset(vm->lpBytecode, BCS_ROM));
VM_ASSERT(vm, bytecodeAddress < getSectionOffset(vm->lpBytecode, vm_sectionAfter(vm, BCS_ROM)));
mvm_dbg_removeBreakpoint(vm, bytecodeAddress);
TsBreakpoint* breakpoint = vm_malloc(vm, sizeof (TsBreakpoint));
if (!breakpoint) {
MVM_FATAL_ERROR(vm, MVM_E_MALLOC_FAIL);
return;
}
breakpoint->bytecodeAddress = bytecodeAddress;
// Add to linked-list
breakpoint->next = vm->pBreakpoints;
vm->pBreakpoints = breakpoint;
}
void mvm_dbg_removeBreakpoint(VM* vm, uint16_t bytecodeAddress) {
CODE_COVERAGE_UNTESTED(589); // Not hit
TsBreakpoint** ppBreakpoint = &vm->pBreakpoints;
TsBreakpoint* pBreakpoint = *ppBreakpoint;
while (pBreakpoint) {
if (pBreakpoint->bytecodeAddress == bytecodeAddress) {
CODE_COVERAGE_UNTESTED(590); // Not hit
// Remove from linked list
*ppBreakpoint = pBreakpoint->next;
vm_free(vm, pBreakpoint);
pBreakpoint = *ppBreakpoint;
} else {
CODE_COVERAGE_UNTESTED(591); // Not hit
ppBreakpoint = &pBreakpoint->next;
pBreakpoint = *ppBreakpoint;
}
}
}
void mvm_dbg_setBreakpointCallback(mvm_VM* vm, mvm_TfBreakpointCallback cb) {
CODE_COVERAGE_UNTESTED(592); // Not hit
// It doesn't strictly need to be null, but is probably a mistake if it's not.
VM_ASSERT(vm, vm->breakpointCallback == NULL);
vm->breakpointCallback = cb;
}
#endif // MVM_INCLUDE_DEBUG_CAPABILITY
/**
* Test out the LONG_PTR macros provided in the port file. lpBytecode should
* point to actual bytecode, whereas pHeader should point to a local copy that's
* been validated.
*/
static TeError vm_validatePortFileMacros(MVM_LONG_PTR_TYPE lpBytecode, mvm_TsBytecodeHeader* pHeader) {
uint32_t x1 = 0x12345678;
uint32_t x2 = 0x12345678;
uint32_t x3 = 0x87654321;
uint32_t x4 = 0x99999999;
uint32_t* px1 = &x1;
uint32_t* px2 = &x2;
uint32_t* px3 = &x3;
uint32_t* px4 = &x4;
MVM_LONG_PTR_TYPE lpx1 = MVM_LONG_PTR_NEW(px1);
MVM_LONG_PTR_TYPE lpx2 = MVM_LONG_PTR_NEW(px2);
MVM_LONG_PTR_TYPE lpx3 = MVM_LONG_PTR_NEW(px3);
MVM_LONG_PTR_TYPE lpx4 = MVM_LONG_PTR_NEW(px4);
if (!((MVM_LONG_PTR_TRUNCATE(lpx1)) == px1)) goto LBL_FAIL;
if (!((MVM_READ_LONG_PTR_1(lpx1)) == 0x78)) goto LBL_FAIL;
if (!((MVM_READ_LONG_PTR_2(lpx1)) == 0x5678)) goto LBL_FAIL;
if (!((MVM_READ_LONG_PTR_1((MVM_LONG_PTR_ADD(lpx1, 1)))) == 0x56)) goto LBL_FAIL;
if (!((MVM_LONG_PTR_SUB((MVM_LONG_PTR_ADD(lpx1, 3)), lpx1)) == 3)) goto LBL_FAIL;
if (!((MVM_LONG_PTR_SUB(lpx1, (MVM_LONG_PTR_ADD(lpx1, 3)))) == -3)) goto LBL_FAIL;
if (!((MVM_LONG_MEM_CMP(lpx1, lpx2, 4)) == 0)) goto LBL_FAIL;
if (!((MVM_LONG_MEM_CMP(lpx1, lpx3, 4)) > 0)) goto LBL_FAIL;
if (!((MVM_LONG_MEM_CMP(lpx1, lpx4, 4)) < 0)) goto LBL_FAIL;
MVM_LONG_MEM_CPY(px4, lpx3, 4);
if (!(x4 == 0x87654321)) goto LBL_FAIL;
x4 = 0x99999999;
// The above tests were testing the case of using a long pointer to point to
// local RAM. We need to also test that everything works when point to the
// actual bytecode. lpBytecode and pHeader should point to data of the same
// value but in different address spaces (ROM and RAM respectively).
if (!((MVM_READ_LONG_PTR_1(lpBytecode)) == pHeader->bytecodeVersion)) goto LBL_FAIL;
if (!((MVM_READ_LONG_PTR_2(lpBytecode)) == *((uint16_t*)pHeader))) goto LBL_FAIL;
if (!((MVM_READ_LONG_PTR_1((MVM_LONG_PTR_ADD(lpBytecode, 2)))) == pHeader->requiredEngineVersion)) goto LBL_FAIL;
if (!((MVM_LONG_PTR_SUB((MVM_LONG_PTR_ADD(lpBytecode, 3)), lpBytecode)) == 3)) goto LBL_FAIL;
if (!((MVM_LONG_PTR_SUB(lpBytecode, (MVM_LONG_PTR_ADD(lpBytecode, 3)))) == -3)) goto LBL_FAIL;
if (!((MVM_LONG_MEM_CMP(lpBytecode, (MVM_LONG_PTR_NEW(pHeader)), 8)) == 0)) goto LBL_FAIL;
if (MVM_NATIVE_POINTER_IS_16_BIT && (sizeof(void*) != 2)) return MVM_E_EXPECTED_POINTER_SIZE_TO_BE_16_BIT;
if ((!MVM_NATIVE_POINTER_IS_16_BIT) && (sizeof(void*) == 2)) return MVM_E_EXPECTED_POINTER_SIZE_NOT_TO_BE_16_BIT;
#if MVM_USE_SINGLE_RAM_PAGE
void* ptr = MVM_MALLOC(2);
MVM_FREE(ptr);
if ((intptr_t)ptr - (intptr_t)MVM_RAM_PAGE_ADDR > 0xffff) return MVM_E_MALLOC_NOT_WITHIN_RAM_PAGE;
#endif // MVM_USE_SINGLE_RAM_PAGE
return MVM_E_SUCCESS;
LBL_FAIL:
return MVM_E_PORT_FILE_MACRO_TEST_FAILURE;
}
uint16_t mvm_getCurrentAddress(VM* vm) {
vm_TsStack* stack = vm->stack;
if (!stack) return 0; // Not currently running
LongPtr lpProgramCounter = stack->reg.lpProgramCounter;
LongPtr lpBytecode = vm->lpBytecode;
uint16_t address = (uint16_t)MVM_LONG_PTR_SUB(lpProgramCounter, lpBytecode);
return address;
}
static Value vm_cloneFixedLengthArray(VM* vm, Value* pArr) {
VM_ASSERT_NOT_USING_CACHED_REGISTERS(vm);
LongPtr* lpSource = DynamicPtr_decode_long(vm, *pArr);
uint16_t headerWord = readAllocationHeaderWord_long(lpSource);
VM_ASSERT(vm, vm_getTypeCodeFromHeaderWord(headerWord) == TC_REF_FIXED_LENGTH_ARRAY);
uint16_t size = vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
uint16_t* newArray = gc_allocateWithHeader(vm, size, TC_REF_FIXED_LENGTH_ARRAY);
// May have moved during allocation
lpSource = DynamicPtr_decode_long(vm, *pArr);
uint16_t* pTarget = newArray;
while (size) {
*pTarget++ = LongPtr_read2_aligned(lpSource);
lpSource = LongPtr_add(lpSource, 2);
size -= 2;
}
return ShortPtr_encode(vm, newArray);
}
static Value vm_safePop(VM* vm, Value* pStackPointerAfterDecr) {
// This is only called in the run-loop, so the registers should be cached
VM_ASSERT(vm, vm->stack->reg.usingCachedRegisters);
if (pStackPointerAfterDecr < getBottomOfStack(vm->stack)) {
MVM_FATAL_ERROR(vm, MVM_E_ASSERTION_FAILED);
}
return *pStackPointerAfterDecr;
}
static inline void vm_checkValueAccess(VM* vm, uint8_t potentialCycleNumber) {
VM_ASSERT(vm, vm->gc_potentialCycleNumber == potentialCycleNumber);
}
static TeError vm_newError(VM* vm, TeError err) {
#if MVM_ALL_ERRORS_FATAL
MVM_FATAL_ERROR(vm, err);
#endif
return err;
}
static void* vm_malloc(VM* vm, size_t size) {
void* result = MVM_MALLOC(size);
#if MVM_SAFE_MODE && MVM_USE_SINGLE_RAM_PAGE
// See comment on MVM_RAM_PAGE_ADDR in microvium_port_example.h
VM_ASSERT(vm, (intptr_t)result - (intptr_t)MVM_RAM_PAGE_ADDR <= 0xFFFF);
#endif
return result;
}
// Note: mvm_free frees the VM, while vm_free is the counterpart to vm_malloc
static void vm_free(VM* vm, void* ptr) {
#if MVM_SAFE_MODE && MVM_USE_SINGLE_RAM_PAGE
// See comment on MVM_RAM_PAGE_ADDR in microvium_port_example.h
VM_ASSERT(vm, (intptr_t)ptr - (intptr_t)MVM_RAM_PAGE_ADDR <= 0xFFFF);
#endif
MVM_FREE(ptr);
}
static mvm_TeError vm_uint8ArrayNew(VM* vm, Value* slot) {
CODE_COVERAGE(344); // Hit
uint16_t size = *slot;
if (!Value_isVirtualUInt12(size)) {
CODE_COVERAGE_ERROR_PATH(345); // Not hit
return MVM_E_INVALID_UINT8_ARRAY_LENGTH;
}
size = VirtualInt14_decode(vm, size);
uint8_t* p = gc_allocateWithHeader(vm, size, TC_REF_UINT8_ARRAY);
*slot = ShortPtr_encode(vm, p);
memset(p, 0, size);
return MVM_E_SUCCESS;
}
mvm_Value mvm_uint8ArrayFromBytes(mvm_VM* vm, const uint8_t* data, size_t sizeBytes) {
CODE_COVERAGE(346); // Hit
if (sizeBytes >= (MAX_ALLOCATION_SIZE + 1)) {
MVM_FATAL_ERROR(vm, MVM_E_ALLOCATION_TOO_LARGE);
return VM_VALUE_UNDEFINED;
}
// Note: gc_allocateWithHeader will also check the size
uint8_t* p = gc_allocateWithHeader(vm, (uint16_t)sizeBytes, TC_REF_UINT8_ARRAY);
Value result = ShortPtr_encode(vm, p);
memcpy(p, data, sizeBytes);
return result;
}
mvm_TeError mvm_uint8ArrayToBytes(mvm_VM* vm, mvm_Value uint8ArrayValue, uint8_t** out_data, size_t* out_size) {
CODE_COVERAGE(348); // Hit
// Note: while it makes sense to allow Uint8Arrays in general to live in ROM,
// I think we can require that those that hit the FFI boundary are never
// optimized into ROM. For efficiency and because I imagine that it's a very
// limited use case to have constant data accessed through this API.
if (!Value_isShortPtr(uint8ArrayValue)) {
CODE_COVERAGE_ERROR_PATH(574); // Not hit
return MVM_E_TYPE_ERROR;
}
void* p = ShortPtr_decode(vm, uint8ArrayValue);
uint16_t headerWord = readAllocationHeaderWord(p);
TeTypeCode typeCode = vm_getTypeCodeFromHeaderWord(headerWord);
if (typeCode != TC_REF_UINT8_ARRAY) {
CODE_COVERAGE_ERROR_PATH(575); // Not hit
return MVM_E_TYPE_ERROR;
}
*out_size = (size_t)vm_getAllocationSizeExcludingHeaderFromHeaderWord(headerWord);
*out_data = p;
return MVM_E_SUCCESS;
}