From f9f9a6cf55733f8803b60e2ac8721bf4e950dffe Mon Sep 17 00:00:00 2001 From: Neal DeBuhr Date: Mon, 21 Feb 2022 18:53:59 +0000 Subject: [PATCH] Integrate SimX (https://github.com/ndebuhr/simx) into the core sim repository, as a workspace member --- .github/workflows/ci.yml | 2 +- .gitignore | 1 + Cargo.toml | 1 + sim/Cargo.toml | 1 + sim/src/models/batcher.rs | 5 + sim/src/models/exclusive_gateway.rs | 5 + sim/src/models/gate.rs | 5 + sim/src/models/generator.rs | 5 + sim/src/models/load_balancer.rs | 12 +- sim/src/models/model.rs | 10 + sim/src/models/model_trait.rs | 4 + sim/src/models/parallel_gateway.rs | 12 +- sim/src/models/processor.rs | 5 + sim/src/models/stochastic_gate.rs | 5 + sim/src/models/stopwatch.rs | 5 + sim/src/models/storage.rs | 5 + sim/tests/batcher_event_rules.json | 1 + sim/tests/custom.rs | 5 + sim/tests/event_rules.rs | 34 ++++ sim/tests/generator_event_rules.json | 1 + simx/Cargo.toml | 46 +++++ simx/LICENSE-APACHE | 1 + simx/LICENSE-MIT | 1 + simx/README.md | 20 ++ simx/src/lib.rs | 288 +++++++++++++++++++++++++++ 25 files changed, 471 insertions(+), 9 deletions(-) create mode 100644 sim/tests/batcher_event_rules.json create mode 100644 sim/tests/event_rules.rs create mode 100644 sim/tests/generator_event_rules.json create mode 100644 simx/Cargo.toml create mode 120000 simx/LICENSE-APACHE create mode 120000 simx/LICENSE-MIT create mode 100644 simx/README.md create mode 100644 simx/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df22dae..cbe203d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: key: ${{ runner.os }}-cargo-${{ matrix.rust }}-${{ hashFiles('./sim/Cargo.lock') }} - name: Run Tests working-directory: ./sim - run: cargo test + run: cargo test --all-features -- --nocapture wasm-pack: name: Test (wasm) diff --git a/.gitignore b/.gitignore index 88222d4..3236b39 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /target /sim/target /sim_derive/target +/simx/target **/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml index e8ea451..0bff0b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,4 +2,5 @@ members = [ "sim", "sim_derive", + "simx", ] \ No newline at end of file diff --git a/sim/Cargo.toml b/sim/Cargo.toml index f07c690..ea90a1a 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -29,6 +29,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" sim_derive = { version = "0.10", path = "../sim_derive" } +simx = { version = "0.10", path = "../simx", optional = true } thiserror = "1.0" wasm-bindgen = "0.2" web-sys = { version = "0.3", features = [ "console" ] } diff --git a/sim/src/models/batcher.rs b/sim/src/models/batcher.rs index 5e5e8c6..e9b8c7b 100644 --- a/sim/src/models/batcher.rs +++ b/sim/src/models/batcher.rs @@ -9,6 +9,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The batching process begins when the batcher receives a job. It will /// then accept additional jobs, adding them to a batch with the first job, /// until a max batching time or max batch size is reached - whichever comes @@ -70,6 +73,7 @@ enum Phase { Release, // Releasing a batch } +#[cfg_attr(feature = "simx", event_rules)] impl Batcher { pub fn new( job_in_port: String, @@ -209,6 +213,7 @@ impl Batcher { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Batcher { fn events_ext( &mut self, diff --git a/sim/src/models/exclusive_gateway.rs b/sim/src/models/exclusive_gateway.rs index 8f6a7b6..30a84ff 100644 --- a/sim/src/models/exclusive_gateway.rs +++ b/sim/src/models/exclusive_gateway.rs @@ -10,6 +10,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The exclusive gateway splits a process flow into a set of possible paths. /// The process will only follow one of the possible paths. Path selection is /// determined by Weighted Index distribution random variates, so this atomic @@ -65,6 +68,7 @@ enum Phase { Pass, // Passing a job from input to output } +#[cfg_attr(feature = "simx", event_rules)] impl ExclusiveGateway { pub fn new( flow_paths_in: Vec, @@ -145,6 +149,7 @@ impl ExclusiveGateway { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for ExclusiveGateway { fn events_ext( &mut self, diff --git a/sim/src/models/gate.rs b/sim/src/models/gate.rs index cc979bb..9876cf5 100644 --- a/sim/src/models/gate.rs +++ b/sim/src/models/gate.rs @@ -9,6 +9,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The gate model passes or blocks jobs, when it is in the open or closed /// state, respectively. The gate can be opened and closed throughout the /// course of a simulation. This model contains no stochastic behavior - job @@ -73,6 +76,7 @@ enum Phase { Pass, } +#[cfg_attr(feature = "simx", event_rules)] impl Gate { pub fn new( job_in_port: String, @@ -193,6 +197,7 @@ impl Gate { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Gate { fn events_ext( &mut self, diff --git a/sim/src/models/generator.rs b/sim/src/models/generator.rs index 47f61c2..7d5fd3e 100644 --- a/sim/src/models/generator.rs +++ b/sim/src/models/generator.rs @@ -9,6 +9,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The generator produces jobs based on a configured interarrival /// distribution. A normalized thinning function is used to enable /// non-stationary job generation. For non-stochastic generation of jobs, a @@ -69,6 +72,7 @@ enum Phase { Generating, } +#[cfg_attr(feature = "simx", event_rules)] impl Generator { pub fn new( message_interdeparture_time: ContinuousRandomVariable, @@ -137,6 +141,7 @@ impl Generator { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Generator { fn events_ext( &mut self, diff --git a/sim/src/models/load_balancer.rs b/sim/src/models/load_balancer.rs index 18138bf..eb4553c 100644 --- a/sim/src/models/load_balancer.rs +++ b/sim/src/models/load_balancer.rs @@ -9,6 +9,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The load balancer routes jobs to a set of possible process paths, using a /// round robin strategy. There is no stochastic behavior in this model. #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] @@ -61,6 +64,7 @@ enum Phase { LoadBalancing, } +#[cfg_attr(feature = "simx", event_rules)] impl LoadBalancer { pub fn new(job_port: String, flow_path_ports: Vec, store_records: bool) -> Self { Self { @@ -124,6 +128,7 @@ impl LoadBalancer { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for LoadBalancer { fn events_ext( &mut self, @@ -137,10 +142,9 @@ impl DevsModel for LoadBalancer { &mut self, services: &mut Services, ) -> Result, SimulationError> { - if self.state.jobs.is_empty() { - self.passivate() - } else { - self.send_job(services) + match self.state.jobs.len() { + 0 => self.passivate(), + _ => self.send_job(services), } } diff --git a/sim/src/models/model.rs b/sim/src/models/model.rs index 9810560..b3da251 100644 --- a/sim/src/models/model.rs +++ b/sim/src/models/model.rs @@ -74,6 +74,16 @@ impl DevsModel for Model { fn until_next_event(&self) -> f64 { self.inner.until_next_event() } + + #[cfg(feature = "simx")] + fn event_rules_scheduling(&self) -> &str { + self.inner.event_rules_scheduling() + } + + #[cfg(feature = "simx")] + fn event_rules(&self) -> String { + self.inner.event_rules() + } } impl Reportable for Model { diff --git a/sim/src/models/model_trait.rs b/sim/src/models/model_trait.rs index 0c883e6..42b80ee 100644 --- a/sim/src/models/model_trait.rs +++ b/sim/src/models/model_trait.rs @@ -44,6 +44,10 @@ pub trait DevsModel: ModelClone + SerializableModel { -> Result, SimulationError>; fn time_advance(&mut self, time_delta: f64); fn until_next_event(&self) -> f64; + #[cfg(feature = "simx")] + fn event_rules_scheduling(&self) -> &str; + #[cfg(feature = "simx")] + fn event_rules(&self) -> String; } /// The additional status and record-keeping methods of `Reportable` provide diff --git a/sim/src/models/parallel_gateway.rs b/sim/src/models/parallel_gateway.rs index 8e01e82..6949d1b 100644 --- a/sim/src/models/parallel_gateway.rs +++ b/sim/src/models/parallel_gateway.rs @@ -10,6 +10,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The parallel gateway splits a job across multiple processing paths. The /// job is duplicated across every one of the processing paths. In addition /// to splitting the process, a second parallel gateway can be used to join @@ -61,6 +64,7 @@ impl Default for State { } } +#[cfg_attr(feature = "simx", event_rules)] impl ParallelGateway { pub fn new( flow_paths_in: Vec, @@ -160,6 +164,7 @@ impl ParallelGateway { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for ParallelGateway { fn events_ext( &mut self, @@ -176,10 +181,9 @@ impl DevsModel for ParallelGateway { &mut self, services: &mut Services, ) -> Result, SimulationError> { - if self.full_collection().is_some() { - self.send_job(services) - } else { - self.passivate() + match self.full_collection() { + Some(_) => self.send_job(services), + None => self.passivate(), } } diff --git a/sim/src/models/processor.rs b/sim/src/models/processor.rs index c25ca6a..fefada8 100644 --- a/sim/src/models/processor.rs +++ b/sim/src/models/processor.rs @@ -10,6 +10,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The processor accepts jobs, processes them for a period of time, and then /// outputs a processed job. The processor can have a configurable queue, of /// size 0 to infinity, inclusive. The default queue size is infinite. The @@ -81,6 +84,7 @@ enum Phase { Passive, } +#[cfg_attr(feature = "simx", event_rules)] impl Processor { pub fn new( service_time: ContinuousRandomVariable, @@ -206,6 +210,7 @@ impl Processor { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Processor { fn events_ext( &mut self, diff --git a/sim/src/models/stochastic_gate.rs b/sim/src/models/stochastic_gate.rs index 5394455..2d29b82 100644 --- a/sim/src/models/stochastic_gate.rs +++ b/sim/src/models/stochastic_gate.rs @@ -10,6 +10,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The stochastic gate blocks (drops) or passes jobs, based on a specified /// Bernoulli distribution. If the Bernoulli random variate is a 0, the job /// will be dropped. If the Bernoulli random variate is a 1, the job will be @@ -67,6 +70,7 @@ pub struct Job { pub pass: bool, } +#[cfg_attr(feature = "simx", event_rules)] impl StochasticGate { pub fn new( pass_distribution: BooleanRandomVariable, @@ -148,6 +152,7 @@ impl StochasticGate { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for StochasticGate { fn events_ext( &mut self, diff --git a/sim/src/models/stopwatch.rs b/sim/src/models/stopwatch.rs index b61ece9..2fb70ed 100644 --- a/sim/src/models/stopwatch.rs +++ b/sim/src/models/stopwatch.rs @@ -10,6 +10,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The stopwatch calculates durations by matching messages on the start and /// stop ports. For example, a "job 1" message arrives at the start port at /// time 0.1, and then a "job 1" message arrives at the stop port at time @@ -95,6 +98,7 @@ pub struct Job { stop: Option, } +#[cfg_attr(feature = "simx", event_rules)] impl Stopwatch { pub fn new( start_port: String, @@ -279,6 +283,7 @@ impl Stopwatch { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Stopwatch { fn events_ext( &mut self, diff --git a/sim/src/models/storage.rs b/sim/src/models/storage.rs index 0f7dabb..a0f74d0 100644 --- a/sim/src/models/storage.rs +++ b/sim/src/models/storage.rs @@ -9,6 +9,9 @@ use crate::utils::errors::SimulationError; use sim_derive::SerializableModel; +#[cfg(feature = "simx")] +use simx::event_rules; + /// The storage model stores a value, and responds with it upon request. /// Values are stored and value requests are handled instantantaneously. #[derive(Debug, Clone, Serialize, Deserialize, SerializableModel)] @@ -66,6 +69,7 @@ enum Phase { JobFetch, } +#[cfg_attr(feature = "simx", event_rules)] impl Storage { pub fn new( put_port: String, @@ -153,6 +157,7 @@ impl Storage { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Storage { fn events_ext( &mut self, diff --git a/sim/tests/batcher_event_rules.json b/sim/tests/batcher_event_rules.json new file mode 100644 index 0000000..52290b7 --- /dev/null +++ b/sim/tests/batcher_event_rules.json @@ -0,0 +1 @@ +[{"event_expression":"new","event_parameters":["job_in_port","job_out_port","max_batch_time","max_batch_size","store_records"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"add_to_batch","event_parameters":["incoming_message","services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Batching"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"start_batch","event_parameters":["incoming_message","services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Batching"],["self.state.until_next_event","self.max_batch_time"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"fill_batch","event_parameters":["incoming_message","services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Release"],["self.state.until_next_event","0.0"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"release_full_queue","event_parameters":["services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Passive"],["self.state.until_next_event","INFINITY"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"release_partial_queue","event_parameters":["services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Batching"],["self.state.until_next_event","self.max_batch_time"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"release_multiple","event_parameters":["services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Release"],["self.state.until_next_event","0.0"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"record","event_parameters":["time","action","subject"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"events_ext","event_parameters":["incoming_message","services"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"add_to_batch","parameters":[],"condition":"(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (Phase :: Batching, true)","delay":null},{"event_expression_target":"start_batch","parameters":[],"condition":"(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (Phase :: Passive, true)","delay":null},{"event_expression_target":"fill_batch","parameters":[],"condition":"(& self.state.phase, self.state.jobs.len() + 1 < self.max_batch_size,) = (_, false)","delay":null}],"cancelling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":null}]}},{"event_expression":"events_int","event_parameters":["services"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"release_full_queue","parameters":[],"condition":"(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (true, false)","delay":null},{"event_expression_target":"release_multiple","parameters":[],"condition":"(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (false, true)","delay":null},{"event_expression_target":"release_partial_queue","parameters":[],"condition":"(self.state.jobs.len() <= self.max_batch_size, self.state.jobs.len() >= 2 *\n self.max_batch_size,) = (false, false)","delay":null}],"cancelling":[]}}] \ No newline at end of file diff --git a/sim/tests/custom.rs b/sim/tests/custom.rs index 0ea34f5..d9fcbf0 100644 --- a/sim/tests/custom.rs +++ b/sim/tests/custom.rs @@ -9,6 +9,9 @@ use sim::utils::errors::SimulationError; use sim_derive::{register, SerializableModel}; use wasm_bindgen_test::*; +#[cfg(feature = "simx")] +use simx::event_rules; + wasm_bindgen_test_configure!(run_in_browser); /// The passive model does nothing @@ -30,6 +33,7 @@ struct State { records: Vec, } +#[cfg_attr(feature = "simx", event_rules)] impl Passive { pub fn new(job_port: String) -> Self { Self { @@ -41,6 +45,7 @@ impl Passive { } } +#[cfg_attr(feature = "simx", event_rules)] impl DevsModel for Passive { fn events_ext( &mut self, diff --git a/sim/tests/event_rules.rs b/sim/tests/event_rules.rs new file mode 100644 index 0000000..226613e --- /dev/null +++ b/sim/tests/event_rules.rs @@ -0,0 +1,34 @@ +#[cfg(feature = "simx")] +use std::fs; + +#[cfg(feature = "simx")] +use sim::input_modeling::ContinuousRandomVariable; +#[cfg(feature = "simx")] +use sim::models::{Batcher, DevsModel, Generator}; + +#[test] +#[cfg(feature = "simx")] +fn batcher_event_rules() { + let batcher = Batcher::new(String::from("job"), String::from("job"), 0.5, 10, false); + + let batcher_event_rules = fs::read_to_string("tests/batcher_event_rules.json") + .expect("Unable to read tests/generator_event_rules.json"); + + assert_eq!(batcher.event_rules(), batcher_event_rules); +} + +#[test] +#[cfg(feature = "simx")] +fn generator_event_rules() { + let generator = Generator::new( + ContinuousRandomVariable::Exp { lambda: 0.5 }, + None, + String::from("job"), + false, + ); + + let generator_event_rules = fs::read_to_string("tests/generator_event_rules.json") + .expect("Unable to read tests/generator_event_rules.json"); + + assert_eq!(generator.event_rules(), generator_event_rules); +} diff --git a/sim/tests/generator_event_rules.json b/sim/tests/generator_event_rules.json new file mode 100644 index 0000000..6e1425a --- /dev/null +++ b/sim/tests/generator_event_rules.json @@ -0,0 +1 @@ +[{"event_expression":"new","event_parameters":["message_interdeparture_time","thinning","job_port","store_records"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"release_job","event_parameters":["services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Generating"],["self.state.until_next_event","interdeparture"],["self.state.until_job","interdeparture"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"initialize_generation","event_parameters":["services"],"event_routine":{"state_transitions":[["self.state.phase","Phase :: Generating"],["self.state.until_next_event","interdeparture"],["self.state.until_job","interdeparture"]],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"record","event_parameters":["time","action","subject"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"events_int","parameters":[],"condition":null,"delay":"\\sigma"}],"cancelling":[]}},{"event_expression":"events_int","event_parameters":["services"],"event_routine":{"state_transitions":[],"scheduling":[{"event_expression_target":"release_job","parameters":[],"condition":"& self.state.phase = Phase :: Generating","delay":null},{"event_expression_target":"initialize_generation","parameters":[],"condition":"& self.state.phase = Phase :: Initializing","delay":null}],"cancelling":[]}}] \ No newline at end of file diff --git a/simx/Cargo.toml b/simx/Cargo.toml new file mode 100644 index 0000000..c05475e --- /dev/null +++ b/simx/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "simx" +version = "0.10.0" +edition = "2018" +license = "MIT OR Apache-2.0" +authors = ["Neal DeBuhr "] +description = "SimX provides Sim package extensions, for research and experimentation" +homepage = "https://github.com/ndebuhr/simx" +repository = "https://github.com/ndebuhr/simx" +readme = "README.md" +keywords = ["simulation", "experimentation", "research", "extension", "modeling"] +categories = ["simulation"] + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["-Oz", "--enable-mutable-globals"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0" +quote = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +syn = { version="1.0", features = ["extra-traits", "full", "parsing", "printing"] } + + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = { version = "0.1.6", optional = true } + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. It is slower than the default +# allocator, however. +# +# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. +wee_alloc = { version = "0.4.5", optional = true } + +[dev-dependencies] +wasm-bindgen-test = "0.3.13" + +[profile.release] +# Tell `rustc` to optimize for small code size. +opt-level = "s" diff --git a/simx/LICENSE-APACHE b/simx/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/simx/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/simx/LICENSE-MIT b/simx/LICENSE-MIT new file mode 120000 index 0000000..76219eb --- /dev/null +++ b/simx/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/simx/README.md b/simx/README.md new file mode 100644 index 0000000..822a3e3 --- /dev/null +++ b/simx/README.md @@ -0,0 +1,20 @@ +
+ +

SimX

+

SimX provides Sim package extensions, for research and experimentation

+
+
+ +## Contributing + +Issues, feature requests, and pull requests are always welcome! + +## License + +This project is licensed under either of [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) or [MIT License](https://opensource.org/licenses/MIT) at your option. + +[Apache License, Version 2.0](LICENSE-APACHE) + +[MIT License](LICENSE-MIT) + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in sim by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/simx/src/lib.rs b/simx/src/lib.rs new file mode 100644 index 0000000..9e97800 --- /dev/null +++ b/simx/src/lib.rs @@ -0,0 +1,288 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use serde::{Deserialize, Serialize}; +use syn::{parse_macro_input, Expr, FnArg, Ident, ImplItem, ImplItemMethod, ItemImpl, Stmt}; + +const EVENTS_INT_EXPRESSION: &str = "events_int"; +const EVENTS_EXT_EXPRESSION: &str = "events_ext"; + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct EventRule { + event_expression: String, + event_parameters: Vec, + event_routine: EventRoutine, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct EventRoutine { + // State variable, value + state_transitions: Vec<(String, String)>, + scheduling: Vec, + cancelling: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct EventEdge { + event_expression_target: String, + parameters: Vec, + condition: Option, + delay: Option, +} + +enum ModelImplementation { + DevsModel, + Other, +} + +enum DevsTransitions { + Internal, + External, +} + +fn model_implementation(item: &ItemImpl) -> Option { + match &item.trait_ { + None => None, + Some(trait_) => { + if trait_.1.segments[0].ident == "DevsModel" { + Some(ModelImplementation::DevsModel) + } else { + Some(ModelImplementation::Other) + } + } + } +} + +fn get_method_args(method: &ImplItemMethod) -> Vec { + method + .sig + .inputs + .iter() + .filter_map(|input| match input { + FnArg::Typed(pat_type) => { + let pat = &pat_type.pat; + Some(quote!(#pat).to_string()) + } + FnArg::Receiver(_) => None, + }) + .collect() +} + +fn get_state_transitions(method: &ImplItemMethod) -> Vec<(String, String)> { + method + .block + .stmts + .iter() + .filter_map(|stmt| match stmt { + Stmt::Semi(Expr::Assign(assign), _) => { + let assign_left = &assign.left; + let assign_right = &assign.right; + Some(( + quote!(#assign_left).to_string(), + quote!(#assign_right).to_string(), + )) + }, + _ => None, + }) + .collect() +} + +fn get_schedulings(method: &ImplItemMethod) -> Option> { + method.block.stmts.iter().find_map(|stmt| { + if let Stmt::Expr(Expr::MethodCall(method_call)) = stmt { + Some( + vec![EventEdge { + event_expression_target: method_call.method.to_string(), + // TODO parameters + parameters: Vec::new(), + condition: None, + delay: None, + }] + ) + } else if let Stmt::Expr(Expr::Match(match_)) = stmt { + Some( + match_ + .arms + .iter() + .filter_map(|arm| { + let match_expr = &match_.expr; + let match_case = &arm.pat; + let (match_function, match_parameters) = match &*arm.body { + Expr::MethodCall(method_call) => { + // TODO - Extract function parameters + (method_call.method.to_string(), Vec::new()) + }, + _ => { + // ("Ok()", "Err(SimulationError::InvalidModelState)",...) + return None + } + }; + Some(EventEdge { + event_expression_target: match_function, + parameters: match_parameters, + condition: Some(format![ + "{} = {}", + quote!(#match_expr).to_string(), + quote!(#match_case).to_string(), + ]), + delay: None, + }) + }) + .collect() + ) + } else { + None + } + }) +} + +fn add_event_rules_transition_method(mut input: ItemImpl) -> TokenStream { + let mut event_rules: Vec = Vec::new(); + + input + .items + .iter() + .filter_map(|method| { + if let ImplItem::Method(method) = method { + Some(method) + } else { + None + } + }) + .for_each(|method| { + let name = method.sig.ident.to_string(); + let arguments = get_method_args(method); + let state_transitions = get_state_transitions(method); + event_rules.push(EventRule { + event_expression: name, + event_parameters: arguments, + event_routine: EventRoutine { + state_transitions, + scheduling: vec![EventEdge { + event_expression_target: EVENTS_INT_EXPRESSION.to_string(), + parameters: Vec::new(), + condition: None, + delay: Some(String::from("\\sigma")), + }], + cancelling: Vec::new(), + }, + }); + }); + let event_rules_json = serde_json::to_string(&event_rules); + match event_rules_json { + Ok(event_rules_str) => { + input.items.push(ImplItem::Verbatim(quote! { + pub fn event_rules_transition( + &self, + ) -> &str { + #event_rules_str + } + })); + TokenStream::from(quote!(#input)) + } + Err(err) => { + let err_string = err.to_string(); + TokenStream::from( + quote!(compile_error!(#err_string)) + ) + } + } +} + +fn add_event_rules_scheduling_method(mut input: ItemImpl) -> TokenStream { + let events_int_ident = Ident::new("events_int", Span::call_site()); + let events_ext_ident = Ident::new("events_ext", Span::call_site()); + + let mut event_rules: Vec = Vec::new(); + + input + .items + .iter() + .filter_map(|method| { + if let ImplItem::Method(method) = method { + if method.sig.ident == events_int_ident { + Some((DevsTransitions::Internal, method)) + } else if method.sig.ident == events_ext_ident { + Some((DevsTransitions::External, method)) + } else { + None + } + } else { + None + } + }) + .for_each(|(transition_type, method)| { + let (name, cancellings) = match &transition_type { + DevsTransitions::Internal => (EVENTS_INT_EXPRESSION.to_string(), Vec::new()), + DevsTransitions::External => ( + EVENTS_EXT_EXPRESSION.to_string(), + vec![EventEdge { + event_expression_target: EVENTS_INT_EXPRESSION.to_string(), + parameters: Vec::new(), + condition: None, + delay: None, + }], + ), + }; + let arguments = get_method_args(method); + if let Some(schedulings) = get_schedulings(method) { + event_rules.push(EventRule { + event_expression: name, + event_parameters: arguments, + event_routine: EventRoutine { + state_transitions: Vec::new(), + scheduling: schedulings, + cancelling: cancellings, + }, + }); + } + }); + let event_rules_json = serde_json::to_string(&event_rules); + match event_rules_json { + Ok(event_rules_str) => { + input.items.push(ImplItem::Verbatim(quote! { + fn event_rules_scheduling( + &self, + ) -> &str { + #event_rules_str + } + + fn event_rules(&self) -> String { + // Avoid deserialization/serialization cycle for reduced complexity and improved performance + // Instead, use a manual, index-filtered string concatenation + let transition_str_len = self.event_rules_transition().len(); + format![ + "{},{}", + &self.event_rules_transition()[..transition_str_len-1], + &self.event_rules_scheduling()[1..] + ] + } + })); + TokenStream::from(quote!(#input)) + } + Err(err) => { + let err_string = err.to_string(); + TokenStream::from( + quote!(compile_error!(#err_string)) + ) + } + } +} + +#[proc_macro_attribute] +pub fn event_rules(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemImpl); + + match model_implementation(&input) { + None => add_event_rules_transition_method(input), + Some(model_implementation) => { + match model_implementation { + ModelImplementation::DevsModel => add_event_rules_scheduling_method(input), + // (Add nothing if other trait implementations) + ModelImplementation::Other => TokenStream::from(quote!(#input)), + } + }, + } +}