Skip to content

Commit

Permalink
add example
Browse files Browse the repository at this point in the history
  • Loading branch information
dragazo committed Nov 15, 2023
1 parent 0da9094 commit ae1f112
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 39 deletions.
152 changes: 152 additions & 0 deletions examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use netsblox_vm::real_time::*;
use netsblox_vm::std_system::*;
use netsblox_vm::bytecode::*;
use netsblox_vm::process::*;
use netsblox_vm::runtime::*;
use netsblox_vm::project::*;
use netsblox_vm::gc::*;
use netsblox_vm::ast;

use std::io::Read;
use std::time::Duration;
use std::sync::Arc;
use std::rc::Rc;

// -----------------------------------------------------------------

const BASE_URL: &'static str = "https://cloud.netsblox.org";

const CLOCK_INTERVAL: Duration = Duration::from_millis(10);
const COLLECT_INTERVAL: Duration = Duration::from_secs(60);

const YIELDS_BEFORE_SLEEP: usize = 64;
const IDLE_SLEEP_TIME: Duration = Duration::from_millis(1);

// -----------------------------------------------------------------

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum NativeType {} // type enum for a NativeValue - we don't have any native values we want to expose, so just use an empty enum

#[derive(Debug)]
enum NativeValue {} // enum for native values that are exposed to the vm - we don't have any we want to expose, so just use an empty enum
impl GetType for NativeValue {
type Output = NativeType;
fn get_type(&self) -> Self::Output {
unreachable!() // because we don't have any native values to get the type of
}
}

struct EntityState; // a type to hold custom entity (sprite or stage) state - we don't have any, so just use a unit struct
impl From<EntityKind<'_, '_, C, StdSystem<C>>> for EntityState {
fn from(_: EntityKind<'_, '_, C, StdSystem<C>>) -> Self {
EntityState
}
}

struct ProcessState; // a type to hold custom process (script) state - we don't have any, so just use a unit struct
impl From<&Entity<'_, C, StdSystem<C>>> for ProcessState {
fn from(_: &Entity<'_, C, StdSystem<C>>) -> Self {
ProcessState
}
}

struct C; // a type to hold all of our custom type definitions for the vm to use
impl CustomTypes<StdSystem<C>> for C {
type NativeValue = NativeValue; // a type to hold any native rust values exposed to the vm
type Intermediate = SimpleValue; // a Send type that serves as an intermediate between vm gc values and normal rust

type EntityState = EntityState; // a type to hold the custom state for an entity (sprite or stage)
type ProcessState = ProcessState; // a type to hold the custom state for a process (script)

// a function to convert intermediate values into native vm values
fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem<C>> {
Value::from_simple(mc, value)
}
}

// our top-level gc arena - this will hold our gc-allocated project and everything it contains
#[derive(Collect)]
#[collect(no_drop)]
struct Env<'gc> {
proj: Gc<'gc, RefLock<Project<'gc, C, StdSystem<C>>>>,
#[collect(require_static)] locs: Locations, // bytecode locations info for generating error traces
}
type EnvArena = Arena<Rootable![Env<'_>]>;

// converts a netsblox xml project containing a single role into a new gc environment object containing a running project
fn get_running_project(xml: &str, system: Rc<StdSystem<C>>) -> EnvArena {
EnvArena::new(|mc| {
let parser = ast::Parser::default();
let ast = parser.parse(xml).unwrap();
assert_eq!(ast.roles.len(), 1); // this should be handled more elegantly in practice - for the sake of this example, we only allow one role

let (bytecode, init_info, locs, _) = ByteCode::compile(&ast.roles[0]).unwrap();

let mut proj = Project::from_init(mc, &init_info, Rc::new(bytecode), Settings::default(), system);
proj.input(mc, Input::Start); // this is equivalent to clicking the green flag button

Env { proj: Gc::new(mc, RefLock::new(proj)), locs }
})
}

fn main() {
// read in an xml file whose path is given as a command line argument
let args = std::env::args().collect::<Vec<_>>();
if args.len() != 2 {
panic!("usage: {} [xml file]", &args[0]);
}
let mut xml = String::new();
std::fs::File::open(&args[1]).expect("failed to open file").read_to_string(&mut xml).expect("failed to read file");

// create a new shared clock and start a thread that updates it at our desired interval
let clock = Arc::new(Clock::new(UtcOffset::UTC, Some(Precision::Medium)));
let clock_clone = clock.clone();
std::thread::spawn(move || loop {
std::thread::sleep(CLOCK_INTERVAL);
clock_clone.update();
});

// create a custom config for the system - in this simple example we just implement the say/think blocks to print to stdout
let config = Config::<C, StdSystem<C>> {
request: None,
command: Some(Rc::new(|_mc, key, command, _proc| match command {
Command::Print { style: _, value } => {
if let Some(value) = value {
println!("{value:?}");
}
key.complete(Ok(())); // any request that you handle must be completed - otherwise the calling process will hang forever
CommandStatus::Handled
}
_ => CommandStatus::UseDefault { key, command }, // anything you don't handle should return the key and command to invoke the default behavior instead
})),
};

// initialize our system with all the info we've put together
let system = Rc::new(StdSystem::new_sync(BASE_URL.to_owned(), None, config, clock.clone()));
let mut env = get_running_project(&xml, system);

// begin running the code - these are some helpers to make things more efficient in terms of memory and cpu resources
let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_SLEEP, Box::new(|| std::thread::sleep(IDLE_SLEEP_TIME)));
let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
loop {
env.mutate(|mc, env| {
let mut proj = env.proj.borrow_mut(mc);
for _ in 0..1024 {
// step the virtual machine forward by one bytecode instruction
let res = proj.step(mc);
if let ProjectStep::Error { error, proc } = &res {
// if we get an error, we can generate an error summary including a stack trace - here we just print out the result
let trace = ErrorSummary::extract(error, proc, &env.locs);
println!("error: {error:?}\ntrace: {trace:?}");
}
// this takes care of performing thread sleep if we get a bunch of no-ops from proj.step back to back
idle_sleeper.consume(&res);
}
});
// if it's time for us to do garbage collection, do it and reset the next collection time
if clock.read(Precision::Low) >= next_collect {
env.collect_all();
next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
}
}
}
37 changes: 27 additions & 10 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const STEPS_PER_IO_ITER: usize = 64;
const MAX_REQUEST_SIZE_BYTES: usize = 1024 * 1024 * 1024;
const YIELDS_BEFORE_IDLE_SLEEP: usize = 256;
const IDLE_SLEEP_TIME: Duration = Duration::from_micros(500);
const CLOCK_INTERVAL: Duration = Duration::from_millis(10);
const COLLECT_INTERVAL: Duration = Duration::from_secs(60);

macro_rules! crash {
($ret:literal : $($tt:tt)*) => {{
Expand Down Expand Up @@ -189,7 +191,7 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
let config = overrides.fallback(&Config {
command: {
let update_flag = update_flag.clone();
Some(Rc::new(move |_, _, key, command, proc| match command {
Some(Rc::new(move |_, key, command, proc| match command {
Command::Print { style: _, value } => {
let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
if let Some(value) = value {
Expand All @@ -205,7 +207,7 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
request: {
let update_flag = update_flag.clone();
let input_queries = input_queries.clone();
Some(Rc::new(move |_, _, key, request, proc| match request {
Some(Rc::new(move |_, key, request, proc| match request {
Request::Input { prompt } => {
let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
input_queries.borrow_mut().push_back((format!("{entity:?} {prompt:?} > "), key));
Expand All @@ -217,11 +219,11 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
},
});

let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock));
let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone()));
let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
print!("public id: {}\r\n", system.get_public_id());

let env = match get_env(role, system) {
let mut env = match get_env(role, system) {
Ok(x) => x,
Err(e) => {
print!("error loading project: {e:?}\r\n");
Expand All @@ -230,6 +232,7 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
};
env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start));

let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
let mut input_sequence = Vec::with_capacity(16);
let in_input_mode = || !input_queries.borrow().is_empty();
'program: loop {
Expand Down Expand Up @@ -273,6 +276,10 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
idle_sleeper.consume(&res);
}
});
if clock.read(Precision::Low) > next_collect {
env.collect_all();
next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
}

if update_flag.get() {
update_flag.set(false);
Expand All @@ -299,7 +306,7 @@ fn run_proj_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String
fn run_proj_non_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: String, role: &ast::Role, overrides: Config<C, StdSystem<C>>, clock: Arc<Clock>) {
let config = overrides.fallback(&Config {
request: None,
command: Some(Rc::new(move |_, _, key, command, proc| match command {
command: Some(Rc::new(move |_, key, command, proc| match command {
Command::Print { style: _, value } => {
let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
if let Some(value) = value { println!("{entity:?} > {value:?}") }
Expand All @@ -310,11 +317,11 @@ fn run_proj_non_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: St
})),
});

let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock));
let system = Rc::new(StdSystem::new_sync(server, Some(project_name), config, clock.clone()));
let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
println!(">>> public id: {}\n", system.get_public_id());

let env = match get_env(role, system) {
let mut env = match get_env(role, system) {
Ok(x) => x,
Err(e) => {
println!(">>> error loading project: {e:?}");
Expand All @@ -323,6 +330,7 @@ fn run_proj_non_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: St
};
env.mutate(|mc, env| env.proj.borrow_mut(mc).input(mc, Input::Start));

let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
loop {
env.mutate(|mc, env| {
let mut proj = env.proj.borrow_mut(mc);
Expand All @@ -334,6 +342,10 @@ fn run_proj_non_tty<C: CustomTypes<StdSystem<C>>>(project_name: &str, server: St
idle_sleeper.consume(&res);
}
});
if clock.read(Precision::Low) > next_collect {
env.collect_all();
next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
}
}
}
fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: String, addr: String, port: u16, overrides: Config<C, StdSystem<C>>, clock: Arc<Clock>, syscalls: &[SyscallMenu]) {
Expand Down Expand Up @@ -385,7 +397,7 @@ fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: String, addr: String, por
let weak_state = Arc::downgrade(&state);
let config = overrides.fallback(&Config {
request: None,
command: Some(Rc::new(move |_, _, key, command, proc| match command {
command: Some(Rc::new(move |_, key, command, proc| match command {
Command::Print { style: _, value } => {
let entity = &*proc.get_call_stack().last().unwrap().entity.borrow();
if let Some(value) = value { tee_println!(weak_state.upgrade() => "{entity:?} > {value:?}") }
Expand All @@ -395,7 +407,7 @@ fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: String, addr: String, por
_ => CommandStatus::UseDefault { key, command },
})),
});
let system = Rc::new(StdSystem::new_sync(nb_server, Some("native-server"), config, clock));
let system = Rc::new(StdSystem::new_sync(nb_server, Some("native-server"), config, clock.clone()));
let mut idle_sleeper = IdleAction::new(YIELDS_BEFORE_IDLE_SLEEP, Box::new(|| thread::sleep(IDLE_SLEEP_TIME)));
println!("public id: {}", system.get_public_id());

Expand Down Expand Up @@ -473,6 +485,7 @@ fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: String, addr: String, por
let (_, empty_role) = open_project(EMPTY_PROJECT, None).unwrap_or_else(|_| crash!(666: "default project failed to load"));
let mut env = get_env(&empty_role, system.clone()).unwrap();

let mut next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
'program: loop {
'input: loop {
match proj_receiver.try_recv() {
Expand Down Expand Up @@ -544,6 +557,10 @@ fn run_server<C: CustomTypes<StdSystem<C>>>(nb_server: String, addr: String, por
idle_sleeper.consume(&res);
}
});
if clock.read(Precision::Low) > next_collect {
env.collect_all();
next_collect = clock.read(Precision::Medium) + COLLECT_INTERVAL;
}
}
}

Expand All @@ -553,7 +570,7 @@ pub fn run<C: CustomTypes<StdSystem<C>>>(mode: Mode, config: Config<C, StdSystem
let clock = Arc::new(Clock::new(utc_offset, Some(Precision::Medium)));
let clock_clone = clock.clone();
thread::spawn(move || loop {
thread::sleep(Duration::from_millis(10));
thread::sleep(CLOCK_INTERVAL);
clock_clone.update();
});

Expand Down
10 changes: 5 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::io::{BufRead, Write, BufReader, BufWriter};

use netsblox_vm::cli::{run, Mode};
use netsblox_vm::template::SyscallMenu;
use netsblox_vm::runtime::{GetType, Value, Type, ErrorCause, EntityKind, Request, RequestStatus, Config, CustomTypes, Key, SimpleValue, Entity};
use netsblox_vm::runtime::{GetType, Value, Type, EntityKind, Request, RequestStatus, Config, CustomTypes, Key, SimpleValue, Entity};
use netsblox_vm::std_system::StdSystem;
use netsblox_vm::gc::Mutation;
use clap::Parser;
Expand Down Expand Up @@ -72,11 +72,11 @@ impl CustomTypes<StdSystem<C>> for C {
type EntityState = EntityState;
type ProcessState = ProcessState;

fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Result<Value<'gc, C, StdSystem<C>>, ErrorCause<C, StdSystem<C>>> {
Ok(match value {
fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, C, StdSystem<C>> {
match value {
Intermediate::Simple(x) => Value::from_simple(mc, x),
Intermediate::Native(x) => Value::Native(Rc::new(x)),
})
}
}
}

Expand All @@ -98,7 +98,7 @@ fn main() {

if args.fs {
let new_config = Config::<C, StdSystem<C>> {
request: Some(Rc::new(move |_, _, key, request, _| match &request {
request: Some(Rc::new(move |_, key, request, _| match &request {
Request::Syscall { name, args } => match name.as_str() {
"open" => {
let (path, mode) = match args.as_slice() {
Expand Down
18 changes: 9 additions & 9 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1675,9 +1675,9 @@ pub enum CommandStatus<'gc, 'a, C: CustomTypes<S>, S: System<C>> {
#[educe(Clone)]
pub struct Config<C: CustomTypes<S>, S: System<C>> {
/// A function used to perform asynchronous requests that yield a value back to the runtime.
pub request: Option<Rc<dyn for<'gc> Fn(&S, &Mutation<'gc>, S::RequestKey, Request<'gc, C, S>, &mut Process<'gc, C, S>) -> RequestStatus<'gc, C, S>>>,
pub request: Option<Rc<dyn for<'gc> Fn(&Mutation<'gc>, S::RequestKey, Request<'gc, C, S>, &mut Process<'gc, C, S>) -> RequestStatus<'gc, C, S>>>,
/// A function used to perform asynchronous tasks whose completion is awaited by the runtime.
pub command: Option<Rc<dyn for<'gc, 'a> Fn(&S, &Mutation<'gc>, S::CommandKey, Command<'gc, 'a, C, S>, &mut Process<'gc, C, S>) -> CommandStatus<'gc, 'a, C, S>>>,
pub command: Option<Rc<dyn for<'gc, 'a> Fn(&Mutation<'gc>, S::CommandKey, Command<'gc, 'a, C, S>, &mut Process<'gc, C, S>) -> CommandStatus<'gc, 'a, C, S>>>,
}
impl<C: CustomTypes<S>, S: System<C>> Default for Config<C, S> {
fn default() -> Self {
Expand All @@ -1692,20 +1692,20 @@ impl<C: CustomTypes<S>, S: System<C>> Config<C, S> {
pub fn fallback(&self, other: &Self) -> Self {
Self {
request: match (self.request.clone(), other.request.clone()) {
(Some(a), Some(b)) => Some(Rc::new(move |system, mc, key, request, proc| {
match a(system, mc, key, request, proc) {
(Some(a), Some(b)) => Some(Rc::new(move |mc, key, request, proc| {
match a(mc, key, request, proc) {
RequestStatus::Handled => RequestStatus::Handled,
RequestStatus::UseDefault { key, request } => b(system, mc, key, request, proc),
RequestStatus::UseDefault { key, request } => b(mc, key, request, proc),
}
})),
(Some(a), None) | (None, Some(a)) => Some(a),
(None, None) => None,
},
command: match (self.command.clone(), other.command.clone()) {
(Some(a), Some(b)) => Some(Rc::new(move |system, mc, key, command, proc| {
match a(system, mc, key, command, proc) {
(Some(a), Some(b)) => Some(Rc::new(move |mc, key, command, proc| {
match a(mc, key, command, proc) {
CommandStatus::Handled => CommandStatus::Handled,
CommandStatus::UseDefault { key, command } => b(system, mc, key, command, proc),
CommandStatus::UseDefault { key, command } => b(mc, key, command, proc),
}
})),
(Some(a), None) | (None, Some(a)) => Some(a),
Expand Down Expand Up @@ -1740,7 +1740,7 @@ pub trait CustomTypes<S: System<Self>>: 'static + Sized {
type ProcessState: 'static + for<'gc, 'a> From<&'a Entity<'gc, Self, S>>;

/// Converts a [`Value`] into a [`CustomTypes::Intermediate`] for use outside of gc context.
fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Result<Value<'gc, Self, S>, ErrorCause<Self, S>>;
fn from_intermediate<'gc>(mc: &Mutation<'gc>, value: Self::Intermediate) -> Value<'gc, Self, S>;
}

/// The time as determined by an implementation of [`System`].
Expand Down
Loading

0 comments on commit ae1f112

Please sign in to comment.