#[cfg(not(target_arch = "wasm32"))]
use core::ffi::{c_char, c_int};
#[cfg(any(
all(not(target_arch = "wasm32"), not(windows)),
all(windows, target_env = "gnu")
))]
use libc;
use runmat_builtins::{Tensor, Value};
use runmat_filesystem::{self as vfs, File, OpenOptions};
use runmat_macros::runtime_builtin;
#[cfg(not(target_arch = "wasm32"))]
use std::ffi::CString;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::builtins::common::fs::expand_user_path;
use crate::builtins::common::spec::{
BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
ReductionNaN, ResidencyPolicy, ShapeRequirements,
};
use crate::builtins::common::tensor;
use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
const BUILTIN_NAME: &str = "dlmwrite";
#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::tabular::dlmwrite")]
pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
name: "dlmwrite",
op_kind: GpuOpKind::Custom("io-dlmwrite"),
supported_precisions: &[],
broadcast: BroadcastSemantics::None,
provider_hooks: &[],
constant_strategy: ConstantStrategy::InlineLiteral,
residency: ResidencyPolicy::GatherImmediately,
nan_mode: ReductionNaN::Include,
two_pass_threshold: None,
workgroup_size: None,
accepts_nan_mode: false,
notes: "Runs entirely on the host; gpuArray inputs are gathered before formatting.",
};
fn dlmwrite_error(message: impl Into<String>) -> RuntimeError {
build_runtime_error(message)
.with_builtin(BUILTIN_NAME)
.build()
}
fn dlmwrite_error_with_source<E>(message: impl Into<String>, source: E) -> RuntimeError
where
E: std::error::Error + Send + Sync + 'static,
{
build_runtime_error(message)
.with_builtin(BUILTIN_NAME)
.with_source(source)
.build()
}
fn map_control_flow(err: RuntimeError) -> RuntimeError {
let identifier = err.identifier().map(|value| value.to_string());
let message = err.message().to_string();
let mut builder = build_runtime_error(message)
.with_builtin(BUILTIN_NAME)
.with_source(err);
if let Some(identifier) = identifier {
builder = builder.with_identifier(identifier);
}
builder.build()
}
#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::tabular::dlmwrite")]
pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
name: "dlmwrite",
shape: ShapeRequirements::Any,
constant_strategy: ConstantStrategy::InlineLiteral,
elementwise: None,
reduction: None,
emits_nan: false,
notes: "Not eligible for fusion; performs synchronous file I/O.",
};
#[runtime_builtin(
name = "dlmwrite",
category = "io/tabular",
summary = "Write numeric matrices to delimiter-separated text files.",
keywords = "dlmwrite,delimiter,precision,append,roffset,coffset",
accel = "cpu",
type_resolver(crate::builtins::io::type_resolvers::num_type),
builtin_path = "crate::builtins::io::tabular::dlmwrite"
)]
async fn dlmwrite_builtin(
filename: Value,
data: Value,
rest: Vec<Value>,
) -> crate::BuiltinResult<Value> {
let gathered_path = gather_if_needed_async(&filename)
.await
.map_err(map_control_flow)?;
let path = resolve_path(&gathered_path)?;
let mut gathered_args = Vec::with_capacity(rest.len());
for value in &rest {
gathered_args.push(
gather_if_needed_async(value)
.await
.map_err(map_control_flow)?,
);
}
let options = parse_arguments(&gathered_args)?;
let gathered_data = gather_if_needed_async(&data)
.await
.map_err(map_control_flow)?;
let tensor =
tensor::value_into_tensor_for("dlmwrite", gathered_data).map_err(dlmwrite_error)?;
ensure_matrix_shape(&tensor)?;
let bytes = write_dlm(&path, &tensor, &options).await?;
Ok(Value::Num(bytes as f64))
}
#[derive(Clone, Debug)]
struct DlmWriteOptions {
delimiter: String,
newline: LineEnding,
roffset: usize,
coffset: usize,
precision: PrecisionSpec,
append: bool,
}
impl Default for DlmWriteOptions {
fn default() -> Self {
Self {
delimiter: ",".to_string(),
newline: LineEnding::platform_default(),
roffset: 0,
coffset: 0,
precision: PrecisionSpec::Significant(5),
append: false,
}
}
}
#[derive(Clone, Copy, Debug)]
enum LineEnding {
Unix,
Pc,
Mac,
}
impl LineEnding {
fn as_str(&self) -> &'static str {
match self {
LineEnding::Unix => "\n",
LineEnding::Pc => "\r\n",
LineEnding::Mac => "\r",
}
}
fn platform_default() -> Self {
if cfg!(windows) {
LineEnding::Pc
} else {
LineEnding::Unix
}
}
}
#[derive(Clone, Debug)]
enum PrecisionSpec {
Significant(u32),
Format(String),
}
fn parse_arguments(args: &[Value]) -> BuiltinResult<DlmWriteOptions> {
let mut options = DlmWriteOptions::default();
if args.is_empty() {
return Ok(options);
}
let mut idx = 0usize;
while idx < args.len() {
if is_append_flag(&args[idx]) {
options.append = true;
idx += 1;
} else {
break;
}
}
if idx < args.len() && is_positional_delimiter_candidate(&args[idx]) {
options.delimiter = parse_delimiter_value(&args[idx])?;
idx += 1;
if idx + 1 < args.len()
&& is_numeric_scalar(&args[idx])
&& is_numeric_scalar(&args[idx + 1])
{
options.roffset = parse_offset_value(&args[idx], "row offset")?;
options.coffset = parse_offset_value(&args[idx + 1], "column offset")?;
idx += 2;
}
while idx < args.len() {
if is_append_flag(&args[idx]) {
options.append = true;
idx += 1;
} else {
break;
}
}
}
while idx < args.len() {
if is_append_flag(&args[idx]) {
options.append = true;
idx += 1;
continue;
}
let name = value_to_lowercase_string(&args[idx]).ok_or_else(|| {
dlmwrite_error(format!(
"dlmwrite: expected name-value pair, got {:?}",
args[idx]
))
})?;
idx += 1;
if idx >= args.len() {
return Err(dlmwrite_error(
"dlmwrite: name-value arguments must appear in pairs",
));
}
let value = &args[idx];
idx += 1;
match name.as_str() {
"delimiter" => {
options.delimiter = parse_delimiter_value(value)?;
}
"precision" => {
options.precision = parse_precision_value(value)?;
}
"newline" => {
options.newline = parse_newline_value(value)?;
}
"roffset" => {
options.roffset = parse_offset_value(value, "row offset")?;
}
"coffset" => {
options.coffset = parse_offset_value(value, "column offset")?;
}
"append" => {
options.append = parse_append_value(value)?;
}
other => {
return Err(dlmwrite_error(format!(
"dlmwrite: unsupported name-value pair '{other}'"
)));
}
}
}
Ok(options)
}
fn is_append_flag(value: &Value) -> bool {
match value {
Value::String(s) => s.trim().eq_ignore_ascii_case("-append"),
Value::CharArray(ca) if ca.rows == 1 => {
let text: String = ca.data.iter().collect();
text.trim().eq_ignore_ascii_case("-append")
}
Value::StringArray(sa) if sa.data.len() == 1 => {
sa.data[0].trim().eq_ignore_ascii_case("-append")
}
_ => false,
}
}
fn is_positional_delimiter_candidate(value: &Value) -> bool {
match value {
Value::String(_) | Value::CharArray(_) | Value::StringArray(_) => {
if let Some(lower) = value_to_lowercase_string(value) {
match lower.as_str() {
"delimiter" | "precision" | "newline" | "roffset" | "coffset" | "append"
| "-append" => return false,
_ => {}
}
}
true
}
_ => false,
}
}
fn parse_delimiter_value(value: &Value) -> BuiltinResult<String> {
match value {
Value::String(s) => interpret_delimiter_string(s),
Value::CharArray(ca) if ca.rows == 1 => {
let text: String = ca.data.iter().collect();
interpret_delimiter_string(&text)
}
Value::StringArray(sa) if sa.data.len() == 1 => interpret_delimiter_string(&sa.data[0]),
_ => Err(dlmwrite_error(
"dlmwrite: delimiter must be a string scalar or character vector",
)),
}
}
fn interpret_delimiter_string(raw: &str) -> BuiltinResult<String> {
if raw.is_empty() {
return Err(dlmwrite_error("dlmwrite: delimiter must not be empty"));
}
if raw == r"\t" {
return Ok("\t".to_string());
}
if raw == r"\n" {
return Ok("\n".to_string());
}
if raw == r"\r" {
return Ok("\r".to_string());
}
Ok(raw.to_string())
}
fn is_numeric_scalar(value: &Value) -> bool {
match value {
Value::Int(_) | Value::Num(_) | Value::Bool(_) => true,
Value::Tensor(t) => t.data.len() == 1,
Value::LogicalArray(logical) => logical.data.len() == 1,
_ => false,
}
}
fn parse_offset_value(value: &Value, context: &str) -> BuiltinResult<usize> {
let scalar =
extract_scalar(value).map_err(|e| dlmwrite_error(format!("dlmwrite: {context} {e}")))?;
if !scalar.is_finite() {
return Err(dlmwrite_error(format!(
"dlmwrite: {context} must be finite"
)));
}
let rounded = scalar.round();
if (rounded - scalar).abs() > 1e-9 {
return Err(dlmwrite_error(format!(
"dlmwrite: {context} must be an integer, got {scalar}"
)));
}
if rounded < 0.0 {
return Err(dlmwrite_error(format!("dlmwrite: {context} must be >= 0")));
}
Ok(rounded as usize)
}
fn parse_precision_value(value: &Value) -> BuiltinResult<PrecisionSpec> {
match value {
Value::Int(i) => {
let digits = i.to_i64();
if digits <= 0 {
return Err(dlmwrite_error(
"dlmwrite: precision must be a positive integer",
));
}
Ok(PrecisionSpec::Significant(digits as u32))
}
Value::Num(n) => {
if !n.is_finite() {
return Err(dlmwrite_error("dlmwrite: precision scalar must be finite"));
}
let rounded = n.round();
if (rounded - n).abs() > 1e-9 {
return Err(dlmwrite_error(
"dlmwrite: precision scalar must be an integer",
));
}
if rounded <= 0.0 {
return Err(dlmwrite_error(
"dlmwrite: precision must be a positive integer",
));
}
Ok(PrecisionSpec::Significant(rounded as u32))
}
Value::Tensor(t) if t.data.len() == 1 => parse_precision_value(&Value::Num(t.data[0])),
Value::LogicalArray(logical) if logical.data.len() == 1 => {
if logical.data[0] != 0 {
Ok(PrecisionSpec::Significant(1))
} else {
Err(dlmwrite_error(
"dlmwrite: precision must be a positive integer",
))
}
}
Value::Bool(b) => {
if *b {
Ok(PrecisionSpec::Significant(1))
} else {
Err(dlmwrite_error(
"dlmwrite: precision must be a positive integer",
))
}
}
Value::String(s) => parse_precision_format(s),
Value::CharArray(ca) if ca.rows == 1 => {
let text: String = ca.data.iter().collect();
parse_precision_format(&text)
}
Value::StringArray(sa) if sa.data.len() == 1 => parse_precision_format(&sa.data[0]),
_ => Err(dlmwrite_error(
"dlmwrite: precision must be numeric or a format string",
)),
}
}
fn parse_precision_format(text: &str) -> BuiltinResult<PrecisionSpec> {
if text.is_empty() {
return Err(dlmwrite_error(
"dlmwrite: precision format string must not be empty",
));
}
Ok(PrecisionSpec::Format(text.to_string()))
}
fn parse_newline_value(value: &Value) -> BuiltinResult<LineEnding> {
let text = value_to_lowercase_string(value).ok_or_else(|| {
dlmwrite_error("dlmwrite: newline must be a string scalar or character vector")
})?;
match text.as_str() {
"pc" | "windows" | "crlf" => Ok(LineEnding::Pc),
"unix" | "lf" => Ok(LineEnding::Unix),
"mac" | "cr" => Ok(LineEnding::Mac),
other => Err(dlmwrite_error(format!(
"dlmwrite: unsupported newline setting '{other}' (expected 'pc' or 'unix')"
))),
}
}
fn parse_append_value(value: &Value) -> BuiltinResult<bool> {
match value {
Value::Bool(b) => Ok(*b),
Value::Int(i) => Ok(i.to_i64() != 0),
Value::Num(n) => Ok(*n != 0.0),
Value::String(s) => parse_bool_string(s),
Value::CharArray(ca) if ca.rows == 1 => {
let text: String = ca.data.iter().collect();
parse_bool_string(&text)
}
Value::StringArray(sa) if sa.data.len() == 1 => parse_bool_string(&sa.data[0]),
_ => Err(dlmwrite_error("dlmwrite: append value must be logical")),
}
}
fn parse_bool_string(text: &str) -> BuiltinResult<bool> {
let lowered = text.trim().to_ascii_lowercase();
match lowered.as_str() {
"true" | "on" | "yes" | "1" => Ok(true),
"false" | "off" | "no" | "0" => Ok(false),
_ => Err(dlmwrite_error("dlmwrite: append value must be logical")),
}
}
fn extract_scalar(value: &Value) -> BuiltinResult<f64> {
match value {
Value::Num(n) => Ok(*n),
Value::Int(i) => Ok(i.to_f64()),
Value::Bool(b) => Ok(if *b { 1.0 } else { 0.0 }),
Value::Tensor(t) if t.data.len() == 1 => Ok(t.data[0]),
Value::LogicalArray(logical) if logical.data.len() == 1 => {
Ok(if logical.data[0] != 0 { 1.0 } else { 0.0 })
}
_ => Err(dlmwrite_error("must be numeric scalar")),
}
}
fn value_to_lowercase_string(value: &Value) -> Option<String> {
tensor::value_to_string(value).map(|s| s.trim().to_ascii_lowercase())
}
fn resolve_path(value: &Value) -> BuiltinResult<PathBuf> {
let raw = match value {
Value::String(s) => s.clone(),
Value::CharArray(ca) if ca.rows == 1 => ca.data.iter().collect(),
Value::StringArray(sa) if sa.data.len() == 1 => sa.data[0].clone(),
_ => {
return Err(dlmwrite_error(
"dlmwrite: filename must be a string scalar or character vector",
))
}
};
if raw.trim().is_empty() {
return Err(dlmwrite_error("dlmwrite: filename must not be empty"));
}
let expanded = expand_user_path(&raw, BUILTIN_NAME).map_err(dlmwrite_error)?;
Ok(Path::new(&expanded).to_path_buf())
}
fn ensure_matrix_shape(tensor: &Tensor) -> BuiltinResult<()> {
if tensor.shape.len() <= 2 {
return Ok(());
}
if tensor.shape[2..].iter().all(|&dim| dim == 1) {
return Ok(());
}
Err(dlmwrite_error(
"dlmwrite: input must be 2-D; reshape before writing",
))
}
async fn write_dlm(
path: &Path,
tensor: &Tensor,
options: &DlmWriteOptions,
) -> BuiltinResult<usize> {
let rows = tensor.rows();
let cols = tensor.cols();
let newline = options.newline.as_str();
let (existing_nonempty, ends_with_newline) = if options.append {
match vfs::metadata_async(path).await {
Ok(meta) if !meta.is_empty() => {
let ends = file_ends_with_newline(path).await.map_err(|err| {
dlmwrite_error_with_source(
format!(
"dlmwrite: failed to inspect existing file \"{}\" ({err})",
path.display()
),
err,
)
})?;
(true, ends)
}
Ok(_) => (false, false),
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
(false, false)
} else {
return Err(dlmwrite_error_with_source(
format!("dlmwrite: unable to inspect \"{}\" ({err})", path.display()),
err,
));
}
}
}
} else {
(false, false)
};
let mut open = OpenOptions::new();
open.create(true);
if options.append {
open.append(true);
} else {
open.write(true).truncate(true);
}
let mut file = open.open(path).map_err(|err| {
dlmwrite_error_with_source(
format!(
"dlmwrite: unable to open \"{}\" for writing ({err})",
path.display()
),
err,
)
})?;
let mut bytes = 0usize;
if options.append && existing_nonempty && !ends_with_newline {
file.write_all(newline.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(
format!("dlmwrite: failed to insert newline before append ({err})"),
err,
)
})?;
bytes += newline.len();
}
for _ in 0..options.roffset {
bytes += write_blank_row(
&mut file,
cols,
options.coffset,
&options.delimiter,
newline,
)?;
}
if rows == 0 || cols == 0 {
file.flush().map_err(|err| {
dlmwrite_error_with_source(format!("dlmwrite: failed to flush output ({err})"), err)
})?;
return Ok(bytes);
}
for row in 0..rows {
let mut fields = Vec::with_capacity(options.coffset + cols);
for _ in 0..options.coffset {
fields.push(String::new());
}
for col in 0..cols {
let idx = row + col * rows;
let value = tensor.data[idx];
fields.push(format_numeric(value, &options.precision)?);
}
let line = fields.join(&options.delimiter);
if !line.is_empty() {
file.write_all(line.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(format!("dlmwrite: failed to write data ({err})"), err)
})?;
bytes += line.len();
}
file.write_all(newline.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(format!("dlmwrite: failed to write newline ({err})"), err)
})?;
bytes += newline.len();
}
file.flush().map_err(|err| {
dlmwrite_error_with_source(format!("dlmwrite: failed to flush output ({err})"), err)
})?;
Ok(bytes)
}
fn write_blank_row(
file: &mut File,
cols: usize,
coffset: usize,
delimiter: &str,
newline: &str,
) -> BuiltinResult<usize> {
let mut bytes = 0usize;
if coffset == 0 && cols == 0 {
file.write_all(newline.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(
format!("dlmwrite: failed to write offset newline ({err})"),
err,
)
})?;
return Ok(newline.len());
}
let mut fields = Vec::with_capacity(coffset + cols);
for _ in 0..coffset {
fields.push(String::new());
}
for _ in 0..cols {
fields.push(String::new());
}
let line = fields.join(delimiter);
if !line.is_empty() {
file.write_all(line.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(format!("dlmwrite: failed to write offset row ({err})"), err)
})?;
bytes += line.len();
}
file.write_all(newline.as_bytes()).map_err(|err| {
dlmwrite_error_with_source(
format!("dlmwrite: failed to write offset newline ({err})"),
err,
)
})?;
bytes += newline.len();
Ok(bytes)
}
async fn file_ends_with_newline(path: &Path) -> io::Result<bool> {
let metadata = vfs::metadata_async(path).await?;
let len = metadata.len();
if len == 0 {
return Ok(false);
}
let mut file = File::open(path)?;
let to_read = len.min(2) as usize;
file.seek(SeekFrom::End(-(to_read as i64)))?;
let mut buffer = vec![0u8; to_read];
file.read_exact(&mut buffer)?;
Ok(buffer.contains(&b'\n') || buffer.contains(&b'\r'))
}
fn format_numeric(value: f64, precision: &PrecisionSpec) -> BuiltinResult<String> {
if value.is_nan() {
return Ok("NaN".to_string());
}
if value.is_infinite() {
return Ok(if value.is_sign_negative() {
"-Inf".to_string()
} else {
"Inf".to_string()
});
}
match precision {
PrecisionSpec::Significant(digits) => {
if *digits == 0 {
return Err(dlmwrite_error("dlmwrite: precision must be positive"));
}
let fmt = format!("%.{digits}g");
let mut rendered = c_format(value, &fmt)?;
if value == 0.0 || rendered == "-0" {
rendered = "0".to_string();
}
Ok(rendered)
}
PrecisionSpec::Format(spec) => {
let rendered = c_format(value, spec)?;
if value == 0.0 && rendered.starts_with("-") {
Ok(rendered.trim_start_matches('-').to_string())
} else {
Ok(rendered)
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
fn c_format(value: f64, spec: &str) -> BuiltinResult<String> {
let fmt = CString::new(spec.as_bytes()).map_err(|_| {
dlmwrite_error("dlmwrite: precision format must not contain embedded null bytes")
})?;
let mut size: usize = 128;
loop {
let mut buffer = vec![0u8; size];
let written = unsafe {
platform_snprintf(
buffer.as_mut_ptr() as *mut c_char,
size,
fmt.as_ptr(),
value,
)
};
if written < 0 {
return Err(dlmwrite_error(
"dlmwrite: failed to apply precision format string",
));
}
let written = written as usize;
if written >= size {
size = written + 1;
continue;
}
buffer.truncate(written);
return String::from_utf8(buffer)
.map_err(|_| dlmwrite_error("dlmwrite: formatted output was not valid UTF-8"));
}
}
#[cfg(target_arch = "wasm32")]
fn c_format(value: f64, spec: &str) -> BuiltinResult<String> {
wasm_format_float(value, spec)
}
#[cfg(all(not(target_arch = "wasm32"), not(windows)))]
unsafe fn platform_snprintf(
buffer: *mut c_char,
size: usize,
fmt: *const c_char,
value: f64,
) -> c_int {
libc::snprintf(buffer, size as libc::size_t, fmt, value)
}
#[cfg(all(windows, target_env = "msvc"))]
extern "C" {
fn _snprintf(buffer: *mut c_char, size: usize, fmt: *const c_char, ...) -> c_int;
}
#[cfg(all(windows, target_env = "msvc"))]
unsafe fn platform_snprintf(
buffer: *mut c_char,
size: usize,
fmt: *const c_char,
value: f64,
) -> c_int {
_snprintf(buffer, size, fmt, value)
}
#[cfg(all(windows, target_env = "gnu"))]
unsafe fn platform_snprintf(
buffer: *mut c_char,
size: usize,
fmt: *const c_char,
value: f64,
) -> c_int {
libc::snprintf(buffer, size as libc::size_t, fmt, value)
}
#[cfg(any(target_arch = "wasm32", test))]
fn wasm_format_float(value: f64, spec: &str) -> BuiltinResult<String> {
let parsed = ParsedFormat::parse(spec)?;
Ok(parsed.render(value))
}
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FloatSpecifier {
Fixed,
Exponent,
General,
}
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum SignFlag {
None,
Plus,
Space,
}
#[cfg(any(target_arch = "wasm32", test))]
#[derive(Clone, Copy, Debug)]
struct ParsedFormat {
specifier: FloatSpecifier,
uppercase: bool,
alternate: bool,
sign: SignFlag,
left_adjust: bool,
zero_pad: bool,
width: Option<usize>,
precision: Option<usize>,
}
#[cfg(any(target_arch = "wasm32", test))]
impl ParsedFormat {
fn parse(input: &str) -> BuiltinResult<Self> {
use std::iter::Peekable;
use std::str::Chars;
fn parse_number(
chars: &mut Peekable<Chars<'_>>,
label: &str,
) -> BuiltinResult<Option<usize>> {
let mut value: usize = 0;
let mut saw_digit = false;
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
saw_digit = true;
value = value
.checked_mul(10)
.and_then(|v| v.checked_add((ch as u8 - b'0') as usize))
.ok_or_else(|| {
dlmwrite_error(format!(
"dlmwrite: {label} too large in precision format"
))
})?;
chars.next();
} else {
break;
}
}
Ok(if saw_digit { Some(value) } else { None })
}
let mut chars = input.chars().peekable();
match chars.next() {
Some('%') => {}
_ => {
return Err(dlmwrite_error(
"dlmwrite: precision format must start with '%'",
));
}
}
let mut left_adjust = false;
let mut sign = SignFlag::None;
let mut zero_pad = false;
let mut alternate = false;
while let Some(&ch) = chars.peek() {
match ch {
'-' => {
left_adjust = true;
zero_pad = false;
chars.next();
}
'+' => {
sign = SignFlag::Plus;
chars.next();
}
' ' => {
if sign != SignFlag::Plus {
sign = SignFlag::Space;
}
chars.next();
}
'0' => {
if !left_adjust {
zero_pad = true;
}
chars.next();
}
'#' => {
alternate = true;
chars.next();
}
_ => break,
}
}
let width = parse_number(&mut chars, "field width")?;
let precision = if matches!(chars.peek(), Some('.')) {
chars.next();
parse_number(&mut chars, "precision")?.or(Some(0))
} else {
None
};
if matches!(chars.peek(), Some('l' | 'L' | 'h')) {
return Err(dlmwrite_error(
"dlmwrite: length modifiers are not supported in precision formats",
));
}
let spec_ch = chars
.next()
.ok_or_else(|| dlmwrite_error("dlmwrite: incomplete precision format"))?;
if chars.next().is_some() {
return Err(dlmwrite_error(
"dlmwrite: unexpected trailing characters in precision format",
));
}
let (specifier, uppercase) = match spec_ch {
'f' => (FloatSpecifier::Fixed, false),
'F' => (FloatSpecifier::Fixed, true),
'e' => (FloatSpecifier::Exponent, false),
'E' => (FloatSpecifier::Exponent, true),
'g' => (FloatSpecifier::General, false),
'G' => (FloatSpecifier::General, true),
other => {
return Err(dlmwrite_error(format!(
"dlmwrite: unsupported precision format specifier '{other}'"
)));
}
};
Ok(Self {
specifier,
uppercase,
alternate,
sign,
left_adjust,
zero_pad: zero_pad && !left_adjust,
width,
precision,
})
}
fn render(&self, value: f64) -> String {
let negative = value.is_sign_negative();
let magnitude = if negative { -value } else { value };
let mut body = match self.specifier {
FloatSpecifier::Fixed => {
format_fixed_body(magnitude, self.precision.unwrap_or(6), self.alternate)
}
FloatSpecifier::Exponent => format_exponential_body(
magnitude,
self.precision.unwrap_or(6),
self.alternate,
self.uppercase,
),
FloatSpecifier::General => format_general_body(
magnitude,
self.precision.unwrap_or(6),
self.alternate,
self.uppercase,
),
};
if self.uppercase {
body.make_ascii_uppercase();
}
let mut prefix = String::new();
if negative {
prefix.push('-');
} else {
match self.sign {
SignFlag::Plus => prefix.push('+'),
SignFlag::Space => prefix.push(' '),
SignFlag::None => {}
}
}
let total_len = prefix.len() + body.len();
if let Some(width) = self.width {
if width > total_len {
let pad = width - total_len;
if self.left_adjust {
let mut result = prefix;
result.push_str(&body);
result.extend(std::iter::repeat_n(' ', pad));
return result;
} else if self.zero_pad {
let mut result = String::with_capacity(width);
result.push_str(&prefix);
result.extend(std::iter::repeat_n('0', pad));
result.push_str(&body);
return result;
} else {
let mut result = String::with_capacity(width);
result.extend(std::iter::repeat_n(' ', pad));
result.push_str(&prefix);
result.push_str(&body);
return result;
}
}
}
let mut result = prefix;
result.push_str(&body);
result
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn format_fixed_body(value: f64, precision: usize, alternate: bool) -> String {
let mut s = format!("{:.*}", precision, value);
if precision == 0 && alternate && !s.contains('.') {
s.push('.');
}
s
}
#[cfg(any(target_arch = "wasm32", test))]
fn format_exponential_body(
value: f64,
precision: usize,
alternate: bool,
uppercase: bool,
) -> String {
let mut s = format!("{:.*e}", precision, value);
normalize_exponent_notation(&mut s);
if uppercase {
s.make_ascii_uppercase();
}
if precision == 0 && alternate {
insert_decimal_point(&mut s);
}
s
}
#[cfg(any(target_arch = "wasm32", test))]
fn format_general_body(value: f64, precision: usize, alternate: bool, uppercase: bool) -> String {
let effective_precision = precision.max(1);
let abs_val = value.abs();
if abs_val == 0.0 {
return if alternate {
let mut s = "0.".to_string();
s.extend(std::iter::repeat_n('0', effective_precision - 1));
s
} else {
"0".to_string()
};
}
let exponent = abs_val.log10().floor() as i32;
let force_exponent = uppercase && alternate;
let use_exponent = force_exponent || exponent < -4 || exponent >= effective_precision as i32;
let mut s = if use_exponent {
let frac = effective_precision.saturating_sub(1);
let mut out = format!("{:.*e}", frac, abs_val);
normalize_exponent_notation(&mut out);
if uppercase {
out.make_ascii_uppercase();
}
out
} else {
let frac = {
let diff = effective_precision as isize - (exponent + 1) as isize;
if diff < 0 {
0
} else {
diff as usize
}
};
let mut out = format!("{:.*}", frac, abs_val);
if uppercase {
out.make_ascii_uppercase();
}
out
};
if alternate {
insert_decimal_point(&mut s);
} else {
trim_trailing_zeros(&mut s);
}
s
}
#[cfg(any(target_arch = "wasm32", test))]
fn insert_decimal_point(s: &mut String) {
if s.contains('.') {
return;
}
if let Some(idx) = find_exponent_index(s) {
s.insert(idx, '.');
} else {
s.push('.');
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn trim_trailing_zeros(s: &mut String) {
if let Some(idx) = find_exponent_index(s) {
let exponent = s[idx..].to_string();
let mut mantissa = s[..idx].to_string();
trim_fraction(&mut mantissa);
s.clear();
s.push_str(&mantissa);
s.push_str(&exponent);
normalize_exponent_notation(s);
} else {
trim_fraction(s);
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn trim_fraction(s: &mut String) {
if let Some(dot_idx) = s.find('.') {
let mut idx = s.len();
while idx > dot_idx + 1 && matches!(s.as_bytes().get(idx - 1), Some(b'0')) {
idx -= 1;
}
if idx == dot_idx + 1 {
idx -= 1;
}
s.truncate(idx);
}
}
#[cfg(any(target_arch = "wasm32", test))]
fn find_exponent_index(s: &str) -> Option<usize> {
s.find('e').or_else(|| s.find('E'))
}
#[cfg(any(target_arch = "wasm32", test))]
fn normalize_exponent_notation(s: &mut String) {
if let Some(idx) = find_exponent_index(s) {
let marker = s.as_bytes()[idx] as char;
let suffix = &s[idx + 1..];
let (sign, digits) = if let Some(first) = suffix.chars().next() {
if first == '+' || first == '-' {
(first, suffix.get(1..).unwrap_or("").to_string())
} else {
('+', suffix.to_string())
}
} else {
('+', String::from("0"))
};
let mut normalized_digits = if digits.is_empty() {
String::from("0")
} else {
digits
};
if normalized_digits.is_empty() {
normalized_digits.push('0');
}
if normalized_digits.len() < 2 {
normalized_digits = format!("{:0>2}", normalized_digits);
}
let mut rebuilt = String::with_capacity(idx + 1 + 1 + normalized_digits.len());
rebuilt.push_str(&s[..idx]);
rebuilt.push(marker);
rebuilt.push(sign);
rebuilt.push_str(&normalized_digits);
*s = rebuilt;
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::builtins::common::test_support;
use runmat_time::unix_timestamp_ms;
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "wgpu")]
use runmat_accelerate::backend::wgpu::provider as wgpu_provider;
use runmat_accelerate_api::HostTensorView;
use runmat_builtins::IntValue;
use crate::builtins::common::fs as fs_helpers;
fn dlmwrite_builtin(filename: Value, data: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
futures::executor::block_on(super::dlmwrite_builtin(filename, data, rest))
}
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
fn error_message(err: RuntimeError) -> String {
err.message().to_string()
}
fn temp_path(ext: &str) -> PathBuf {
let millis = unix_timestamp_ms();
let unique = NEXT_ID.fetch_add(1, Ordering::Relaxed);
let mut path = std::env::temp_dir();
path.push(format!(
"runmat_dlmwrite_{}_{}_{}.{}",
std::process::id(),
millis,
unique,
ext
));
path
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn wasm_precision_parser_handles_common_specs() {
fn fmt(value: f64, spec: &str) -> String {
super::wasm_format_float(value, spec).expect("formatting failed")
}
assert_eq!(fmt(12.3456, "%.2f"), "12.35");
assert_eq!(fmt(-12.3456, "%+08.1f"), "-00012.3");
assert_eq!(fmt(0.001234, "%.4g"), "0.001234");
assert_eq!(fmt(12345.0, "%.3g"), "1.23e+04");
assert_eq!(fmt(1.5, "%#.0f"), "2.");
assert_eq!(fmt(1.5, "%#.2e"), "1.50e+00");
assert_eq!(fmt(1.5, "%#.2G"), "1.5E+00");
}
fn platform_newline() -> &'static str {
LineEnding::platform_default().as_str()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_writes_default_comma() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(Value::from(filename), Value::Tensor(tensor), Vec::new()).unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("1,2,3{nl}4,5,6{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_accepts_positional_delimiter_and_offsets() {
let path = temp_path("txt");
let tensor = Tensor::new(vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0], vec![2, 3]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![
Value::from(";"),
Value::Int(IntValue::I32(1)),
Value::Int(IntValue::I32(2)),
],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!(";;;;{nl};;1;2;3{nl};;4;5;6{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_supports_append_and_offsets() {
let path = temp_path("csv");
let tensor_a = Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]).unwrap();
let tensor_b = Tensor::new(vec![10.0, 13.0, 11.0, 14.0, 12.0, 15.0], vec![3, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename.clone()),
Value::Tensor(tensor_a),
Vec::new(),
)
.unwrap();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor_b),
vec![
Value::from("-append"),
Value::from("roffset"),
Value::Int(IntValue::I32(1)),
Value::from("coffset"),
Value::Int(IntValue::I32(1)),
],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(
contents,
format!("1,2,3{nl},,{nl},10,14{nl},13,12{nl},11,15{nl}")
);
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_precision_digits() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![12.34567, std::f64::consts::PI], vec![1, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("precision"), Value::Int(IntValue::I32(3))],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("12.3,3.14{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_precision_format_string() {
let path = temp_path("txt");
let tensor = Tensor::new(vec![0.25, 0.5, 0.75, 1.5], vec![2, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![
Value::from("precision"),
Value::from("%.4f"),
Value::from("delimiter"),
Value::from("\t"),
],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("0.2500\t0.7500{nl}0.5000\t1.5000{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_newline_pc() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("newline"), Value::from("pc")],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = LineEnding::Pc.as_str();
assert_eq!(contents, format!("1{nl}2{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_coffset_inserts_empty_fields() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 3.0, 2.0, 4.0], vec![2, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("coffset"), Value::Int(IntValue::I32(2))],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!(",,1,2{nl},,3,4{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_handles_gpu_tensors() {
test_support::with_test_provider(|provider| {
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]).unwrap();
let view = HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).unwrap();
let path = temp_path("csv");
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(Value::from(filename), Value::GpuTensor(handle), Vec::new()).unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("1,2,3{nl}"));
let _ = fs::remove_file(path);
});
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
#[cfg(feature = "wgpu")]
fn dlmwrite_handles_wgpu_provider_gather() {
let _ =
wgpu_provider::register_wgpu_provider(wgpu_provider::WgpuProviderOptions::default());
let provider = runmat_accelerate_api::provider().expect("wgpu provider");
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
let view = HostTensorView {
data: &tensor.data,
shape: &tensor.shape,
};
let handle = provider.upload(&view).unwrap();
let path = temp_path("csv");
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(Value::from(filename), Value::GpuTensor(handle), Vec::new()).unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("1,2{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_interprets_control_sequence_delimiters() {
let path = temp_path("txt");
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("delimiter"), Value::from("\\t")],
)
.unwrap();
let contents = fs::read_to_string(&path).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("1\t2{nl}"));
let _ = fs::remove_file(path);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_rejects_negative_offsets() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![1, 3]).unwrap();
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("roffset"), Value::Int(IntValue::I32(-1))],
)
.unwrap_err(),
);
assert!(message.to_ascii_lowercase().contains("row offset"));
assert!(!path.exists());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_rejects_fractional_offsets() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("coffset"), Value::Num(1.5)],
)
.unwrap_err(),
);
assert!(message.to_ascii_lowercase().contains("integer"));
assert!(!path.exists());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_rejects_empty_delimiter() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("")],
)
.unwrap_err(),
);
assert!(message.to_ascii_lowercase().contains("delimiter"));
assert!(!path.exists());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_precision_zero_error() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).unwrap();
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("precision"), Value::Int(IntValue::I32(0))],
)
.unwrap_err(),
);
assert!(message.to_ascii_lowercase().contains("precision"));
assert!(!path.exists());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_requires_name_value_pairs() {
let path = temp_path("csv");
let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::Tensor(tensor),
vec![Value::from("delimiter")],
)
.unwrap_err(),
);
assert!(message
.to_ascii_lowercase()
.contains("name-value arguments must appear in pairs"));
assert!(!path.exists());
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_expands_home_directory() {
let Some(mut home) = fs_helpers::home_directory() else {
return;
};
let filename = format!(
"runmat_dlmwrite_home_{}_{}.csv",
std::process::id(),
NEXT_ID.fetch_add(1, Ordering::Relaxed)
);
home.push(&filename);
let tilde_path = format!("~/{}", filename);
let tensor = Tensor::new(vec![42.0], vec![1, 1]).unwrap();
dlmwrite_builtin(Value::from(tilde_path), Value::Tensor(tensor), Vec::new()).unwrap();
let contents = fs::read_to_string(&home).unwrap();
let nl = platform_newline();
assert_eq!(contents, format!("42{nl}"));
let _ = fs::remove_file(home);
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
#[test]
fn dlmwrite_rejects_non_numeric_inputs() {
let path = temp_path("csv");
let filename = path.to_string_lossy().into_owned();
let message = error_message(
dlmwrite_builtin(
Value::from(filename),
Value::String("abc".into()),
Vec::new(),
)
.unwrap_err(),
);
assert!(message.contains("dlmwrite"));
}
}