ratslang 0.4.0

Simple configuration language for working with physical systems.
Documentation

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:

  • Solving units: The language should inherently handle units when configuring physical systems.
  • Combining configs: It should be easier to combine existing configuration files rather than copying them.
  • Simple and extensible: The implementation should be small, simple, and easy to extend.
  • Type-safe units: Physical values are stored using the 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.


Variables

  • Dynamic types
  • Mutable
  • Copy-on-assignment
  • C-style naming

Types

  • Boolean: true, false
  • Integer: Example: 42, -100
  • Floating-point: Example: 69.42, -3.14
  • String: Quotes can be omitted if the string doesn't conflict with a previously defined variable. Example: "my string", another_string_without_quotes
  • Path: Example: ./somewhere/relative.dat, /or/absolute, ./../backneed/dotfirst.launch.py
  • Array/Matrix: Newlines after commas are also supported for readability. [ <Type>, <Type>, ... ], [ 42, World, [ "nested" ] ], [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]
  • Time (stored as uom::si::f64::Time):
    • Second with SI prefixes: ys through Ys (e.g., 10s, 0.5s, 1ms, 2ks)
    • Hour: h, hour, hours. Example: 2h, 1.5hour
    • Minute: min, minute, minutes. Example: 30min, 5minutes
    • Year: a, year, years. Example: 1a, 2.5years
    • Day: d, day, days. Example: 7d, 1.5days
    • Shake: shake, shakes. Example: 10shake
    • Sidereal variants (astronomical): second_sidereal, hour_sidereal, day_sidereal, year_sidereal. Example: 1day_sidereal, 23.5hours_sidereal
    • Tropical year: year_tropical, years_tropical. Example: 1year_tropical
  • Length (stored as uom::si::f64::Length):
    • Meter with SI prefixes: ym through Ym (e.g., 10m, 0.5m, 1mm, 2km)
    • Imperial/US: ft/foot, in/inch, mi/mile, yd/yard. Example: 5ft, 12inches, 3.5miles
    • Other common: ch/chain, rd/rod, fathom. Example: 1fathom, 2chains
    • Astronomical: ua/astronomical_unit, light_year, parsec (or l.y., pc). Example: 1.5au, 4.37light_years
    • Nautical: M/nautical_mile. Example: 10nautical_miles
    • Small units: angstrom (or Å), micron (or µ), mil, microinch. Example: 10angstrom, 0.5microns
    • Atomic: bohr_radius (or a₀), fermi. Example: 1bohr_radius
  • Range: Including unbound variants and empty ...
    • Time: Example: 1ms..5.3h, 6s.., 30min..2d
    • Length: Example: 1mm..100m, ..4m
    • Numbers: Example: -4..l, 6.00001..6.0001

Includes

In 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

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.

Basic Usage

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

Extracting Annotations

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

Use Cases

  • Minimal configurations: Use @minimal to generate deployment-ready configs with only essential parameters
  • Feature flags: Use @feature_name to extract configurations for specific features
  • Environment-specific settings: Use @prod, @dev, @test for environment variants
  • Documentation examples: Use @documented to generate user-facing configuration examples

Library Usage

Add 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 }
)?;

  • Ratslang files typically use the .rl extension.
  • Syntax highlighting is available with this tree-sitter grammar or this VS Code extension. For Markdown files, you can use the awk language for syntax highlighting. It is not perfect but works reasonably well.
  • Compile errors are beautifully rendered thanks to Ariadne ❤️.

Ratslang: More a slang, less a lang.


Future Plans

The following features and improvements are planned:

  • Expanded Units and Scales: Add more unit types (mass, angle, temperature, etc.) powered by the uom crate.
  • Opt-in Language Versioning: Implement an opt-in versioning system for .rl files.

License