use wasmtime::{ArrayRef, ArrayRefPre, ArrayType, Caller, Rooted, Val};
use super::MAX_RESULT_UNITS;
use super::transforms::{Text, Units};
use crate::runtime::StoreData;
use crate::runtime::fuel;
use crate::runtime::host::{fatal_host_error, range_error};
use crate::runtime::limits::HostBytes;
const CHUNK_UNITS: usize = 2048;
const _: () = assert!(CHUNK_UNITS <= MAX_RESULT_UNITS);
pub(super) struct Run {
pub source: Rooted<ArrayRef>,
pub start: usize,
pub period: usize,
pub len: usize,
}
impl Run {
pub fn copy(source: Rooted<ArrayRef>, range: std::ops::Range<usize>) -> Self {
Self {
source,
start: range.start,
period: range.len(),
len: range.len(),
}
}
}
pub(super) fn allocate(
caller: &mut Caller<'_, StoreData>,
payload_ty: &ArrayType,
len: usize,
) -> wasmtime::Result<Rooted<ArrayRef>> {
let len = u32::try_from(len).map_err(|_| range_error("Invalid string length"))?;
let pre = ArrayRefPre::new(&mut *caller, payload_ty.clone());
ArrayRef::new(&mut *caller, &pre, &Val::I32(0), len)
}
pub(super) fn fill_run(
caller: &mut Caller<'_, StoreData>,
dst: Rooted<ArrayRef>,
at: usize,
run: &Run,
) -> wasmtime::Result<()> {
if run.len == 0 {
return Ok(());
}
if run.period == 0 {
return Err(fatal_host_error(
"string run cycles through an empty source",
));
}
if run.period <= CHUNK_UNITS {
fill_repeating(caller, dst, at, run)
} else {
fill_streaming(caller, dst, at, run)
}
}
fn fill_repeating(
caller: &mut Caller<'_, StoreData>,
dst: Rooted<ArrayRef>,
at: usize,
run: &Run,
) -> wasmtime::Result<()> {
let mut buffer = [0u16; CHUNK_UNITS];
let block = CHUNK_UNITS / run.period * run.period;
read_units_at(
caller,
run.source,
run.start,
chunk(&mut buffer, run.period)?,
)?;
for start in (run.period..block).step_by(run.period) {
buffer.copy_within(..run.period, start);
}
let mut written = 0;
while written < run.len {
let n = (run.len - written).min(block);
write_units_at(caller, dst, at + written, chunk(&mut buffer, n)?)?;
written += n;
}
Ok(())
}
fn fill_streaming(
caller: &mut Caller<'_, StoreData>,
dst: Rooted<ArrayRef>,
at: usize,
run: &Run,
) -> wasmtime::Result<()> {
let mut buffer = [0u16; CHUNK_UNITS];
let mut written = 0;
while written < run.len {
let offset = written % run.period;
let n = (run.len - written)
.min(run.period - offset)
.min(CHUNK_UNITS);
let units = chunk(&mut buffer, n)?;
read_units_at(caller, run.source, run.start + offset, units)?;
write_units_at(caller, dst, at + written, units)?;
written += n;
}
Ok(())
}
pub(super) struct GcUnits<'a, 'b> {
caller: &'a mut Caller<'b, StoreData>,
source: Rooted<ArrayRef>,
len: usize,
window: [u16; CHUNK_UNITS],
window_start: usize,
window_len: usize,
}
impl<'a, 'b> GcUnits<'a, 'b> {
pub(super) fn new(
caller: &'a mut Caller<'b, StoreData>,
source: Rooted<ArrayRef>,
len: usize,
) -> Self {
Self {
caller,
source,
len,
window: [0; CHUNK_UNITS],
window_start: 0,
window_len: 0,
}
}
}
impl Units for GcUnits<'_, '_> {
fn len(&self) -> usize {
self.len
}
fn unit(&mut self, index: usize) -> wasmtime::Result<u16> {
let cached = index
.checked_sub(self.window_start)
.filter(|offset| *offset < self.window_len)
.and_then(|offset| self.window.get(offset));
if let Some(unit) = cached {
return Ok(*unit);
}
if index >= self.len {
return Err(fatal_host_error("string read past its end"));
}
let start = index / CHUNK_UNITS * CHUNK_UNITS;
let n = (self.len - start).min(CHUNK_UNITS);
read_units_at(
&mut *self.caller,
self.source,
start,
chunk(&mut self.window, n)?,
)?;
self.window_start = start;
self.window_len = n;
self.window
.get(index - start)
.copied()
.ok_or_else(|| fatal_host_error("string window misses its index"))
}
}
pub(super) struct GcText<'a, 'b> {
input: GcUnits<'a, 'b>,
payload_ty: ArrayType,
guess: Rooted<ArrayRef>,
guess_len: usize,
written: usize,
pending: [u16; CHUNK_UNITS],
pending_len: usize,
spill: Spill,
}
impl<'a, 'b> GcText<'a, 'b> {
pub(super) fn new(
input: GcUnits<'a, 'b>,
payload_ty: ArrayType,
guess: Rooted<ArrayRef>,
guess_len: usize,
) -> Self {
Self {
input,
payload_ty,
guess,
guess_len,
written: 0,
pending: [0; CHUNK_UNITS],
pending_len: 0,
spill: Spill::default(),
}
}
pub(super) fn finish(mut self) -> wasmtime::Result<Rooted<ArrayRef>> {
self.flush()?;
if self.written == self.guess_len && self.spill.units.is_empty() {
return Ok(self.guess);
}
let caller = &mut *self.input.caller;
let total = self.written + self.spill.units.len();
fuel::charge(
&mut *caller,
fuel::COPY,
total as u64 + self.spill.units.len() as u64,
)?;
let exact = allocate(caller, &self.payload_ty, total)?;
fill_run(caller, exact, 0, &Run::copy(self.guess, 0..self.written))?;
write_units_at(caller, exact, self.written, &self.spill.units)?;
Ok(exact)
}
fn push(&mut self, unit: u16) -> wasmtime::Result<()> {
let total = self.written + self.pending_len + self.spill.units.len();
if total >= MAX_RESULT_UNITS {
return Err(range_error("Invalid string length"));
}
if total >= self.guess_len {
return self.spill.push(&mut *self.input.caller, unit);
}
let slot = self
.pending
.get_mut(self.pending_len)
.ok_or_else(|| fatal_host_error("string output buffer overrun"))?;
*slot = unit;
self.pending_len += 1;
if self.pending_len == CHUNK_UNITS {
self.flush()?;
}
Ok(())
}
fn flush(&mut self) -> wasmtime::Result<()> {
let units = self
.pending
.get(..self.pending_len)
.ok_or_else(|| fatal_host_error("string output buffer overrun"))?;
write_units_at(&mut *self.input.caller, self.guess, self.written, units)?;
self.written += self.pending_len;
self.pending_len = 0;
Ok(())
}
}
impl Units for GcText<'_, '_> {
fn len(&self) -> usize {
self.input.len()
}
fn unit(&mut self, index: usize) -> wasmtime::Result<u16> {
self.input.unit(index)
}
}
impl Text for GcText<'_, '_> {
fn emit(&mut self, units: &[u16]) -> wasmtime::Result<()> {
for &unit in units {
self.push(unit)?;
}
Ok(())
}
}
#[derive(Default)]
struct Spill {
units: Vec<u16>,
_reservation: Option<HostBytes>,
}
impl Spill {
fn push(&mut self, caller: &mut Caller<'_, StoreData>, unit: u16) -> wasmtime::Result<()> {
if self.units.len() == self.units.capacity() {
self.grow(caller)?;
}
self.units.push(unit);
Ok(())
}
fn grow(&mut self, caller: &mut Caller<'_, StoreData>) -> wasmtime::Result<()> {
let capacity = self
.units
.capacity()
.saturating_mul(2)
.clamp(CHUNK_UNITS, MAX_RESULT_UNITS);
let bytes = capacity
.checked_mul(std::mem::size_of::<u16>())
.and_then(|bytes| u64::try_from(bytes).ok())
.ok_or_else(|| fatal_host_error("string output size overflow"))?;
let additional = capacity
.checked_sub(self.units.len())
.ok_or_else(|| fatal_host_error("string spill outgrew its limit"))?;
let reservation = HostBytes::new(&caller.data().tenant_limits, bytes)?;
self.units
.try_reserve_exact(additional)
.map_err(fatal_host_error)?;
self._reservation = Some(reservation);
Ok(())
}
}
fn chunk(buffer: &mut [u16; CHUNK_UNITS], n: usize) -> wasmtime::Result<&mut [u16]> {
buffer
.get_mut(..n)
.ok_or_else(|| fatal_host_error("string chunk exceeds its buffer"))
}
fn read_units_at(
caller: &mut Caller<'_, StoreData>,
source: Rooted<ArrayRef>,
at: usize,
units: &mut [u16],
) -> wasmtime::Result<()> {
let at = u32::try_from(at).map_err(fatal_host_error)?;
source
.read_i16(&mut *caller, at, units)
.map_err(fatal_host_error)
}
fn write_units_at(
caller: &mut Caller<'_, StoreData>,
dst: Rooted<ArrayRef>,
at: usize,
units: &[u16],
) -> wasmtime::Result<()> {
let at = u32::try_from(at).map_err(fatal_host_error)?;
dst.write_i16(&mut *caller, at, units)
.map_err(fatal_host_error)
}