Lines
100 %
Functions
42.86 %
Branches
//! `OutputSerializer` + the single runtime-append output protocol.
//!
//! Every output entity (debug value, tag, delete) is written at the RUNTIME
//! `next_write_pos` the `OutputHeader` carries, then `next_write_pos` and
//! `output_entity_count` are advanced at runtime. This is the ONE protocol: a
//! compile-time-known write (a program result) and a dynamic-count write (a
//! `create-tag` inside a loop) both append, so they never overwrite each other.
//! [`LOCAL_ENTITY_BASE`] holds the current entity's absolute address
//! (`output_base + next_write_pos`); writers store their header + data fields
//! relative to it. Tag strings still grow DOWN from the buffer end (the entity
//! parser reads them as distance-from-end); debug-value strings grow UP right
//! after the value and are the last thing written, so neither disturbs the
//! contiguous `ENTITY_HEADER_SIZE + data_size` entity walk.
use std::mem::offset_of;
use scripting_format::{
ENTITY_HEADER_SIZE, EntityHeader, EntityType, MAGIC_OUTP, OUTPUT_HEADER_SIZE, Operation,
OutputHeader,
};
use crate::compiler::emit::FunctionEmitter;
use crate::compiler::expr::{LOCAL_ENTITY_BASE, LOCAL_TEMP_I32};
#[derive(Clone)]
pub struct OutputSerializer {
pub(super) output_start_idx: u32,
pub(super) output_base_local: u32,
}
impl OutputSerializer {
pub fn new(output_base_local: u32) -> Self {
Self {
output_start_idx: 0,
output_base_local,
/// Writes the `OutputHeader` at the buffer start: zero entities, the
/// strings frontier at the buffer end (set lazily by the first string
/// write), and `next_write_pos` just past the header — where the first
/// entity will append.
pub fn begin_output(&mut self, emit: &mut FunctionEmitter) {
let local = self.output_base_local;
emit.store_i32_dynamic(
local,
offset_of!(OutputHeader, magic) as u32,
MAGIC_OUTP as i32,
);
offset_of!(OutputHeader, output_entity_count) as u32,
0,
offset_of!(OutputHeader, output_start_idx) as u32,
self.output_start_idx as i32,
offset_of!(OutputHeader, next_write_pos) as u32,
OUTPUT_HEADER_SIZE as i32,
/// Loads `LOCAL_ENTITY_BASE = output_base + next_write_pos` — the absolute
/// address of the entity about to be written.
pub(super) fn load_entity_base(&self, emit: &mut FunctionEmitter) {
emit.local_get(local);
emit.i32_load(offset_of!(OutputHeader, next_write_pos) as u64);
emit.i32_add();
emit.local_set(LOCAL_ENTITY_BASE);
/// Appends an entity header at `next_write_pos` (via [`Self::load_entity_base`]),
/// bumps `output_entity_count` at runtime, and returns the entity-relative
/// offset of the data region (`ENTITY_HEADER_SIZE`). The caller then writes
/// data fields relative to `LOCAL_ENTITY_BASE` and finishes with
/// [`Self::advance_past`]. `next_write_pos` is NOT advanced here.
pub(super) fn append_entity_header(
&mut self,
emit: &mut FunctionEmitter,
entity_type: EntityType,
operation: Operation,
flags: u8,
parent_idx: i32,
data_size: u32,
) -> u32 {
self.load_entity_base(emit);
let ent = LOCAL_ENTITY_BASE;
emit.store_u8_dynamic(
ent,
offset_of!(EntityHeader, entity_type) as u32,
entity_type as u8,
offset_of!(EntityHeader, operation) as u32,
operation as u8,
emit.store_u8_dynamic(ent, offset_of!(EntityHeader, flags) as u32, flags);
emit.store_i32_dynamic(ent, offset_of!(EntityHeader, parent_idx) as u32, parent_idx);
// data_offset is relative to output_base: next_write_pos + header size =
// (entity_base - output_base) + ENTITY_HEADER_SIZE.
emit.local_get(ent);
emit.i32_const(offset_of!(EntityHeader, data_offset) as i32);
emit.local_get(self.output_base_local);
emit.i32_sub();
emit.i32_const(ENTITY_HEADER_SIZE as i32);
emit.i32_store_raw();
offset_of!(EntityHeader, data_size) as u32,
data_size as i32,
self.bump_entity_count(emit);
ENTITY_HEADER_SIZE as u32
/// `output_entity_count += 1` at runtime.
pub(super) fn bump_entity_count(&self, emit: &mut FunctionEmitter) {
let base = self.output_base_local;
let count_off = offset_of!(OutputHeader, output_entity_count) as u32;
emit.local_get(base);
emit.i32_const(count_off as i32);
emit.i32_load(u64::from(count_off));
emit.i32_const(1);
/// Advances `next_write_pos` by a COMPILE-TIME-known byte count (header +
/// fixed data). For an entity that also appended grow-up strings, follow
/// with [`Self::advance_past_runtime`].
pub(super) fn advance_past(&self, emit: &mut FunctionEmitter, bytes: u32) {
let off = offset_of!(OutputHeader, next_write_pos) as u32;
emit.i32_const(off as i32);
emit.i32_load(u64::from(off));
emit.i32_const(bytes as i32);
/// Advances `next_write_pos` by a RUNTIME byte count already on the wasm
/// stack (for grow-up string payloads of unknown length).
pub(super) fn advance_past_runtime(&self, emit: &mut FunctionEmitter) {
// stack: [extra_bytes]
let extra = LOCAL_TEMP_I32;
emit.local_set(extra);
emit.local_get(extra);