| Crates.io | ratslang |
| lib.rs | ratslang |
| version | 0.4.0 |
| created_at | 2025-11-30 12:30:33.007031+00 |
| updated_at | 2026-01-20 13:32:32.908157+00 |
| description | Simple configuration language for working with physical systems. |
| homepage | https://codeberg.org/uos-ce/ratslang |
| repository | https://codeberg.org/uos-ce/ratslang |
| max_upload_size | |
| id | 1958176 |
| size | 204,852 |
A compact configuration language for physical systems.
Ratslang is a compact configuration language, delivered as a Rust library.
It was born out of frustration with the lack of proper types for time and length in configuration files. When we configure physical systems like robots, even a single zero more or less can have massive consequences. Sometimes, we don't even realize there's a problem until it's too late. The famous Loss of the Mars Climate Orbiter is a prime example of this issue.
The core motivations behind Ratslang are:
uom crate with full precision - no rounding.Let's take a look at how it works:
# a comment
variable = true
time = 1s
time_is_running = 1ms..2min # ranges convert automatically
# nanometers and micrometers for precision work
precision_length = 500nm..10um
len = 1cm..1m
# _ signals a variable the interpreter is looking for.
_internal = time_is_running
my.super.long.prefix.var = 0..100 # ranges on namespaced variable "var"
# nested
my.super {
long.prefix {
next_var = "UTF-🎱 Strings"
}
something_else = -99.018
}
mat = [ [ 6, 1, 9 ],
[ 3, 1, 8 ] ]
Currently, Ratslang doesn't support expressions like arithmetic, loops, or conditional statements. This is a deliberate choice, and it's still undecided if it will ever include such features. Some keywords are already reserved for potential future use.
true, false42, -10069.42, -3.14"my string", another_string_without_quotes./somewhere/relative.dat, /or/absolute, ./../backneed/dotfirst.launch.py[ <Type>, <Type>, ... ], [ 42, World, [ "nested" ] ], [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]uom::si::f64::Time):
ys through Ys (e.g., 10s, 0.5s, 1ms, 2ks)h, hour, hours. Example: 2h, 1.5hourmin, minute, minutes. Example: 30min, 5minutesa, year, years. Example: 1a, 2.5yearsd, day, days. Example: 7d, 1.5daysshake, shakes. Example: 10shakesecond_sidereal, hour_sidereal, day_sidereal, year_sidereal. Example: 1day_sidereal, 23.5hours_siderealyear_tropical, years_tropical. Example: 1year_tropicaluom::si::f64::Length):
ym through Ym (e.g., 10m, 0.5m, 1mm, 2km)ft/foot, in/inch, mi/mile, yd/yard. Example: 5ft, 12inches, 3.5milesch/chain, rd/rod, fathom. Example: 1fathom, 2chainsua/astronomical_unit, light_year, parsec (or l.y., pc). Example: 1.5au, 4.37light_yearsM/nautical_mile. Example: 10nautical_milesangstrom (or Å), micron (or µ), mil, microinch. Example: 10angstrom, 0.5micronsbohr_radius (or a₀), fermi. Example: 1bohr_radius...
1ms..5.3h, 6s.., 30min..2d1mm..100m, ..4m-4..l, 6.00001..6.0001In Ratslang, including is done by assigning a path to the current namespace. All variables will then get the respective prefix.
= ./path_relative_to_current_file.rl
strangefile {
= ./../namespacing_contents.rl
}
Annotations allow you to mark sections of your configuration for selective extraction and filtering. This is particularly useful for generating minimal configurations, feature-specific settings, or environment-dependent parameters.
Annotate a single line with # @annotation_name:
# @minimal
timeout = 5000
# @dev
debug_enabled = true
production_only = false
Annotate an entire namespace block:
# @minimal
sensor {
type = Lidar
range = 100m
}
sensor.calibration_file = /path/to/calibration.dat
Use to_string_filtered() to extract only annotated sections:
use ratslang::compile_code;
let source = r#"
# @minimal
timeout = 5000
# @minimal
sensor {
type = Lidar
}
sensor.range = 100m
"#;
let ast = compile_code(source).unwrap();
let minimal_config = ast.to_string_filtered("minimal").unwrap();
// Result: only timeout and sensor block
@minimal to generate deployment-ready configs with only essential parameters@feature_name to extract configurations for specific features@prod, @dev, @test for environment variants@documented to generate user-facing configuration examplesAdd this to your Cargo.toml.
[dependencies]
ratslang = "0.4.0"
First, you compile a Ratslang file to get a cleaned Abstract Syntax Tree (AST) with all variables resolved.
let file = std::path::Path::new("./your_file.rl");
let ast = ratslang::compile_file(&file.to_path_buf(), None, None).unwrap();
Then, you can safely read the variables you need — either with the provided helper macros for concise code, or manually using Rust's powerful pattern matching.
Using helper macros (recommended):
use ratslang::{
compile_file,
resolve_string, resolve_bool, resolve_int, resolve_float,
resolve_int_range, resolve_length_range_meters_float, resolve_time_range_seconds_float,
resolve_path,
};
// Local configs combining user vars and optional defaults
struct Configs {
user: ratslang::VariableHistory,
defaults: ratslang::VariableHistory,
}
let file = std::path::Path::new("./your_file.rl");
let ast = compile_file(&file.to_path_buf(), None, None).unwrap();
let configs = Configs { user: ast.vars, defaults: ratslang::VariableHistory::new(vec![]) };
// Simple values
let name: String = resolve_string!(configs, "name")?;
let enabled: bool = resolve_bool!(configs, "variable")?;
let k: i64 = resolve_int!(configs, "k_neighbors")?;
let ratio: f64 = resolve_float!(configs, "something_else")?;
// Paths
let path: String = resolve_path!(configs, "_file")?; // e.g., `_file = /abs/or/relative`
// Ranges (with sensible defaults used when bounds are missing)
let (d_min, d_max) = resolve_length_range_meters_float!(configs, "len", 0.0, 10.0)?; // meters as f64
let (t_min, t_max) = resolve_time_range_seconds_float!(configs, "time_is_running", 0.0, 60.0)?; // seconds as f64
let (i_min, i_max) = resolve_int_range!(configs, "my.super.long.prefix.var", 0, 100)?;
Working with uom types directly:
Physical values in Ratslang are stored as uom types (Length and Time), giving you full access to type-safe unit conversions:
use ratslang::{compile_code, Rhs, Val, UnitVal, Unit};
use ratslang::{meter, millimeter, micrometer, nanometer}; // Length units
use ratslang::{second, millisecond, microsecond, nanosecond}; // Time units
let code = r#"
sensor_range = 500um
timeout = 1500ns
"#;
let ast = compile_code(code).unwrap();
// Get a length value and convert to any unit
if let Some(Rhs::Val(Val::UnitedVal(uv))) = ast.vars.resolve("sensor_range").unwrap() {
// Access the underlying uom::si::f64::Length
let length = uv.as_length().unwrap();
// Convert to any unit with full precision using uom getters
println!("Range: {} micrometers", length.get::<micrometer>()); // 500.0
println!("Range: {} millimeters", length.get::<millimeter>()); // 0.5
println!("Range: {} meters", length.get::<meter>()); // 0.0005
println!("Range: {} nanometers", length.get::<nanometer>()); // 500000.0
}
// Get a time value
if let Some(Rhs::Val(Val::UnitedVal(uv))) = ast.vars.resolve("timeout").unwrap() {
let time = uv.as_time().unwrap();
// Convert to any unit with full precision using uom getters
println!("Timeout: {} nanoseconds", time.get::<nanosecond>()); // 1500.0
println!("Timeout: {} microseconds", time.get::<microsecond>()); // 1.5
println!("Timeout: {} milliseconds", time.get::<millisecond>()); // 0.0015
}
Manual resolution:
use anyhow::anyhow;
use ratslang::{compile_file, Rhs, Val, NumVal};
// Local configs combining user vars and optional defaults
struct Configs {
user: ratslang::VariableHistory,
defaults: ratslang::VariableHistory,
}
let file = std::path::Path::new("./your_file.rl");
let ast = compile_file(&file.to_path_buf(), None, None).unwrap();
let vars = ast.vars.filter_ns(&["_my_namespace"]);
// Resolve a variable and pattern-match its type manually
let value = vars
.resolve("_some_var")?
.map_or(Ok("a_default_value".to_owned()), |rhs| {
Ok(match rhs {
Rhs::Val(Val::StringVal(s)) => s,
_ => {
return Err(anyhow!(
"Unexpected type for _my_namespace._some_var, expected String."
));
}
})
})?;
// Or use the generic resolve_var! macro
use ratslang::resolve_var;
let configs = Configs { user: vars, defaults: ratslang::VariableHistory::new(vec![]) };
let k_neighbors: usize = resolve_var!(configs, k_neighborhood, as usize,
Rhs::Val(Val::NumVal(NumVal::Integer(i))) => { i }
)?;
.rl extension.awk language for syntax highlighting. It is not perfect but works reasonably well.Ratslang: More a slang, less a lang.
The following features and improvements are planned:
uom crate..rl files.