From 8486a1c883386eb0ded700862257a50c216cfafe Mon Sep 17 00:00:00 2001 From: "Stephen M. Coakley" Date: Sat, 3 Feb 2024 01:31:22 -0600 Subject: [PATCH] Early return statements --- runtime/src/builtins.rs | 11 +-- runtime/src/controlflow.rs | 29 +++++++ runtime/src/eval.rs | 158 +++++++++++++++++++++---------------- runtime/src/fiber.rs | 11 ++- runtime/src/init.rt | 6 +- runtime/src/lib.rs | 5 +- runtime/src/macros.rs | 22 ------ runtime/src/modules.rs | 1 + runtime/src/table.rs | 22 ++++++ rustfmt.toml | 1 - stdlib/src/init.rt | 2 +- syntax/src/ast.rs | 9 +++ syntax/src/grammar.pest | 21 +++-- syntax/src/parser.rs | 23 ++++++ 14 files changed, 208 insertions(+), 113 deletions(-) create mode 100644 runtime/src/controlflow.rs diff --git a/runtime/src/builtins.rs b/runtime/src/builtins.rs index 6ae3cda..1b95386 100644 --- a/runtime/src/builtins.rs +++ b/runtime/src/builtins.rs @@ -7,6 +7,8 @@ use crate::{ prelude::*, scope::Scope, string::RipString, + table, + throw, }; use riptide_syntax::source::SourceFile; use std::convert::TryInto; @@ -21,7 +23,6 @@ pub(crate) fn load_module() -> Result { "load" => Value::ForeignFn(load.into()), "nil" => Value::ForeignFn(nil.into()), "nth" => Value::ForeignFn(nth.into()), - "pass" => Value::ForeignFn(pass.into()), "throw" => Value::ForeignFn(throw.into()), "try" => Value::ForeignFn(try_fn.into()), "typeof" => Value::ForeignFn(type_of.into()), @@ -61,7 +62,7 @@ async fn type_of(_: &mut Fiber, args: Vec) -> Result { /// Parse a string as code, returning it as an executable closure. async fn load(fiber: &mut Fiber, args: Vec) -> Result { - let script: RipString = match args.get(0).and_then(Value::as_string) { + let script: RipString = match args.first().and_then(Value::as_string) { Some(s) => s.clone(), None => throw!("first argument must be a string"), }; @@ -73,7 +74,7 @@ async fn load(fiber: &mut Fiber, args: Vec) -> Result { } async fn nth(_: &mut Fiber, args: Vec) -> Result { - let list = match args.get(0).and_then(Value::as_list) { + let list = match args.first().and_then(Value::as_list) { Some(s) => s.to_vec(), None => throw!("first argument must be a list"), }; @@ -91,10 +92,6 @@ async fn nil(_: &mut Fiber, _: Vec) -> Result { Ok(Value::Nil) } -async fn pass(_fiber: &mut Fiber, args: Vec) -> Result { - Ok(args.first().cloned().unwrap_or(Value::Nil)) -} - /// Throw an exception. async fn throw(_: &mut Fiber, args: Vec) -> Result { match args.first() { diff --git a/runtime/src/controlflow.rs b/runtime/src/controlflow.rs new file mode 100644 index 0000000..8a23755 --- /dev/null +++ b/runtime/src/controlflow.rs @@ -0,0 +1,29 @@ +//! Helpers for navigating control flow within the interpreter. +//! +//! This is not exposed in the runtime API, as manipulating control flow is a +//! privileged operation. + +use crate::{Exception, Value}; + +/// Control flow is handled in the interpreter entirely using return values. +pub(crate) type ControlFlow = std::ops::ControlFlow; + +/// When performing an early exit of normal control flow, this is the action being +/// performed. +pub(crate) enum BreakAction { + /// Break out of the closest function boundary with the given return value. + /// This bubbles up through the stack until the nearest function invocation + /// is reached. + Return(Value), + + /// Throw an exception. This is bubbled up through the stack until caught. + Throw(Exception), +} + +macro_rules! throw_cf { + ($($arg:tt)*) => { + return ::std::ops::ControlFlow::Break(BreakAction::Throw($crate::Exception::from(format!($($arg)*)))) + }; +} + +pub(crate) use throw_cf; diff --git a/runtime/src/eval.rs b/runtime/src/eval.rs index f11922f..3fca36d 100644 --- a/runtime/src/eval.rs +++ b/runtime/src/eval.rs @@ -1,21 +1,26 @@ //! This module contains the core logic of the interpreter. -use riptide_syntax::{ - parse, - ast::*, - source::*, -}; -use super::{ +use crate::{ closure::Closure, + controlflow::{throw_cf, BreakAction, ControlFlow}, exceptions::Exception, fiber::Fiber, foreign::ForeignFn, scope::Scope, + string::RipString, + table, table::Table, + throw, value::Value, }; -use gc::Gc; use futures::future::try_join_all; +use gc::Gc; +use riptide_syntax::{ + parse, + ast::*, + source::*, +}; +use std::ops::ControlFlow::Continue; /// Compile the given source code as a closure. pub(crate) fn compile(fiber: &mut Fiber, file: impl Into) -> Result { @@ -23,36 +28,22 @@ pub(crate) fn compile(fiber: &mut Fiber, file: impl Into) -> Result< let file_name = file.name().to_string(); match parse(file) { - Ok(block) => compile_block(fiber, block), - Err(e) => throw!("error parsing {}: {}", file_name, e), - } -} - -pub(crate) fn compile_anonymous_closure(file: impl Into) -> Result { - let file = file.into(); - let file_name = file.name().to_string(); - - match parse(file) { - Ok(block) => Ok(Closure { - block, - scope: None, - name: None, - }), + Ok(block) => Ok(compile_block(fiber, block)), Err(e) => throw!("error parsing {}: {}", file_name, e), } } /// Compile a block into an executable closure. -fn compile_block(fiber: &mut Fiber, block: Block) -> Result { +fn compile_block(fiber: &mut Fiber, block: Block) -> Closure { // Constructing a closure is quite easy since our interpreter is based // around evaluating AST nodes directly within an environment. All we have to // do aside from persisting the AST is capture the current environment. - Ok(Closure { + Closure { block, scope: fiber.current_scope().cloned(), name: None, - }) + } } /// Invoke the given value as a function with the given arguments. @@ -106,11 +97,14 @@ pub(crate) async fn invoke_closure(fiber: &mut Fiber, closure: &Closure, args: V // Evaluate each statement in order. for statement in closure.block.statements.clone().into_iter() { match evaluate_statement(*fiber, statement).await { - Ok(return_value) => last_return_value = return_value, + Continue(return_value) => last_return_value = return_value, + + // Stop block execution and return the given value. + ControlFlow::Break(BreakAction::Return(value)) => return Ok(value), // Exception thrown; our scope guard from earlier will ensure that // the stack is unwound. - Err(mut exception) => { + ControlFlow::Break(BreakAction::Throw(mut exception)) => { if exception.backtrace.is_empty() { exception.backtrace = fiber.backtrace().cloned().collect(); } @@ -149,11 +143,16 @@ async fn invoke_native(fiber: &mut Fiber, function: &ForeignFn, args: Vec } #[async_recursion::async_recursion(?Send)] -async fn evaluate_statement(fiber: &mut Fiber, statement: Statement) -> Result { +async fn evaluate_statement(fiber: &mut Fiber, statement: Statement) -> ControlFlow { match statement { Statement::Import(statement) => { evaluate_import_statement(fiber, statement).await?; - Ok(Default::default()) + Continue(Default::default()) + }, + Statement::Return(None) => ControlFlow::Break(BreakAction::Return(Value::Nil)), + Statement::Return(Some(expr)) => { + let value = evaluate_expr(fiber, expr).await?; + ControlFlow::Break(BreakAction::Return(value)) }, Statement::Pipeline(pipeline) => evaluate_pipeline(fiber, pipeline).await, Statement::Assignment(AssignmentStatement {target, value}) => { @@ -162,7 +161,7 @@ async fn evaluate_statement(fiber: &mut Fiber, statement: Statement) -> Result { @@ -180,13 +179,13 @@ async fn evaluate_statement(fiber: &mut Fiber, statement: Statement) -> Result Result<(), Exception> { - let module_contents = fiber.load_module(statement.path.as_str()).await?; +async fn evaluate_import_statement(fiber: &mut Fiber, statement: ImportStatement) -> ControlFlow<()> { + let module_contents = result_to_control_flow(fiber.load_module(statement.path.as_str()).await)?; match statement.clause { ImportClause::Items(imports) => { @@ -206,10 +205,10 @@ async fn evaluate_import_statement(fiber: &mut Fiber, statement: ImportStatement } } - Ok(()) + Continue(()) } -async fn evaluate_pipeline(fiber: &mut Fiber, pipeline: Pipeline) -> Result { +async fn evaluate_pipeline(fiber: &mut Fiber, pipeline: Pipeline) -> ControlFlow { match pipeline.0.len() { // If there's only one call in the pipeline, we don't need to fork and // can just execute the function by itself. @@ -220,27 +219,34 @@ async fn evaluate_pipeline(fiber: &mut Fiber, pipeline: Pipeline) -> Result { let mut futures = Vec::new(); - let mut ios = fiber.io.try_clone()?.split_n(count)?.into_iter(); + let mut ios = match fiber.io.try_clone().and_then(|io| io.split_n(count)) { + Ok(io) => io.into_iter(), + Err(e) => return ControlFlow::Break(BreakAction::Throw(e.into())), + }; for call in pipeline.0.iter() { let mut fiber = fiber.fork(); fiber.io = ios.next().unwrap(); futures.push(async move { - evaluate_call(&mut fiber, call.clone()).await + match evaluate_call(&mut fiber, call.clone()).await { + Continue(value) => Ok(value), + ControlFlow::Break(action) => Err(action), + } }); } - try_join_all(futures) - .await - .map(Value::List) + match try_join_all(futures).await { + Ok(values) => Continue(Value::List(values)), + Err(action) => ControlFlow::Break(action), + } } } } #[async_recursion::async_recursion(?Send)] -async fn evaluate_call(fiber: &mut Fiber, call: Call) -> Result { - match call { +async fn evaluate_call(fiber: &mut Fiber, call: Call) -> ControlFlow { + result_to_control_flow(match call { Call::Named {function, args} => { let name = function; let function = fiber.get(&name); @@ -258,10 +264,10 @@ async fn evaluate_call(fiber: &mut Fiber, call: Call) -> Result) -> Result, Exception> { +async fn evaluate_call_args(fiber: &mut Fiber, args: Vec) -> ControlFlow> { let mut arg_values = Vec::with_capacity(args.len()); for arg in args { @@ -275,21 +281,21 @@ async fn evaluate_call_args(fiber: &mut Fiber, args: Vec) -> Result Result { +async fn evaluate_expr(fiber: &mut Fiber, expr: Expr) -> ControlFlow { match expr { - Expr::Number(number) => Ok(Value::Number(number)), - Expr::String(string) => Ok(Value::from(string)), - Expr::Regex(RegexLiteral(src)) => Ok(Value::Regex(src)), + Expr::Number(number) => Continue(Value::Number(number)), + Expr::String(string) => Continue(Value::from(string)), + Expr::Regex(RegexLiteral(src)) => Continue(Value::Regex(src)), Expr::CvarReference(cvar) => evaluate_cvar(fiber, cvar).await, Expr::CvarScope(cvar_scope) => evaluate_cvar_scope(fiber, cvar_scope).await, Expr::Substitution(substitution) => evaluate_substitution(fiber, substitution).await, @@ -298,41 +304,52 @@ async fn evaluate_expr(fiber: &mut Fiber, expr: Expr) -> Result evaluate_interpolated_string(fiber, string).await, Expr::MemberAccess(MemberAccess(lhs, rhs)) => evaluate_member_access(fiber, *lhs, rhs).await, Expr::Block(block) => evaluate_block(fiber, block), + Expr::Subroutine(subroutine) => evaluate_subroutine(fiber, subroutine), Expr::Pipeline(pipeline) => evaluate_pipeline(fiber, pipeline).await, } } -fn evaluate_block(fiber: &mut Fiber, block: Block) -> Result { - compile_block(fiber, block).map(Value::from) +fn evaluate_block(fiber: &mut Fiber, block: Block) -> ControlFlow { + Continue(compile_block(fiber, block).into()) +} + +fn evaluate_subroutine(fiber: &mut Fiber, subroutine: Subroutine) -> ControlFlow { + let variable_name = RipString::from(subroutine.name.as_str()); + let closure = compile_block(fiber, subroutine.block).with_name(subroutine.name); + let value = Value::Block(closure); + + fiber.set(variable_name, value.clone()); + + Continue(value) } -async fn evaluate_member_access(fiber: &mut Fiber, lhs: Expr, rhs: String) -> Result { - Ok(evaluate_expr(fiber, lhs).await?.get(rhs)) +async fn evaluate_member_access(fiber: &mut Fiber, lhs: Expr, rhs: String) -> ControlFlow { + Continue(evaluate_expr(fiber, lhs).await?.get(rhs)) } -async fn evaluate_cvar(fiber: &mut Fiber, cvar: CvarReference) -> Result { - Ok(fiber.get_cvar(cvar.0)) +async fn evaluate_cvar(fiber: &mut Fiber, cvar: CvarReference) -> ControlFlow { + Continue(fiber.get_cvar(cvar.0)) } -async fn evaluate_cvar_scope(fiber: &mut Fiber, cvar_scope: CvarScope) -> Result { - let closure = compile_block(fiber, cvar_scope.scope)?; +async fn evaluate_cvar_scope(fiber: &mut Fiber, cvar_scope: CvarScope) -> ControlFlow { + let closure = compile_block(fiber, cvar_scope.scope); let cvars = table! { cvar_scope.name.0 => evaluate_expr(fiber, *cvar_scope.value).await?, }; - invoke_closure(fiber, &closure, vec![], cvars, table!()).await + result_to_control_flow(invoke_closure(fiber, &closure, vec![], cvars, table!()).await) } -async fn evaluate_substitution(fiber: &mut Fiber, substitution: Substitution) -> Result { +async fn evaluate_substitution(fiber: &mut Fiber, substitution: Substitution) -> ControlFlow { match substitution { - Substitution::Variable(name) => Ok(fiber.get(name)), + Substitution::Variable(name) => Continue(fiber.get(name)), Substitution::Pipeline(pipeline) => evaluate_pipeline(fiber, pipeline).await, _ => unimplemented!(), } } -async fn evaluate_table_literal(fiber: &mut Fiber, literal: TableLiteral) -> Result { +async fn evaluate_table_literal(fiber: &mut Fiber, literal: TableLiteral) -> ControlFlow { let table = Table::default(); for entry in literal.0 { @@ -342,20 +359,20 @@ async fn evaluate_table_literal(fiber: &mut Fiber, literal: TableLiteral) -> Res table.set(key.to_string(), value); } - Ok(Value::from(table)) + Continue(Value::from(table)) } -async fn evaluate_list_literal(fiber: &mut Fiber, list: ListLiteral) -> Result { +async fn evaluate_list_literal(fiber: &mut Fiber, list: ListLiteral) -> ControlFlow { let mut values = Vec::new(); for expr in list.0 { values.push(evaluate_expr(fiber, expr).await?); } - Ok(Value::List(values)) + Continue(Value::List(values)) } -async fn evaluate_interpolated_string(fiber: &mut Fiber, string: InterpolatedString) -> Result { +async fn evaluate_interpolated_string(fiber: &mut Fiber, string: InterpolatedString) -> ControlFlow { let mut rendered = String::new(); for part in string.0.into_iter() { @@ -365,5 +382,12 @@ async fn evaluate_interpolated_string(fiber: &mut Fiber, string: InterpolatedStr }.as_str()); } - Ok(Value::from(rendered)) + Continue(Value::from(rendered)) +} + +fn result_to_control_flow(result: Result) -> ControlFlow { + match result { + Ok(value) => Continue(value), + Err(exception) => ControlFlow::Break(BreakAction::Throw(exception)), + } } diff --git a/runtime/src/fiber.rs b/runtime/src/fiber.rs index 9ca0595..048aabf 100644 --- a/runtime/src/fiber.rs +++ b/runtime/src/fiber.rs @@ -1,11 +1,14 @@ -use super::{ - eval, exceptions::Exception, scope::Scope, string::RipString, - table::Table, value::Value, -}; use crate::{ + eval, + exceptions::Exception, io::{IoContext, Input, Output}, modules::{ModuleIndex, NativeModule}, + scope::Scope, + string::RipString, syntax::source::SourceFile, + table, + table::Table, + value::Value, }; use gc::Gc; use std::{rc::Rc, sync::atomic::{AtomicUsize, Ordering}}; diff --git a/runtime/src/init.rt b/runtime/src/init.rt index 0f230de..ad6ebd2 100644 --- a/runtime/src/init.rt +++ b/runtime/src/init.rt @@ -1,4 +1,4 @@ -import 'builtins' for pass +import 'builtins' for * $GLOBALS->modules = [ # A list of module loader functions. @@ -8,6 +8,6 @@ $GLOBALS->modules = [ loaded: [:] ] -$GLOBALS->pwd = { - pass @cwd +$GLOBALS->pwd = sub pwd { + return @cwd } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 3d33d16..cfcac84 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,15 +1,14 @@ use std::{env, time::Instant}; -#[macro_use] -mod macros; - mod builtins; mod closure; +mod controlflow; mod eval; mod exceptions; mod fiber; mod foreign; pub mod io; +mod macros; mod modules; mod scope; mod string; diff --git a/runtime/src/macros.rs b/runtime/src/macros.rs index 7297a9a..09b1c6f 100644 --- a/runtime/src/macros.rs +++ b/runtime/src/macros.rs @@ -1,25 +1,3 @@ -/// Convenience macro for creating a table. -#[macro_export] -macro_rules! table { - () => { - $crate::Table::default() - }; - - ( - $( - $key:expr => $value:expr, - )* - ) => { - { - let table = table!(); - $( - table.set($key, $crate::Value::from($value)); - )* - table - } - }; -} - /// Convenience macro for throwing a runtime exception. #[macro_export] macro_rules! throw { diff --git a/runtime/src/modules.rs b/runtime/src/modules.rs index f132cb3..b242a86 100644 --- a/runtime/src/modules.rs +++ b/runtime/src/modules.rs @@ -3,6 +3,7 @@ use crate::{ prelude::*, syntax::source::SourceFile, + throw, }; use std::{ cell::RefCell, diff --git a/runtime/src/table.rs b/runtime/src/table.rs index d080eef..759ef18 100644 --- a/runtime/src/table.rs +++ b/runtime/src/table.rs @@ -7,6 +7,28 @@ use std::{ iter::FromIterator, }; +/// Convenience macro for creating a table. +#[macro_export] +macro_rules! table { + () => { + $crate::Table::default() + }; + + ( + $( + $key:expr => $value:expr, + )* + ) => { + { + let table = table!(); + $( + table.set($key, $crate::Value::from($value)); + )* + table + } + }; +} + /// Implementation of a "table". Tables are used like a map or object. /// /// Only string keys are allowed. diff --git a/rustfmt.toml b/rustfmt.toml index 3fcae1d..224c419 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,4 +1,3 @@ -edition = "2018" imports_layout = "HorizontalVertical" merge_imports = true overflow_delimited_expr = true diff --git a/stdlib/src/init.rt b/stdlib/src/init.rt index e410f39..41bb4c6 100644 --- a/stdlib/src/init.rt +++ b/stdlib/src/init.rt @@ -19,7 +19,7 @@ $GLOBALS->eq = $eq $GLOBALS->command = $command # Evaluates a string as code. -$GLOBALS->eval =