├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── arson-core │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ ├── builtin │ │ ├── array.rs │ │ ├── flow.rs │ │ ├── function.rs │ │ ├── mod.rs │ │ ├── numeric.rs │ │ ├── operator.rs │ │ ├── string.rs │ │ └── variable.rs │ │ ├── context.rs │ │ ├── error.rs │ │ ├── lib.rs │ │ ├── loader.rs │ │ └── primitives │ │ ├── array.rs │ │ ├── function.rs │ │ ├── mod.rs │ │ ├── node.rs │ │ ├── numbers.rs │ │ ├── object.rs │ │ ├── string.rs │ │ ├── symbol.rs │ │ └── variable.rs ├── arson-dtb │ ├── Cargo.toml │ ├── src │ │ ├── convert.rs │ │ ├── crypt │ │ │ ├── mod.rs │ │ │ ├── new.rs │ │ │ ├── noop.rs │ │ │ └── old.rs │ │ ├── lib.rs │ │ ├── read.rs │ │ └── write.rs │ └── tests │ │ └── main.rs ├── arson-fmtlib │ ├── Cargo.toml │ ├── src │ │ ├── consts.rs │ │ ├── expr.rs │ │ ├── lib.rs │ │ └── token.rs │ └── tests │ │ ├── expr.rs │ │ └── token.rs ├── arson-fs │ ├── Cargo.toml │ └── src │ │ ├── driver.rs │ │ ├── drivers │ │ ├── basic.rs │ │ ├── mock.rs │ │ └── mod.rs │ │ ├── filesystem.rs │ │ ├── lib.rs │ │ └── path.rs ├── arson-parse │ ├── Cargo.toml │ ├── src │ │ ├── diagnostics.rs │ │ ├── encoding.rs │ │ ├── lexer.rs │ │ ├── lib.rs │ │ └── parser.rs │ └── tests │ │ ├── test_files │ │ ├── thorough.dta │ │ └── thorough_errors.dta │ │ └── thorough │ │ ├── lexer.rs │ │ ├── main.rs │ │ └── parser.rs ├── arson-stdlib │ ├── Cargo.toml │ └── src │ │ ├── fs.rs │ │ ├── lib.rs │ │ ├── math.rs │ │ ├── options.rs │ │ ├── process.rs │ │ └── stdio.rs └── arson │ ├── Cargo.toml │ └── src │ └── lib.rs ├── deny.toml ├── examples ├── LICENSE-APACHE ├── LICENSE-MIT ├── diagnostics │ ├── Cargo.toml │ ├── examples │ │ └── diagnostics.rs │ └── run │ │ ├── fail.dta │ │ └── success.dta └── hello-world │ ├── Cargo.toml │ ├── examples │ └── hello-world.rs │ └── run │ └── main.dta ├── rustfmt.toml └── tools ├── arson-fmt ├── Cargo.toml └── src │ └── main.rs ├── arson-repl ├── Cargo.toml └── src │ ├── main.rs │ └── terminal.rs ├── arson-run ├── Cargo.toml └── src │ └── main.rs └── arsonc ├── Cargo.toml └── src └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose --all-features 21 | - name: Run tests 22 | run: cargo test --verbose --all-features 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE cache/config directories 2 | .vscode/ 3 | 4 | # General-purpose ignore 5 | [Ii]gnore/ 6 | [Ii]gnored/ 7 | *.[Ii]gnore.* 8 | *.[Ii]gnored.* 9 | 10 | # Cargo build output 11 | target/ 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/EmbarkStudios/cargo-deny 3 | rev: 0.16.1 4 | hooks: 5 | - id: cargo-deny 6 | args: ["--all-features", "check"] -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/*", 4 | "examples/*", 5 | "tools/*", 6 | ] 7 | resolver = "2" 8 | 9 | [workspace.package] 10 | edition = "2021" 11 | rust-version = "1.83" 12 | version = "0.3.0" 13 | license = "LGPL-3.0-or-later" 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arson 2 | 3 | An independent implementation of the Data Array (DTA) scripting language used by Harmonix. 4 | 5 | ## License 6 | 7 | This repository is licensed under the GNU Lesser General Public License, version 3 or later. 8 | The example code under the [`examples`](examples) folder is licensed under either of 9 | 10 | - Apache License, Version 2.0, ([LICENSE-APACHE](examples/LICENSE-APACHE) or ) 11 | - MIT license ([LICENSE-MIT](examples/LICENSE-MIT) or ) 12 | 13 | at your option. 14 | 15 | Copyright (C) 2024 MiloHax 16 | 17 | These programs are free software: you can redistribute them and/or modify 18 | them under the terms of the GNU Lesser General Public License as published by 19 | the Free Software Foundation, either version 3 of the License, or 20 | (at your option) any later version. 21 | 22 | These programs are distributed in the hope that they will be useful, 23 | but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | GNU Lesser General Public License for more details. 26 | 27 | You should have received a copy of the GNU Lesser General Public License 28 | along with these programs. If not, see . 29 | -------------------------------------------------------------------------------- /crates/arson-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-core" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | # Due to the proprietary origins of the language this library seeks to implement, 10 | # it's probably best not to allow it to be published to any registries. 11 | # Additionally, the name conflicts with existing packages, and so a different 12 | # name would need to be picked in the case where it does get published. 13 | publish = false 14 | 15 | [features] 16 | default = ["text-loading", "dynamic-typenames"] 17 | 18 | file-system = ["dep:arson-fs"] 19 | 20 | text-loading = ["dep:arson-parse"] 21 | file-loading = ["text-loading", "file-system"] 22 | 23 | dynamic-typenames = [] 24 | 25 | [dependencies] 26 | arson-parse = { path = "../arson-parse", optional = true } 27 | arson-fs = { path = "../arson-fs", optional = true } 28 | 29 | thiserror = "1.0.65" 30 | 31 | [build-dependencies] 32 | autocfg = "1.4.0" 33 | -------------------------------------------------------------------------------- /crates/arson-core/build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | fn main() { 4 | let cfg = autocfg::new(); 5 | 6 | autocfg::emit_possibility("error_generic_member_access"); 7 | if cfg.probe_raw("#![feature(error_generic_member_access)]").is_ok() { 8 | autocfg::emit("error_generic_member_access"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/array.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::panic::AssertUnwindSafe; 4 | 5 | use crate::prelude::*; 6 | use crate::{EvaluationError, ExecutionError}; 7 | 8 | pub fn register_funcs(context: &mut Context) { 9 | size::register_funcs(context); 10 | elem::register_funcs(context); 11 | manip::register_funcs(context); 12 | search::register_funcs(context); 13 | algo::register_funcs(context); 14 | } 15 | 16 | fn with_array(array: &NodeValue, f: impl FnOnce(&NodeSlice) -> ExecuteResult) -> ExecuteResult { 17 | match array { 18 | NodeValue::Array(array) => f(&array.borrow()?), 19 | NodeValue::Command(array) => f(array), 20 | NodeValue::Property(array) => f(array), 21 | _ => Err(EvaluationError::NotConvertible { src: array.get_kind(), dest: NodeKind::Array }.into()), 22 | } 23 | } 24 | 25 | mod size { 26 | use super::*; 27 | 28 | pub fn register_funcs(context: &mut Context) { 29 | context.register_func("size", self::size); 30 | context.register_func("resize", self::resize); 31 | context.register_func("reserve", self::reserve); 32 | } 33 | 34 | fn size(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 35 | arson_assert_len!(args, 1); 36 | let array = args.evaluate(context, 0)?; 37 | 38 | with_array(&array, |array| array.len().try_into()) 39 | } 40 | 41 | fn resize(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 42 | arson_assert_len!(args, 2); 43 | let array = args.array(context, 0)?; 44 | let size = args.size_integer(context, 1)?; 45 | 46 | array.borrow_mut()?.resize_with(size, Default::default); 47 | Ok(Node::HANDLED) 48 | } 49 | 50 | fn reserve(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 51 | arson_assert_len!(args, 2); 52 | let array = args.array(context, 0)?; 53 | let additional = args.size_integer(context, 1)?; 54 | 55 | array.borrow_mut()?.reserve(additional); 56 | Ok(Node::HANDLED) 57 | } 58 | } 59 | 60 | mod elem { 61 | use super::*; 62 | 63 | pub fn register_funcs(context: &mut Context) { 64 | context.register_func("elem", self::elem); 65 | context.register_func("first_elem", self::first_elem); 66 | context.register_func("last_elem", self::last_elem); 67 | context.register_func("set_elem", self::set_elem); 68 | } 69 | 70 | fn elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 71 | arson_assert_len!(args, 2); 72 | let array = args.evaluate(context, 0)?; 73 | let index = args.size_integer(context, 1)?; 74 | 75 | with_array(&array, |array| array.get(index).cloned()) 76 | } 77 | 78 | fn first_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 79 | arson_assert_len!(args, 1); 80 | let array = args.evaluate(context, 0)?; 81 | 82 | with_array(&array, |array| match array.first() { 83 | Some(last) => Ok(last.into()), 84 | None => arson_fail!("cannot get the first element of an empty array"), 85 | }) 86 | } 87 | 88 | fn last_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 89 | arson_assert_len!(args, 1); 90 | let array = args.evaluate(context, 0)?; 91 | 92 | with_array(&array, |array| match array.last() { 93 | Some(last) => Ok(last.into()), 94 | None => arson_fail!("cannot get the last element of an empty array"), 95 | }) 96 | } 97 | 98 | fn set_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 99 | arson_assert_len!(args, 3); 100 | let array = args.array(context, 0)?; 101 | let index = args.size_integer(context, 1)?; 102 | let value: Node = args.evaluate(context, 2)?.into(); 103 | 104 | let mut borrow = array.borrow_mut()?; 105 | *borrow.get_mut(index)? = value.clone(); 106 | Ok(value) 107 | } 108 | } 109 | 110 | mod manip { 111 | use super::*; 112 | 113 | pub fn register_funcs(context: &mut Context) { 114 | context.register_func("push_back", self::push_back); 115 | context.register_func("pop_back", self::pop_back); 116 | 117 | context.register_func("insert_elem", self::insert_elem); 118 | context.register_func("insert_elems", self::insert_elems); 119 | context.register_func("remove_elem", self::remove_elem); 120 | context.register_func("remove_elems", self::remove_elems); 121 | } 122 | 123 | fn push_back(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 124 | arson_assert_len!(args, 2); 125 | let array = args.array(context, 0)?; 126 | let value = args.evaluate(context, 1)?; 127 | 128 | let mut borrow = array.borrow_mut()?; 129 | borrow.push(value); 130 | Ok(Node::HANDLED) 131 | } 132 | 133 | fn pop_back(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 134 | arson_assert_len!(args, 1); 135 | let array = args.array(context, 0)?; 136 | 137 | let mut borrow = array.borrow_mut()?; 138 | match borrow.pop() { 139 | Some(value) => Ok(value), 140 | None => Ok(Node::UNHANDLED), 141 | } 142 | } 143 | 144 | fn insert_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 145 | arson_assert_len!(args, 3); 146 | let array = args.array(context, 0)?; 147 | let index = args.size_integer(context, 1)?; 148 | let value = args.evaluate(context, 2)?; 149 | 150 | let mut borrow = array.borrow_mut()?; 151 | borrow.insert(index, value.clone())?; 152 | Ok(value.into()) 153 | } 154 | 155 | fn insert_elems(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 156 | arson_assert_len!(args, 3); 157 | let array = args.array(context, 0)?; 158 | let index = args.size_integer(context, 1)?; 159 | let values = args.array(context, 2)?; 160 | 161 | let mut borrow = array.borrow_mut()?; 162 | borrow.insert_slice(index, &values.borrow()?)?; 163 | Ok(Node::HANDLED) 164 | } 165 | 166 | fn remove_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 167 | arson_assert_len!(args, 2); 168 | let array = args.array(context, 0)?; 169 | let value: Node = args.evaluate(context, 1)?.into(); 170 | 171 | let mut borrow = array.borrow_mut()?; 172 | borrow.remove_item(&value); 173 | Ok(Node::HANDLED) 174 | } 175 | 176 | fn remove_elems(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 177 | arson_assert_len!(args, 3); 178 | let array = args.array(context, 0)?; 179 | let index = args.size_integer(context, 1)?; 180 | let count = args.size_integer(context, 2)?; 181 | 182 | let mut borrow = array.borrow_mut()?; 183 | borrow.drain(index..index + count)?; 184 | Ok(Node::HANDLED) 185 | } 186 | } 187 | 188 | mod search { 189 | use super::*; 190 | 191 | pub fn register_funcs(context: &mut Context) { 192 | context.register_func("find", self::find); 193 | context.register_func("find_exists", self::find_exists); 194 | context.register_func("find_elem", self::find_elem); 195 | context.register_func("contains", self::contains); 196 | } 197 | 198 | fn contains(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 199 | arson_assert_len!(args, 2); 200 | let array = args.evaluate(context, 0)?; 201 | let value: Node = args.evaluate(context, 1)?.into(); 202 | 203 | with_array(&array, |array| Ok(array.contains(&value).into())) 204 | } 205 | 206 | fn find(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 207 | let mut array = args.array(context, 0)?; 208 | 209 | for (i, arg) in args.slice(1..)?.iter().enumerate() { 210 | let tag = arg.array_tag(context)?; 211 | let borrow = array.borrow()?; 212 | match borrow.find_tag_opt(tag.clone()) { 213 | Some(found) => { 214 | drop(borrow); 215 | array = found; 216 | }, 217 | None => arson_fail!("Couldn't find key {} (depth {})", tag, i), 218 | } 219 | } 220 | 221 | Ok(array.into()) 222 | } 223 | 224 | fn find_exists(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 225 | let mut array = args.array(context, 0)?; 226 | 227 | for arg in args.slice(1..)? { 228 | let tag = arg.array_tag(context)?; 229 | let borrow = array.borrow()?; 230 | match borrow.find_tag_opt(tag) { 231 | Some(found) => { 232 | drop(borrow); 233 | array = found; 234 | }, 235 | None => return Ok(Node::UNHANDLED), 236 | } 237 | } 238 | 239 | Ok(array.into()) 240 | } 241 | 242 | fn find_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 243 | arson_assert_len!(args, 2); 244 | let array = args.evaluate(context, 0)?; 245 | let target: Node = args.evaluate(context, 1)?.into(); 246 | 247 | with_array(&array, |array| { 248 | for (i, value) in array.iter().enumerate() { 249 | if *value != target { 250 | continue; 251 | } 252 | 253 | if let Some(var) = args.get_opt(2) { 254 | let i: Node = i.try_into()?; 255 | var.variable()?.set(context, i); 256 | } 257 | 258 | return Ok(Node::TRUE); 259 | } 260 | 261 | Ok(Node::FALSE) 262 | }) 263 | } 264 | } 265 | 266 | mod algo { 267 | use super::*; 268 | 269 | pub fn register_funcs(context: &mut Context) { 270 | context.register_func("sort", self::sort); 271 | context.register_func("sort_by", self::sort_by); 272 | } 273 | 274 | fn sort(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 275 | arson_assert_len!(args, 1); 276 | let array = args.array(context, 0)?; 277 | 278 | let array = AssertUnwindSafe(array); 279 | let result = std::panic::catch_unwind(|| -> ExecuteResult { 280 | let mut borrow = array.borrow_mut()?; 281 | borrow.sort(); 282 | Ok(Node::HANDLED) 283 | }); 284 | 285 | convert_panic(result) 286 | } 287 | 288 | fn sort_by(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 289 | let mut context = AssertUnwindSafe(context); 290 | let args = AssertUnwindSafe(args); 291 | let result = std::panic::catch_unwind(move || -> ExecuteResult { 292 | arson_assert_len!(args, 4); 293 | let array = args.array(context.0, 0)?; 294 | let left_var = args.variable(1)?; 295 | let right_var = args.variable(2)?; 296 | let predicate = args.command(3)?; 297 | 298 | let mut borrow = array.borrow_mut()?; 299 | borrow.sort_by(|left, right| { 300 | left_var.set(context.0, left); 301 | right_var.set(context.0, right); 302 | let ordering = match context.execute(predicate) { 303 | Ok(value) => value, 304 | Err(error) => std::panic::panic_any(error), 305 | }; 306 | let ordering = ordering.unevaluated().integer().expect(""); 307 | match ordering.0 { 308 | ..0 => std::cmp::Ordering::Less, 309 | 0 => std::cmp::Ordering::Equal, 310 | 1.. => std::cmp::Ordering::Greater, 311 | } 312 | }); 313 | Ok(Node::HANDLED) 314 | }); 315 | 316 | convert_panic(result) 317 | } 318 | } 319 | 320 | // TODO: Find a more reusable place to put this 321 | fn convert_panic(result: Result>) -> ExecuteResult { 322 | let error = match result { 323 | Ok(result) => return result, 324 | Err(error) => error, 325 | }; 326 | 327 | let error = match error.downcast::() { 328 | Ok(inner) => return Err(*inner), 329 | Err(error) => error, 330 | }; 331 | 332 | let msg = match error.downcast::() { 333 | Ok(s) => *s, 334 | Err(error) => match error.downcast_ref::<&'static str>() { 335 | Some(s) => s.to_string(), 336 | None => "function panicked for an unknown reason".to_string(), 337 | }, 338 | }; 339 | 340 | Err(ExecutionError::Failure(msg).into()) 341 | } 342 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/flow.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | 5 | pub fn register_funcs(context: &mut Context) { 6 | control::register_funcs(context); 7 | r#loop::register_funcs(context); 8 | scope::register_funcs(context); 9 | } 10 | 11 | mod control { 12 | use super::*; 13 | 14 | pub fn register_funcs(context: &mut Context) { 15 | context.register_func("if", self::r#if); 16 | context.register_func("if_else", self::if_else); 17 | context.register_func("unless", self::unless); 18 | } 19 | 20 | fn r#if(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 21 | if args.boolean(context, 0)? { 22 | for node in args.get(1..)? { 23 | node.command()?.execute(context)?; 24 | } 25 | } 26 | 27 | Ok(Node::HANDLED) 28 | } 29 | 30 | fn if_else(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 31 | arson_assert_len!(args, 3); 32 | if args.boolean(context, 0)? { 33 | Ok(args.evaluate(context, 1)?.into()) 34 | } else { 35 | Ok(args.evaluate(context, 2)?.into()) 36 | } 37 | } 38 | 39 | fn unless(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 40 | if !args.boolean(context, 0)? { 41 | for node in args.get(1..)? { 42 | node.command()?.execute(context)?; 43 | } 44 | } 45 | 46 | Ok(Node::HANDLED) 47 | } 48 | } 49 | 50 | mod r#loop { 51 | use super::*; 52 | 53 | pub fn register_funcs(context: &mut Context) { 54 | context.register_func("while", self::r#while); 55 | context.register_func("foreach", self::foreach); 56 | context.register_func("foreach_int", self::foreach_int); 57 | } 58 | 59 | fn r#while(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 60 | while args.boolean(context, 0)? { 61 | for node in args.get(1..)? { 62 | node.command()?.execute(context)?; 63 | } 64 | } 65 | 66 | Ok(Node::HANDLED) 67 | } 68 | 69 | fn foreach(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 70 | let variable = args.variable(0)?; 71 | 72 | for node in args.array(context, 1)?.borrow()?.iter() { 73 | let value = node.evaluate(context)?; 74 | variable.set(context, value); 75 | for node in args.get(2..)? { 76 | node.command()?.execute(context)?; 77 | } 78 | } 79 | 80 | Ok(Node::HANDLED) 81 | } 82 | 83 | fn foreach_int(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 84 | fn run_loop( 85 | context: &mut Context, 86 | block: &NodeSlice, 87 | variable: &Variable, 88 | values: impl Iterator, 89 | ) -> ExecuteResult { 90 | for value in values { 91 | variable.set(context, value); 92 | for node in block { 93 | node.command()?.execute(context)?; 94 | } 95 | } 96 | 97 | Ok(Node::HANDLED) 98 | } 99 | 100 | let variable = args.variable(0)?; 101 | let start = args.integer(context, 1)?.0; 102 | let end = args.integer(context, 2)?.0; 103 | let block = args.slice(3..)?; 104 | 105 | if start > end { 106 | run_loop(context, block, variable, (end..start).rev()) 107 | } else { 108 | run_loop(context, block, variable, start..end) 109 | } 110 | } 111 | } 112 | 113 | mod scope { 114 | use super::*; 115 | 116 | pub fn register_funcs(context: &mut Context) { 117 | context.register_func("do", self::r#do); 118 | context.register_func("with", self::with_block); 119 | } 120 | 121 | fn r#do(context: &mut Context, mut args: &NodeSlice) -> ExecuteResult { 122 | let mut saved_variables = VariableStack::new(context); 123 | saved_variables.push_initializers(&mut args)?; 124 | 125 | let result = saved_variables.context().execute_block(args); 126 | drop(saved_variables); // ensure drop does not occur until after execution 127 | result 128 | } 129 | 130 | fn with_block(_context: &mut Context, _args: &NodeSlice) -> ExecuteResult { 131 | todo!("`with` func") 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/function.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | 5 | pub fn register_funcs(context: &mut Context) { 6 | context.register_func("func", self::func); 7 | context.register_func("closure", self::closure); 8 | } 9 | 10 | struct Function { 11 | name: Symbol, 12 | body: NodeArray, 13 | } 14 | 15 | impl Object for Function { 16 | fn name(&self) -> Option<&String> { 17 | Some(self.name.name()) 18 | } 19 | 20 | fn as_any(&self) -> &dyn std::any::Any { 21 | self 22 | } 23 | 24 | fn handle(&self, context: &mut Context, msg: &NodeSlice) -> ExecuteResult { 25 | context.execute_args(&self.body, msg) 26 | } 27 | } 28 | 29 | impl std::fmt::Debug for Function { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | f.debug_tuple("ScriptFunction").field(&self.name).finish() 32 | } 33 | } 34 | 35 | struct Closure { 36 | captures: Vec, 37 | body: NodeArray, 38 | } 39 | 40 | impl Object for Closure { 41 | fn name(&self) -> Option<&String> { 42 | None 43 | } 44 | 45 | fn as_any(&self) -> &dyn std::any::Any { 46 | self 47 | } 48 | 49 | fn handle(&self, context: &mut Context, msg: &NodeSlice) -> ExecuteResult { 50 | let mut saved_variables = VariableStack::new(context); 51 | saved_variables.push_saved(&self.captures); 52 | 53 | let result = saved_variables.context().execute_args(&self.body, msg); 54 | drop(saved_variables); // ensure drop does not occur until after execution 55 | result 56 | } 57 | } 58 | 59 | impl std::fmt::Debug for Closure { 60 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 61 | f.debug_tuple("ScriptClosure").finish_non_exhaustive() 62 | } 63 | } 64 | 65 | fn func(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 66 | let name = args.symbol(context, 0)?; 67 | let body = args.slice(1..)?.to_owned(); 68 | let function = Function { name: name.clone(), body }; 69 | 70 | Ok(context.register_object(name, function).into()) 71 | } 72 | 73 | fn closure(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 74 | let captures_raw = args.array(context, 0)?; 75 | let body = args.slice(1..)?.to_owned(); 76 | 77 | let mut captures = Vec::new(); 78 | for capture in captures_raw.borrow()?.iter() { 79 | let variable = capture.variable()?; 80 | captures.push(variable.save(context)) 81 | } 82 | 83 | let closure = Closure { captures, body }; 84 | Ok(closure.into()) 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | 91 | #[test] 92 | fn func() -> crate::Result { 93 | let mut context = Context::new(); 94 | 95 | let sym_func = context.add_symbol("func"); 96 | let sym_plus = context.add_symbol("+"); 97 | let sym_three = context.add_symbol("three"); 98 | let sym_add = context.add_symbol("add"); 99 | 100 | let var_num1 = Variable::new("num1", &mut context); 101 | let var_num2 = Variable::new("num2", &mut context); 102 | 103 | assert_eq!(context.get_func("three"), None); 104 | assert_eq!(context.get_func("add"), None); 105 | assert_eq!(context.get_object("three"), None); 106 | assert_eq!(context.get_object("add"), None); 107 | 108 | // no arguments 109 | /* 110 | {func three 111 | {+ 1 2} 112 | } 113 | */ 114 | let script = NodeCommand::from(arson_array![ 115 | sym_func.clone(), 116 | sym_three, 117 | NodeCommand::from(arson_array![sym_plus.clone(), 1, 2]), 118 | ]); 119 | let func = context.execute(&script)?; 120 | 121 | let func = func.unevaluated().object().expect("func returns an object"); 122 | assert!(func.is::()); 123 | assert_eq!(context.get_object("three"), Some(func.clone())); 124 | 125 | assert_eq!(func.handle(&mut context, NodeSlice::empty())?, Node::from(3)); 126 | 127 | // with arguments 128 | /* 129 | {func add ($num1 $num2) 130 | {+ $num1 $num2} 131 | } 132 | */ 133 | let script = NodeCommand::from(arson_array![ 134 | sym_func, 135 | sym_add, 136 | arson_array![var_num1.clone(), var_num2.clone()], 137 | NodeCommand::from(arson_array![sym_plus, var_num1, var_num2]), 138 | ]); 139 | let func = context.execute(&script)?; 140 | 141 | let func = func.unevaluated().object().expect("func returns an object"); 142 | assert!(func.is::()); 143 | assert_eq!(context.get_object("add"), Some(func.clone())); 144 | 145 | assert_eq!(func.handle(&mut context, arson_slice![1, 2])?, Node::from(3)); 146 | 147 | Ok(()) 148 | } 149 | 150 | #[test] 151 | fn closure() -> crate::Result { 152 | let mut context = Context::new(); 153 | 154 | let sym_do = context.add_symbol("do"); 155 | let sym_closure = context.add_symbol("closure"); 156 | let sym_plus = context.add_symbol("+"); 157 | 158 | let var_num1 = Variable::new("num1", &mut context); 159 | let var_num2 = Variable::new("num2", &mut context); 160 | let var_base = Variable::new("base", &mut context); 161 | 162 | // no arguments 163 | /* 164 | {do 165 | ($num1 5) 166 | ($num2 10) 167 | {closure 168 | ($num1 $num2) 169 | {+ $num1 $num2} 170 | } 171 | } 172 | */ 173 | #[rustfmt::skip] 174 | let script = NodeCommand::from(arson_array![ 175 | sym_do.clone(), 176 | arson_array![var_num1.clone(), 5], 177 | arson_array![var_num2.clone(), 10], 178 | NodeCommand::from(arson_array![ 179 | sym_closure.clone(), 180 | arson_array![var_num1.clone(), var_num2.clone()], 181 | NodeCommand::from(arson_array![sym_plus.clone(), var_num1.clone(), var_num2.clone()]) 182 | ]), 183 | ]); 184 | let closure = context.execute(&script)?; 185 | 186 | let closure = closure.unevaluated().object().expect("closure returns an object"); 187 | assert!(closure.is::()); 188 | 189 | assert_eq!(closure.handle(&mut context, NodeSlice::empty())?, Node::from(15)); 190 | 191 | // with arguments 192 | /* 193 | {do 194 | ($base 25) 195 | {closure 196 | ($base) 197 | ($num1 $num2) 198 | {+ $num1 $num2 $base} 199 | } 200 | } 201 | */ 202 | #[rustfmt::skip] 203 | let script = NodeCommand::from(arson_array![ 204 | sym_do.clone(), 205 | arson_array![var_base.clone(), 25], 206 | NodeCommand::from(arson_array![ 207 | sym_closure.clone(), 208 | arson_array![var_base.clone()], 209 | arson_array![var_num1.clone(), var_num2.clone()], 210 | NodeCommand::from(arson_array![sym_plus.clone(), var_num1.clone(), var_num2.clone(), var_base.clone()]) 211 | ]), 212 | ]); 213 | let closure = context.execute(&script)?; 214 | 215 | let closure = closure.unevaluated().object().expect("closure returns an object"); 216 | assert!(closure.is::()); 217 | 218 | assert_eq!(closure.handle(&mut context, arson_slice![5, 10])?, Node::from(40)); 219 | 220 | Ok(()) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | use crate::SymbolTable; 5 | 6 | mod array; 7 | mod flow; 8 | mod function; 9 | mod numeric; 10 | mod operator; 11 | mod string; 12 | mod variable; 13 | 14 | pub(crate) struct BuiltinState { 15 | pub unquote: Symbol, 16 | pub unquote_abbrev: Symbol, 17 | } 18 | 19 | impl BuiltinState { 20 | pub(crate) fn new(symbol_table: &mut SymbolTable) -> Self { 21 | Self { 22 | unquote: symbol_table.add("unquote"), 23 | unquote_abbrev: symbol_table.add(","), 24 | } 25 | } 26 | } 27 | 28 | pub fn register_funcs(context: &mut Context) { 29 | context.add_macro_define("ARSON"); 30 | 31 | array::register_funcs(context); 32 | flow::register_funcs(context); 33 | function::register_funcs(context); 34 | numeric::register_funcs(context); 35 | operator::register_funcs(context); 36 | string::register_funcs(context); 37 | variable::register_funcs(context); 38 | } 39 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/numeric.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | use crate::{FloatValue, Integer, IntegerValue, Number}; 5 | 6 | pub fn register_funcs(context: &mut Context) { 7 | context.add_macro("TRUE", arson_array![1]); 8 | context.add_macro("FALSE", arson_array![0]); 9 | 10 | bits::register_funcs(context); 11 | sign::register_funcs(context); 12 | limit::register_funcs(context); 13 | round::register_funcs(context); 14 | convert::register_funcs(context); 15 | } 16 | 17 | mod bits { 18 | use super::*; 19 | 20 | pub fn register_funcs(context: &mut Context) { 21 | context.register_func("highest_bit", self::highest_bit); 22 | context.register_func("lowest_bit", self::lowest_bit); 23 | context.register_func("count_bits", self::count_bits); 24 | } 25 | 26 | fn first_active_bit>(value: IntegerValue, mut bit_range: I) -> IntegerValue { 27 | match bit_range.find(|i| value & (1 << i) != 0) { 28 | Some(i) => 1 << i, 29 | None => 0, 30 | } 31 | } 32 | 33 | fn highest_bit(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 34 | arson_assert_len!(args, 1); 35 | let value = args.integer(context, 0)?; 36 | let result = first_active_bit(value.0, (0..IntegerValue::BITS).rev()); 37 | Ok(result.into()) 38 | } 39 | 40 | fn lowest_bit(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 41 | arson_assert_len!(args, 1); 42 | let value = args.integer(context, 0)?; 43 | let result = first_active_bit(value.0, 0..IntegerValue::BITS); 44 | Ok(result.into()) 45 | } 46 | 47 | fn count_bits(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 48 | arson_assert_len!(args, 1); 49 | let result = args.integer(context, 0)?.0.count_ones() as IntegerValue; 50 | Ok(result.into()) 51 | } 52 | } 53 | 54 | mod sign { 55 | use super::*; 56 | 57 | pub fn register_funcs(context: &mut Context) { 58 | context.register_func("abs", self::abs); 59 | context.register_func("sign", self::sign); 60 | } 61 | 62 | fn abs(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 63 | arson_assert_len!(args, 1); 64 | match args.number(context, 0)? { 65 | Number::Integer(value) => Ok(value.0.saturating_abs().into()), 66 | Number::Float(value) => Ok(value.abs().into()), 67 | } 68 | } 69 | 70 | fn sign(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 71 | arson_assert_len!(args, 1); 72 | match args.number(context, 0)? { 73 | Number::Integer(value) => Ok(value.0.signum().into()), 74 | Number::Float(value) => Ok(value.signum().into()), 75 | } 76 | } 77 | } 78 | 79 | mod limit { 80 | use super::*; 81 | 82 | pub fn register_funcs(context: &mut Context) { 83 | context.register_func("min", self::min); 84 | context.register_func("max", self::max); 85 | context.register_func("clamp", self::clamp); 86 | 87 | context.register_func("min_eq", self::min_assign); 88 | context.register_func("max_eq", self::max_assign); 89 | context.register_func("clamp_eq", self::clamp_assign); 90 | } 91 | 92 | fn min(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 93 | args.number_chain( 94 | context, 95 | |left, right| Ok(left.min(right)), 96 | |left, right| Ok(left.min(right)), 97 | ) 98 | } 99 | 100 | fn max(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 101 | args.number_chain( 102 | context, 103 | |left, right| Ok(left.max(right)), 104 | |left, right| Ok(left.max(right)), 105 | ) 106 | } 107 | 108 | fn clamp(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 109 | fn integer_clamp(min: Integer, max: Integer, value: Integer) -> ExecuteResult { 110 | arson_assert!(min <= max, "Invalid clamp range: min ({min}) is greater than max ({max})"); 111 | Ok(value.clamp(min, max).into()) 112 | } 113 | 114 | fn float_clamp(min: FloatValue, max: FloatValue, value: FloatValue) -> ExecuteResult { 115 | use std::cmp::Ordering; 116 | 117 | // Manual handling of `min <= max` to make the `None` case explicitly defined, 118 | // per Clippy recommendation. The behavior would be identical otherwise, 119 | // but this allows us to set a specific error message for this case. 120 | match min.partial_cmp(&max) { 121 | Some(Ordering::Less) => (/* continue on */), 122 | Some(Ordering::Equal) => (/* continue on */), 123 | Some(Ordering::Greater) => { 124 | arson_fail!("Invalid clamp range: min ({min}) is greater than max ({max})") 125 | }, 126 | None => arson_fail!("Invalid clamp range: min ({min}) and max ({max}) are not comparable"), 127 | } 128 | 129 | arson_assert!(!min.is_nan(), "Min cannot be NaN"); 130 | arson_assert!(!max.is_nan(), "Max cannot be NaN"); 131 | 132 | Ok(value.clamp(min, max).into()) 133 | } 134 | 135 | arson_assert_len!(args, 3); 136 | 137 | let value = args.number(context, 0)?; 138 | let min = args.number(context, 1)?; 139 | let max = args.number(context, 2)?; 140 | 141 | let Number::Integer(min) = min else { 142 | return float_clamp(min.float(), max.float(), value.float()); 143 | }; 144 | let Number::Integer(max) = max else { 145 | return float_clamp(min.0 as FloatValue, max.float(), value.float()); 146 | }; 147 | let Number::Integer(value) = value else { 148 | return float_clamp(min.0 as FloatValue, max.0 as FloatValue, value.float()); 149 | }; 150 | 151 | integer_clamp(min, max, value) 152 | } 153 | 154 | fn min_assign(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 155 | let result = self::min(context, args)?; 156 | args.set_variable(context, 0, result.clone())?; 157 | Ok(result) 158 | } 159 | 160 | fn max_assign(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 161 | let result = self::max(context, args)?; 162 | args.set_variable(context, 0, result.clone())?; 163 | Ok(result) 164 | } 165 | 166 | fn clamp_assign(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 167 | arson_assert_len!(args, 3); 168 | 169 | let result = clamp(context, args)?; 170 | args.set_variable(context, 0, result.clone())?; 171 | Ok(result) 172 | } 173 | } 174 | 175 | mod round { 176 | use super::*; 177 | 178 | pub fn register_funcs(context: &mut Context) { 179 | context.register_func("ceil", self::ceiling); 180 | context.register_func("floor", self::floor); 181 | context.register_func("trunc", self::truncate); 182 | context.register_func("round", self::round); 183 | } 184 | 185 | fn ceiling(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 186 | arson_assert_len!(args, 1); 187 | Ok(args.float(context, 0)?.ceil().into()) 188 | } 189 | 190 | fn floor(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 191 | arson_assert_len!(args, 1); 192 | Ok(args.float(context, 0)?.floor().into()) 193 | } 194 | 195 | fn truncate(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 196 | arson_assert_len!(args, 1); 197 | Ok(args.float(context, 0)?.trunc().into()) 198 | } 199 | 200 | fn round(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 201 | arson_assert_len!(args, 1); 202 | Ok(args.float(context, 0)?.round().into()) 203 | } 204 | } 205 | 206 | mod convert { 207 | use super::*; 208 | 209 | pub fn register_funcs(context: &mut Context) { 210 | context.register_func("int", self::int); 211 | context.register_func("float", self::float); 212 | } 213 | 214 | fn int(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 215 | arson_assert_len!(args, 1); 216 | match args.evaluate(context, 0)? { 217 | NodeValue::Integer(value) => Ok(value.into()), 218 | NodeValue::Float(value) => Ok((value as IntegerValue).into()), 219 | NodeValue::String(value) => Ok(value.parse::()?.into()), 220 | NodeValue::Symbol(value) => Ok(value.name().parse::()?.into()), 221 | // NodeValue::Object(value) => Ok(value.as_ptr() as usize as IntegerValue), 222 | value => arson_fail!("value of type {:?} is not convertible to an integer", value.get_kind()), 223 | } 224 | } 225 | 226 | fn float(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 227 | arson_assert_len!(args, 1); 228 | match args.evaluate(context, 0)? { 229 | NodeValue::Integer(value) => Ok((value.0 as FloatValue).into()), 230 | NodeValue::Float(value) => Ok(value.into()), 231 | NodeValue::String(value) => Ok(value.parse::()?.into()), 232 | NodeValue::Symbol(value) => Ok(value.name().parse::()?.into()), 233 | value => arson_fail!("value of type {:?} is not convertible to a float", value.get_kind()), 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/string.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | use crate::{NumericError, StringError}; 5 | 6 | pub fn register_funcs(context: &mut Context) { 7 | basic::register_funcs(context); 8 | compare::register_funcs(context); 9 | search::register_funcs(context); 10 | manip::register_funcs(context); 11 | convert::register_funcs(context); 12 | } 13 | 14 | mod basic { 15 | use super::*; 16 | 17 | pub fn register_funcs(context: &mut Context) { 18 | context.register_func("str_elem", self::str_elem); 19 | context.register_func("strlen", self::strlen); 20 | } 21 | 22 | fn str_elem(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 23 | arson_assert_len!(args, 2); 24 | let string = args.string(context, 0)?; 25 | let index = args.size_integer(context, 1)?; 26 | 27 | match string.get(index..index + 1) { 28 | Some(char) => Ok(crate::intern_string(char).into()), 29 | None => { 30 | if index > string.len() { 31 | Err(NumericError::IndexOutOfRange(index, 0..string.len()).into()) 32 | } else { 33 | Err(StringError::NotCharBoundary(index).into()) 34 | } 35 | }, 36 | } 37 | } 38 | 39 | fn strlen(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 40 | arson_assert_len!(args, 1); 41 | let string = args.string(context, 0)?; 42 | 43 | string.len().try_into() 44 | } 45 | } 46 | 47 | mod compare { 48 | use super::*; 49 | 50 | pub fn register_funcs(context: &mut Context) { 51 | context.register_func("streq", self::streq); 52 | context.register_func("strieq", self::strieq); 53 | context.register_func("strcmp", self::strcmp); 54 | } 55 | 56 | fn streq(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 57 | arson_assert_len!(args, 2); 58 | let left = args.string(context, 0)?; 59 | let right = args.string(context, 1)?; 60 | 61 | Ok((left == right).into()) 62 | } 63 | 64 | fn strieq(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 65 | arson_assert_len!(args, 2); 66 | let left = args.string(context, 0)?; 67 | let right = args.string(context, 1)?; 68 | 69 | Ok(left.eq_ignore_ascii_case(&right).into()) 70 | } 71 | 72 | fn strcmp(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 73 | arson_assert_len!(args, 2); 74 | let left = args.string(context, 0)?; 75 | let right = args.string(context, 1)?; 76 | 77 | Ok(left.cmp(&right).into()) 78 | } 79 | } 80 | 81 | mod search { 82 | use super::*; 83 | 84 | pub fn register_funcs(context: &mut Context) { 85 | context.register_func("has_substr", self::has_substr); 86 | context.register_func("has_any_substr", self::has_any_substr); 87 | context.register_func("find_substr", self::find_substr); 88 | 89 | context.register_func("startswith", self::startswith); 90 | context.register_func("endswith", self::endswith); 91 | 92 | context.register_func("search_replace", self::search_replace); 93 | } 94 | 95 | fn has_substr(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 96 | arson_assert_len!(args, 2); 97 | let haystack = args.string(context, 0)?; 98 | let needle = args.string(context, 1)?; 99 | Ok(haystack.contains(needle.as_ref()).into()) 100 | } 101 | 102 | fn has_any_substr(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 103 | arson_assert_len!(args, 2); 104 | let haystack = args.string(context, 0)?; 105 | let needles = args.array(context, 1)?; 106 | 107 | for node in needles.borrow()?.iter() { 108 | let needle = node.string(context)?; 109 | if haystack.contains(needle.as_ref()) { 110 | return Ok(Node::TRUE); 111 | } 112 | } 113 | 114 | Ok(Node::FALSE) 115 | } 116 | 117 | fn find_substr(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 118 | arson_assert_len!(args, 2); 119 | let haystack = args.string(context, 0)?; 120 | let needle = args.string(context, 1)?; 121 | 122 | match haystack.find(needle.as_ref()) { 123 | Some(index) => match Node::try_from(index) { 124 | Ok(index) => Ok(index), 125 | Err(_) => Ok((-1).into()), 126 | }, 127 | None => Ok((-1).into()), 128 | } 129 | } 130 | 131 | fn startswith(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 132 | arson_assert_len!(args, 2); 133 | let haystack = args.string(context, 0)?; 134 | let needle = args.string(context, 1)?; 135 | Ok(haystack.starts_with(needle.as_ref()).into()) 136 | } 137 | 138 | fn endswith(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 139 | arson_assert_len!(args, 2); 140 | let haystack = args.string(context, 0)?; 141 | let needle = args.string(context, 1)?; 142 | Ok(haystack.ends_with(needle.as_ref()).into()) 143 | } 144 | 145 | fn search_replace(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 146 | arson_assert_len!(args, 4); 147 | let source = args.string(context, 0)?; 148 | let search = args.string(context, 1)?; 149 | let replace = args.string(context, 2)?; 150 | let dest_var = args.variable(3)?; 151 | 152 | if source.contains(search.as_ref()) { 153 | let replaced = source.replace(search.as_ref(), replace.as_ref()); 154 | dest_var.set(context, replaced); 155 | Ok(Node::TRUE) 156 | } else { 157 | dest_var.set(context, source); 158 | Ok(Node::FALSE) 159 | } 160 | } 161 | } 162 | 163 | mod manip { 164 | use super::*; 165 | 166 | pub fn register_funcs(context: &mut Context) { 167 | context.register_func("substr", self::substr); 168 | context.register_func("strcat", self::strcat); 169 | context.register_func("tolower", self::tolower); 170 | context.register_func("toupper", self::toupper); 171 | } 172 | 173 | fn substr(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 174 | arson_assert_len!(args, 3); 175 | let string = args.string(context, 0)?; 176 | let start = args.size_integer(context, 1)?; 177 | let end = args.size_integer(context, 2)?; 178 | 179 | match string.get(start..end) { 180 | Some(slice) => Ok(slice.into()), 181 | None => { 182 | if start > string.len() || end > string.len() { 183 | Err(NumericError::slice_out_of_range(start..end, 0..string.len()).into()) 184 | } else if !string.is_char_boundary(start) { 185 | Err(StringError::NotCharBoundary(start).into()) 186 | } else { 187 | Err(StringError::NotCharBoundary(end).into()) 188 | } 189 | }, 190 | } 191 | } 192 | 193 | fn strcat(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 194 | let string_var = args.variable(0)?; 195 | let mut string = string_var.get(context).string(context)?.as_ref().clone(); 196 | 197 | for node in args.get(1..)? { 198 | let piece = node.string(context)?; 199 | string.push_str(piece.as_ref()); 200 | } 201 | 202 | string_var.set(context, string); 203 | 204 | Ok(Node::HANDLED) 205 | } 206 | 207 | fn tolower(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 208 | arson_assert_len!(args, 1); 209 | let string = args.string(context, 0)?; 210 | Ok(string.to_lowercase().into()) 211 | } 212 | 213 | fn toupper(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 214 | arson_assert_len!(args, 1); 215 | let string = args.string(context, 0)?; 216 | Ok(string.to_uppercase().into()) 217 | } 218 | } 219 | 220 | mod convert { 221 | use super::*; 222 | 223 | pub fn register_funcs(context: &mut Context) { 224 | context.register_func("char", self::char); 225 | context.register_func("symbol", self::symbol); 226 | context.register_func("string_flags", self::string_flags); 227 | } 228 | 229 | fn char(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 230 | arson_assert_len!(args, 1); 231 | let value = args.integer(context, 0)?; 232 | 233 | let value = match u32::try_from(value.0) { 234 | Ok(value) => value, 235 | Err(error) => return Err(NumericError::IntegerConversion(error).into()), 236 | }; 237 | 238 | match char::from_u32(value) { 239 | Some(char) => Ok(char.to_string().into()), 240 | None => arson_fail!("value {value} is not a valid Unicode scalar value"), 241 | } 242 | } 243 | 244 | fn symbol(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 245 | arson_assert_len!(args, 1); 246 | let symbol = args.force_symbol(context, 0)?; 247 | Ok(symbol.into()) 248 | } 249 | 250 | fn string_flags(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 251 | arson_assert_len!(args, 2); 252 | let mask = args.integer(context, 0)?; 253 | let flag_defines = args.array(context, 1)?; 254 | 255 | let mut flags_string = String::new(); 256 | 257 | for node in flag_defines.borrow()?.iter() { 258 | let define_name = node.force_symbol(context)?; 259 | let define = match context.get_macro(&define_name) { 260 | Some(define) => define, 261 | None => arson_fail!("flag macro {define_name} does not exist"), 262 | }; 263 | 264 | arson_assert_len!(define, 1, "flag macro {define_name} has too many elements"); 265 | let NodeValue::Integer(flag) = define.unevaluated(0)? else { 266 | arson_fail!("value of flag macro {define_name} is not an integer"); 267 | }; 268 | 269 | if (mask & flag).0 != 0 { 270 | if !flags_string.is_empty() { 271 | flags_string.push('|'); 272 | } 273 | 274 | flags_string.push_str(define_name.name()); 275 | } 276 | } 277 | 278 | Ok(flags_string.into()) 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /crates/arson-core/src/builtin/variable.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use crate::prelude::*; 4 | 5 | pub fn register_funcs(context: &mut Context) { 6 | context.add_macro("kDataInt", arson_array![NodeKind::Integer]); 7 | context.add_macro("kDataFloat", arson_array![NodeKind::Float]); 8 | context.add_macro("kDataString", arson_array![NodeKind::String]); 9 | context.add_macro("kDataSymbol", arson_array![NodeKind::Symbol]); 10 | context.add_macro("kDataVar", arson_array![NodeKind::Variable]); 11 | 12 | context.add_macro("kDataFunc", arson_array![NodeKind::Function]); 13 | context.add_macro("kDataObject", arson_array![NodeKind::Object]); 14 | 15 | context.add_macro("kDataArray", arson_array![NodeKind::Array]); 16 | context.add_macro("kDataCommand", arson_array![NodeKind::Command]); 17 | context.add_macro("kDataProperty", arson_array![NodeKind::Property]); 18 | 19 | // Already handled by the parser, but may as well for completeness 20 | context.add_macro("kDataUnhandled", arson_array![NodeKind::Unhandled]); 21 | 22 | context.register_func("type", self::r#type); 23 | 24 | context.register_func("set", self::set); 25 | context.register_func("set_var", self::set_var); 26 | context.register_func("set_this", self::set_this); 27 | 28 | context.register_func("var", self::var); 29 | } 30 | 31 | fn r#type(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 32 | arson_assert_len!(args, 1); 33 | let value = args.evaluate(context, 0)?; 34 | Ok(value.get_kind().into()) 35 | } 36 | 37 | fn set(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 38 | arson_assert_len!(args, 2); 39 | let value = args.evaluate(context, 1)?; 40 | args.set_variable(context, 0, value.clone())?; 41 | Ok(value.into()) 42 | } 43 | 44 | fn set_var(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 45 | arson_assert_len!(args, 2); 46 | 47 | let name = args.force_symbol(context, 0)?; 48 | let value = args.evaluate(context, 1)?; 49 | context.set_variable(name, value.clone()); 50 | 51 | Ok(value.into()) 52 | } 53 | 54 | fn set_this(_context: &mut Context, _args: &NodeSlice) -> ExecuteResult { 55 | todo!("set_this") 56 | } 57 | 58 | fn var(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 59 | arson_assert_len!(args, 1); 60 | let name = args.force_symbol(context, 0)?; 61 | Ok(context.get_variable(name)) 62 | } 63 | -------------------------------------------------------------------------------- /crates/arson-core/src/context.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::any::{Any, TypeId}; 4 | use std::collections::HashMap; 5 | use std::rc::Rc; 6 | 7 | #[cfg(feature = "file-system")] 8 | use arson_fs::FileSystem; 9 | 10 | use crate::builtin::BuiltinState; 11 | use crate::prelude::*; 12 | use crate::{FindDataPredicate, SymbolTable}; 13 | 14 | /// The result of a script execution. 15 | pub type ExecuteResult = crate::Result; 16 | 17 | #[non_exhaustive] 18 | #[derive(thiserror::Error, Debug)] 19 | pub enum ExecutionError { 20 | #[error("bad length {actual}, expected {expected}")] 21 | LengthMismatch { expected: usize, actual: usize }, 22 | 23 | #[error("no state registered for name {0}")] 24 | StateNotFound(&'static str), 25 | 26 | #[error("no handler registered for name {0}")] 27 | HandlerNotFound(String), 28 | 29 | #[error("value {0} (of kind {1:?}) is not a valid handler")] 30 | NotAHandler(String, NodeKind), 31 | 32 | #[error("{0}")] 33 | Failure(String), 34 | } 35 | 36 | pub trait ContextState: Any {} 37 | 38 | pub struct Context { 39 | symbol_table: SymbolTable, 40 | 41 | macros: SymbolMap>, 42 | variables: SymbolMap, 43 | functions: SymbolMap, 44 | objects: SymbolMap, 45 | 46 | #[cfg(feature = "file-system")] 47 | file_system: Option, 48 | pub(crate) builtin_state: BuiltinState, 49 | 50 | states: HashMap>, 51 | } 52 | 53 | impl Context { 54 | pub fn new() -> Self { 55 | let mut symbol_table = SymbolTable::new(); 56 | let builtin_state = BuiltinState::new(&mut symbol_table); 57 | 58 | let mut context = Self { 59 | symbol_table, 60 | 61 | macros: SymbolMap::new(), 62 | variables: SymbolMap::new(), 63 | functions: SymbolMap::new(), 64 | objects: SymbolMap::new(), 65 | 66 | #[cfg(feature = "file-system")] 67 | file_system: None, 68 | builtin_state, 69 | 70 | states: HashMap::new(), 71 | }; 72 | 73 | crate::builtin::register_funcs(&mut context); 74 | 75 | context 76 | } 77 | 78 | pub fn register_state(&mut self, state: S) { 79 | self.states.insert(state.type_id(), Box::new(state)); 80 | } 81 | 82 | pub fn get_state(&self) -> crate::Result<&S> { 83 | self.get_state_opt() 84 | .ok_or_else(|| ExecutionError::StateNotFound(std::any::type_name::()).into()) 85 | } 86 | 87 | pub fn get_state_opt(&self) -> Option<&S> { 88 | self.states.get(&TypeId::of::()).and_then(|s| s.downcast_ref()) 89 | } 90 | 91 | pub fn get_state_mut(&mut self) -> crate::Result<&mut S> { 92 | self.get_state_mut_opt() 93 | .ok_or_else(|| ExecutionError::StateNotFound(std::any::type_name::()).into()) 94 | } 95 | 96 | pub fn get_state_mut_opt(&mut self) -> Option<&mut S> { 97 | self.states.get_mut(&TypeId::of::()).and_then(|s| s.downcast_mut()) 98 | } 99 | 100 | pub fn add_symbol(&mut self, name: &str) -> Symbol { 101 | self.symbol_table.add(name) 102 | } 103 | 104 | // pub fn remove_symbol(&mut self, name: &Symbol) { 105 | // self.symbol_table.remove(name); 106 | // } 107 | 108 | pub fn get_symbol(&self, name: &str) -> Option { 109 | self.symbol_table.get(name) 110 | } 111 | 112 | pub fn add_macro(&mut self, name: impl IntoSymbol, array: impl Into>) { 113 | let name = name.into_symbol(self); 114 | self.macros.insert(name, array.into()); 115 | } 116 | 117 | pub fn add_macro_define(&mut self, name: impl IntoSymbol) { 118 | self.add_macro(name, arson_array![1]); 119 | } 120 | 121 | pub fn remove_macro(&mut self, name: impl IntoSymbol) { 122 | let name = name.into_symbol(self); 123 | self.macros.remove(&name); 124 | } 125 | 126 | pub fn get_macro(&self, name: impl IntoSymbol) -> Option> { 127 | name.get_symbol(self).and_then(|name| self.macros.get(&name).cloned()) 128 | } 129 | 130 | pub fn find_macro(&self, prefix: &str, predicate: impl FindDataPredicate) -> Option<&NodeArray> { 131 | for (name, r#macro) in &self.macros { 132 | let Some(tag) = r#macro.unevaluated_opt(0) else { 133 | continue; 134 | }; 135 | if predicate.matches(tag) && name.name().starts_with(prefix) { 136 | return Some(r#macro); 137 | } 138 | } 139 | 140 | None 141 | } 142 | 143 | pub fn get_variable(&self, name: impl IntoSymbol) -> Node { 144 | self.get_variable_opt(name).unwrap_or(Node::UNHANDLED) 145 | } 146 | 147 | pub fn get_variable_opt(&self, name: impl IntoSymbol) -> Option { 148 | name.get_symbol(self).and_then(|name| self.variables.get(&name).cloned()) 149 | } 150 | 151 | pub fn set_variable(&mut self, name: impl IntoSymbol, value: impl Into) -> Option { 152 | let name = name.into_symbol(self); 153 | self.variables.insert(name, value.into()) 154 | } 155 | 156 | pub fn register_func(&mut self, name: impl IntoSymbol, func: F) -> bool 157 | where 158 | // note: mutable closures are not allowed due to the out-to-in execution model, which causes 159 | // problems when a command argument to a function calls the very same function again 160 | F: Fn(&mut Context, &NodeSlice) -> ExecuteResult + 'static, 161 | { 162 | let name = name.into_symbol(self); 163 | self.functions.insert(name, HandleFn::new(func)).is_none() 164 | } 165 | 166 | pub fn get_func(&self, name: impl IntoSymbol) -> Option { 167 | name.get_symbol(self).and_then(|name| self.functions.get(&name).cloned()) 168 | } 169 | 170 | pub fn register_object(&mut self, name: impl IntoSymbol, object: O) -> ObjectRef 171 | where 172 | O: Object + 'static, 173 | { 174 | let name = name.into_symbol(self); 175 | let object = Rc::new(object); 176 | self.objects.insert(name, object.clone()); 177 | object 178 | } 179 | 180 | pub fn get_object(&self, name: impl IntoSymbol) -> Option { 181 | name.get_symbol(self).and_then(|name| self.objects.get(&name).cloned()) 182 | } 183 | 184 | pub fn execute(&mut self, command: &NodeCommand) -> ExecuteResult { 185 | let result = match command.evaluate(self, 0)? { 186 | NodeValue::Symbol(symbol) => { 187 | let args = command.slice(1..)?; 188 | if let Some(func) = self.functions.get(&symbol).cloned() { 189 | func.call(self, args)? 190 | } else if let Some(obj) = self.objects.get(&symbol) { 191 | obj.clone().handle(self, args)? 192 | } else { 193 | return Err(ExecutionError::HandlerNotFound(symbol.to_name()).into()); 194 | } 195 | }, 196 | NodeValue::String(name) => { 197 | match self.get_symbol(name.as_ref()).and_then(|name| self.objects.get(&name)) { 198 | Some(obj) => obj.clone().handle(self, command.slice(1..)?)?, 199 | None => return Err(ExecutionError::HandlerNotFound((*name).clone()).into()), 200 | } 201 | }, 202 | NodeValue::Function(function) => function.call(self, command.slice(1..)?)?, 203 | result => { 204 | return Err(ExecutionError::NotAHandler(result.to_string(), result.get_kind()).into()) 205 | }, 206 | }; 207 | 208 | if let NodeValue::Unhandled = result.unevaluated() { 209 | todo!("default handler") 210 | } 211 | 212 | Ok(result) 213 | } 214 | 215 | pub fn execute_block(&mut self, script: &NodeSlice) -> ExecuteResult { 216 | for node in script.slice(..script.len() - 1)? { 217 | node.command()?.execute(self)?; 218 | } 219 | 220 | script.evaluate(self, script.len() - 1).map(|v| v.into()) 221 | } 222 | 223 | pub fn execute_args(&mut self, mut script: &NodeSlice, args: &NodeSlice) -> ExecuteResult { 224 | let mut saved_variables = VariableStack::new(self); 225 | saved_variables.push_args(&mut script, args)?; 226 | 227 | let result = saved_variables.context().execute_block(script); 228 | drop(saved_variables); // ensure drop does not occur until after execution 229 | result 230 | } 231 | } 232 | 233 | impl Default for Context { 234 | fn default() -> Self { 235 | Self::new() 236 | } 237 | } 238 | 239 | /// Trait to make APIs which require [`Symbol`]s more convenient to use. 240 | /// 241 | /// ```rust 242 | /// use std::rc::Rc; 243 | /// 244 | /// use arson_core::{arson_array, Context, IntoSymbol}; 245 | /// 246 | /// let mut context = Context::new(); 247 | /// 248 | /// // Context::add_macro makes use of this trait. 249 | /// // You can use either a Symbol, which gets used as-is... 250 | /// let symbol = context.add_symbol("kDefine"); 251 | /// context.add_macro(&symbol, arson_array![1]); 252 | /// assert_eq!(context.get_macro(&symbol), Some(Rc::new(arson_array![1]))); 253 | /// 254 | /// // ...or a &str, which gets converted to a Symbol behind the scenes. 255 | /// context.add_macro("kDefine", arson_array![2]); 256 | /// assert_eq!(context.get_macro("kDefine"), Some(Rc::new(arson_array![2]))); 257 | /// 258 | /// // An implementation example, which sets a variable 259 | /// // with the given name to the text "some text". 260 | /// fn do_something_with_symbol(context: &mut Context, name: impl IntoSymbol) { 261 | /// context.set_variable(name, "some text"); 262 | /// } 263 | /// 264 | /// let symbol = context.add_symbol("text"); 265 | /// do_something_with_symbol(&mut context, &symbol); 266 | /// assert_eq!(context.get_variable(&symbol), "some text".into()); 267 | /// 268 | /// do_something_with_symbol(&mut context, "text2"); 269 | /// assert_eq!(context.get_variable("text2"), "some text".into()); 270 | /// ``` 271 | pub trait IntoSymbol { 272 | fn into_symbol(self, context: &mut Context) -> Symbol; 273 | fn get_symbol(self, context: &Context) -> Option; 274 | } 275 | 276 | impl> IntoSymbol for N { 277 | fn into_symbol(self, context: &mut Context) -> Symbol { 278 | context.add_symbol(self.as_ref()) 279 | } 280 | 281 | fn get_symbol(self, context: &Context) -> Option { 282 | context.get_symbol(self.as_ref()) 283 | } 284 | } 285 | 286 | impl IntoSymbol for Symbol { 287 | fn into_symbol(self, _context: &mut Context) -> Symbol { 288 | self 289 | } 290 | 291 | fn get_symbol(self, _context: &Context) -> Option { 292 | Some(self) 293 | } 294 | } 295 | 296 | impl IntoSymbol for &Symbol { 297 | fn into_symbol(self, _context: &mut Context) -> Symbol { 298 | self.clone() 299 | } 300 | 301 | fn get_symbol(self, _context: &Context) -> Option { 302 | Some(self.clone()) 303 | } 304 | } 305 | 306 | #[cfg(feature = "text-loading")] 307 | impl Context { 308 | pub fn load_text(&mut self, options: LoadOptions, text: &str) -> Result { 309 | crate::loader::load_text(self, options, text) 310 | } 311 | } 312 | 313 | #[cfg(feature = "file-system")] 314 | impl Context { 315 | pub fn with_filesystem_driver(self, driver: impl arson_fs::FileSystemDriver + 'static) -> Self { 316 | Self { file_system: Some(FileSystem::new(driver)), ..self } 317 | } 318 | 319 | fn no_fs_error() -> std::io::Error { 320 | std::io::Error::new(std::io::ErrorKind::Unsupported, "no file system driver registered") 321 | } 322 | 323 | pub fn file_system(&self) -> std::io::Result<&FileSystem> { 324 | self.file_system_opt().ok_or_else(Self::no_fs_error) 325 | } 326 | 327 | pub fn file_system_mut(&mut self) -> std::io::Result<&mut FileSystem> { 328 | self.file_system_opt_mut().ok_or_else(Self::no_fs_error) 329 | } 330 | 331 | pub fn file_system_opt(&self) -> Option<&FileSystem> { 332 | self.file_system.as_ref() 333 | } 334 | 335 | pub fn file_system_opt_mut(&mut self) -> Option<&mut FileSystem> { 336 | self.file_system.as_mut() 337 | } 338 | } 339 | 340 | #[cfg(feature = "file-loading")] 341 | impl Context { 342 | pub fn load_path>( 343 | &mut self, 344 | options: LoadOptions, 345 | path: P, 346 | ) -> Result { 347 | crate::loader::load_path(self, options, path) 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /crates/arson-core/src/error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::backtrace::Backtrace; 4 | 5 | #[cfg(feature = "text-loading")] 6 | use crate::LoadError; 7 | use crate::{ArrayError, EvaluationError, ExecutionError, NumericError, ObjectError, StringError}; 8 | 9 | pub type Result = std::result::Result; 10 | 11 | /// Data is separated out and boxed to keep the size of [`Error`] down, 12 | /// and consequently the size of [`Result`]. 13 | #[derive(Debug)] 14 | struct ErrorData { 15 | kind: ErrorKind, 16 | location: Backtrace, 17 | } 18 | 19 | pub struct Error { 20 | data: Box, 21 | } 22 | 23 | impl self::Error { 24 | pub fn new(kind: ErrorKind) -> Self { 25 | Self { 26 | data: Box::new(ErrorData { kind, location: Backtrace::capture() }), 27 | } 28 | } 29 | 30 | pub fn from_custom(error: E) -> Self { 31 | Self::new(ErrorKind::Custom(Box::new(error))) 32 | } 33 | 34 | pub fn kind(&self) -> &ErrorKind { 35 | &self.data.kind 36 | } 37 | 38 | pub fn backtrace(&self) -> &Backtrace { 39 | &self.data.location 40 | } 41 | } 42 | 43 | impl std::error::Error for self::Error { 44 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 45 | Some(&self.data.kind) 46 | } 47 | 48 | #[cfg(error_generic_member_access)] 49 | fn provide<'a>(&'a self, request: &mut std::error::Request<'a>) { 50 | request.provide_ref::(self.backtrace()); 51 | } 52 | } 53 | 54 | impl std::fmt::Debug for self::Error { 55 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 56 | // Skip straight to displaying `data` for simplicity 57 | self.data.fmt(f) 58 | } 59 | } 60 | 61 | impl std::fmt::Display for self::Error { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | self.data.kind.fmt(f) 64 | } 65 | } 66 | 67 | impl> From for self::Error { 68 | fn from(value: E) -> Self { 69 | Self::new(value.into()) 70 | } 71 | } 72 | 73 | #[non_exhaustive] 74 | #[derive(thiserror::Error, Debug)] 75 | pub enum ErrorKind { 76 | #[error(transparent)] 77 | EvaluationError(#[from] EvaluationError), 78 | 79 | #[error(transparent)] 80 | ExecutionError(#[from] ExecutionError), 81 | 82 | #[error(transparent)] 83 | NumericError(NumericError), 84 | 85 | #[error(transparent)] 86 | StringError(#[from] StringError), 87 | 88 | #[error(transparent)] 89 | ArrayError(#[from] ArrayError), 90 | 91 | #[error(transparent)] 92 | ObjectError(#[from] ObjectError), 93 | 94 | #[error(transparent)] 95 | IoError(#[from] std::io::Error), 96 | 97 | #[cfg(feature = "text-loading")] 98 | #[error(transparent)] 99 | LoadError(#[from] LoadError), 100 | 101 | #[error(transparent)] 102 | Custom(Box), 103 | } 104 | 105 | impl From for crate::ErrorKind { 106 | fn from(value: std::io::ErrorKind) -> Self { 107 | Self::IoError(value.into()) 108 | } 109 | } 110 | 111 | impl> From for crate::ErrorKind { 112 | fn from(value: E) -> Self { 113 | Self::NumericError(value.into()) 114 | } 115 | } 116 | 117 | #[macro_export] 118 | macro_rules! arson_assert { 119 | ($cond:expr $(,)?) => { 120 | if !$cond { 121 | $crate::arson_fail!("Assertion failed: {}", stringify!($cond)); 122 | } 123 | }; 124 | ($cond:expr, $($arg:tt)+) => { 125 | if !$cond { 126 | $crate::arson_fail!($($arg)+); 127 | } 128 | }; 129 | } 130 | 131 | #[macro_export] 132 | macro_rules! arson_fail { 133 | ($($arg:tt)+) => { 134 | return Err( 135 | $crate::ExecutionError::Failure(format!($($arg)+)).into() 136 | ) 137 | }; 138 | } 139 | 140 | #[macro_export] 141 | macro_rules! arson_assert_len { 142 | ($array:ident, $len:expr) => { 143 | if $array.len() != $len { 144 | return Err($crate::ExecutionError::LengthMismatch { 145 | expected: $len, 146 | actual: $array.len(), 147 | }.into()); 148 | } 149 | }; 150 | ($array:ident, $len:expr, $($arg:expr)+) => { 151 | if $array.len() != $len { 152 | $crate::arson_fail!("{}: expected {}, got {}", format!($($arg)+), $len, $array.len()); 153 | } 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /crates/arson-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | #![cfg_attr(error_generic_member_access, feature(error_generic_member_access))] 4 | 5 | /// Transform one fragment into another. 6 | macro_rules! meta_morph { 7 | (|$_:tt| $($i:tt)*) => { 8 | $($i)* 9 | }; 10 | } 11 | 12 | /// Select the first fragment if it exists, otherwise the second. 13 | macro_rules! meta_select { 14 | ($first:tt, $($second:tt)*) => { 15 | $first 16 | }; 17 | // to avoid local ambiguity issues 18 | (meta_morph!(|$_:tt| $($first:tt)*), $($second:tt)*) => { 19 | $($first)* 20 | }; 21 | (, $($second:tt)*) => { 22 | $($second)* 23 | }; 24 | } 25 | 26 | mod builtin; 27 | mod primitives; 28 | 29 | pub use primitives::*; 30 | 31 | mod context; 32 | mod error; 33 | #[cfg(feature = "text-loading")] 34 | mod loader; 35 | 36 | pub use context::*; 37 | pub use error::*; 38 | #[cfg(feature = "text-loading")] 39 | pub use loader::*; 40 | 41 | pub mod prelude { 42 | pub use super::context::{Context, ContextState, ExecuteResult}; 43 | #[cfg(feature = "text-loading")] 44 | pub use super::loader::{LoadError, LoadOptions}; 45 | pub use super::primitives::{ 46 | ArrayRef, 47 | HandleFn, 48 | Node, 49 | NodeArray, 50 | NodeCommand, 51 | NodeKind, 52 | NodeProperty, 53 | NodeSlice, 54 | NodeValue, 55 | Object, 56 | ObjectRef, 57 | Symbol, 58 | SymbolMap, 59 | Variable, 60 | VariableSave, 61 | VariableStack, 62 | }; 63 | pub use super::{arson_array, arson_assert, arson_assert_len, arson_fail, arson_slice}; 64 | } 65 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/function.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::rc::Rc; 4 | 5 | use crate::{Context, ExecuteResult, NodeSlice}; 6 | 7 | // TODO once stabilized: https://github.com/rust-lang/rust/issues/41517 8 | // trait HandleFnInner = Fn(&mut Context, &NodeSlice) -> ExecuteResult; 9 | 10 | /// A function which is callable from script. 11 | #[derive(Clone)] 12 | #[allow( 13 | clippy::type_complexity, 14 | reason = "no benefit due to inability to use trait object types as generics bounds" 15 | )] 16 | pub struct HandleFn(Rc ExecuteResult>); 17 | 18 | impl HandleFn { 19 | pub fn new(f: impl Fn(&mut Context, &NodeSlice) -> ExecuteResult + 'static) -> Self { 20 | Self(Rc::new(f)) 21 | } 22 | 23 | pub fn call(&self, context: &mut Context, args: &NodeSlice) -> ExecuteResult { 24 | self.0(context, args) 25 | } 26 | 27 | pub fn total_cmp(&self, other: &Self) -> std::cmp::Ordering { 28 | let left = Rc::as_ptr(&self.0) as *const (); 29 | let right = Rc::as_ptr(&other.0) as *const (); 30 | left.cmp(&right) 31 | } 32 | } 33 | 34 | impl ExecuteResult + 'static> From for HandleFn { 35 | fn from(value: F) -> Self { 36 | Self::new(value) 37 | } 38 | } 39 | 40 | impl PartialEq for HandleFn { 41 | fn eq(&self, other: &Self) -> bool { 42 | Rc::ptr_eq(&self.0, &other.0) 43 | } 44 | } 45 | 46 | impl std::fmt::Debug for HandleFn { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | f.debug_tuple("HandleFn").finish_non_exhaustive() 49 | } 50 | } 51 | 52 | impl std::fmt::Display for HandleFn { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | write!(f, "", Rc::as_ptr(&self.0) as *const () as usize) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | mod array; 4 | mod function; 5 | mod node; 6 | mod numbers; 7 | mod object; 8 | mod string; 9 | mod symbol; 10 | mod variable; 11 | 12 | pub use array::*; 13 | pub use function::*; 14 | pub use node::*; 15 | pub use numbers::*; 16 | pub use object::*; 17 | pub use string::*; 18 | pub use symbol::*; 19 | pub use variable::*; 20 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/numbers.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::num::Wrapping; 4 | use std::ops::{self, RangeBounds}; 5 | 6 | #[cfg(feature = "text-loading")] 7 | pub use arson_parse::{FloatValue, IntegerValue}; 8 | 9 | use crate::Node; 10 | 11 | #[cfg(not(feature = "text-loading"))] 12 | pub type IntegerValue = i64; 13 | #[cfg(not(feature = "text-loading"))] 14 | pub type FloatValue = f64; 15 | 16 | /// An integer value with wrapping/overflow semantics. 17 | pub type Integer = Wrapping; 18 | 19 | /// A numerical value, either integer or floating-point. 20 | #[derive(Debug, PartialEq, PartialOrd, Clone, Copy)] 21 | pub enum Number { 22 | /// An integer value (see [`Integer`]). 23 | Integer(Integer), 24 | /// A floating-point value (see [`Float`]). 25 | Float(FloatValue), 26 | } 27 | 28 | impl Number { 29 | pub fn integer(&self) -> Integer { 30 | match self { 31 | Number::Integer(value) => *value, 32 | Number::Float(value) => Wrapping(*value as IntegerValue), 33 | } 34 | } 35 | 36 | pub fn float(&self) -> FloatValue { 37 | match self { 38 | Number::Integer(value) => value.0 as FloatValue, 39 | Number::Float(value) => *value, 40 | } 41 | } 42 | } 43 | 44 | impl From for Number { 45 | fn from(value: Integer) -> Self { 46 | Self::Integer(value) 47 | } 48 | } 49 | 50 | impl From for Number { 51 | fn from(value: FloatValue) -> Self { 52 | Self::Float(value) 53 | } 54 | } 55 | 56 | #[non_exhaustive] 57 | #[derive(thiserror::Error, Debug)] 58 | pub enum NumericError { 59 | Overflow, 60 | IndexOutOfRange(usize, ops::Range), 61 | SliceOutOfRange { 62 | slice_start: ops::Bound, 63 | slice_end: ops::Bound, 64 | expected_range: ops::Range, 65 | }, 66 | 67 | IntegerConversion(#[from] std::num::TryFromIntError), 68 | IntegerParse(#[from] std::num::ParseIntError), 69 | FloatParse(#[from] std::num::ParseFloatError), 70 | } 71 | 72 | impl NumericError { 73 | pub fn slice_out_of_range( 74 | slice_range: impl RangeBounds, 75 | expected_range: ops::Range, 76 | ) -> Self { 77 | Self::SliceOutOfRange { 78 | slice_start: slice_range.start_bound().cloned(), 79 | slice_end: slice_range.end_bound().cloned(), 80 | expected_range, 81 | } 82 | } 83 | } 84 | 85 | impl std::fmt::Display for NumericError { 86 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 87 | match self { 88 | NumericError::Overflow => f.write_str("an undesired integer overflow occurred"), 89 | NumericError::IndexOutOfRange(index, range) => { 90 | write!(f, "index {index} outside of range {range:?}") 91 | }, 92 | NumericError::SliceOutOfRange { 93 | slice_start: start, 94 | slice_end: end, 95 | expected_range: range, 96 | } => { 97 | // "slice range {start:?}..{end:?} outside of range {range:?}" 98 | 99 | f.write_str("slice range ")?; 100 | 101 | match start { 102 | ops::Bound::Included(start) => write!(f, "{start}")?, 103 | ops::Bound::Excluded(start) => write!(f, "{}", start + 1)?, 104 | ops::Bound::Unbounded => (), 105 | } 106 | f.write_str("..")?; 107 | match end { 108 | ops::Bound::Included(end) => write!(f, "={end}")?, 109 | ops::Bound::Excluded(end) => write!(f, "{end}")?, 110 | ops::Bound::Unbounded => (), 111 | } 112 | 113 | write!(f, " outside of range {range:?}") 114 | }, 115 | NumericError::IntegerConversion(inner) => inner.fmt(f), 116 | NumericError::IntegerParse(inner) => inner.fmt(f), 117 | NumericError::FloatParse(inner) => inner.fmt(f), 118 | } 119 | } 120 | } 121 | 122 | pub trait NodeSliceIndex { 123 | type Output: ?Sized; 124 | 125 | fn get(self, slice: &[Node]) -> crate::Result<&Self::Output>; 126 | fn get_mut(self, slice: &mut [Node]) -> crate::Result<&mut Self::Output>; 127 | 128 | fn get_opt(self, slice: &[Node]) -> Option<&Self::Output>; 129 | fn get_mut_opt(self, slice: &mut [Node]) -> Option<&mut Self::Output>; 130 | } 131 | 132 | impl NodeSliceIndex for usize { 133 | type Output = Node; 134 | 135 | fn get(self, slice: &[Node]) -> crate::Result<&Self::Output> { 136 | slice 137 | .get(self) 138 | .ok_or_else(|| NumericError::IndexOutOfRange(self, 0..slice.len()).into()) 139 | } 140 | 141 | fn get_mut(self, slice: &mut [Node]) -> crate::Result<&mut Self::Output> { 142 | let length = slice.len(); // done here due to borrow rules 143 | slice 144 | .get_mut(self) 145 | .ok_or_else(|| NumericError::IndexOutOfRange(self, 0..length).into()) 146 | } 147 | 148 | fn get_opt(self, slice: &[Node]) -> Option<&Self::Output> { 149 | slice.get(self) 150 | } 151 | 152 | fn get_mut_opt(self, slice: &mut [Node]) -> Option<&mut Self::Output> { 153 | slice.get_mut(self) 154 | } 155 | } 156 | 157 | macro_rules! range_error_impl { 158 | ($($type:tt)+) => { 159 | impl NodeSliceIndex for $($type)+ { 160 | type Output = [Node]; 161 | 162 | fn get(self, slice: &[Node]) -> crate::Result<&Self::Output> { 163 | slice.get(self.clone()).ok_or_else(|| { 164 | NumericError::slice_out_of_range(self, 0..slice.len()).into() 165 | }) 166 | } 167 | 168 | fn get_mut(self, slice: &mut [Node]) -> crate::Result<&mut Self::Output> { 169 | let length = slice.len(); // done here due to borrow rules 170 | slice.get_mut(self.clone()).ok_or_else(|| { 171 | NumericError::slice_out_of_range(self, 0..length).into() 172 | }) 173 | } 174 | 175 | fn get_opt(self, slice: &[Node]) -> Option<&Self::Output> { 176 | slice.get(self) 177 | } 178 | 179 | fn get_mut_opt(self, slice: &mut [Node]) -> Option<&mut Self::Output> { 180 | slice.get_mut(self) 181 | } 182 | } 183 | } 184 | } 185 | 186 | range_error_impl!(ops::Range); 187 | range_error_impl!(ops::RangeTo); 188 | range_error_impl!(ops::RangeFrom); 189 | range_error_impl!(ops::RangeFull); 190 | range_error_impl!(ops::RangeInclusive); 191 | range_error_impl!(ops::RangeToInclusive); 192 | range_error_impl!((ops::Bound, ops::Bound)); 193 | 194 | // unstable 195 | // std::range::Range 196 | // std::range::RangeInclusive 197 | // std::range::RangeFrom 198 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/object.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::any::Any; 4 | 5 | use crate::{Context, ExecuteResult, NodeSlice}; 6 | 7 | #[non_exhaustive] 8 | #[derive(thiserror::Error, Debug)] 9 | pub enum ObjectError { 10 | #[cfg(feature = "dynamic-typenames")] 11 | #[error("cannot cast from {actual} to {expected}")] 12 | BadObjectCast { expected: &'static str, actual: &'static str }, 13 | 14 | #[cfg(not(feature = "dynamic-typenames"))] 15 | #[error("cannot perform the requested typecast")] 16 | BadObjectCast, 17 | } 18 | 19 | pub type ObjectRef = std::rc::Rc; 20 | 21 | pub trait Object: Any + std::fmt::Debug { 22 | /// Gets the name for this object, if any. 23 | fn name(&self) -> Option<&String>; 24 | 25 | /// Sends a message/command to the object to be handled. 26 | /// 27 | /// # A note about argument evaluation 28 | /// 29 | /// Because arguments can themselves be commands, handlers must be careful 30 | /// about evaluating all arguments *before* performing any actions which 31 | /// take some form of lock, such as a Mutex or a RefCell. A command such as 32 | /// `{game set_score {+ {game get_score} 50}}` is valid, and will call into 33 | /// `set_score` *first*. The call to `get_score` will only occur once the 34 | /// argument for `set_score` is evaluated inside its implementation. 35 | /// 36 | /// This is also the reason why the `self` argument here is *immutable* as 37 | /// opposed to *mutable*. Making `self` mutable here results in all sorts 38 | /// of logistical issues in implementation, and additionally makes the 39 | /// side-effect nature of argument evaluation less sound for the reasons 40 | /// specified above. 41 | fn handle(&self, context: &mut Context, msg: &NodeSlice) -> ExecuteResult; 42 | 43 | /// Interstitial workaround for [lack of trait upcasting coersion](https://github.com/rust-lang/rust/issues/65991). 44 | /// 45 | /// Simply implement as: 46 | /// 47 | /// ```rust,no_run 48 | /// # struct Asdf; 49 | /// # impl Asdf { 50 | /// fn as_any(&self) -> &dyn std::any::Any { 51 | /// self 52 | /// } 53 | /// # } 54 | /// ``` 55 | fn as_any(&self) -> &dyn Any; 56 | 57 | /// Gets the type name for this object. 58 | /// 59 | /// There is no particular reason to override this, simply leave it as-is. 60 | #[cfg(feature = "dynamic-typenames")] 61 | fn type_name(&self) -> &'static str { 62 | std::any::type_name::() 63 | } 64 | } 65 | 66 | impl dyn Object { 67 | pub fn is(&self) -> bool { 68 | self.as_any().is::() 69 | } 70 | 71 | pub fn downcast(&self) -> crate::Result<&T> { 72 | self.downcast_opt().ok_or_else(|| { 73 | #[cfg(feature = "dynamic-typenames")] 74 | return ObjectError::BadObjectCast { 75 | expected: std::any::type_name::(), 76 | actual: self.type_name(), 77 | } 78 | .into(); 79 | 80 | #[cfg(not(feature = "dynamic-typenames"))] 81 | return ObjectError::BadObjectCast.into(); 82 | }) 83 | } 84 | 85 | pub fn downcast_opt(&self) -> Option<&T> { 86 | self.as_any().downcast_ref() 87 | } 88 | 89 | fn as_ptr(&self) -> *const () { 90 | self as *const _ as *const () 91 | } 92 | 93 | pub fn total_cmp(&self, other: &Self) -> std::cmp::Ordering { 94 | match self.as_ptr() == other.as_ptr() { 95 | true => std::cmp::Ordering::Equal, 96 | #[cfg(feature = "dynamic-typenames")] 97 | false => match self.type_name().cmp(other.type_name()) { 98 | std::cmp::Ordering::Equal => self.name().cmp(&other.name()), 99 | result => result, 100 | }, 101 | #[cfg(not(feature = "dynamic-typenames"))] 102 | false => self.name().cmp(&other.name()), 103 | } 104 | } 105 | } 106 | 107 | impl PartialEq for dyn Object { 108 | fn eq(&self, other: &Self) -> bool { 109 | self.as_ptr() == other.as_ptr() 110 | } 111 | } 112 | 113 | impl PartialEq for dyn Object { 114 | fn eq(&self, other: &ObjectRef) -> bool { 115 | self.as_ptr() == other.as_ptr() 116 | } 117 | } 118 | 119 | impl PartialEq for ObjectRef { 120 | fn eq(&self, other: &dyn Object) -> bool { 121 | self.as_ptr() == other.as_ptr() 122 | } 123 | } 124 | 125 | impl std::fmt::Display for dyn Object { 126 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 127 | match self.name() { 128 | Some(name) if !name.is_empty() => f.write_str(name), 129 | #[cfg(feature = "dynamic-typenames")] 130 | _ => write!(f, "", self.type_name()), 131 | #[cfg(not(feature = "dynamic-typenames"))] 132 | _ => f.write_str(""), 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/string.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::cell::Cell; 4 | 5 | use crate::{Context, NodeSlice}; 6 | 7 | #[non_exhaustive] 8 | #[derive(thiserror::Error, Debug)] 9 | pub enum StringError { 10 | #[error("index {0} is not a UTF-8 character boundary")] 11 | NotCharBoundary(usize), 12 | } 13 | 14 | /// Concatenates a slice of nodes into a string, without a separator between each. 15 | pub struct ConcatSlice<'a> { 16 | context: Cell>, 17 | args: &'a NodeSlice, 18 | } 19 | 20 | impl<'a> ConcatSlice<'a> { 21 | pub fn new(context: &'a mut Context, args: &'a NodeSlice) -> Self { 22 | Self { context: Cell::new(Some(context)), args } 23 | } 24 | } 25 | 26 | impl std::fmt::Display for ConcatSlice<'_> { 27 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 28 | match self.context.take() { 29 | Some(context) => { 30 | for arg in self.args { 31 | match arg.evaluate(context) { 32 | Ok(value) => value.fmt(f)?, 33 | Err(err) => write!(f, "")?, 34 | } 35 | } 36 | self.context.set(Some(context)); 37 | }, 38 | None => { 39 | for arg in self.args { 40 | arg.unevaluated().fmt(f)?; 41 | } 42 | }, 43 | } 44 | 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/symbol.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::collections::HashMap; 4 | use std::hash::Hash; 5 | use std::rc::Rc; 6 | 7 | /// Maps symbols by hash to an associated value. Alias for [`HashMap`]. 8 | pub type SymbolMap = HashMap; 9 | 10 | /// A unique identifier for a scripting element, such as a type or method. 11 | #[derive(Clone)] 12 | pub struct Symbol { 13 | name: Rc, 14 | } 15 | 16 | impl Symbol { 17 | /// Returns the underlying name of this symbol. 18 | pub const fn name(&self) -> &Rc { 19 | &self.name 20 | } 21 | 22 | pub fn to_name(&self) -> String { 23 | self.name.as_ref().clone() 24 | } 25 | } 26 | 27 | // Comparisons are done by pointer only for efficiency, as symbols are guaranteed to 28 | // be unique by the symbol table they are created from. 29 | 30 | impl PartialEq for Symbol { 31 | fn eq(&self, other: &Self) -> bool { 32 | Rc::ptr_eq(&self.name, &other.name) 33 | } 34 | } 35 | 36 | impl Eq for Symbol {} 37 | 38 | impl PartialOrd for Symbol { 39 | fn partial_cmp(&self, other: &Self) -> Option { 40 | Some(self.cmp(other)) 41 | } 42 | } 43 | 44 | impl Ord for Symbol { 45 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 46 | Rc::as_ptr(&self.name).cmp(&Rc::as_ptr(&other.name)) 47 | } 48 | } 49 | 50 | impl Hash for Symbol { 51 | fn hash(&self, state: &mut H) { 52 | Rc::as_ptr(&self.name).hash(state); 53 | } 54 | } 55 | 56 | impl std::fmt::Debug for Symbol { 57 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 58 | write!(f, "'{}'", self.name) 59 | } 60 | } 61 | 62 | impl std::fmt::Display for Symbol { 63 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 64 | // placed into variable to keep formatting nicer 65 | let pattern = [' ', '\x0b', '\t', '\r', '\n', '\x0c', '(', ')', '[', ']', '{', '}', '\'']; 66 | if self.name.contains(pattern) { 67 | write!(f, "'{}'", self.name) 68 | } else if self.name.is_empty() { 69 | write!(f, "''") 70 | } else { 71 | self.name.fmt(f) 72 | } 73 | } 74 | } 75 | 76 | /// Private helper method to create a new Symbol from a string. 77 | #[inline] 78 | fn new_symbol(str: &str) -> Symbol { 79 | Symbol { name: Rc::new(str.to_owned()) } 80 | } 81 | 82 | pub(crate) struct SymbolTable { 83 | table: HashMap, 84 | } 85 | 86 | impl SymbolTable { 87 | /// Constructs a new [`SymbolTable`]. 88 | pub fn new() -> Self { 89 | Self { table: HashMap::new() } 90 | } 91 | 92 | /// Adds a symbol by name to the table. 93 | /// The created symbol is also returned for convenience. 94 | pub fn add(&mut self, name: &str) -> Symbol { 95 | if let Some(existing) = self.get(name) { 96 | return existing; 97 | } 98 | 99 | let sym = new_symbol(name); 100 | self.table.insert(sym.name().as_ref().clone(), sym.clone()); 101 | sym.clone() 102 | } 103 | 104 | /// Returns the corresponding symbol for a given name, if one exists. 105 | pub fn get(&self, name: &str) -> Option { 106 | self.table.get(name).cloned() 107 | } 108 | 109 | // TODO: Find a way to implement this more soundly 110 | // Being able to just remove a symbol and invalidate any existing instances 111 | // of that symbol is really bad lol 112 | // /// Removes the given symbol from the table. 113 | // pub fn remove(&mut self, symbol: &Symbol) { 114 | // self.table.remove(&*symbol.name); 115 | // } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | 122 | mod symbol { 123 | use super::*; 124 | 125 | #[test] 126 | fn eq() { 127 | let sym1 = new_symbol("asdf"); 128 | let sym2 = sym1.clone(); 129 | 130 | assert_eq!(sym1, sym2) 131 | } 132 | 133 | #[test] 134 | fn ref_equality_only() { 135 | let sym1 = new_symbol("asdf"); 136 | let sym2 = new_symbol("jkl"); 137 | let sym3 = new_symbol("jkl"); 138 | 139 | // None of these symbols share the same box for their name 140 | // and so won't ever be equal 141 | assert_ne!(sym1, sym2); 142 | assert_ne!(sym1, sym3); 143 | assert_ne!(sym2, sym3); 144 | } 145 | 146 | #[test] 147 | fn display() { 148 | assert_eq!(new_symbol("").to_string(), "''"); 149 | 150 | assert_eq!(new_symbol("asdf").to_string(), "asdf"); 151 | assert_eq!(new_symbol("jkl;").to_string(), "jkl;"); 152 | 153 | assert_eq!(new_symbol("with space").to_string(), "'with space'"); 154 | assert_eq!(new_symbol(" ").to_string(), "' '"); 155 | 156 | assert_eq!(new_symbol("\x0b").to_string(), "'\x0b'"); 157 | assert_eq!(new_symbol("\t").to_string(), "'\t'"); 158 | assert_eq!(new_symbol("\r").to_string(), "'\r'"); 159 | assert_eq!(new_symbol("\n").to_string(), "'\n'"); 160 | assert_eq!(new_symbol("\x0c").to_string(), "'\x0c'"); 161 | 162 | assert_eq!(new_symbol("(").to_string(), "'('"); 163 | assert_eq!(new_symbol(")").to_string(), "')'"); 164 | assert_eq!(new_symbol("[").to_string(), "'['"); 165 | assert_eq!(new_symbol("]").to_string(), "']'"); 166 | assert_eq!(new_symbol("{").to_string(), "'{'"); 167 | assert_eq!(new_symbol("}").to_string(), "'}'"); 168 | assert_eq!(new_symbol("'").to_string(), "'''"); 169 | } 170 | } 171 | 172 | mod table { 173 | use super::*; 174 | 175 | #[test] 176 | fn add() { 177 | let mut table = SymbolTable::new(); 178 | let symbol = table.add("asdf"); 179 | 180 | assert!(table.table.contains_key("asdf")); 181 | assert!(table.table.contains_key(&*symbol.name)); 182 | } 183 | 184 | #[test] 185 | fn get() { 186 | let mut table = SymbolTable::new(); 187 | let symbol = table.add("asdf"); 188 | let symbol2 = table.get("asdf").expect("The symbol should be added"); 189 | 190 | assert_eq!(symbol, symbol2); 191 | } 192 | 193 | // #[test] 194 | // fn remove() { 195 | // let mut table = SymbolTable::new(); 196 | // table.add("asdf"); 197 | 198 | // let symbol = table.get("asdf").expect("The symbol should be added"); 199 | // table.remove(&symbol); 200 | // assert!(table.get("asdf") == None); 201 | // } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /crates/arson-core/src/primitives/variable.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::rc::Rc; 4 | 5 | use super::{Node, NodeSlice, NodeValue, Symbol}; 6 | use crate::{arson_assert_len, Context}; 7 | 8 | #[derive(Clone)] 9 | pub struct Variable { 10 | symbol: Symbol, 11 | } 12 | 13 | impl Variable { 14 | pub fn new(name: &str, context: &mut Context) -> Self { 15 | let symbol = context.add_symbol(name); 16 | Self { symbol } 17 | } 18 | 19 | pub const fn symbol(&self) -> &Symbol { 20 | &self.symbol 21 | } 22 | 23 | pub const fn name(&self) -> &Rc { 24 | self.symbol.name() 25 | } 26 | 27 | pub fn get(&self, context: &mut Context) -> Node { 28 | context.get_variable(&self.symbol) 29 | } 30 | 31 | pub fn get_opt(&self, context: &mut Context) -> Option { 32 | context.get_variable_opt(&self.symbol) 33 | } 34 | 35 | pub fn set(&self, context: &mut Context, value: impl Into) -> Option { 36 | context.set_variable(&self.symbol, value) 37 | } 38 | 39 | pub fn save(&self, context: &mut Context) -> VariableSave { 40 | VariableSave::new(context, self) 41 | } 42 | } 43 | 44 | impl std::fmt::Debug for Variable { 45 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 46 | write!(f, "${}", self.name()) 47 | } 48 | } 49 | 50 | impl std::fmt::Display for Variable { 51 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 52 | write!(f, "${}", self.name()) 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | pub struct VariableSave { 58 | pub variable: Variable, 59 | pub value: Node, 60 | } 61 | 62 | impl VariableSave { 63 | pub fn new(context: &mut Context, variable: &Variable) -> Self { 64 | let value = variable.get(context); 65 | Self { variable: variable.clone(), value } 66 | } 67 | 68 | pub fn restore(&self, context: &mut Context) { 69 | self.variable.set(context, self.value.clone()); 70 | } 71 | } 72 | 73 | pub struct VariableStack<'ctx> { 74 | context: &'ctx mut Context, 75 | stack: Vec, 76 | } 77 | 78 | impl<'ctx> VariableStack<'ctx> { 79 | pub fn new(context: &'ctx mut Context) -> Self { 80 | Self { context, stack: Vec::new() } 81 | } 82 | 83 | pub fn context(&mut self) -> &mut Context { 84 | self.context 85 | } 86 | 87 | pub fn push(&mut self, variable: &Variable, value: impl Into) { 88 | let old = variable.save(self.context); 89 | self.stack.push(old); 90 | variable.set(self.context, value); 91 | } 92 | 93 | pub fn pop(&mut self) -> Option { 94 | let entry = self.stack.pop()?; 95 | entry.restore(self.context); 96 | Some(entry.variable) 97 | } 98 | 99 | pub fn clear(&mut self) { 100 | while self.pop().is_some() {} 101 | } 102 | 103 | pub fn push_args(&mut self, script: &mut &NodeSlice, args: &NodeSlice) -> crate::Result { 104 | if let NodeValue::Array(parameters) = script.unevaluated(0)? { 105 | *script = script.slice(1..)?; 106 | 107 | let parameters = parameters.borrow()?; 108 | arson_assert_len!(args, parameters.len(), "script parameter list has the wrong size"); 109 | 110 | for i in 0..parameters.len() { 111 | let variable = parameters.variable(i)?; 112 | let value = args.evaluate(self.context, i)?; 113 | self.push(variable, value); 114 | } 115 | } 116 | 117 | Ok(()) 118 | } 119 | 120 | pub fn push_initializers(&mut self, args: &mut &NodeSlice) -> crate::Result { 121 | while let NodeValue::Array(var_decl) = args.unevaluated(0)? { 122 | *args = args.slice(1..)?; 123 | 124 | let var_decl = var_decl.borrow()?; 125 | let variable = var_decl.variable(0)?; 126 | 127 | let initializer = var_decl.slice(1..)?; 128 | if let Some(value) = initializer.get_opt(0) { 129 | arson_assert_len!(initializer, 1, "too many values in initializer for {variable}"); 130 | let value = value.evaluate(self.context)?; 131 | self.push(variable, value); 132 | } else { 133 | self.push(variable, Node::UNHANDLED); 134 | } 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | pub fn push_saved(&mut self, saved: &[VariableSave]) { 141 | for saved in saved { 142 | self.push(&saved.variable, saved.value.clone()); 143 | } 144 | } 145 | } 146 | 147 | impl Drop for VariableStack<'_> { 148 | fn drop(&mut self) { 149 | self.clear(); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /crates/arson-dtb/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-dtb" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson-parse = { path = "../../crates/arson-parse", features = ["encoding"] } 13 | 14 | byteorder = "1.5.0" 15 | thiserror = "1.0.65" 16 | -------------------------------------------------------------------------------- /crates/arson-dtb/src/crypt/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io; 4 | 5 | mod new; 6 | mod noop; 7 | mod old; 8 | 9 | pub use new::*; 10 | pub use noop::*; 11 | pub use old::*; 12 | 13 | pub trait CryptAlgorithm { 14 | const DEFAULT_SEED: u32; 15 | 16 | fn next(&mut self) -> u32; 17 | } 18 | 19 | pub struct CryptReader { 20 | reader: Reader, 21 | crypt: Crypt, 22 | } 23 | 24 | impl CryptReader { 25 | pub fn new(reader: R, crypt: C) -> Self { 26 | Self { reader, crypt } 27 | } 28 | } 29 | 30 | impl io::Read for CryptReader { 31 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 32 | let count = self.reader.read(buf)?; 33 | for byte in &mut buf[..count] { 34 | *byte ^= self.crypt.next() as u8; 35 | } 36 | 37 | Ok(count) 38 | } 39 | } 40 | 41 | impl io::Seek for CryptReader { 42 | fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 43 | self.reader.seek(pos) 44 | } 45 | } 46 | 47 | pub struct CryptWriter { 48 | writer: Writer, 49 | crypt: Crypt, 50 | } 51 | 52 | impl CryptWriter { 53 | pub fn new(writer: W, crypt: C) -> Self { 54 | Self { writer, crypt } 55 | } 56 | } 57 | 58 | impl io::Write for CryptWriter { 59 | fn write(&mut self, buf: &[u8]) -> io::Result { 60 | let mut crypt_buf = [0u8; 512]; 61 | 62 | let mut total_written = 0; 63 | while total_written < buf.len() { 64 | let end = (total_written + crypt_buf.len()).min(buf.len()); 65 | let chunk = &buf[total_written..end]; 66 | 67 | for i in 0..chunk.len() { 68 | crypt_buf[i] = chunk[i] ^ self.crypt.next() as u8; 69 | } 70 | 71 | let buf = &crypt_buf[..chunk.len()]; 72 | let written = match self.writer.write(buf) { 73 | Ok(written) => written, 74 | Err(err) => { 75 | // Write requires that Ok be returned if any bytes were consumed 76 | if total_written > 0 { 77 | return Ok(total_written); 78 | } else { 79 | return Err(err); 80 | } 81 | }, 82 | }; 83 | 84 | total_written += written; 85 | } 86 | 87 | Ok(total_written) 88 | } 89 | 90 | fn flush(&mut self) -> io::Result<()> { 91 | self.writer.flush() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /crates/arson-dtb/src/crypt/new.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use super::CryptAlgorithm; 4 | 5 | pub struct NewRandom { 6 | seed: i32, 7 | } 8 | 9 | impl NewRandom { 10 | pub const fn new(seed: u32) -> Self { 11 | let seed = seed as i32; 12 | let seed = match seed { 13 | 0 => 1, 14 | seed if seed < 0 => -seed, 15 | seed => seed, 16 | }; 17 | 18 | Self { seed } 19 | } 20 | 21 | pub const fn default() -> Self { 22 | Self::new(Self::DEFAULT_SEED) 23 | } 24 | } 25 | 26 | impl CryptAlgorithm for NewRandom { 27 | const DEFAULT_SEED: u32 = 0x30171609; // seed used by dtab 28 | 29 | fn next(&mut self) -> u32 { 30 | let a = self.seed.wrapping_rem(0x1F31D).wrapping_mul(0x41A7); 31 | let b = self.seed.wrapping_div(0x1F31D).wrapping_mul(0xB14); 32 | 33 | let c = match a.wrapping_sub(b) { 34 | c if c <= 0 => c.wrapping_add(0x7FFFFFFF), 35 | c => c, 36 | }; 37 | 38 | self.seed = c; 39 | c as u32 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/arson-dtb/src/crypt/noop.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use super::CryptAlgorithm; 4 | 5 | pub struct NoopCrypt; 6 | 7 | impl CryptAlgorithm for NoopCrypt { 8 | const DEFAULT_SEED: u32 = 0; 9 | 10 | fn next(&mut self) -> u32 { 11 | // 0 is the identity value for XOR 12 | 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/arson-dtb/src/crypt/old.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use super::CryptAlgorithm; 4 | 5 | pub struct OldRandom { 6 | index1: usize, 7 | index2: usize, 8 | table: [u32; 256], 9 | } 10 | 11 | impl OldRandom { 12 | pub const fn new(mut seed: u32) -> Self { 13 | const fn permute(value: u32) -> u32 { 14 | value.wrapping_mul(0x41C64E6D).wrapping_add(12345) 15 | } 16 | 17 | let mut table = [0u32; 256]; 18 | let mut i = 0; 19 | while i < table.len() { 20 | let value = permute(seed); 21 | seed = permute(value); 22 | table[i] = (seed & 0x7FFF0000) | (value >> 16); 23 | i += 1; 24 | } 25 | 26 | Self { index1: 0, index2: 103, table } 27 | } 28 | 29 | pub const fn default() -> Self { 30 | Self::new(Self::DEFAULT_SEED) 31 | } 32 | } 33 | 34 | impl CryptAlgorithm for OldRandom { 35 | const DEFAULT_SEED: u32 = 0x52534F4C; // seed used by DtbCrypt 36 | 37 | fn next(&mut self) -> u32 { 38 | fn increment(mut index: usize) -> usize { 39 | index = index.wrapping_add(1); 40 | if index > 248 { 41 | index = 0x00; 42 | } 43 | index 44 | } 45 | 46 | let a = self.table[self.index1]; 47 | let b = self.table[self.index2]; 48 | let value = a ^ b; 49 | 50 | self.table[self.index1] = value; 51 | self.index1 = increment(self.index1); 52 | self.index2 = increment(self.index2); 53 | 54 | value 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /crates/arson-dtb/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | mod convert; 4 | mod crypt; 5 | mod read; 6 | mod write; 7 | 8 | pub use convert::*; 9 | pub use read::*; 10 | pub use write::*; 11 | 12 | pub mod prelude { 13 | pub use super::{ 14 | DataArray, 15 | DataKind, 16 | DataNode, 17 | DecryptionSettings, 18 | EncryptionMode, 19 | EncryptionSettings, 20 | FormatVersion, 21 | ReadSettings, 22 | WriteSettings, 23 | }; 24 | } 25 | 26 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 27 | pub enum FormatVersion { 28 | // TODO 29 | // /// The format used for Rnd-era games (Amplitude and earlier). 30 | // Rnd, 31 | /// The format used for Milo games (Rock Band 3 and earlier). 32 | Milo, 33 | /// The format used for Forge games (Fantasia and later). 34 | Forge, 35 | } 36 | 37 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 38 | pub enum EncryptionMode { 39 | /// Old-style encryption (pre-GH2). 40 | Old, 41 | /// New-style encryption (GH2 onwards). 42 | New, 43 | } 44 | 45 | #[repr(u32)] 46 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] 47 | pub enum DataKind { 48 | Integer = 0, 49 | Float = 1, 50 | Variable = 2, 51 | Function = 3, 52 | Object = 4, 53 | Symbol = 5, 54 | Unhandled = 6, 55 | 56 | Ifdef = 7, 57 | Else = 8, 58 | Endif = 9, 59 | 60 | Array = 16, 61 | Command = 17, 62 | String = 18, 63 | Property = 19, 64 | Glob = 20, 65 | 66 | Define = 32, 67 | Include = 33, 68 | Merge = 34, 69 | Ifndef = 35, 70 | Autorun = 36, 71 | Undefine = 37, 72 | } 73 | 74 | impl TryFrom for DataKind { 75 | type Error = (); 76 | 77 | fn try_from(value: u32) -> Result { 78 | let kind = match value { 79 | 0 => Self::Integer, 80 | 1 => Self::Float, 81 | 2 => Self::Variable, 82 | 3 => Self::Function, 83 | 4 => Self::Object, 84 | 5 => Self::Symbol, 85 | 6 => Self::Unhandled, 86 | 87 | 7 => Self::Ifdef, 88 | 8 => Self::Else, 89 | 9 => Self::Endif, 90 | 91 | 16 => Self::Array, 92 | 17 => Self::Command, 93 | 18 => Self::String, 94 | 19 => Self::Property, 95 | 20 => Self::Glob, 96 | 97 | 32 => Self::Define, 98 | 33 => Self::Include, 99 | 34 => Self::Merge, 100 | 35 => Self::Ifndef, 101 | 36 => Self::Autorun, 102 | 37 => Self::Undefine, 103 | 104 | _ => return Err(()), 105 | }; 106 | Ok(kind) 107 | } 108 | } 109 | 110 | #[derive(Debug, Clone, PartialEq)] 111 | pub enum DataNode { 112 | Integer(i32), 113 | Float(f32), 114 | Variable(String), 115 | Function(String), 116 | Object(String), 117 | Symbol(String), 118 | Unhandled, 119 | 120 | Ifdef(String), 121 | Else, 122 | Endif, 123 | 124 | Array(DataArray), 125 | Command(DataArray), 126 | String(String), 127 | Property(DataArray), 128 | Glob(Vec), 129 | 130 | Define(String, DataArray), 131 | Include(String), 132 | Merge(String), 133 | Ifndef(String), 134 | Autorun(DataArray), 135 | Undefine(String), 136 | } 137 | 138 | impl DataNode { 139 | pub fn get_kind(&self) -> DataKind { 140 | match self { 141 | Self::Integer(_) => DataKind::Integer, 142 | Self::Float(_) => DataKind::Float, 143 | Self::Variable(_) => DataKind::Variable, 144 | Self::Function(_) => DataKind::Function, 145 | Self::Object(_) => DataKind::Object, 146 | Self::Symbol(_) => DataKind::Symbol, 147 | Self::Unhandled => DataKind::Unhandled, 148 | 149 | Self::Ifdef(_) => DataKind::Ifdef, 150 | Self::Else => DataKind::Else, 151 | Self::Endif => DataKind::Endif, 152 | 153 | Self::Array(_) => DataKind::Array, 154 | Self::Command(_) => DataKind::Command, 155 | Self::String(_) => DataKind::String, 156 | Self::Property(_) => DataKind::Property, 157 | Self::Glob(_) => DataKind::Glob, 158 | 159 | Self::Define(_, _) => DataKind::Define, 160 | Self::Include(_) => DataKind::Include, 161 | Self::Merge(_) => DataKind::Merge, 162 | Self::Ifndef(_) => DataKind::Ifndef, 163 | Self::Autorun(_) => DataKind::Autorun, 164 | Self::Undefine(_) => DataKind::Undefine, 165 | } 166 | } 167 | } 168 | 169 | #[derive(Debug, Clone, PartialEq)] 170 | pub struct DataArray { 171 | line: usize, 172 | id: usize, 173 | nodes: Vec, 174 | } 175 | 176 | impl DataArray { 177 | pub fn new(line: usize, id: usize) -> Self { 178 | Self { line, id, nodes: Vec::new() } 179 | } 180 | 181 | pub fn with_capacity(line: usize, id: usize, capacity: usize) -> Self { 182 | Self { line, id, nodes: Vec::with_capacity(capacity) } 183 | } 184 | 185 | pub fn from_nodes(line: usize, id: usize, nodes: Vec) -> Self { 186 | Self { line, id, nodes } 187 | } 188 | 189 | pub fn line(&self) -> usize { 190 | self.line 191 | } 192 | 193 | pub fn id(&self) -> usize { 194 | self.id 195 | } 196 | } 197 | 198 | impl std::ops::Deref for DataArray { 199 | type Target = Vec; 200 | 201 | fn deref(&self) -> &Self::Target { 202 | &self.nodes 203 | } 204 | } 205 | 206 | impl std::ops::DerefMut for DataArray { 207 | fn deref_mut(&mut self) -> &mut Self::Target { 208 | &mut self.nodes 209 | } 210 | } 211 | 212 | impl IntoIterator for DataArray { 213 | type Item = as IntoIterator>::Item; 214 | type IntoIter = as IntoIterator>::IntoIter; 215 | 216 | fn into_iter(self) -> Self::IntoIter { 217 | self.nodes.into_iter() 218 | } 219 | } 220 | 221 | impl<'a> IntoIterator for &'a DataArray { 222 | type Item = <&'a Vec as IntoIterator>::Item; 223 | type IntoIter = <&'a Vec as IntoIterator>::IntoIter; 224 | 225 | fn into_iter(self) -> Self::IntoIter { 226 | #[expect( 227 | clippy::into_iter_on_ref, 228 | reason = "intentionally forwarding to into_iter" 229 | )] 230 | (&self.nodes).into_iter() 231 | } 232 | } 233 | 234 | impl<'a> IntoIterator for &'a mut DataArray { 235 | type Item = <&'a mut Vec as IntoIterator>::Item; 236 | type IntoIter = <&'a mut Vec as IntoIterator>::IntoIter; 237 | 238 | fn into_iter(self) -> Self::IntoIter { 239 | #[expect( 240 | clippy::into_iter_on_ref, 241 | reason = "intentionally forwarding to into_iter" 242 | )] 243 | (&mut self.nodes).into_iter() 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /crates/arson-dtb/tests/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io::Cursor; 4 | use std::sync::LazyLock; 5 | 6 | use arson_dtb::{ 7 | DataArray, 8 | DataNode, 9 | DecryptionSettings, 10 | EncryptionMode, 11 | EncryptionSettings, 12 | FormatVersion, 13 | ReadSettings, 14 | WriteSettings, 15 | }; 16 | use arson_parse::encoding::DtaEncoding; 17 | 18 | static TEST_ARRAY: LazyLock = LazyLock::new(|| { 19 | DataArray::from_nodes(1, 0, vec![ 20 | DataNode::Integer(10), 21 | DataNode::Float(1.0), 22 | DataNode::Variable("foo".to_owned()), 23 | DataNode::Function("foo".to_owned()), 24 | DataNode::Object("foo".to_owned()), 25 | DataNode::Symbol("foo".to_owned()), 26 | DataNode::Unhandled, 27 | ]) 28 | }); 29 | 30 | #[rustfmt::skip] 31 | static TEST_DATA_UNENCRYPTED: &[u8] = &[ 32 | 1, // "exists" flag 33 | 7, 0, // size 34 | 1, 0, // line 35 | 0, 0, // id 36 | 37 | 0, 0, 0, 0, 10, 0, 0, 0, // DataNode::Integer(10) 38 | 1, 0, 0, 0, 0x00, 0x00, 0x80, 0x3F, // DataNode::Float(1.0) 39 | 2, 0, 0, 0, 3, 0, 0, 0, b'f', b'o', b'o', // DataNode::Variable("foo") 40 | 3, 0, 0, 0, 3, 0, 0, 0, b'f', b'o', b'o', // DataNode::Function("foo") 41 | 4, 0, 0, 0, 3, 0, 0, 0, b'f', b'o', b'o', // DataNode::Object("foo") 42 | 5, 0, 0, 0, 3, 0, 0, 0, b'f', b'o', b'o', // DataNode::Symbol("foo") 43 | 6, 0, 0, 0, 0, 0, 0, 0, // DataNode::Unhandled 44 | ]; 45 | 46 | static TEST_DATA_OLDSTYLE: &[u8] = &[ 47 | 76, 79, 83, 82, 223, 88, 103, 182, 96, 249, 200, 226, 38, 138, 78, 137, 187, 220, 63, 172, 191, 112, 180, 48 | 75, 225, 175, 24, 211, 47, 93, 200, 101, 131, 26, 51, 76, 142, 56, 253, 40, 96, 98, 230, 23, 159, 25, 49 | 247, 22, 163, 58, 239, 123, 115, 144, 86, 240, 162, 51, 172, 0, 18, 166, 115, 134, 75, 68, 203, 53, 177, 50 | 142, 201, 180, 154, 231, 28, 47, 2, 16, 200, 51 | ]; 52 | 53 | static TEST_DATA_NEWSTYLE: &[u8] = &[ 54 | 9, 22, 23, 48, 136, 255, 26, 172, 15, 33, 50, 202, 190, 99, 19, 107, 233, 185, 3, 196, 188, 126, 177, 55 | 125, 84, 25, 250, 58, 217, 99, 207, 102, 45, 49, 33, 17, 49, 173, 31, 193, 80, 131, 90, 77, 124, 138, 60, 56 | 128, 98, 4, 108, 69, 44, 166, 25, 34, 150, 52, 143, 112, 41, 5, 255, 117, 145, 165, 112, 226, 153, 205, 57 | 240, 104, 191, 32, 96, 14, 253, 131, 9, 58 | ]; 59 | 60 | const READ_SETTINGS: ReadSettings = ReadSettings { 61 | format: Some(FormatVersion::Milo), 62 | encoding: Some(DtaEncoding::Utf8), 63 | decryption: DecryptionSettings { mode: None, key: None }, 64 | }; 65 | 66 | const WRITE_SETTINGS: WriteSettings = WriteSettings { 67 | format: FormatVersion::Milo, 68 | encoding: DtaEncoding::Utf8, 69 | encryption: EncryptionSettings { mode: None, key: None }, 70 | }; 71 | 72 | fn test_encryption(source_array: &DataArray, source_bytes: &[u8], mode: Option) { 73 | // read 74 | let mut read_settings = READ_SETTINGS.with_decryption_mode(mode); 75 | let read_array = arson_dtb::read(Cursor::new(source_bytes), &mut read_settings).unwrap(); 76 | assert_eq!(read_array, *source_array); 77 | 78 | // write 79 | let mut written_bytes = Vec::new(); 80 | let write_settings = WRITE_SETTINGS 81 | .with_encryption_mode(read_settings.decryption.mode) 82 | .with_encryption_key(read_settings.decryption.key); 83 | arson_dtb::write(&read_array, Cursor::new(&mut written_bytes), write_settings).unwrap(); 84 | assert_eq!(written_bytes, source_bytes); 85 | 86 | // cycle 87 | for _i in 0..25 { 88 | let mut read_settings = READ_SETTINGS.with_decryption_mode(mode); 89 | let array = arson_dtb::read(Cursor::new(&written_bytes), &mut read_settings).unwrap(); 90 | assert_eq!(array, *source_array); 91 | 92 | if let Some(key) = read_settings.decryption.key { 93 | let a = key.wrapping_rem(0x1F31D).wrapping_mul(0x41A7); 94 | let b = key.wrapping_div(0x1F31D).wrapping_mul(0xB14); 95 | let key = match a - b { 96 | c if c <= 0 => c + 0x7FFFFFFF, 97 | c => c, 98 | }; 99 | read_settings.decryption.key = Some(key); 100 | } 101 | 102 | let write_settings = WRITE_SETTINGS 103 | .with_encryption_mode(read_settings.decryption.mode) 104 | .with_encryption_key(read_settings.decryption.key); 105 | 106 | written_bytes.clear(); 107 | arson_dtb::write(&array, Cursor::new(&mut written_bytes), write_settings).unwrap(); 108 | } 109 | } 110 | 111 | #[test] 112 | fn unencrypted() { 113 | test_encryption(&*TEST_ARRAY, TEST_DATA_UNENCRYPTED, None); 114 | } 115 | 116 | #[test] 117 | fn oldstyle() { 118 | test_encryption(&*TEST_ARRAY, TEST_DATA_OLDSTYLE, Some(EncryptionMode::Old)); 119 | } 120 | 121 | #[test] 122 | fn newstyle() { 123 | test_encryption(&*TEST_ARRAY, TEST_DATA_NEWSTYLE, Some(EncryptionMode::New)); 124 | } 125 | -------------------------------------------------------------------------------- /crates/arson-fmtlib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-fmtlib" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson-parse = { path = "../../crates/arson-parse" } 13 | 14 | thiserror = "1.0.65" 15 | -------------------------------------------------------------------------------- /crates/arson-fmtlib/src/consts.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::collections::HashMap; 4 | use std::str::FromStr; 5 | use std::sync::LazyLock; 6 | 7 | use arson_parse::{BlockCommentToken, Expression, ExpressionValue, Token, TokenValue}; 8 | 9 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] 10 | pub enum CommentDirective { 11 | FormattingOn, 12 | FormattingOff, 13 | } 14 | 15 | /// Commands which are known to execute blocks and should not be compacted into a single line. 16 | /// 17 | /// The value stored by each key is the number of arguments to 18 | /// keep on the same line as the command name itself. 19 | pub static BLOCK_COMMANDS: LazyLock> = LazyLock::new(|| { 20 | HashMap::from_iter([ 21 | // Control flow 22 | ("if", 1), // {if {condition} {...} ...} 23 | ("if_else", 1), // {if_else {condition} {...} {...}} 24 | ("unless", 1), // {unless {condition} {...} ...} 25 | ("switch", 1), // {switch $var (case_1 ...) (case_2 ...) ...} 26 | // Variable scoping 27 | ("do", 0), // {do (...) {...} ...} 28 | ("with", 1), // {with $object {...} ...} 29 | // Loops 30 | ("foreach", 2), // {foreach $var $array {...} ...} 31 | ("foreach_int", 3), // {foreach_int $var 0 5 {...} ...} 32 | ("while", 1), // {while {condition} {...} ...} 33 | // Functions 34 | ("func", 1), // {func name ($arg1 ...) {...} ...} 35 | ("closure", 1), // {closure ($var1 ...) ($arg1 ...) {...} ...} 36 | // Etc. 37 | ("time", 1), // {time $var1 ... {...} ...} 38 | ]) 39 | }); 40 | 41 | /// Non-block commands which should have arguments kept on the same line as the command name 42 | /// when not written as a single line. 43 | pub static COMMAND_SAME_LINE_ARGS: LazyLock> = LazyLock::new(|| { 44 | HashMap::from_iter([ 45 | // Modify-assign operators 46 | ("+=", 1), // {+= $value {...}} 47 | ("-=", 1), // {-= $value {...}} 48 | ("*=", 1), // {*= $value {...}} 49 | ("/=", 1), // {/= $value {...}} 50 | ("%=", 1), // {%= $value {...}} 51 | ("&=", 1), // {&= $value {...}} 52 | ("|=", 1), // {|= $value {...}} 53 | ("^=", 1), // {^= $value {...}} 54 | // Modify-assign functions 55 | ("clamp_eq", 1), // {clamp_eq $value {...}} 56 | ("max_eq", 1), // {max_eq $value {...}} 57 | ("min_eq", 1), // {min_eq $value {...}} 58 | ("mask_eq", 1), // {mask_eq $value {...}} 59 | // Variable storage 60 | ("set", 1), // {set $var {...}} 61 | ("set_var", 1), // {set_var var {...}} 62 | // Object creation 63 | ("new", 1), // {new ClassName (...)} 64 | ]) 65 | }); 66 | 67 | pub fn pack_args_on_same_line(name: &str) -> Option { 68 | BLOCK_COMMANDS 69 | .get(name) 70 | .or_else(|| COMMAND_SAME_LINE_ARGS.get(name)) 71 | .copied() 72 | } 73 | 74 | impl TryFrom<&Token<'_>> for CommentDirective { 75 | type Error = (); 76 | 77 | fn try_from(value: &Token<'_>) -> Result { 78 | match &value.value { 79 | TokenValue::Comment(text) => text.parse::(), 80 | TokenValue::BlockComment(BlockCommentToken { body, .. }) => body.text.parse::(), 81 | _ => Err(()), 82 | } 83 | } 84 | } 85 | 86 | impl TryFrom<&Expression<'_>> for CommentDirective { 87 | type Error = (); 88 | 89 | fn try_from(value: &Expression<'_>) -> Result { 90 | match &value.value { 91 | ExpressionValue::Comment(text) => text.parse::(), 92 | ExpressionValue::BlockComment(BlockCommentToken { body, .. }) => { 93 | body.text.parse::() 94 | }, 95 | _ => Err(()), 96 | } 97 | } 98 | } 99 | 100 | impl FromStr for CommentDirective { 101 | type Err = (); 102 | 103 | fn from_str(comment: &str) -> Result { 104 | const PREFIX: &str = "arson-fmt"; 105 | 106 | let comment = comment.trim_start(); 107 | let Some(directive) = comment.strip_prefix(PREFIX) else { 108 | return Err(()); 109 | }; 110 | 111 | let directive = match directive.split_once(':') { 112 | Some((directive, _comment)) => directive.trim(), 113 | None => directive.trim(), 114 | }; 115 | 116 | let result = match directive { 117 | "on" => Self::FormattingOn, 118 | "off" => Self::FormattingOff, 119 | _ => return Err(()), 120 | }; 121 | 122 | Ok(result) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /crates/arson-fmtlib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | //! A formatter library for DTA files. 4 | 5 | #![warn(missing_docs)] 6 | 7 | use arson_parse::ParseError; 8 | 9 | mod consts; 10 | pub mod expr; 11 | pub mod token; 12 | 13 | /// The indentation to use when formatting. 14 | #[derive(Debug, Clone, Copy)] 15 | pub enum Indentation { 16 | /// Use tabs when formatting. 17 | /// The inner size value is how many characters a tab should be considered to be. 18 | Tabs(usize), 19 | 20 | /// Use spaces when formatting. 21 | Spaces(usize), 22 | } 23 | 24 | /// Options for formatting. 25 | #[derive(Debug, Clone)] 26 | pub struct Options { 27 | /// The indentation style to use. 28 | pub indentation: Indentation, 29 | /// The maximum width of arrays in the output. 30 | pub max_array_width: usize, 31 | /// The maximum width of lines in the output. 32 | pub max_line_width: usize, 33 | } 34 | 35 | impl Default for Options { 36 | fn default() -> Self { 37 | Self { 38 | indentation: Indentation::Spaces(3), 39 | max_array_width: 75, 40 | max_line_width: 90, 41 | } 42 | } 43 | } 44 | 45 | /// Formats the given input text to a new string. 46 | /// 47 | /// This will first attempt to do a full parse on the input text and use an expression-based formatter. 48 | /// Failing that, it will fall back to a more forgiving but less capable token-based formatter 49 | /// which formats on a best-effort basis. 50 | /// 51 | /// # Example 52 | /// 53 | /// ```rust 54 | /// let input = "(1 2 3 4) (a b c d) (outer (inner \"some text\")) (foo (bar \"some text\") (baz 10.0))"; 55 | /// let options = arson_fmtlib::Options::default(); 56 | /// let formatted = arson_fmtlib::format_to_string(input, options).unwrap(); 57 | /// assert_eq!(&formatted, "\ 58 | /// (1 2 3 4)\ 59 | /// \n(a b c d)\ 60 | /// \n(outer (inner \"some text\"))\ 61 | /// \n(foo\ 62 | /// \n (bar \"some text\")\ 63 | /// \n (baz 10.0)\ 64 | /// \n)\ 65 | /// ") 66 | /// ``` 67 | pub fn format_to_string(input: &str, options: Options) -> Result { 68 | match expr::Formatter::new(input, options.clone()) { 69 | Ok(f) => Ok(f.to_string()), 70 | Err(error) => Err((token::Formatter::new(input, options).to_string(), error)), 71 | } 72 | } 73 | 74 | /// The formatter for DTA text. 75 | /// 76 | /// This formatter does output through the [`std::fmt::Display`] trait 77 | /// to make it possible to output to both [`std::fmt::Write`] and [`std::io::Write`] 78 | /// without going through an intermediate [`String`]. 79 | /// Use [`format`], [`write`], or [`to_string`](ToString::to_string) to perform the 80 | /// actual formatting and output. 81 | /// 82 | /// # Example 83 | /// 84 | /// ```rust 85 | /// let input = "(1 2 3 4) (a b c d) (outer (inner \"some text\")) (foo (bar \"some text\") (baz 10.0))"; 86 | /// let options = arson_fmtlib::Options::default(); 87 | /// let formatter = arson_fmtlib::expr::Formatter::new(input, options).unwrap(); 88 | /// 89 | /// let formatted = formatter.to_string(); 90 | /// assert_eq!(&formatted, "\ 91 | /// (1 2 3 4)\ 92 | /// \n(a b c d)\ 93 | /// \n(outer (inner \"some text\"))\ 94 | /// \n(foo\ 95 | /// \n (bar \"some text\")\ 96 | /// \n (baz 10.0)\ 97 | /// \n)\ 98 | /// ") 99 | /// ``` 100 | pub enum Formatter<'src> { 101 | /// Expression-based formatter (see [`expr::Formatter`]). 102 | Expression(expr::Formatter<'src>), 103 | /// Token-based formatter (see [`token::Formatter`]). 104 | Token(token::Formatter<'src>), 105 | } 106 | 107 | impl<'src> Formatter<'src> { 108 | /// Creates a new [`Formatter`] with the given input and options. 109 | pub fn new(input: &'src str, options: Options) -> (Self, Option) { 110 | match expr::Formatter::new(input, options.clone()) { 111 | Ok(formatter) => (Self::Expression(formatter), None), 112 | Err(error) => { 113 | let formatter = token::Formatter::new(input, options); 114 | (Self::Token(formatter), Some(error)) 115 | }, 116 | } 117 | } 118 | } 119 | 120 | impl std::fmt::Display for Formatter<'_> { 121 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 122 | match self { 123 | Self::Expression(formatter) => formatter.fmt(f), 124 | Self::Token(formatter) => formatter.fmt(f), 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /crates/arson-fs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-fs" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | thiserror = "1.0.65" 13 | -------------------------------------------------------------------------------- /crates/arson-fs/src/driver.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io::{self, Read, Write}; 4 | use std::time::SystemTime; 5 | 6 | use super::AbsolutePath; 7 | 8 | /// Conjunction of the [`Read`] and [`Write`] traits. 9 | /// 10 | /// This trait exists to work around `dyn Read + Write` not being allowed, 11 | /// and has no unique functionality. It is auto-implemented for all types 12 | /// which implement both traits. 13 | pub trait ReadWrite: Read + Write {} 14 | impl ReadWrite for T {} 15 | 16 | /// Metadata information for a file system entry. 17 | pub enum Metadata { 18 | File { 19 | modified: io::Result, 20 | accessed: io::Result, 21 | created: io::Result, 22 | 23 | len: u64, 24 | 25 | is_readonly: bool, 26 | is_symlink: bool, 27 | }, 28 | Directory { 29 | modified: io::Result, 30 | accessed: io::Result, 31 | created: io::Result, 32 | 33 | is_symlink: bool, 34 | }, 35 | } 36 | 37 | /// A file system driver used to back [`FileSystem`](super::FileSystem). 38 | pub trait FileSystemDriver { 39 | /// Retrieves metadata for the entry referred to by the given path. 40 | fn metadata(&self, path: &AbsolutePath) -> io::Result; 41 | 42 | /// Opens a file in write-only mode, creating if it doesn't exist yet, and truncating if it does. 43 | fn create(&self, path: &AbsolutePath) -> io::Result>; 44 | 45 | /// Creates a new file in read-write mode. Errors if the file already exists. 46 | fn create_new(&self, path: &AbsolutePath) -> io::Result>; 47 | 48 | /// Opens a file with read permissions. 49 | fn open(&self, path: &AbsolutePath) -> io::Result>; 50 | 51 | /// Opens a file with execute permissions. 52 | /// 53 | /// This function is used when opening a file which is to be executed as a script. 54 | /// The file otherwise has standard read permissions, and this function exists 55 | /// primarily to allow for additional filtering where desired. 56 | fn open_execute(&self, path: &AbsolutePath) -> io::Result>; 57 | } 58 | 59 | impl Metadata { 60 | pub fn is_file(&self) -> bool { 61 | matches!(self, Self::File { .. }) 62 | } 63 | 64 | pub fn is_dir(&self) -> bool { 65 | matches!(self, Self::Directory { .. }) 66 | } 67 | 68 | pub fn is_symlink(&self) -> bool { 69 | match self { 70 | Self::File { is_symlink, .. } => *is_symlink, 71 | Self::Directory { is_symlink, .. } => *is_symlink, 72 | } 73 | } 74 | 75 | pub fn modified(&self) -> &io::Result { 76 | match self { 77 | Self::File { modified, .. } => modified, 78 | Self::Directory { modified, .. } => modified, 79 | } 80 | } 81 | 82 | pub fn accessed(&self) -> &io::Result { 83 | match self { 84 | Self::File { accessed, .. } => accessed, 85 | Self::Directory { accessed, .. } => accessed, 86 | } 87 | } 88 | 89 | pub fn created(&self) -> &io::Result { 90 | match self { 91 | Self::File { created, .. } => created, 92 | Self::Directory { created, .. } => created, 93 | } 94 | } 95 | } 96 | 97 | impl From for Metadata { 98 | fn from(value: std::fs::Metadata) -> Self { 99 | Self::from(&value) 100 | } 101 | } 102 | 103 | impl From<&std::fs::Metadata> for Metadata { 104 | fn from(value: &std::fs::Metadata) -> Self { 105 | if value.is_file() { 106 | Self::File { 107 | len: value.len(), 108 | 109 | is_readonly: value.permissions().readonly(), 110 | is_symlink: value.is_symlink(), 111 | 112 | modified: value.modified(), 113 | accessed: value.accessed(), 114 | created: value.created(), 115 | } 116 | } else { 117 | Self::Directory { 118 | is_symlink: value.is_symlink(), 119 | 120 | modified: value.modified(), 121 | accessed: value.accessed(), 122 | created: value.created(), 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /crates/arson-fs/src/drivers/basic.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::fs::File; 4 | use std::io::{self, Read, Write}; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use crate::{AbsolutePath, FileSystemDriver, Metadata, ReadWrite}; 8 | 9 | /// An extremely basic file system driver which simply provides raw access to a specific directory. 10 | /// 11 | /// Because [`AbsolutePath`] and [`FileSystem`](super::FileSystem) handle path sandboxing inherently, 12 | /// this driver prevents sandbox escapes naturally. To doubly ensure this, a paranoid safety assert 13 | /// is done when resolving the virtual path to the on-disk one to ensure no parent directory components 14 | /// slip through. 15 | pub struct BasicFileSystemDriver { 16 | mount_dir: PathBuf, 17 | } 18 | 19 | impl BasicFileSystemDriver { 20 | /// Creates a new [`BasicFileSystemDriver`] with the given mount directory. 21 | /// 22 | /// # Errors 23 | /// 24 | /// Returns an error if the given path could not be canonicalized or is not a directory. 25 | pub fn new>(mount_dir: P) -> io::Result { 26 | let mount_dir = mount_dir.as_ref().canonicalize()?; 27 | if !mount_dir.is_dir() { 28 | return Err(io::ErrorKind::NotADirectory.into()); 29 | } 30 | 31 | Ok(Self { mount_dir }) 32 | } 33 | 34 | fn resolve_path(&self, path: &AbsolutePath) -> PathBuf { 35 | path.to_fs_path(&self.mount_dir) 36 | } 37 | } 38 | 39 | impl FileSystemDriver for BasicFileSystemDriver { 40 | fn metadata(&self, path: &AbsolutePath) -> io::Result { 41 | self.resolve_path(path).metadata().map(Metadata::from) 42 | } 43 | 44 | fn create(&self, path: &AbsolutePath) -> io::Result> { 45 | File::create(self.resolve_path(path)).map::, _>(|f| Box::new(f)) 46 | } 47 | 48 | fn create_new(&self, path: &AbsolutePath) -> io::Result> { 49 | File::create_new(self.resolve_path(path)).map::, _>(|f| Box::new(f)) 50 | } 51 | 52 | fn open(&self, path: &AbsolutePath) -> io::Result> { 53 | File::open(self.resolve_path(path)).map::, _>(|f| Box::new(f)) 54 | } 55 | 56 | fn open_execute(&self, path: &AbsolutePath) -> io::Result> { 57 | self.open(path) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /crates/arson-fs/src/drivers/mock.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::collections::HashMap; 4 | use std::io::{self, Read, Write}; 5 | use std::rc::Rc; 6 | use std::time::SystemTime; 7 | 8 | use crate::{AbsolutePath, FileSystemDriver, Metadata, ReadWrite}; 9 | 10 | #[derive(Debug, Clone)] 11 | struct MockFile { 12 | bytes: Rc>, 13 | position: usize, 14 | } 15 | 16 | impl MockFile { 17 | fn new(bytes: Rc>) -> Self { 18 | Self { bytes, position: 0 } 19 | } 20 | } 21 | 22 | impl Read for MockFile { 23 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 24 | let Some(bytes) = self.bytes.get(self.position..) else { 25 | return Ok(0); 26 | }; 27 | 28 | let count = bytes.len().min(buf.len()); 29 | let src = &bytes[0..count]; 30 | let dest = &mut buf[0..count]; 31 | 32 | dest.copy_from_slice(src); 33 | self.position += count; 34 | Ok(count) 35 | } 36 | } 37 | 38 | /// A mock file system driver which stores files in-memory. 39 | /// 40 | /// This driver is intended for tests only, and should not be used in production. 41 | pub struct MockFileSystemDriver { 42 | files: HashMap>>, 43 | } 44 | 45 | impl MockFileSystemDriver { 46 | pub fn new() -> Self { 47 | Self { files: HashMap::new() } 48 | } 49 | 50 | pub fn add_file(&mut self, path: AbsolutePath, bytes: &[u8]) { 51 | self.files.insert(path, Rc::new(bytes.to_owned())); 52 | } 53 | 54 | pub fn add_text_file(&mut self, path: AbsolutePath, text: &str) { 55 | self.add_file(path, text.as_bytes()); 56 | } 57 | } 58 | 59 | impl FileSystemDriver for MockFileSystemDriver { 60 | fn metadata(&self, path: &AbsolutePath) -> io::Result { 61 | match self.files.get(path) { 62 | Some(bytes) => Ok(Metadata::File { 63 | modified: Ok(SystemTime::UNIX_EPOCH), 64 | accessed: Ok(SystemTime::UNIX_EPOCH), 65 | created: Ok(SystemTime::UNIX_EPOCH), 66 | len: bytes.len() as u64, 67 | is_readonly: true, 68 | is_symlink: false, 69 | }), 70 | None => Err(io::ErrorKind::NotFound.into()), 71 | } 72 | } 73 | 74 | fn create(&self, _path: &AbsolutePath) -> io::Result> { 75 | // TODO: write support 76 | Err(io::Error::new( 77 | io::ErrorKind::Unsupported, 78 | "MockFileSystemDriver does not support creating files", 79 | )) 80 | } 81 | 82 | fn create_new(&self, _path: &AbsolutePath) -> io::Result> { 83 | // TODO: write support 84 | Err(io::Error::new( 85 | io::ErrorKind::Unsupported, 86 | "MockFileSystemDriver does not support creating files", 87 | )) 88 | } 89 | 90 | fn open(&self, path: &AbsolutePath) -> io::Result> { 91 | match self.files.get(path).cloned() { 92 | Some(bytes) => Ok(Box::new(MockFile::new(bytes))), 93 | None => Err(io::ErrorKind::NotFound.into()), 94 | } 95 | } 96 | 97 | fn open_execute(&self, path: &AbsolutePath) -> io::Result> { 98 | self.open(path) 99 | } 100 | } 101 | 102 | impl Default for MockFileSystemDriver { 103 | fn default() -> Self { 104 | Self::new() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /crates/arson-fs/src/drivers/mod.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | mod basic; 4 | mod mock; 5 | 6 | pub use basic::*; 7 | pub use mock::*; 8 | -------------------------------------------------------------------------------- /crates/arson-fs/src/filesystem.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io::{self, Read, Write}; 4 | 5 | use super::{AbsolutePath, FileSystemDriver, Metadata, ReadWrite, VirtualPath}; 6 | 7 | /// A file system implementation to be used from scripts. 8 | /// 9 | /// All methods which take a path accept relative paths, 10 | /// and resolve them relative to the [current working directory](FileSystem::cwd) 11 | /// of the file system. 12 | pub struct FileSystem { 13 | driver: Box, 14 | cwd: AbsolutePath, 15 | } 16 | 17 | impl FileSystem { 18 | /// Creates a new [`FileSystem`] with the given driver. 19 | pub fn new(driver: T) -> Self { 20 | Self { driver: Box::new(driver), cwd: AbsolutePath::new() } 21 | } 22 | 23 | /// Gets the current working directory, used to resolve relative paths. 24 | pub fn cwd(&self) -> &AbsolutePath { 25 | &self.cwd 26 | } 27 | 28 | /// Sets a new working directory and returns the old one. 29 | /// 30 | /// Relative paths are resolved relative to the existing working directory, 31 | /// and this resolved path is what will become the new working directory. 32 | /// 33 | /// # Errors 34 | /// 35 | /// Returns an error if the given path does not exist as a directory. 36 | pub fn set_cwd>(&mut self, path: P) -> AbsolutePath { 37 | let mut path = self.canonicalize(path); 38 | std::mem::swap(&mut self.cwd, &mut path); 39 | path 40 | } 41 | 42 | /// Constructs the absolute form of a given path, resolving relative to the 43 | /// [current working directory](FileSystem::cwd). 44 | /// 45 | /// See [`VirtualPath::canonicalize`] for full details. 46 | pub fn canonicalize>(&self, path: P) -> AbsolutePath { 47 | path.as_ref().make_absolute(&self.cwd) 48 | } 49 | 50 | /// Retrieves metadata for the given path, if it exists. 51 | pub fn metadata>(&self, path: P) -> io::Result { 52 | self.driver.metadata(&self.canonicalize(path)) 53 | } 54 | 55 | /// Determines whether the given path exists in the file system. 56 | pub fn exists>(&self, path: P) -> bool { 57 | self.metadata(path).is_ok() 58 | } 59 | 60 | /// Determines whether the given path exists and refers to a file. 61 | pub fn is_file>(&self, path: P) -> bool { 62 | self.metadata(path).map_or(false, |m| m.is_file()) 63 | } 64 | 65 | /// Determines whether the given path exists and refers to a directory. 66 | pub fn is_dir>(&self, path: P) -> bool { 67 | self.metadata(path).map_or(false, |m| m.is_dir()) 68 | } 69 | 70 | /// Opens a file in write-only mode, creating if it doesn't exist yet, and truncating if it does. 71 | pub fn create>(&self, path: P) -> io::Result> { 72 | self.driver.create(&self.canonicalize(path)) 73 | } 74 | 75 | /// Creates a new file in read-write mode. Errors if the file already exists. 76 | pub fn create_new>(&self, path: P) -> io::Result> { 77 | self.driver.create_new(&self.canonicalize(path)) 78 | } 79 | 80 | /// Opens a file with read permissions. 81 | pub fn open>(&self, path: P) -> io::Result> { 82 | self.driver.open(&self.canonicalize(path)) 83 | } 84 | 85 | /// Opens a file with execute permissions. 86 | pub fn open_execute>(&self, path: P) -> io::Result> { 87 | self.driver.open_execute(&self.canonicalize(path)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /crates/arson-fs/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | pub mod drivers; 4 | 5 | mod driver; 6 | mod filesystem; 7 | mod path; 8 | 9 | pub use driver::*; 10 | pub use filesystem::*; 11 | pub use path::*; 12 | 13 | pub mod prelude { 14 | pub use super::{AbsolutePath, FileSystem, VirtualPath, VirtualPathBuf}; 15 | } 16 | -------------------------------------------------------------------------------- /crates/arson-parse/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-parse" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [features] 12 | reporting = ["dep:codespan-reporting"] 13 | encoding = ["dep:encoding_rs"] 14 | 15 | [dependencies] 16 | codespan-reporting = { version = "0.11.1", optional = true } 17 | encoding_rs = { version = "0.8.35", optional = true } 18 | lazy-regex = "3.3.0" 19 | logos = { version = "0.14.2", features = ["forbid_unsafe"] } 20 | thiserror = "1.0.65" 21 | -------------------------------------------------------------------------------- /crates/arson-parse/src/diagnostics.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | #[cfg(feature = "reporting")] 4 | use codespan_reporting::diagnostic::{Diagnostic as CodespanDiagnostic, Label, Severity}; 5 | use logos::Span; 6 | 7 | use crate::{ArrayKind, TokenKind}; 8 | 9 | #[derive(thiserror::Error, Debug, PartialEq)] 10 | pub struct Diagnostic { 11 | #[source] 12 | kind: DiagnosticKind, 13 | location: Span, 14 | } 15 | 16 | impl Diagnostic { 17 | pub fn new(kind: DiagnosticKind, location: Span) -> Self { 18 | Self { kind, location } 19 | } 20 | 21 | pub fn kind(&self) -> &DiagnosticKind { 22 | &self.kind 23 | } 24 | 25 | pub fn location(&self) -> Span { 26 | self.location.clone() 27 | } 28 | 29 | pub(crate) fn sort_cmp(&self, other: &Self) -> std::cmp::Ordering { 30 | let ord = std::cmp::Ord::cmp(&self.location.start, &other.location.start); 31 | if !matches!(ord, std::cmp::Ordering::Equal) { 32 | return ord; 33 | } 34 | self.kind.sort_cmp(&other.kind) 35 | } 36 | } 37 | 38 | impl std::fmt::Display for Diagnostic { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | std::fmt::Display::fmt(&self.kind, f) 41 | } 42 | } 43 | 44 | #[derive(thiserror::Error, Debug, Clone, Default, PartialEq)] 45 | pub enum DiagnosticKind { 46 | #[error("unexpected end of file")] 47 | UnexpectedEof, 48 | #[default] 49 | #[error("invalid token")] 50 | InvalidToken, 51 | 52 | #[error("internal error: failed to trim token delimiters")] 53 | TrimDelimiterError { trim_range: Span, actual_length: usize }, 54 | #[error("integer parse error")] 55 | IntegerParseError(#[from] std::num::ParseIntError), 56 | #[error("float parse error")] 57 | FloatParseError(#[from] std::num::ParseFloatError), 58 | 59 | #[error("unrecognized parser directive")] 60 | BadDirective, 61 | #[error("directive is missing an argument")] 62 | MissingDirectiveArgument { 63 | missing: TokenKind, 64 | description: DirectiveArgumentDescription, 65 | }, 66 | #[error("incorrect argument to directive")] 67 | IncorrectDirectiveArgument { 68 | expected: TokenKind, 69 | expected_description: DirectiveArgumentDescription, 70 | actual: TokenKind, 71 | expecting_location: Span, 72 | }, 73 | 74 | #[error("unexpected conditional directive")] 75 | UnexpectedConditional, 76 | #[error("unmatched conditional directive")] 77 | UnmatchedConditional, 78 | #[error("unbalanced conditional block")] 79 | UnbalancedConditional, 80 | 81 | #[error("unmatched {kind} delimiter")] 82 | UnmatchedBrace { kind: ArrayKind, open: bool }, 83 | 84 | #[error("block comment was not closed")] 85 | UnclosedBlockComment, 86 | } 87 | 88 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] 89 | pub enum DirectiveArgumentDescription { 90 | MacroName, 91 | FilePath, 92 | MacroBody, 93 | CommandBody, 94 | } 95 | 96 | impl std::fmt::Display for DirectiveArgumentDescription { 97 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 98 | match self { 99 | DirectiveArgumentDescription::MacroName => f.write_str("macro name"), 100 | DirectiveArgumentDescription::FilePath => f.write_str("file path"), 101 | DirectiveArgumentDescription::MacroBody => f.write_str("macro body"), 102 | DirectiveArgumentDescription::CommandBody => f.write_str("command body"), 103 | } 104 | } 105 | } 106 | 107 | #[cfg(feature = "reporting")] 108 | impl Diagnostic { 109 | // Manually formatted for consistency between match arms, 110 | // and because rustfmt does more harm than good here for both readability and maintenance 111 | #[rustfmt::skip] 112 | pub fn to_codespan(&self, file_id: FileId) -> CodespanDiagnostic { 113 | let diagnostic = CodespanDiagnostic::error() 114 | .with_message(self.to_string()) 115 | .with_labels(vec![Label::primary(file_id.clone(), self.location.clone())]); 116 | 117 | // There are gaps in the error codes here to ease adding additional errors in the future. 118 | // TODO: These gaps should be removed once things are stabilized. 119 | match &self.kind { 120 | DiagnosticKind::UnexpectedEof => diagnostic 121 | .with_code("DTA0000"), 122 | DiagnosticKind::InvalidToken => diagnostic 123 | .with_code("DTA0001"), 124 | 125 | DiagnosticKind::TrimDelimiterError { trim_range, actual_length } => { 126 | CodespanDiagnostic { severity: Severity::Bug, ..diagnostic } 127 | .with_code("DTA0005") 128 | .with_notes(vec![format!( 129 | "tried to extract ({trim_range:?}) from text with length ({actual_length})" 130 | )]) 131 | }, 132 | DiagnosticKind::IntegerParseError(error) => diagnostic 133 | .with_code("DTA0006") 134 | .with_notes(vec![error.to_string()]), 135 | DiagnosticKind::FloatParseError(error) => diagnostic 136 | .with_code("DTA0007") 137 | .with_notes(vec![error.to_string()]), 138 | 139 | DiagnosticKind::BadDirective => diagnostic 140 | .with_code("DTA0010"), 141 | DiagnosticKind::MissingDirectiveArgument { missing, description: arg_description } => diagnostic 142 | .with_code("DTA0011") 143 | .with_labels(vec![ 144 | Label::secondary(file_id, self.location.end..self.location.end) 145 | .with_message(format!("expected {missing} ({arg_description})")) 146 | ]), 147 | DiagnosticKind::IncorrectDirectiveArgument { 148 | expected: _, 149 | expected_description, 150 | actual, 151 | expecting_location, 152 | } => diagnostic 153 | .with_code("DTA0012") 154 | .with_labels(vec![Label::secondary(file_id, expecting_location.clone()) 155 | .with_message(format!( 156 | "directive here was expecting {expected_description}, found {actual} instead" 157 | ))]), 158 | 159 | DiagnosticKind::UnexpectedConditional => diagnostic 160 | .with_code("DTA0015"), 161 | DiagnosticKind::UnmatchedConditional => diagnostic 162 | .with_code("DTA0016") 163 | .with_notes(vec!["#else or #endif required to close the conditional block".to_owned()]), 164 | DiagnosticKind::UnbalancedConditional => diagnostic 165 | .with_code("DTA0017") 166 | .with_notes(vec!["all arrays in conditionals must be self-contained".to_owned()]), 167 | 168 | DiagnosticKind::UnmatchedBrace { kind, open } => diagnostic 169 | .with_code("DTA0020") 170 | .with_notes(vec![ 171 | format!("'{}' required to close the {kind}", kind.delimiter(*open)) 172 | ]), 173 | 174 | DiagnosticKind::UnclosedBlockComment => diagnostic 175 | .with_code("DTA0025") 176 | .with_notes(vec!["'*/' required to close the comment".to_owned()]), 177 | } 178 | } 179 | } 180 | 181 | impl DiagnosticKind { 182 | pub(crate) fn sort_cmp(&self, other: &Self) -> std::cmp::Ordering { 183 | fn discriminant(value: &DiagnosticKind) -> u32 { 184 | match value { 185 | DiagnosticKind::UnexpectedEof => 0, 186 | DiagnosticKind::InvalidToken => 1, 187 | 188 | DiagnosticKind::TrimDelimiterError { .. } => 5, 189 | DiagnosticKind::IntegerParseError(_) => 6, 190 | DiagnosticKind::FloatParseError(_) => 7, 191 | 192 | DiagnosticKind::BadDirective => 10, 193 | DiagnosticKind::MissingDirectiveArgument { .. } => 11, 194 | DiagnosticKind::IncorrectDirectiveArgument { .. } => 12, 195 | 196 | DiagnosticKind::UnexpectedConditional => 15, 197 | DiagnosticKind::UnmatchedConditional => 16, 198 | DiagnosticKind::UnbalancedConditional => 17, 199 | 200 | DiagnosticKind::UnmatchedBrace { .. } => 20, 201 | 202 | DiagnosticKind::UnclosedBlockComment => 25, 203 | } 204 | } 205 | 206 | discriminant(self).cmp(&discriminant(other)) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /crates/arson-parse/src/encoding.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::borrow::Cow; 4 | 5 | /// Supported encodings for DTA text. 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub enum DtaEncoding { 8 | Utf8, 9 | Utf16BigEndian, 10 | Utf16LittleEndian, 11 | Latin1, 12 | } 13 | 14 | #[derive(thiserror::Error, Debug)] 15 | #[error("could not encode the text to {0:?}")] 16 | pub struct EncodeError(pub DtaEncoding); 17 | 18 | #[derive(thiserror::Error, Debug)] 19 | pub struct DecodeError(pub Option); 20 | 21 | impl std::fmt::Display for DecodeError { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self.0 { 24 | Some(encoding) => write!(f, "could not decode the bytes using encoding {encoding:?}"), 25 | None => f.write_str("could not decode the bytes via default encoding detection"), 26 | } 27 | } 28 | } 29 | 30 | macro_rules! decode_default_impl { 31 | ($bytes:ident, $encoding_object:ident, $encoding_variant:ident) => {{ 32 | match encoding_rs::$encoding_object.decode_without_bom_handling_and_without_replacement($bytes) { 33 | Some(decoded) => Ok(decoded), 34 | None => Err(DecodeError(Some(DtaEncoding::$encoding_variant))), 35 | } 36 | }}; 37 | } 38 | 39 | /// Decodes the given bytes according to the provided encoding. 40 | pub fn decode( 41 | bytes: &[u8], 42 | encoding: Option, 43 | ) -> Result<(Cow<'_, str>, DtaEncoding), DecodeError> { 44 | match encoding { 45 | Some(DtaEncoding::Utf8) => Ok((decode_utf8(bytes)?, DtaEncoding::Utf8)), 46 | Some(DtaEncoding::Utf16BigEndian) => Ok((decode_utf16_be(bytes)?, DtaEncoding::Utf16BigEndian)), 47 | Some(DtaEncoding::Utf16LittleEndian) => Ok((decode_utf16_le(bytes)?, DtaEncoding::Utf16LittleEndian)), 48 | Some(DtaEncoding::Latin1) => Ok((decode_latin1(bytes)?, DtaEncoding::Latin1)), 49 | None => decode_default(bytes), 50 | } 51 | } 52 | 53 | pub fn decode_default(bytes: &[u8]) -> Result<(Cow<'_, str>, DtaEncoding), DecodeError> { 54 | // Attempt to decode as UTF first, it will more reliably result in a 55 | // decoding error if it's not the right encoding due to the format details. 56 | // 57 | // 0x80-0xFF are only used for multi-byte sequences, and these sequences follow a 58 | // specific format that real Latin-1 text is pretty much guaranteed to break. 59 | // Almost all Latin-1 special characters are in the 0xC0-0xFF range, which UTF-8 60 | // uses exclusively as the first byte for multi-byte code points, so consecutive 61 | // special characters will not result in a valid byte sequence. 62 | let (decoded, actual_encoding, malformed) = encoding_rs::UTF_8.decode(bytes); 63 | if !malformed && std::ptr::addr_eq(actual_encoding, encoding_rs::UTF_8) { 64 | return Ok((decoded, DtaEncoding::Utf8)); 65 | } 66 | 67 | // Attempt Latin-1 next (specifically Windows-1252, because it has more 68 | // printable characters which are more likely intended) 69 | let (decoded, actual_encoding, malformed) = encoding_rs::WINDOWS_1252.decode(bytes); 70 | if !malformed && std::ptr::addr_eq(actual_encoding, encoding_rs::WINDOWS_1252) { 71 | return Ok((decoded, DtaEncoding::Latin1)); 72 | } 73 | 74 | Err(DecodeError(None)) 75 | } 76 | 77 | pub fn decode_utf8(bytes: &[u8]) -> Result, DecodeError> { 78 | decode_default_impl!(bytes, UTF_8, Utf8) 79 | } 80 | 81 | pub fn decode_utf16_be(bytes: &[u8]) -> Result, DecodeError> { 82 | decode_default_impl!(bytes, UTF_16BE, Utf16BigEndian) 83 | } 84 | 85 | pub fn decode_utf16_le(bytes: &[u8]) -> Result, DecodeError> { 86 | decode_default_impl!(bytes, UTF_16LE, Utf16LittleEndian) 87 | } 88 | 89 | pub fn decode_latin1(bytes: &[u8]) -> Result, DecodeError> { 90 | decode_default_impl!(bytes, WINDOWS_1252, Latin1) 91 | } 92 | 93 | macro_rules! encode_default_impl { 94 | ($text:ident, $encoding_object:ident, $encoding_variant:ident) => {{ 95 | let (encoded, actual_encoding, unmappable) = encoding_rs::$encoding_object.encode($text); 96 | if !unmappable && std::ptr::addr_eq(actual_encoding, encoding_rs::$encoding_object) { 97 | return Ok(encoded); 98 | } 99 | 100 | Err(EncodeError(DtaEncoding::$encoding_variant)) 101 | }}; 102 | } 103 | 104 | pub fn encode(text: &str, encoding: DtaEncoding) -> Result, EncodeError> { 105 | match encoding { 106 | DtaEncoding::Utf8 => encode_utf8(text), 107 | DtaEncoding::Utf16BigEndian => encode_utf16_be(text), 108 | DtaEncoding::Utf16LittleEndian => encode_utf16_le(text), 109 | DtaEncoding::Latin1 => encode_latin1(text), 110 | } 111 | } 112 | 113 | pub fn encode_utf8(text: &str) -> Result, EncodeError> { 114 | encode_default_impl!(text, UTF_8, Utf8) 115 | } 116 | 117 | pub fn encode_utf16_be(text: &str) -> Result, EncodeError> { 118 | encode_default_impl!(text, UTF_16BE, Utf16BigEndian) 119 | } 120 | 121 | pub fn encode_utf16_le(text: &str) -> Result, EncodeError> { 122 | encode_default_impl!(text, UTF_16LE, Utf16LittleEndian) 123 | } 124 | 125 | pub fn encode_latin1(text: &str) -> Result, EncodeError> { 126 | encode_default_impl!(text, WINDOWS_1252, Latin1) 127 | } 128 | -------------------------------------------------------------------------------- /crates/arson-parse/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | /// Transform one fragment into another. 4 | macro_rules! meta_morph { 5 | (|$_:tt| $($i:tt)*) => { 6 | $($i)* 7 | }; 8 | } 9 | 10 | /// Returns the result of a pattern match, panicking if the pattern wasn't matched. 11 | macro_rules! match_unwrap { 12 | ($expression:expr, $pattern:pat => $result:expr) => { 13 | match $expression { 14 | $pattern => $result, 15 | _ => panic!("pattern \"{}\" was not matched", stringify!($pattern)), 16 | } 17 | }; 18 | } 19 | 20 | mod diagnostics; 21 | #[cfg(feature = "encoding")] 22 | pub mod encoding; 23 | mod lexer; 24 | mod parser; 25 | 26 | pub use diagnostics::*; 27 | pub use lexer::*; 28 | pub use parser::*; 29 | 30 | pub mod prelude { 31 | pub use super::diagnostics::{Diagnostic, DiagnosticKind}; 32 | pub use super::lexer::{Token, TokenValue, Tokenizer}; 33 | pub use super::parser::{Expression, ExpressionValue}; 34 | } 35 | 36 | #[cfg(feature = "reporting")] 37 | // Re-export so dependers don't have to sync versions 38 | pub use codespan_reporting as reporting; 39 | -------------------------------------------------------------------------------- /crates/arson-parse/tests/test_files/thorough.dta: -------------------------------------------------------------------------------- 1 | ; SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | ; integers 4 | 1 +2 -3 5 | 01 +02 -03 6 | 7 | ; hex numbers 8 | 0x1 0xA 0xa 9 | 0xFFFFFFFF 10 | 0xFFFFFFFFFFFFFFFF 11 | ; invalid (too big for 64 bits) 12 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 13 | ; invalid (lexed as symbols) 14 | 0x x1 15 | +0x2 -0x3 16 | +0xB -0xC 17 | +0xb -0xc 18 | 19 | ; floats 20 | 1.0 +2.0 -3.0 21 | 1. +2. -3. 22 | .1 +.2 -.3 23 | ; these are valid 24 | . +. -. 25 | 26 | ; floats with exponents 27 | ; valid - invalid 28 | 1.0E1 +2.0E1 -3.0E1 1.0-E1 +2.0-E1 -3.0-E1 29 | 1.0E+1 +2.0E+1 -3.0E+1 1.0-E+1 +2.0-E+1 -3.0-E+1 30 | 1.0E-1 +2.0E-1 -3.0E-1 1.0-E-1 +2.0-E-1 -3.0-E-1 31 | 32 | 1.E1 +2.E1 -3.E1 1.-E1 +2.-E1 -3.-E1 33 | 1.E+1 +2.E+1 -3.E+1 1.-E+1 +2.-E+1 -3.-E+1 34 | 1.E-1 +2.E-1 -3.E-1 1.-E-1 +2.-E-1 -3.-E-1 35 | 36 | .1E1 +.2E1 -.3E1 .1-E1 +.2-E1 -.3-E1 37 | .1E+1 +.2E+1 -.3E+1 .1-E+1 +.2-E+1 -.3-E+1 38 | .1E-1 +.2E-1 -.3E-1 .1-E-1 +.2-E-1 -.3-E-1 39 | 40 | .E1 +.E1 -.E1 .-E1 +.-E1 -.-E1 41 | .E+1 +.E+1 -.E+1 .-E+1 +.-E+1 -.-E+1 42 | .E-1 +.E-1 -.E-1 .-E-1 +.-E-1 -.-E-1 43 | 44 | ; strings 45 | "asdf" 46 | "" "" 47 | 48 | " 49 | asdf 50 | jkl 51 | qwerty 52 | " 53 | 54 | 55 | ; symbols 56 | asdf 57 | jkl 58 | qwerty 59 | 60 | ; quoted symbols 61 | 'asdf' 62 | '' '' 63 | 64 | ' 65 | asdf 66 | jkl 67 | qwerty 68 | ' 69 | 70 | ; variables 71 | $asdf 72 | $jkl 73 | $qwerty 74 | 75 | ; kDataUnhandled is its own token 76 | kDataUnhandled 77 | 78 | 79 | ; arrays 80 | (array 1 2) ; array 81 | {+ 1 2} ; command 82 | [property] ; property 83 | 84 | 85 | ; directives 86 | #include_opt ../file.dta 87 | #include ../file.dta 88 | #merge ../file.dta 89 | #ifdef kDefine 90 | #undef kDefine 91 | #endif 92 | #ifndef kDefine 93 | #define kDefine (1) 94 | #else 95 | #autorun {action} 96 | #endif 97 | ; invalid 98 | #bad 99 | ## 100 | 101 | ; *not* directives, these are lexed as symbols 102 | # 103 | # # ; space-separated 104 | # # ; tab-separated 105 | ; lexed as symbols and arrays 106 | #(#) ; '#', '(', '#', ')' 107 | #{#} ; '#', '{', '#', '}' 108 | #[#] ; '#', '[', '#', ']' 109 | 110 | 111 | ; line comment 112 | ;; 113 | ; ; 114 | ; ; 115 | ;;;;;;;; 116 | ;nospace 117 | asdf;jkl ; invalid, lexed as part of the symbol 118 | 119 | /* 120 | block comment 121 | */ 122 | 123 | /*asdf*/ ; invalid, lexed as a symbol 124 | /*jkl */ 125 | 126 | /**/ ; invalid, lexed as a symbol 127 | /* */ 128 | /* */ 129 | 130 | ; stray block-comment close, lexed as a symbol 131 | */ 132 | 133 | /*****/ ; invalid, lexed as a symbol 134 | 135 | /***** ; invalid, lexed as a symbol 136 | 137 | /* 138 | 139 | ***** 140 | 141 | ***/ 142 | 143 | ; comments between directives 144 | #include_opt ; asdf 145 | ../file.dta ; asdf 146 | #include ; asdf 147 | ../file.dta ; asdf 148 | #merge ; asdf 149 | ../file.dta ; asdf 150 | #ifdef ; asdf 151 | kDefine ; asdf 152 | #undef ; asdf 153 | kDefine ; asdf 154 | #endif ; asdf 155 | #ifndef ; asdf 156 | kDefine ; asdf 157 | #define ; asdf 158 | kDefine ; asdf 159 | (1) ; asdf 160 | #else ; asdf 161 | #autorun ; asdf 162 | {action} ; asdf 163 | #endif ; asdf 164 | 165 | ; block comments between directives 166 | /* asdf */ #include_opt /* asdf */ ../file.dta /* asdf */ 167 | /* asdf */ #include /* asdf */ ../file.dta /* asdf */ 168 | /* asdf */ #merge /* asdf */ ../file.dta /* asdf */ 169 | /* asdf */ #ifdef /* asdf */ kDefine /* asdf */ 170 | /* asdf */ #undef /* asdf */ kDefine /* asdf */ 171 | /* asdf */ #endif /* asdf */ 172 | /* asdf */ #ifndef /* asdf */ kDefine /* asdf */ 173 | /* asdf */ #define /* asdf */ kDefine /* asdf */ (1) /* asdf */ 174 | /* asdf */ #else /* asdf */ 175 | /* asdf */ #autorun /* asdf */ {action} /* asdf */ 176 | /* asdf */ #endif /* asdf */ 177 | -------------------------------------------------------------------------------- /crates/arson-parse/tests/test_files/thorough_errors.dta: -------------------------------------------------------------------------------- 1 | ; SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | (unmatched_array ;) 4 | {unmatched_command ;} 5 | [unmatched_property ;] 6 | 7 | (too_big 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 8 | 9 | (bad_directive #bad) 10 | 11 | (incorrect_directives 12 | #define 1 13 | #undef 1 14 | #include 1 15 | #include_opt 1 16 | #merge 1 17 | 18 | #define kDefine 1 19 | #autorun action 20 | 21 | #ifdef 1 "..." #else "..." #endif 22 | #ifndef 1 "..." #endif 23 | ) 24 | 25 | (unbalanced 26 | #ifdef kDefine 27 | "..." 28 | ) 29 | #else 30 | "..." 31 | ) 32 | #endif 33 | 34 | #ifndef kDefine 35 | "..." 36 | ) 37 | #endif 38 | ) 39 | 40 | unexpected 41 | #endif 42 | #else #endif 43 | 44 | unmatched 45 | #ifndef kDefine 46 | #ifdef kDefine #else 47 | 48 | /* Unclosed block comment -------------------------------------------------------------------------------- /crates/arson-parse/tests/thorough/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | mod lexer; 4 | mod parser; 5 | -------------------------------------------------------------------------------- /crates/arson-stdlib/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-stdlib" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson-core = { path = "../arson-core", features = ["file-loading"] } 13 | arson-fs = { path = "../arson-fs" } 14 | 15 | thiserror = "1.0.65" 16 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/fs.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io; 4 | 5 | use arson_core::prelude::*; 6 | use arson_fs::prelude::*; 7 | use arson_fs::Metadata; 8 | 9 | use crate::StdlibOptions; 10 | 11 | pub fn register_funcs(context: &mut Context) { 12 | context.register_func("basename", self::basename); 13 | context.register_func("dirname", self::dirname); 14 | 15 | context.register_func("read_file", self::read_file); 16 | context.register_func("write_file", self::write_file); 17 | context.register_func("run", self::run); 18 | 19 | context.register_func("file_exists", self::file_exists); 20 | context.register_func("file_read_only", self::file_read_only); 21 | context.register_func("file_list", self::file_list); 22 | context.register_func("file_list_paths", self::file_list_paths); 23 | } 24 | 25 | pub fn basename(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 26 | arson_assert_len!(args, 1); 27 | 28 | let path_str = args.string(context, 0)?; 29 | let path = VirtualPath::new(path_str.as_ref()); 30 | 31 | match path.file_stem() { 32 | Some(base_name) => Ok(base_name.into()), 33 | None => Ok(path_str.clone().into()), 34 | } 35 | } 36 | 37 | pub fn dirname(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 38 | arson_assert_len!(args, 1); 39 | 40 | let path_str = args.string(context, 0)?; 41 | let path = VirtualPath::new(path_str.as_ref()); 42 | 43 | match path.parent().and_then(|p| p.file_name()) { 44 | Some(dir_name) => Ok(dir_name.into()), 45 | None => Ok(path_str.clone().into()), 46 | } 47 | } 48 | 49 | pub fn read_file(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 50 | arson_assert_len!(args, 1); 51 | 52 | let options = context.get_state::().map(|s| s.file_load_options.clone())?; 53 | 54 | let path = args.string(context, 0)?; 55 | let array = context.load_path(options, path.as_ref())?; 56 | Ok(array.into()) 57 | } 58 | 59 | pub fn write_file(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 60 | arson_assert_len!(args, 2); 61 | 62 | let path = args.string(context, 0)?; 63 | let array = args.array(context, 1)?; 64 | 65 | let mut file = context.file_system()?.create(path.as_ref())?; 66 | for node in array.borrow()?.iter() { 67 | writeln!(file, "{node:#}")?; 68 | } 69 | 70 | Ok(Node::HANDLED) 71 | } 72 | 73 | pub fn run(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 74 | arson_assert_len!(args, 1); 75 | 76 | let options = context.get_state::().map(|s| s.file_load_options.clone())?; 77 | 78 | let path = args.string(context, 0)?; 79 | match context.load_path(options, path.as_ref()) { 80 | Ok(array) => context.execute_block(&array), 81 | Err(_err) => { 82 | // TODO: log error 83 | Ok(Node::HANDLED) 84 | }, 85 | } 86 | } 87 | 88 | pub fn file_exists(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 89 | arson_assert_len!(args, 1); 90 | 91 | let path = args.string(context, 0)?; 92 | let exists = context.file_system()?.is_file(path.as_ref()); 93 | Ok(exists.into()) 94 | } 95 | 96 | pub fn file_read_only(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 97 | arson_assert_len!(args, 1); 98 | 99 | let path = args.string(context, 0)?; 100 | match context.file_system()?.metadata(path.as_ref())? { 101 | Metadata::File { is_readonly, .. } => Ok(is_readonly.into()), 102 | Metadata::Directory { .. } => Err(io::Error::from(io::ErrorKind::NotADirectory).into()), 103 | } 104 | } 105 | 106 | pub fn file_list(_context: &mut Context, _args: &NodeSlice) -> ExecuteResult { 107 | todo!("file_list") 108 | } 109 | 110 | pub fn file_list_paths(_context: &mut Context, _args: &NodeSlice) -> ExecuteResult { 111 | todo!("file_list_paths") 112 | } 113 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use arson_core::Context; 4 | 5 | pub mod fs; 6 | pub mod math; 7 | pub mod process; 8 | pub mod stdio; 9 | 10 | mod options; 11 | pub use options::*; 12 | 13 | pub fn register_funcs(context: &mut Context) { 14 | assert!( 15 | context.get_state::().is_ok(), 16 | "StdlibOptions state must be registered before registering stdlib" 17 | ); 18 | 19 | fs::register_funcs(context); 20 | math::register_funcs(context); 21 | process::register_funcs(context); 22 | stdio::register_funcs(context); 23 | } 24 | 25 | pub mod prelude { 26 | pub use super::options::*; 27 | } 28 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/math.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use arson_core::prelude::*; 4 | use arson_core::Number; 5 | 6 | pub fn register_funcs(context: &mut Context) { 7 | context.add_macro("PI", arson_array![std::f64::consts::PI]); 8 | context.add_macro("TAU", arson_array![std::f64::consts::TAU]); 9 | context.add_macro("EULER", arson_array![std::f64::consts::E]); 10 | 11 | exponential::register_funcs(context); 12 | trigonometry::register_funcs(context); 13 | } 14 | 15 | pub mod exponential { 16 | use super::*; 17 | 18 | pub fn register_funcs(context: &mut Context) { 19 | context.register_func("pow", self::power); 20 | context.register_func("sqrt", self::square_root); 21 | context.register_func("cbrt", self::cube_root); 22 | context.register_func("hypot", self::hypotenuse); 23 | 24 | context.register_func("exp", self::power_of_e); 25 | context.register_func("exp2", self::power_of_2); 26 | context.register_func("expm1", self::power_of_e_minus_one); 27 | context.register_func("log", self::logarithm_natural); 28 | context.register_func("log10", self::logarithm_base_10); 29 | context.register_func("log2", self::logarithm_base_2); 30 | } 31 | 32 | pub fn power(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 33 | arson_assert_len!(args, 2); 34 | match args.number(context, 0)? { 35 | Number::Integer(left) => Ok(left.0.pow(args.integer(context, 1)?.0 as u32).into()), 36 | Number::Float(left) => match args.number(context, 1)? { 37 | Number::Integer(right) => Ok(left.powi(right.0 as i32).into()), 38 | Number::Float(right) => Ok(left.powf(right).into()), 39 | }, 40 | } 41 | } 42 | 43 | pub fn square_root(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 44 | arson_assert_len!(args, 1); 45 | Ok(args.float(context, 0)?.sqrt().into()) 46 | } 47 | 48 | pub fn cube_root(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 49 | arson_assert_len!(args, 1); 50 | Ok(args.float(context, 0)?.cbrt().into()) 51 | } 52 | 53 | pub fn hypotenuse(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 54 | arson_assert_len!(args, 2); 55 | let x = args.float(context, 0)?; 56 | let y = args.float(context, 1)?; 57 | Ok(x.hypot(y).into()) 58 | } 59 | 60 | pub fn power_of_e(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 61 | arson_assert_len!(args, 1); 62 | Ok(args.float(context, 0)?.exp().into()) 63 | } 64 | 65 | pub fn power_of_2(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 66 | arson_assert_len!(args, 1); 67 | Ok(args.float(context, 0)?.exp2().into()) 68 | } 69 | 70 | pub fn power_of_e_minus_one(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 71 | arson_assert_len!(args, 1); 72 | Ok(args.float(context, 0)?.exp_m1().into()) 73 | } 74 | 75 | pub fn logarithm_natural(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 76 | arson_assert_len!(args, 2); 77 | let x = args.float(context, 0)?; 78 | let y = args.float(context, 1)?; 79 | Ok(x.log(y).into()) 80 | } 81 | 82 | pub fn logarithm_base_10(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 83 | arson_assert_len!(args, 1); 84 | Ok(args.float(context, 0)?.log10().into()) 85 | } 86 | 87 | pub fn logarithm_base_2(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 88 | arson_assert_len!(args, 1); 89 | Ok(args.float(context, 0)?.log2().into()) 90 | } 91 | } 92 | 93 | pub mod trigonometry { 94 | use super::*; 95 | 96 | pub fn register_funcs(context: &mut Context) { 97 | context.register_func("sin", self::sine); 98 | context.register_func("cos", self::cosine); 99 | context.register_func("tan", self::tangent); 100 | context.register_func("asin", self::arc_sine); 101 | context.register_func("acos", self::arc_cosine); 102 | context.register_func("atan", self::arc_tangent); 103 | context.register_func("atan2", self::arc_tangent_quadrant); 104 | 105 | context.register_func("sinh", self::sine_hyperbolic); 106 | context.register_func("cosh", self::cosine_hyperbolic); 107 | context.register_func("tanh", self::tangent_hyperbolic); 108 | context.register_func("asinh", self::arc_sine_hyperbolic); 109 | context.register_func("acosh", self::arc_cosine_hyperbolic); 110 | context.register_func("atanh", self::arc_tangent_hyperbolic); 111 | } 112 | 113 | pub fn sine(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 114 | arson_assert_len!(args, 1); 115 | Ok(args.float(context, 0)?.sin().into()) 116 | } 117 | 118 | pub fn cosine(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 119 | arson_assert_len!(args, 1); 120 | Ok(args.float(context, 0)?.cos().into()) 121 | } 122 | 123 | pub fn tangent(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 124 | arson_assert_len!(args, 1); 125 | Ok(args.float(context, 0)?.tan().into()) 126 | } 127 | 128 | pub fn arc_sine(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 129 | arson_assert_len!(args, 1); 130 | Ok(args.float(context, 0)?.asin().into()) 131 | } 132 | 133 | pub fn arc_cosine(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 134 | arson_assert_len!(args, 1); 135 | Ok(args.float(context, 0)?.acos().into()) 136 | } 137 | 138 | pub fn arc_tangent(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 139 | arson_assert_len!(args, 1); 140 | Ok(args.float(context, 0)?.atan().into()) 141 | } 142 | 143 | pub fn arc_tangent_quadrant(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 144 | arson_assert_len!(args, 2); 145 | let x = args.float(context, 0)?; 146 | let y = args.float(context, 1)?; 147 | Ok(x.atan2(y).into()) 148 | } 149 | 150 | pub fn sine_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 151 | arson_assert_len!(args, 1); 152 | Ok(args.float(context, 0)?.sinh().into()) 153 | } 154 | 155 | pub fn cosine_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 156 | arson_assert_len!(args, 1); 157 | Ok(args.float(context, 0)?.cosh().into()) 158 | } 159 | 160 | pub fn tangent_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 161 | arson_assert_len!(args, 1); 162 | Ok(args.float(context, 0)?.tanh().into()) 163 | } 164 | 165 | pub fn arc_sine_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 166 | arson_assert_len!(args, 1); 167 | Ok(args.float(context, 0)?.asinh().into()) 168 | } 169 | 170 | pub fn arc_cosine_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 171 | arson_assert_len!(args, 1); 172 | Ok(args.float(context, 0)?.acosh().into()) 173 | } 174 | 175 | pub fn arc_tangent_hyperbolic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 176 | arson_assert_len!(args, 1); 177 | Ok(args.float(context, 0)?.atanh().into()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/options.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use arson_core::prelude::*; 4 | 5 | pub struct StdlibOptions { 6 | pub file_load_options: LoadOptions, 7 | } 8 | 9 | impl ContextState for StdlibOptions {} 10 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/process.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use arson_core::prelude::*; 4 | use arson_core::ConcatSlice; 5 | 6 | #[derive(Debug)] 7 | pub struct ExitError(pub Option); 8 | 9 | impl ExitError { 10 | pub fn is_exit(error: &arson_core::Error) -> Option<&Option> { 11 | if let arson_core::ErrorKind::Custom(error) = error.kind() { 12 | if let Some(error) = error.downcast_ref::() { 13 | return Some(&error.0); 14 | } 15 | } 16 | 17 | None 18 | } 19 | } 20 | 21 | impl std::fmt::Display for ExitError { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | write!(f, "exit error") 24 | } 25 | } 26 | 27 | impl std::error::Error for ExitError {} 28 | 29 | pub fn register_funcs(context: &mut Context) { 30 | context.register_func("exit", self::exit); 31 | context.register_func("abort", self::abort); 32 | context.register_func("panic", self::panic); 33 | } 34 | 35 | pub fn exit(_context: &mut Context, args: &NodeSlice) -> ExecuteResult { 36 | arson_assert_len!(args, 0); 37 | Err(arson_core::Error::from_custom(ExitError(None))) 38 | } 39 | 40 | pub fn abort(_context: &mut Context, args: &NodeSlice) -> ExecuteResult { 41 | arson_assert_len!(args, 0); 42 | Err(arson_core::Error::from_custom(ExitError(Some( 43 | "an abnormal exit has occurred".to_owned(), 44 | )))) 45 | } 46 | 47 | pub fn panic(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 48 | let message = ConcatSlice::new(context, args).to_string(); 49 | Err(arson_core::Error::from_custom(ExitError(Some(message)))) 50 | } 51 | -------------------------------------------------------------------------------- /crates/arson-stdlib/src/stdio.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use arson_core::prelude::*; 4 | use arson_core::ConcatSlice; 5 | 6 | pub fn register_funcs(context: &mut Context) { 7 | context.register_func("print", self::print); 8 | context.register_func("sprint", self::sprint); 9 | } 10 | 11 | pub fn print(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 12 | if !args.is_empty() { 13 | println!("{}", ConcatSlice::new(context, args)); 14 | } 15 | 16 | Ok(Node::HANDLED) 17 | } 18 | 19 | pub fn sprint(context: &mut Context, args: &NodeSlice) -> ExecuteResult { 20 | if !args.is_empty() { 21 | Ok(ConcatSlice::new(context, args).to_string().into()) 22 | } else { 23 | Ok(String::new().into()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /crates/arson/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [features] 12 | default = ["arson-fs", "arson-stdlib", "file-loading"] 13 | 14 | # Optional components 15 | arson-fs = ["dep:arson-fs", "arson-core/file-system"] 16 | arson-parse = ["dep:arson-parse"] 17 | arson-stdlib = ["dep:arson-stdlib"] 18 | 19 | # arson-core features 20 | file-loading = ["text-loading", "arson-fs", "arson-core/file-loading"] 21 | text-loading = ["arson-core/text-loading"] 22 | 23 | # arson-parse features 24 | parse-reporting = ["arson-parse", "arson-parse/reporting"] 25 | 26 | [dependencies] 27 | arson-core = { path = "../arson-core", default-features = false } 28 | arson-fs = { path = "../arson-fs", optional = true } 29 | arson-parse = { path = "../arson-parse", optional = true } 30 | arson-stdlib = { path = "../arson-stdlib", optional = true } 31 | -------------------------------------------------------------------------------- /crates/arson/src/lib.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | pub use arson_core as core; 4 | pub use arson_core::*; 5 | #[cfg(feature = "arson-fs")] 6 | pub use arson_fs as fs; 7 | #[cfg(feature = "arson-parse")] 8 | pub use arson_parse as parse; 9 | #[cfg(feature = "arson-stdlib")] 10 | pub use arson_stdlib as stdlib; 11 | 12 | pub mod prelude { 13 | pub use arson_core::prelude::*; 14 | #[cfg(feature = "arson-fs")] 15 | pub use arson_fs::prelude::*; 16 | #[cfg(feature = "arson-stdlib")] 17 | pub use arson_stdlib::prelude::*; 18 | } 19 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | unused-allowed-license = "allow" 3 | allow = [ 4 | "Apache-2.0", 5 | "BSD-3-Clause", 6 | "BSL-1.0", 7 | "LGPL-3.0", 8 | "MIT", 9 | "Unicode-DFS-2016", 10 | ] 11 | 12 | [licenses.private] 13 | ignore = true 14 | -------------------------------------------------------------------------------- /examples/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 MiloHax. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /examples/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MiloHax 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/diagnostics/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "parsing-example" 3 | version = "0.3.0" 4 | license = "MIT OR Apache-2.0" 5 | 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false # Standalone example; should not be published 10 | 11 | [dependencies] 12 | arson-parse = { path = "../../crates/arson-parse", features = ["reporting"] } 13 | -------------------------------------------------------------------------------- /examples/diagnostics/examples/diagnostics.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use std::fs::File; 4 | use std::path::Path; 5 | 6 | use arson_parse::{reporting as codespan_reporting, ParseOptions}; 7 | use codespan_reporting::files::SimpleFile; 8 | use codespan_reporting::term; 9 | use codespan_reporting::term::termcolor::{ColorChoice, StandardStream}; 10 | use codespan_reporting::term::Chars; 11 | 12 | fn main() { 13 | let mount_dir = Path::new(file!()).join("../../run"); 14 | 15 | let files = ["success.dta", "fail.dta"]; 16 | for file in files { 17 | println!("Parsing file {file}"); 18 | 19 | print_parsed(&mount_dir, file); 20 | 21 | println!(); 22 | } 23 | } 24 | 25 | fn print_parsed(mount_dir: &Path, file: &str) { 26 | let text = { 27 | let file = File::open(mount_dir.join(file)).expect("failed to open file"); 28 | std::io::read_to_string(file).expect("failed to read file") 29 | }; 30 | 31 | let options = ParseOptions { include_comments: true }; 32 | match arson_parse::parse_text(&text, options) { 33 | Ok(parsed) => { 34 | println!("File {file}:"); 35 | for expr in parsed { 36 | println!("- ({:?}) {:?}", expr.location, expr.value); 37 | } 38 | }, 39 | Err(error) => { 40 | let db = SimpleFile::new(file, &text); 41 | let writer = StandardStream::stderr(ColorChoice::Auto); 42 | let config = codespan_reporting::term::Config { chars: Chars::ascii(), ..Default::default() }; 43 | 44 | for diagnostic in error.diagnostics { 45 | term::emit(&mut writer.lock(), &config, &db, &diagnostic.to_codespan(())) 46 | .expect("failed to emit diagnostic"); 47 | } 48 | }, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/diagnostics/run/fail.dta: -------------------------------------------------------------------------------- 1 | ; SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | (message "Oh no" ; sneaky comment ) 4 | (too_big 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) 5 | 6 | #define 1 7 | 8 | (action {print "This isn't how you autorun!"}) 9 | #autorun action 10 | 11 | #bad_directive 12 | 13 | (unbalanced 14 | #ifdef kDefine 15 | "This isn't supported, much more difficult to reason about. 16 | The original parser doesn't even allow it deliberately, 17 | and this example will handily cause problems there." 18 | ) 19 | #else 20 | "It's also just a pretty weird way of doing things." 21 | ) 22 | #endif 23 | 24 | (incorrect_ifdef 25 | #ifdef 1 26 | "These conditionals will still be parsed correctly." 27 | #else 28 | "The 1 in front of ifndef will be treated as part of the conditional's contents, 29 | instead of taking up the slot for the symbol name." 30 | #endif 31 | ) 32 | 33 | (unmatched 34 | #ifdef kDefine 35 | ) 36 | 37 | /* Someone forgot to write the rest of this file... -------------------------------------------------------------------------------- /examples/diagnostics/run/success.dta: -------------------------------------------------------------------------------- 1 | ; SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | (message "Hello!") 4 | (value 25) 5 | (action {print "Ran the action"}) 6 | 7 | #ifdef kDefine 8 | (conditional "kDefine was set!") 9 | (optional "I am optional") 10 | #else 11 | (conditional "kDefine was not set!") 12 | #endif 13 | -------------------------------------------------------------------------------- /examples/hello-world/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-world" 3 | version = "0.3.0" 4 | license = "MIT OR Apache-2.0" 5 | 6 | edition.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false # Standalone example; should not be published 10 | 11 | [dependencies] 12 | arson = { path = "../../crates/arson" } 13 | -------------------------------------------------------------------------------- /examples/hello-world/examples/hello-world.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT OR Apache-2.0 2 | 3 | use std::path::Path; 4 | 5 | use arson::fs::drivers::BasicFileSystemDriver; 6 | use arson::prelude::*; 7 | 8 | fn main() -> arson::Result { 9 | println!("> Hello from native!"); 10 | 11 | // Mount `run` directory for scripts 12 | let mount_dir = Path::new(file!()).join("../../run"); 13 | let driver = BasicFileSystemDriver::new(mount_dir)?; 14 | 15 | // Make context 16 | let mut context = Context::new().with_filesystem_driver(driver); 17 | context.register_state(StdlibOptions { 18 | file_load_options: LoadOptions { allow_include: true, allow_autorun: true }, 19 | }); 20 | arson::stdlib::register_funcs(&mut context); 21 | println!("Created context."); 22 | 23 | // Load main.dta file 24 | let options = LoadOptions { allow_include: true, allow_autorun: true }; 25 | let file = context.load_path(options, "/main.dta")?; 26 | println!("Loaded main.dta"); 27 | 28 | // Execute (main {...}) script 29 | let script = file.find_tag((&mut context, "main"))?; 30 | context.execute_block(script.borrow()?.slice(1..)?)?; 31 | println!("Ran main.dta!"); 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /examples/hello-world/run/main.dta: -------------------------------------------------------------------------------- 1 | ; SPDX-License-Identifier: MIT OR Apache-2.0 2 | (main 3 | {print "Hello from script!"} 4 | {print "1 + 2 is " {+ 1 2}} 5 | {print "sqrt(50) is " {sqrt 50}} 6 | {print "nonexistent(50) is " {nonexistent 50}} 7 | ) -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 110 2 | array_width = 75 3 | attr_fn_like_width = 70 4 | chain_width = 75 5 | comment_width = 80 6 | doc_comment_code_block_width = 80 7 | fn_call_width = 75 8 | single_line_let_else_max_width = 0 9 | struct_lit_width = 50 10 | struct_variant_width = 50 11 | use_small_heuristics = "Default" 12 | 13 | format_code_in_doc_comments = true 14 | 15 | hex_literal_case = "Upper" 16 | 17 | group_imports = "StdExternalCrate" 18 | imports_granularity = "Module" 19 | imports_layout = "HorizontalVertical" 20 | 21 | combine_control_expr = false 22 | match_block_trailing_comma = true 23 | overflow_delimited_expr = true 24 | use_field_init_shorthand = true 25 | -------------------------------------------------------------------------------- /tools/arson-fmt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-fmt" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson-fmtlib = { path = "../../crates/arson-fmtlib" } 13 | arson-parse = { path = "../../crates/arson-parse", features = ["encoding","reporting"] } 14 | 15 | anyhow = "1.0.95" 16 | clap = { version = "4.5.23", features = ["derive"] } 17 | thiserror = "1.0.65" 18 | -------------------------------------------------------------------------------- /tools/arson-fmt/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::path::{Path, PathBuf}; 4 | 5 | use anyhow::{bail, Context}; 6 | use arson_fmtlib::{expr, token, Formatter, Options}; 7 | use arson_parse::reporting::files::SimpleFile; 8 | use arson_parse::reporting::term::termcolor::{ColorChoice, StandardStream}; 9 | use arson_parse::reporting::term::{self, Chars}; 10 | use arson_parse::ParseError; 11 | use clap::Parser; 12 | 13 | /// A formatter for DTA files. 14 | #[derive(clap::Parser, Debug)] 15 | struct Arguments { 16 | /// The formatter mode to use. 17 | #[arg(short, long)] 18 | mode: Option, 19 | 20 | /// The maximum width of arrays in the output. 21 | #[arg(short = 'a', long)] 22 | max_array_width: Option, 23 | 24 | /// Suppress parsing errors that occur as part of formatting the output file. 25 | #[arg(short, long)] 26 | suppress_errors: bool, 27 | 28 | /// The input file to be formatted. 29 | input_path: PathBuf, 30 | 31 | /// The file to output to. 32 | /// 33 | /// Defaults to modifying the input file. 34 | output_path: Option, 35 | } 36 | 37 | /// The formatter mode to use. 38 | #[derive(clap::ValueEnum, Debug, Clone, Copy)] 39 | enum FormatMode { 40 | /// Richer expression-based formatting. 41 | /// Requires text to be fully parsable. 42 | Expression, 43 | /// Less capable token-based formatting. 44 | /// Formats text regardless of parsability. 45 | Token, 46 | } 47 | 48 | fn main() -> anyhow::Result<()> { 49 | let args = Arguments::parse(); 50 | let file_bytes = std::fs::read(&args.input_path).context("failed to open input file")?; 51 | 52 | let (file_text, encoding) = 53 | arson_parse::encoding::decode_default(&file_bytes).context("failed to decode input file")?; 54 | let file_text = file_text.into_owned(); 55 | drop(file_bytes); // conserve memory 56 | 57 | let mut options = Options::default(); 58 | args.max_array_width.inspect(|value| options.max_array_width = *value); 59 | // args.max_line_width.inspect(|value| options.max_line_width = *value); 60 | 61 | let formatter = match args.mode { 62 | Some(FormatMode::Expression) => match expr::Formatter::new(&file_text, options) { 63 | Ok(formatter) => Formatter::Expression(formatter), 64 | Err(error) => { 65 | if !args.suppress_errors { 66 | write_parse_errors(error, &args.input_path, &file_text); 67 | } 68 | bail!("failed to parse file") 69 | }, 70 | }, 71 | Some(FormatMode::Token) => Formatter::Token(token::Formatter::new(&file_text, options)), 72 | None => match Formatter::new(&file_text, options) { 73 | (formatter, None) => formatter, 74 | (formatter, Some(error)) => { 75 | if !args.suppress_errors { 76 | write_parse_errors(error, &args.input_path, &file_text); 77 | } 78 | formatter 79 | }, 80 | }, 81 | }; 82 | 83 | let output_text = formatter.to_string(); 84 | let output_path = args.output_path.unwrap_or_else(|| args.input_path.clone()); 85 | let output_bytes = arson_parse::encoding::encode(&output_text, encoding)?; 86 | std::fs::write(&output_path, &output_bytes).context("failed to write output file")?; 87 | 88 | Ok(()) 89 | } 90 | 91 | fn write_parse_errors(error: ParseError, input_path: &Path, input_text: &str) { 92 | let writer = StandardStream::stderr(ColorChoice::Auto); 93 | let config = term::Config { chars: Chars::ascii(), ..Default::default() }; 94 | 95 | let file = SimpleFile::new(input_path.to_string_lossy(), input_text); 96 | for error in error.diagnostics { 97 | _ = term::emit(&mut writer.lock(), &config, &file, &error.to_codespan(())); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tools/arson-repl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-repl" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson = { path = "../../crates/arson", features = ["parse-reporting"] } 13 | 14 | anyhow = "1.0.95" 15 | clap = { version = "4.5.23", features = ["derive"] } 16 | rustyline = "15.0.0" 17 | -------------------------------------------------------------------------------- /tools/arson-repl/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::io::Write; 4 | 5 | use anyhow::{bail, Context}; 6 | use arson::fs::drivers::BasicFileSystemDriver; 7 | use arson::parse::reporting::files::SimpleFile; 8 | use arson::parse::reporting::term::termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; 9 | use arson::parse::reporting::term::{self, Chars}; 10 | use arson::parse::DiagnosticKind; 11 | use arson::prelude::*; 12 | use arson::stdlib::process::ExitError; 13 | use clap::Parser; 14 | use rustyline::error::ReadlineError; 15 | use rustyline::DefaultEditor; 16 | 17 | mod terminal; 18 | 19 | /// The REPL for Arson's DTA implementation. 20 | #[derive(Debug, clap::Parser)] 21 | struct Arguments { 22 | /// skip configuration prompts and use default settings 23 | #[arg(long, short)] 24 | skip_prompt: bool, 25 | 26 | /// the directory to mount for scripting access 27 | #[arg(long, short)] 28 | mount_dir: Option, 29 | 30 | /// the text editing mode to use 31 | #[arg(long, short)] 32 | editor_mode: Option, 33 | } 34 | 35 | #[derive(Debug, Clone, Copy, clap::ValueEnum)] 36 | enum EditorModeArgument { 37 | Emacs, 38 | Vi, 39 | } 40 | 41 | fn main() -> anyhow::Result<()> { 42 | let args = Arguments::parse(); 43 | 44 | let mut context = make_context(&args)?; 45 | let mut editor = make_editor(&args)?; 46 | 47 | run(&mut context, &mut editor) 48 | } 49 | 50 | fn make_context(args: &Arguments) -> anyhow::Result { 51 | let mut context = arson::Context::new(); 52 | context.register_state(StdlibOptions { 53 | file_load_options: LoadOptions { allow_include: true, allow_autorun: true }, 54 | }); 55 | arson::stdlib::register_funcs(&mut context); 56 | 57 | if let Some(ref directory) = args.mount_dir { 58 | let driver = BasicFileSystemDriver::new(directory).context("failed to create file driver")?; 59 | context = context.with_filesystem_driver(driver); 60 | } else if !args.skip_prompt { 61 | loop { 62 | let directory = terminal::prompt_str("Script mount directory? (leave empty to skip)"); 63 | if directory.is_empty() { 64 | break; 65 | } 66 | 67 | let driver = match BasicFileSystemDriver::new(&directory) { 68 | Ok(driver) => driver, 69 | Err(error) => { 70 | eprintln!("Could not create file driver: {error}"); 71 | continue; 72 | }, 73 | }; 74 | context = context.with_filesystem_driver(driver); 75 | 76 | println!("Note that all paths used in scripts will be relative to this directory."); 77 | break; 78 | } 79 | } 80 | 81 | Ok(context) 82 | } 83 | 84 | fn make_editor(args: &Arguments) -> anyhow::Result { 85 | use rustyline::config::Configurer; 86 | use rustyline::{Config, EditMode}; 87 | 88 | #[rustfmt::skip] // do not pack into the same line 89 | let config = Config::builder() 90 | .auto_add_history(true) 91 | .indent_size(3) 92 | .tab_stop(3) 93 | .build(); 94 | 95 | let mut editor = DefaultEditor::with_config(config).context("failed to create text reader")?; 96 | 97 | if let Some(ref mode) = args.editor_mode { 98 | let mode = match mode { 99 | EditorModeArgument::Emacs => EditMode::Emacs, 100 | EditorModeArgument::Vi => EditMode::Vi, 101 | }; 102 | editor.set_edit_mode(mode); 103 | } else if !args.skip_prompt { 104 | let mode = 105 | terminal::prompt_option("Text editor mode?", [("emacs", EditMode::Emacs), ("vi", EditMode::Vi)]); 106 | if let Some(mode) = mode { 107 | editor.set_edit_mode(mode); 108 | } 109 | } 110 | 111 | Ok(editor) 112 | } 113 | 114 | fn run(context: &mut arson::Context, editor: &mut DefaultEditor) -> anyhow::Result<()> { 115 | loop { 116 | let array = match read_input(context, editor) { 117 | Some(array) => array, 118 | None => return Ok(()), 119 | }; 120 | 121 | for node in array { 122 | match node.evaluate(context) { 123 | Ok(evaluated) => println!("{evaluated}"), 124 | Err(error) => { 125 | if let Some(message) = ExitError::is_exit(&error) { 126 | match message { 127 | Some(message) => bail!("exit error: {message}"), 128 | None => return Ok(()), 129 | } 130 | } 131 | 132 | eprintln!("Evaluation error: {error}\n{}", error.backtrace()) 133 | }, 134 | }; 135 | 136 | // Hint for `exit` command 137 | if let NodeValue::Symbol(symbol) = node.unevaluated() { 138 | let name = symbol.name().as_ref(); 139 | if name == "exit" || name == "quit" || name == "close" { 140 | let stdout = StandardStream::stdout(ColorChoice::Auto); 141 | let mut lock = stdout.lock(); 142 | 143 | _ = lock.set_color(ColorSpec::new().set_fg(Some(Color::Cyan))); 144 | 145 | writeln!(lock, "hint: use {{exit}} to exit")?; 146 | 147 | _ = lock.reset(); 148 | lock.flush()?; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | fn read_input(context: &mut arson::Context, editor: &mut DefaultEditor) -> Option { 156 | let mut prompt = ">>> "; 157 | let mut text = String::new(); 158 | loop { 159 | let line = match editor.readline(prompt) { 160 | Ok(line) => line, 161 | Err(ReadlineError::Interrupted) => return Some(NodeArray::new()), 162 | Err(ReadlineError::Eof) => return None, 163 | Err(ReadlineError::WindowResized) => todo!("window resized"), 164 | Err(error) => { 165 | eprintln!("Error while reading line: {error}"); 166 | return Some(NodeArray::new()); 167 | }, 168 | }; 169 | 170 | text.push_str(&line); 171 | 172 | let options = LoadOptions { allow_include: true, allow_autorun: true }; 173 | match context.load_text(options, &text) { 174 | Ok(array) => return Some(array), 175 | Err(error) => { 176 | if let LoadError::Parse(ref error) = error { 177 | // Allow multi-line input while unmatched braces are present 178 | if error.unclosed_array_count > 0 179 | || error.diagnostics.iter().any(|e| { 180 | matches!( 181 | e.kind(), 182 | DiagnosticKind::UnmatchedConditional | DiagnosticKind::UnclosedBlockComment 183 | ) 184 | }) 185 | { 186 | prompt = "... "; 187 | continue; 188 | } 189 | } 190 | 191 | emit_load_error(&text, error); 192 | return Some(NodeArray::new()); 193 | }, 194 | } 195 | } 196 | } 197 | 198 | fn emit_load_error(text: &str, error: LoadError) { 199 | match error { 200 | LoadError::Parse(error) => { 201 | let writer = StandardStream::stderr(ColorChoice::Auto); 202 | let config = term::Config { chars: Chars::ascii(), ..Default::default() }; 203 | 204 | let file = SimpleFile::new("", text); 205 | for error in error.diagnostics { 206 | let _ = term::emit(&mut writer.lock(), &config, &file, &error.to_codespan(())); 207 | } 208 | }, 209 | LoadError::Inner(errors) => { 210 | // TODO: use codespan_diagnostics for this 211 | eprintln!("Errors while loading input:"); 212 | for error in errors { 213 | eprintln!("- {error}"); 214 | } 215 | }, 216 | _ => eprintln!("Error while loading input: {error}"), 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tools/arson-repl/src/terminal.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | #![allow(dead_code)] 4 | 5 | use std::io; 6 | 7 | #[macro_export] 8 | macro_rules! print_flush { 9 | ($($arg:tt)*) => {{ 10 | use ::std::io::Write; 11 | let mut stdout = ::std::io::stdout().lock(); 12 | write!(stdout, $($arg)*).expect("failed to write to stdout"); 13 | stdout.flush().expect("failed to flush stdout"); 14 | }} 15 | } 16 | 17 | pub fn read_line() -> String { 18 | let mut line = String::new(); 19 | io::stdin().read_line(&mut line).expect("failed to read line"); 20 | line.trim().to_owned() 21 | } 22 | 23 | pub fn prompt_question(question: &str) -> bool { 24 | print_flush!("{question} (y/n) "); 25 | 26 | loop { 27 | let answer = read_line(); 28 | 29 | if answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes") { 30 | return true; 31 | } else if answer.eq_ignore_ascii_case("n") || answer.eq_ignore_ascii_case("no") { 32 | return false; 33 | } 34 | 35 | print_flush!("Unrecognized answer, please try again: "); 36 | } 37 | } 38 | 39 | pub fn prompt_option(question: &str, options: [(&str, R); N]) -> Option { 40 | println!("{question}"); 41 | for (name, _) in &options { 42 | println!("- {name}"); 43 | } 44 | print_flush!("Selection (leave empty to skip): "); 45 | 46 | loop { 47 | let answer = read_line(); 48 | if answer.is_empty() { 49 | return None; 50 | } 51 | 52 | for (name, value) in &options { 53 | if answer == *name { 54 | return Some(value.clone()); 55 | } 56 | } 57 | 58 | print_flush!("Unrecognized answer, please try again: "); 59 | } 60 | } 61 | 62 | pub fn prompt_str(message: &str) -> String { 63 | print_flush!("{message}: "); 64 | read_line() 65 | } 66 | -------------------------------------------------------------------------------- /tools/arson-run/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arson-run" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson = { path = "../../crates/arson", features = ["parse-reporting"] } 13 | 14 | anyhow = "1.0.95" 15 | clap = { version = "4.5.23", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /tools/arson-run/src/main.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | use std::path::{Path, PathBuf}; 4 | 5 | use anyhow::{bail, Context}; 6 | use arson::fs::drivers::BasicFileSystemDriver; 7 | use arson::stdlib::StdlibOptions; 8 | use arson::LoadOptions; 9 | use clap::Parser; 10 | 11 | /// A basic runtime for tool scripts written in DTA.. 12 | #[derive(Debug, clap::Parser)] 13 | struct Arguments { 14 | /// The script to run. 15 | path: PathBuf, 16 | 17 | /// The directory to mount as the base scripting directory. 18 | #[arg(long, short)] 19 | mount_dir: Option, 20 | } 21 | 22 | fn main() -> anyhow::Result<()> { 23 | let args = Arguments::parse(); 24 | 25 | fn canon_fail(path: &Path) -> String { 26 | format!("failed to resolve path {}", path.display()) 27 | } 28 | 29 | let path = args.path.canonicalize().with_context(|| canon_fail(&args.path))?; 30 | let mount_dir = match args.mount_dir { 31 | Some(mount_dir) => mount_dir.canonicalize().with_context(|| canon_fail(&mount_dir))?, 32 | None => path.parent().unwrap().to_path_buf(), 33 | }; 34 | 35 | let Ok(file_path) = path.strip_prefix(&mount_dir) else { 36 | // TODO: find a way to remove this limitation? 37 | bail!("script is not contained within mount directory"); 38 | }; 39 | 40 | let driver = BasicFileSystemDriver::new(mount_dir)?; 41 | 42 | // Make context 43 | let mut context = arson::Context::new().with_filesystem_driver(driver); 44 | context.register_state(StdlibOptions { 45 | file_load_options: LoadOptions { allow_include: true, allow_autorun: true }, 46 | }); 47 | arson::stdlib::register_funcs(&mut context); 48 | 49 | // Load script file 50 | let options = LoadOptions { allow_include: true, allow_autorun: true }; 51 | let file = context.load_path(options, file_path.to_string_lossy().as_ref())?; 52 | 53 | // Execute (main {...}) script 54 | let script = file.find_tag((&mut context, "main"))?; 55 | context.execute_block(script.borrow()?.slice(1..)?)?; 56 | 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /tools/arsonc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "arsonc" 3 | 4 | version.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | rust-version.workspace = true 8 | 9 | publish = false 10 | 11 | [dependencies] 12 | arson-dtb = { path = "../../crates/arson-dtb" } 13 | arson-fmtlib = { path = "../../crates/arson-fmtlib" } 14 | arson-parse = { path = "../../crates/arson-parse", features = ["reporting", "encoding"] } 15 | 16 | anyhow = "1.0.95" 17 | clap = { version = "4.5.23", features = ["derive"] } 18 | --------------------------------------------------------------------------------