initial version

This commit is contained in:
2025-12-21 15:36:46 +01:00
commit 876241f455
17 changed files with 936 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
[build]
target = "wasm32-unknown-unknown"

1
bqst-core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

151
bqst-core/Cargo.lock generated Normal file
View File

@@ -0,0 +1,151 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "bqst-core"
version = "0.1.0"
dependencies = [
"dashu",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "dashu"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0"
dependencies = [
"dashu-base",
"dashu-float",
"dashu-int",
"dashu-macros",
"dashu-ratio",
"rustversion",
]
[[package]]
name = "dashu-base"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289"
[[package]]
name = "dashu-float"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7"
dependencies = [
"dashu-base",
"dashu-int",
"num-modular",
"num-order",
"rustversion",
"static_assertions",
]
[[package]]
name = "dashu-int"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf"
dependencies = [
"cfg-if",
"dashu-base",
"num-modular",
"num-order",
"rustversion",
"static_assertions",
]
[[package]]
name = "dashu-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10"
dependencies = [
"dashu-base",
"dashu-float",
"dashu-int",
"dashu-ratio",
"paste",
"proc-macro2",
"quote",
"rustversion",
]
[[package]]
name = "dashu-ratio"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9"
dependencies = [
"dashu-base",
"dashu-float",
"dashu-int",
"num-modular",
"num-order",
"rustversion",
]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"

14
bqst-core/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "bqst-core"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[profile.release]
opt-level = 3
strip = "symbols"
[dependencies]
dashu = { version = "0.4.2", default-features = false, features = ["num-order"] }

54
bqst-core/src/api.rs Normal file
View File

@@ -0,0 +1,54 @@
use std::fmt::Debug;
use dashu::rational::RBig;
use crate::{PrecDec, prefixes, qty::Qty, unit};
macro_rules! _t {
($e:expr) => {
$e.map_err(|e| Box::new(e) as Box<dyn Debug>)
};
}
fn parse_decimal<const B: u32>(s: &str) -> Result<RBig, dashu::base::ParseError> {
if let Some((int_part, frac_part)) = s.split_once('.') {
let denominator = B.pow(frac_part.len() as u32);
let numerator = RBig::from_str_radix(&format!("{}{}", int_part, frac_part), B)?;
Ok(numerator / RBig::from(denominator))
} else {
Ok(RBig::from_str_radix(s, B)?)
}
}
pub fn qty(q: &str) -> Result<Qty, Box<dyn Debug>> {
let split_idx = q
.char_indices()
.rev()
.find(|&(_, c)| c.is_ascii_digit())
.map(|(i, _)| i + 1) // found digit and digits are 1 byte so no worries byt ugly
.unwrap_or(0);
let (amt, unt) = q.split_at(split_idx);
let mut amt: PrecDec = if amt.is_empty() {
PrecDec::from(1)
} else {
_t!(parse_decimal::<10>(amt))?
};
let unt = if let Some((_, rem, exp)) = prefixes::extract_prefix_and_rem(unt.trim()) {
let e = PrecDec::from(10isize.pow(exp.unsigned_abs().into()));
if exp < 0 {
amt /= e;
} else {
amt *= e;
}
rem
} else {
unt
};
let unt = _t!(unit::Unit::try_from(unt.trim()))?;
Ok(Qty { n: amt, u: unt })
}

83
bqst-core/src/lib.rs Normal file
View File

@@ -0,0 +1,83 @@
use dashu::rational::RBig;
pub mod api;
pub mod prefixes;
pub mod prot;
pub mod qty;
pub mod unit;
pub type PrecDec = RBig;
macro_rules! prot_try {
($e:expr) => {
match $e {
Ok(v) => v,
Err(e) => {
prot::send_str(format!("{}:{}:{}: {e:?}", file!(), line!(), column!()));
return 1;
}
}
};
}
#[unsafe(export_name = "qty")]
pub extern "C" fn qty(arg1_len: usize) -> usize {
let qty = prot_try!(unsafe { prot::recv_string(arg1_len) });
let qty = prot_try!(api::qty(&qty));
prot::send_bytes(qty.into_bytes());
0
}
#[unsafe(export_name = "raw_num")]
pub extern "C" fn raw_num(prec_len: usize, qty_len: usize) -> usize {
let (prec, qty) = prot_try!(unsafe { prot::recv_byte_n_qty(prec_len, qty_len) });
prot::send_bytes(format!("{:.*}", prec as usize, qty.n.to_f64().value()));
0
}
#[unsafe(export_name = "fmt")]
pub extern "C" fn fmt(prec_len: usize, qty_len: usize) -> usize {
let (prec, qty) = prot_try!(unsafe { prot::recv_byte_n_qty(prec_len, qty_len) });
prot::send_bytes(format!("{qty:.*}", prec as usize));
0
}
#[unsafe(export_name = "op_add")]
pub extern "C" fn op_add(lhs_len: usize, rhs_len: usize) -> usize {
let (lhs, rhs) = prot_try!(unsafe { prot::recv_2qty(lhs_len, rhs_len) });
prot::send_bytes((lhs + rhs).into_bytes());
0
}
#[unsafe(export_name = "op_sub")]
pub extern "C" fn op_sub(lhs_len: usize, rhs_len: usize) -> usize {
let (lhs, rhs) = prot_try!(unsafe { prot::recv_2qty(lhs_len, rhs_len) });
prot::send_bytes((lhs - rhs).into_bytes());
0
}
#[unsafe(export_name = "op_mul")]
pub extern "C" fn op_mul(lhs_len: usize, rhs_len: usize) -> usize {
let (lhs, rhs) = prot_try!(unsafe { prot::recv_2qty(lhs_len, rhs_len) });
prot::send_bytes((lhs * rhs).into_bytes());
0
}
#[unsafe(export_name = "op_div")]
pub extern "C" fn op_div(lhs_len: usize, rhs_len: usize) -> usize {
let (lhs, rhs) = prot_try!(unsafe { prot::recv_2qty(lhs_len, rhs_len) });
prot::send_bytes((lhs / rhs).into_bytes());
0
}
// #[unsafe(export_name = "debug_bytes")]
// pub extern "C" fn b(blen: usize) -> usize {
// let q = unsafe { prot::recv_bytes(blen) };
// prot::send_bytes(format!("{q:?}"));
// 0
// }

43
bqst-core/src/prefixes.rs Normal file
View File

@@ -0,0 +1,43 @@
pub type Prefix = (&'static str, char);
// each step is 10**±3
pub static BIG_PREFS: &[Prefix] = &[
("kilo", 'k'),
("mega", 'M'),
("giga", 'G'),
("tera", 'T'),
("peta", 'P'),
];
pub static SMOL_PREFS: &[Prefix] = &[
("milli", 'm'),
("micro", 'µ'),
("nano", 'n'),
("pico", 'p'),
("femto", 'f'),
];
pub fn extract_prefix_and_rem(s: &str) -> Option<(Prefix, &str, i8)> {
for (i, b) in BIG_PREFS.iter().enumerate() {
if let Some(stripped) = s.strip_prefix(b.0) {
return Some((*b, stripped, (i as i8 + 1) * 3));
}
}
for (i, b) in SMOL_PREFS.iter().enumerate() {
if let Some(stripped) = s.strip_prefix(b.0) {
return Some((*b, stripped, (i as i8 + 1) * -3));
}
}
for (i, b) in BIG_PREFS.iter().enumerate() {
if let Some(stripped) = s.strip_prefix(b.1) {
return Some((*b, stripped, (i as i8 + 1) * 3));
}
}
for (i, b) in SMOL_PREFS.iter().enumerate() {
if let Some(stripped) = s.strip_prefix(b.1) {
return Some((*b, stripped, (i as i8 + 1) * -3));
}
}
None
}

74
bqst-core/src/prot.rs Normal file
View File

@@ -0,0 +1,74 @@
use std::mem::MaybeUninit;
use crate::qty::Qty;
#[link(wasm_import_module = "typst_env")]
unsafe extern "C" {
#[link_name = "wasm_minimal_protocol_send_result_to_host"]
pub fn send_result_to_host(ptr: *const u8, len: usize);
#[link_name = "wasm_minimal_protocol_write_args_to_buffer"]
pub fn write_args_to_buffer(ptr: *mut MaybeUninit<u8>);
}
pub fn send_bytes(s: impl AsRef<[u8]>) {
let r = s.as_ref();
unsafe { send_result_to_host(r.as_ptr(), r.len()) };
}
pub fn send_str(s: impl AsRef<str>) {
let r = s.as_ref();
unsafe { send_result_to_host(r.as_ptr(), r.len()) };
}
/// # Safety
///
/// `len` MUST be the taken size from the fn arg
pub unsafe fn recv_bytes(len: usize) -> Box<[u8]> {
let mut bytes = Box::<[u8]>::new_uninit_slice(len);
unsafe {
write_args_to_buffer(bytes.as_mut_ptr());
bytes.assume_init()
}
}
/// # Safety
///
/// `len` MUST be the taken size from the fn arg
pub unsafe fn recv_string(len: usize) -> Result<String, std::string::FromUtf8Error> {
let b = unsafe { recv_bytes(len) };
String::from_utf8(b.into_vec())
}
/// # Safety
///
/// Must be the size of valid and come from qty bytes
pub unsafe fn recv_qty(len: usize) -> Result<Qty, String> {
let qty = unsafe { recv_bytes(len) };
unsafe { Qty::try_from_bytes(&qty).map_err(|e| format!("{e:?}")) }
}
/// # Safety
///
/// Must be the size of valid and come from qty bytes
pub unsafe fn recv_byte_n_qty(byte_len: usize, len: usize) -> Result<(u8, Qty), String> {
assert_eq!(byte_len, 1, "byte_len should be 1");
let qty = unsafe { recv_bytes(byte_len + len) };
unsafe {
Qty::try_from_bytes(&qty[1..])
.map_err(|e| format!("{e:?}"))
.map(|s| (qty[0], s))
}
}
/// # Safety
///
/// Must be the size of valid and come from qty bytes
pub unsafe fn recv_2qty(len1: usize, len2: usize) -> Result<(Qty, Qty), String> {
let qtybuf = unsafe { recv_bytes(len1 + len2) };
let qty1 = unsafe { Qty::try_from_bytes(&qtybuf[0..len1]).map_err(|e| format!("{e:?}")) }?;
let qty2 = unsafe { Qty::try_from_bytes(&qtybuf[len1..]).map_err(|e| format!("{e:?}")) }?;
Ok((qty1, qty2))
}

181
bqst-core/src/qty.rs Normal file
View File

@@ -0,0 +1,181 @@
use std::{
fmt::Display,
ops::{Add, Div, Mul, Sub},
};
use crate::{
PrecDec,
prefixes::{BIG_PREFS, SMOL_PREFS},
unit::Unit,
};
#[derive(Debug, PartialEq, Eq)]
pub struct Qty {
pub n: PrecDec,
pub u: Unit,
}
impl Display for Qty {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn normalize_num(mut n: PrecDec) -> (PrecDec, Option<char>) {
let mut i = 0;
if n.is_zero() {
return (n, None);
}
if n > PrecDec::from(1000) {
while n > PrecDec::from(1000) && i + 1 < BIG_PREFS.len() {
i += 1;
n /= PrecDec::from(1000);
}
return (n, Some(BIG_PREFS[i - 1].1));
}
if n < PrecDec::from(1) {
while n < PrecDec::from(1) && i + 1 < SMOL_PREFS.len() {
i += 1;
n *= PrecDec::from(1000);
}
return (n, Some(SMOL_PREFS[i - 1].1));
}
(n, None)
}
let is_none_unit = self.u == Unit::None;
let (n, pref) = if is_none_unit {
(self.n.clone(), None)
} else {
normalize_num(self.n.clone())
};
if n != PrecDec::from(1) || is_none_unit {
let flt = n.to_f64().value();
Display::fmt(&flt, f)?;
if !is_none_unit {
write!(f, " ")?;
}
}
if !is_none_unit {
if let Some(pref) = pref {
write!(f, "{}", pref)?;
}
write!(f, "{}", TryInto::<char>::try_into(self.u).unwrap())?;
}
Ok(())
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub struct InvalidQtyBytes<'a>(&'a [u8]);
// impl Qty {
// pub fn into_bytes(self) -> Vec<u8> {
// let mut r = Vec::new();
// r.push(self.u as u8);
// {
// let (ibig, exp) = self.n.into_repr().into_parts();
// r.extend_from_slice(&exp.to_le_bytes());
// r.extend(ibig.to_string().as_bytes());
// }
// r
// }
// /// # Safety
// ///
// /// `b` must come from valid bytes
// pub unsafe fn try_from_bytes(b: &[u8]) -> Result<Self, InvalidQtyBytes<'_>> {
// let u = unsafe { std::mem::transmute::<u8, Unit>(b[0]) };
// let n = {
// let le_bytes: [u8; 4] = [b[1], b[2], b[3], b[4]]; // T-T
// let exp = isize::from_le_bytes(le_bytes);
// let ibig_str = str::from_utf8(&b[5..])
// .map_err(|_| InvalidQtyBytes(b))?
// .to_string();
// let ibig: IBig = ibig_str.parse().map_err(|_| InvalidQtyBytes(b))?;
// PrecDec::from_parts(ibig, exp)
// };
// Ok(Self { u, n })
// }
// }
impl Qty {
pub fn into_bytes(self) -> Vec<u8> {
let mut r = Vec::new();
r.push(self.u as u8);
let (n, d) = self.n.into_parts();
r.extend(format!("{n}/{d}").as_bytes());
r
}
/// # Safety
///
/// `b` must come from valid bytes
pub unsafe fn try_from_bytes(b: &[u8]) -> Result<Self, InvalidQtyBytes<'_>> {
let u = unsafe { std::mem::transmute::<u8, Unit>(b[0]) };
let n = {
let s = str::from_utf8(&b[1..]).expect("should be valid utf8");
let (ns, ds) = s.split_once('/').expect("couldnt split");
let (n, d) = (ns.parse().unwrap(), ds.parse().unwrap());
PrecDec::from_parts(n, d)
};
Ok(Self { u, n })
}
}
impl Add for Qty {
type Output = Qty;
fn add(mut self, rhs: Self) -> Self::Output {
assert_eq!(
self.u, rhs.u,
"attempted to add qty's of different dimensions"
);
self.n += rhs.n;
self
}
}
impl Sub for Qty {
type Output = Qty;
fn sub(mut self, rhs: Self) -> Self::Output {
assert_eq!(
self.u, rhs.u,
"attempted to sub qty's of different dimensions"
);
self.n -= rhs.n;
self
}
}
impl Mul for Qty {
type Output = Qty;
fn mul(mut self, rhs: Self) -> Self::Output {
self.u *= rhs.u;
self.n *= rhs.n;
self
}
}
impl Div for Qty {
type Output = Qty;
fn div(mut self, rhs: Self) -> Self::Output {
self.u /= rhs.u;
self.n /= rhs.n;
self
}
}

103
bqst-core/src/unit.rs Normal file
View File

@@ -0,0 +1,103 @@
use std::ops::{DivAssign, MulAssign};
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Unit {
Ampere,
Watt,
Volt,
Ohm,
None,
}
use Unit::*;
#[derive(Debug)]
pub struct UnknownUnit;
impl TryFrom<Unit> for char {
type Error = UnknownUnit;
fn try_from(value: Unit) -> Result<Self, Self::Error> {
match value {
Ampere => Ok('A'),
Watt => Ok('W'),
Volt => Ok('V'),
Ohm => Ok('Ω'),
None => Err(UnknownUnit),
}
}
}
impl TryFrom<char> for Unit {
type Error = UnknownUnit;
fn try_from(value: char) -> Result<Self, Self::Error> {
match value {
'V' => Ok(Volt),
'A' => Ok(Ampere),
'W' => Ok(Watt),
'Ω' => Ok(Ohm),
_ => Err(UnknownUnit),
}
}
}
impl TryFrom<&str> for Unit {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"V" | "volt" | "volts" => Ok(Volt),
"A" | "amp" | "amps" | "ampere" | "amperes" => Ok(Ampere),
"W" | "watt" | "watts" => Ok(Watt),
"" | "ohm" | "ohms" => Ok(Ohm),
"" => Ok(None),
_ => Err(()),
}
}
}
impl MulAssign<Unit> for Unit {
fn mul_assign(&mut self, rhs: Unit) {
*self = match (*self, rhs) {
(Ampere, Volt) | (Volt, Ampere) => Watt,
(Ampere, Ohm) | (Ohm, Ampere) => Volt,
(Watt, _) | (_, Watt) | (Ampere, Ampere) | (Volt, Volt) | (Ohm, Ohm) => {
panic!("Unavailable unit")
}
(Volt, Ohm) | (Ohm, Volt) => panic!("Unknown unit"),
(t, None) => t,
(None, t) => t,
}
}
}
impl DivAssign<Unit> for Unit {
fn div_assign(&mut self, rhs: Unit) {
*self = match (*self, rhs) {
(t, None) => t,
// FIXME: bodge to allow calculating parallel circuit
// resistance (1 / sum(1 / R_i))
(None, Ohm) => Ohm,
(None, _) => panic!("idk the inverse of any of the types"),
(a, b) if a == b => None,
(Ampere, Ampere) | (Watt, Watt) | (Volt, Volt) | (Ohm, Ohm) => None,
(Watt, Ampere) => Volt,
(Watt, Volt) => Ampere,
(Volt, Ampere) => Ohm,
(Volt, Ohm) => Ampere,
(Ampere, Volt)
| (Ampere, Ohm)
| (Ohm, Volt)
| (Ohm, Ampere)
| (Watt, Ohm)
| (_, Watt) => {
panic!("Unavailable unit")
}
}
}
}