Expand bytecode spec and add VM skeleton

This commit is contained in:
Sergey Chernov 2026-01-25 17:00:08 +03:00
parent bc9e557814
commit f42ea0a04c
8 changed files with 440 additions and 4 deletions

View File

@ -176,7 +176,24 @@ Note: Any opcode can be compiled to FALLBACK if not implemented in a VM pass.
### Fallback ### Fallback
- EVAL_FALLBACK T -> S - EVAL_FALLBACK T -> S
## 6) Function Header (binary container) ## 6) Const Pool Encoding (v0)
Each const entry is encoded as:
[tag:U8] [payload...]
Tags:
- 0x00: NULL
- 0x01: BOOL (payload: U8 0/1)
- 0x02: INT (payload: S64, little-endian)
- 0x03: REAL (payload: F64, IEEE-754, little-endian)
- 0x04: STRING (payload: U32 length + UTF-8 bytes)
- 0x05: OBJ_REF (payload: U32 index into external Obj table)
Notes:
- OBJ_REF is reserved for embedding prebuilt Obj handles if needed.
- Strings use UTF-8; length is bytes, not chars.
## 7) Function Header (binary container)
Suggested layout for a bytecode function blob: Suggested layout for a bytecode function blob:
- magic: U32 ("LYBC") - magic: U32 ("LYBC")
@ -190,10 +207,34 @@ Suggested layout for a bytecode function blob:
- constPool: [const entries...] - constPool: [const entries...]
- code: [bytecode...] - code: [bytecode...]
Const pool entries are encoded as type-tagged values (Obj/Int/Real/Bool/String) Const pool entries use the encoding described in section 6.
in a simple tagged format. This is intentionally unspecified in v0.
## 7) Notes ## 8) Sample Bytecode (illustrative)
Example Lyng:
val x = 2
val y = 3
val z = x + y
Assume:
- localCount = 3 (x,y,z)
- argCount = 0
- slot width = 1 byte
- const pool: [INT 2, INT 3]
Bytecode:
CONST_INT k0 -> s0
CONST_INT k1 -> s1
ADD_INT s0, s1 -> s2
RET_VOID
Encoded (opcode values symbolic):
[OP_CONST_INT][k0][s0]
[OP_CONST_INT][k1][s1]
[OP_ADD_INT][s0][s1][s2]
[OP_RET_VOID]
## 9) Notes
- Mixed-mode is allowed: compiler can emit FALLBACK ops for unsupported nodes. - Mixed-mode is allowed: compiler can emit FALLBACK ops for unsupported nodes.
- The VM must be suspendable; on suspension, store ip + minimal operand state. - The VM must be suspendable; on suspension, store ip + minimal operand state.

View File

@ -0,0 +1,28 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.obj.Obj
sealed class BytecodeConst {
object Null : BytecodeConst()
data class Bool(val value: Boolean) : BytecodeConst()
data class IntVal(val value: Long) : BytecodeConst()
data class RealVal(val value: Double) : BytecodeConst()
data class StringVal(val value: String) : BytecodeConst()
data class ObjRef(val value: Obj) : BytecodeConst()
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
interface BytecodeDecoder {
fun readOpcode(code: ByteArray, ip: Int): Opcode
fun readSlot(code: ByteArray, ip: Int): Int
fun readConstId(code: ByteArray, ip: Int, width: Int): Int
fun readIp(code: ByteArray, ip: Int, width: Int): Int
}
object Decoder8 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int = code[ip].toInt() and 0xFF
override fun readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
object Decoder16 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int =
(code[ip].toInt() and 0xFF) or ((code[ip + 1].toInt() and 0xFF) shl 8)
override fun readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
object Decoder32 : BytecodeDecoder {
override fun readOpcode(code: ByteArray, ip: Int): Opcode =
Opcode.fromCode(code[ip]) ?: error("Unknown opcode: ${code[ip]}")
override fun readSlot(code: ByteArray, ip: Int): Int = readUInt(code, ip, 4)
override fun readConstId(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
override fun readIp(code: ByteArray, ip: Int, width: Int): Int =
readUInt(code, ip, width)
}
private fun readUInt(code: ByteArray, ip: Int, width: Int): Int {
var result = 0
var shift = 0
var idx = ip
var remaining = width
while (remaining-- > 0) {
result = result or ((code[idx].toInt() and 0xFF) shl shift)
shift += 8
idx++
}
return result
}

View File

@ -0,0 +1,71 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjNull
class BytecodeFrame(
val localCount: Int,
val argCount: Int,
) {
val slotCount: Int = localCount + argCount
val argBase: Int = localCount
private val slotTypes: ByteArray = ByteArray(slotCount) { SlotType.UNKNOWN.code }
private val objSlots: Array<Obj?> = arrayOfNulls(slotCount)
private val intSlots: LongArray = LongArray(slotCount)
private val realSlots: DoubleArray = DoubleArray(slotCount)
private val boolSlots: BooleanArray = BooleanArray(slotCount)
fun getSlotType(slot: Int): SlotType = SlotType.values().first { it.code == slotTypes[slot] }
fun setSlotType(slot: Int, type: SlotType) {
slotTypes[slot] = type.code
}
fun getObj(slot: Int): Obj = objSlots[slot] ?: ObjNull
fun setObj(slot: Int, value: Obj) {
objSlots[slot] = value
slotTypes[slot] = SlotType.OBJ.code
}
fun getInt(slot: Int): Long = intSlots[slot]
fun setInt(slot: Int, value: Long) {
intSlots[slot] = value
slotTypes[slot] = SlotType.INT.code
}
fun getReal(slot: Int): Double = realSlots[slot]
fun setReal(slot: Int, value: Double) {
realSlots[slot] = value
slotTypes[slot] = SlotType.REAL.code
}
fun getBool(slot: Int): Boolean = boolSlots[slot]
fun setBool(slot: Int, value: Boolean) {
boolSlots[slot] = value
slotTypes[slot] = SlotType.BOOL.code
}
fun clearSlot(slot: Int) {
slotTypes[slot] = SlotType.UNKNOWN.code
objSlots[slot] = null
intSlots[slot] = 0L
realSlots[slot] = 0.0
boolSlots[slot] = false
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
data class BytecodeFunction(
val name: String,
val localCount: Int,
val slotWidth: Int,
val ipWidth: Int,
val constIdWidth: Int,
val constants: List<BytecodeConst>,
val code: ByteArray,
) {
init {
require(slotWidth == 1 || slotWidth == 2 || slotWidth == 4) { "slotWidth must be 1,2,4" }
require(ipWidth == 2 || ipWidth == 4) { "ipWidth must be 2 or 4" }
require(constIdWidth == 2 || constIdWidth == 4) { "constIdWidth must be 2 or 4" }
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
import net.sergeych.lyng.Scope
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjVoid
class BytecodeVm {
suspend fun execute(fn: BytecodeFunction, scope: Scope, args: List<Obj>): Obj {
val frame = BytecodeFrame(fn.localCount, args.size)
for (i in args.indices) {
frame.setObj(frame.argBase + i, args[i])
}
val decoder = when (fn.slotWidth) {
1 -> Decoder8
2 -> Decoder16
4 -> Decoder32
else -> error("Unsupported slot width: ${fn.slotWidth}")
}
var ip = 0
val code = fn.code
while (ip < code.size) {
val op = decoder.readOpcode(code, ip)
ip += 1
when (op) {
Opcode.NOP -> {
// no-op
}
Opcode.RET_VOID -> return ObjVoid
else -> error("Opcode not implemented: $op")
}
}
return ObjVoid
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
enum class Opcode(val code: Byte) {
NOP(0x00),
MOVE_OBJ(0x01),
MOVE_INT(0x02),
MOVE_REAL(0x03),
MOVE_BOOL(0x04),
CONST_OBJ(0x05),
CONST_INT(0x06),
CONST_REAL(0x07),
CONST_BOOL(0x08),
CONST_NULL(0x09),
INT_TO_REAL(0x10),
REAL_TO_INT(0x11),
BOOL_TO_INT(0x12),
INT_TO_BOOL(0x13),
ADD_INT(0x20),
SUB_INT(0x21),
MUL_INT(0x22),
DIV_INT(0x23),
MOD_INT(0x24),
NEG_INT(0x25),
INC_INT(0x26),
DEC_INT(0x27),
ADD_REAL(0x30),
SUB_REAL(0x31),
MUL_REAL(0x32),
DIV_REAL(0x33),
NEG_REAL(0x34),
AND_INT(0x40),
OR_INT(0x41),
XOR_INT(0x42),
SHL_INT(0x43),
SHR_INT(0x44),
USHR_INT(0x45),
INV_INT(0x46),
CMP_EQ_INT(0x50),
CMP_NEQ_INT(0x51),
CMP_LT_INT(0x52),
CMP_LTE_INT(0x53),
CMP_GT_INT(0x54),
CMP_GTE_INT(0x55),
CMP_EQ_REAL(0x56),
CMP_NEQ_REAL(0x57),
CMP_LT_REAL(0x58),
CMP_LTE_REAL(0x59),
CMP_GT_REAL(0x5A),
CMP_GTE_REAL(0x5B),
CMP_EQ_BOOL(0x5C),
CMP_NEQ_BOOL(0x5D),
CMP_EQ_INT_REAL(0x60),
CMP_EQ_REAL_INT(0x61),
CMP_LT_INT_REAL(0x62),
CMP_LT_REAL_INT(0x63),
CMP_LTE_INT_REAL(0x64),
CMP_LTE_REAL_INT(0x65),
CMP_GT_INT_REAL(0x66),
CMP_GT_REAL_INT(0x67),
CMP_GTE_INT_REAL(0x68),
CMP_GTE_REAL_INT(0x69),
NOT_BOOL(0x70),
AND_BOOL(0x71),
OR_BOOL(0x72),
JMP(0x80),
JMP_IF_TRUE(0x81),
JMP_IF_FALSE(0x82),
RET(0x83),
RET_VOID(0x84),
CALL_DIRECT(0x90),
CALL_VIRTUAL(0x91),
CALL_FALLBACK(0x92),
GET_FIELD(0xA0),
SET_FIELD(0xA1),
GET_INDEX(0xA2),
SET_INDEX(0xA3),
EVAL_FALLBACK(0xB0),
;
companion object {
private val byCode: Map<Byte, Opcode> = values().associateBy { it.code }
fun fromCode(code: Byte): Opcode? = byCode[code]
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright 2026 Sergey S. Chernov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.sergeych.lyng.bytecode
enum class SlotType(val code: Byte) {
UNKNOWN(0),
OBJ(1),
INT(2),
REAL(3),
BOOL(4),
}