From f45b8c3a415b5a2a6ece56117472b0efa4028d90 Mon Sep 17 00:00:00 2001 From: spookydonut Date: Wed, 4 Mar 2020 19:58:21 +0800 Subject: [PATCH] Add basic unreachable code detection (#123) * brancheval * truthy * mostly finished * finished * a * stupid negative steps * add tests * remove debugging prints --- src/dreamchecker/lib.rs | 230 ++++++++++++++++++-- src/dreamchecker/test_helpers.rs | 30 ++- src/dreamchecker/tests/branch_eval_tests.rs | 74 +++++++ src/dreamchecker/type_expr.rs | 152 +++++++++---- src/dreammaker/ast.rs | 145 ++++++++++++ src/dreammaker/parser.rs | 5 + src/langserver/debugger/local_names.rs | 1 + src/langserver/find_references.rs | 1 + 8 files changed, 571 insertions(+), 67 deletions(-) create mode 100644 src/dreamchecker/tests/branch_eval_tests.rs diff --git a/src/dreamchecker/lib.rs b/src/dreamchecker/lib.rs index 101cf70e0..1fe49ea6e 100644 --- a/src/dreamchecker/lib.rs +++ b/src/dreamchecker/lib.rs @@ -949,6 +949,91 @@ pub fn check_var_defs(objtree: &ObjectTree, context: &Context) { // ---------------------------------------------------------------------------- // Procedure analyzer +#[derive(Debug)] +pub struct ControlFlow { + pub returns: bool, + pub continues: bool, + pub breaks: bool, + pub fuzzy: bool, +} + +impl ControlFlow { + pub fn alltrue() -> ControlFlow { + ControlFlow { + returns: true, + continues: true, + breaks: true, + fuzzy: false, + } + } + pub fn allfalse() -> ControlFlow { + ControlFlow { + returns: false, + continues: false, + breaks: false, + fuzzy: false, + } + } + pub fn terminates(&self) -> bool { + return !self.fuzzy && ( self.returns || self.continues || self.breaks ) + } + + pub fn terminates_loop(&self) -> bool { + return !self.fuzzy && ( self.returns || self.breaks ) + } + + pub fn no_else(&mut self) { + self.returns = false; + self.continues = false; + self.breaks = false; + } + + pub fn merge(&mut self, other: ControlFlow) { + if !self.fuzzy && other.returns { + self.returns = true; + } + if other.fuzzy { + self.returns = false; + } + if other.continues { + self.continues = true; + } + if other.breaks { + self.breaks = true; + } + if other.fuzzy { + self.fuzzy = true; + } + } + + pub fn merge_false(&mut self, other: ControlFlow) { + if !other.returns { + self.returns = false; + } + if !other.continues { + self.continues = false; + } + if !other.breaks { + self.breaks = false; + } + if other.fuzzy { + self.fuzzy = true; + } + } + + pub fn finalize(&mut self) { + if self.returns || self.breaks || self.continues { + self.fuzzy = false; + } + } + + pub fn end_loop(&mut self) { + self.returns = false; + self.continues = false; + self.breaks = false; + self.fuzzy = false; + } +} #[derive(Debug, Clone)] struct LocalVar<'o> { @@ -1047,13 +1132,48 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } } - fn visit_block(&mut self, block: &'o [Spanned], local_vars: &mut HashMap>) { + fn visit_block(&mut self, block: &'o [Spanned], local_vars: &mut HashMap>) -> ControlFlow { + let mut term = ControlFlow::allfalse(); for stmt in block.iter() { - self.visit_statement(stmt.location, &stmt.elem, local_vars); + if term.terminates() { + error(stmt.location,"possible unreachable code here") + .register(self.context); + return term // stop evaluating + } + let state = self.visit_statement(stmt.location, &stmt.elem, local_vars); + term.merge(state); } + return term } - fn visit_statement(&mut self, location: Location, statement: &'o Statement, local_vars: &mut HashMap>) { + fn loop_condition_check(&mut self, location: Location, expression: &'o Expression) { + match expression.is_truthy() { + Some(true) => { + error(location,"loop condition is always true") + .register(self.context); + }, + Some(false) => { + error(location,"loop condition is always false") + .register(self.context); + } + _ => () + }; + } + + fn visit_control_condition(&mut self, location: Location, expression: &'o Expression) { + if expression.is_const_eval() { + error(location,"control flow condition is a constant evalutation") + .register(self.context); + } + else if let Some(term) = expression.as_term() { + if term.is_static() { + error(location,"control flow condition is a static term") + .register(self.context); + } + } + } + + fn visit_statement(&mut self, location: Location, statement: &'o Statement, local_vars: &mut HashMap>) -> ControlFlow { match statement { Statement::Expr(expr) => { match expr { @@ -1085,28 +1205,74 @@ impl<'o, 's> AnalyzeProc<'o, 's> { let return_type = self.visit_expression(location, expr, None, local_vars); local_vars.get_mut(".").unwrap().analysis = return_type; // TODO: break out of the analysis for this branch? + return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } + }, + Statement::Return(None) => { return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } }, + Statement::Crash(expr) => { + self.visit_expression(location, expr, None, local_vars); + return ControlFlow { returns: true, continues: false, breaks: false, fuzzy: false } }, - Statement::Return(None) => {}, Statement::Throw(expr) => { self.visit_expression(location, expr, None, local_vars); }, Statement::While { condition, block } => { let mut scoped_locals = local_vars.clone(); self.visit_expression(location, condition, None, &mut scoped_locals); - self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals); + state.end_loop(); + return state }, Statement::DoWhile { block, condition } => { let mut scoped_locals = local_vars.clone(); - self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals); + if state.terminates_loop() { + error(location,"do while terminates without ever reaching condition") + .register(self.context); + return state + } self.visit_expression(location, condition, None, &mut scoped_locals); + + state.end_loop(); + return state }, Statement::If { arms, else_arm } => { + let mut allterm = ControlFlow::alltrue(); + let mut alwaystrue = false; for (condition, ref block) in arms.iter() { let mut scoped_locals = local_vars.clone(); + self.visit_control_condition(condition.location, &condition.elem); + if alwaystrue { + error(condition.location,"unreachable if block, preceeding if/elseif condition(s) are always true") + .register(self.context); + } self.visit_expression(condition.location, &condition.elem, None, &mut scoped_locals); - self.visit_block(block, &mut scoped_locals); + let state = self.visit_block(block, &mut scoped_locals); + match condition.elem.is_truthy() { + Some(true) => { + error(condition.location,"if condition is always true") + .register(self.context); + allterm.merge_false(state); + alwaystrue = true; + }, + Some(false) => { + error(condition.location,"if condition is always false") + .register(self.context); + }, + None => allterm.merge_false(state) + }; } if let Some(else_arm) = else_arm { - self.visit_block(else_arm, &mut local_vars.clone()); + if alwaystrue { + // TODO: fix location for else blocks + error(location,"unreachable else block, preceeding if/elseif condition(s) are always true") + .register(self.context); + } + let state = self.visit_block(else_arm, &mut local_vars.clone()); + allterm.merge_false(state); + } else { + allterm.no_else(); + return allterm } + allterm.finalize(); + return allterm }, Statement::ForLoop { init, test, inc, block } => { let mut scoped_locals = local_vars.clone(); @@ -1114,12 +1280,16 @@ impl<'o, 's> AnalyzeProc<'o, 's> { self.visit_statement(location, init, &mut scoped_locals); } if let Some(test) = test { + self.loop_condition_check(location, test); + self.visit_control_condition(location, test); self.visit_expression(location, test, None, &mut scoped_locals); } if let Some(inc) = inc { self.visit_statement(location, inc, &mut scoped_locals); } - self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals); + state.end_loop(); + return state }, Statement::ForList { in_list, block, var_type, name, .. } => { let mut scoped_locals = local_vars.clone(); @@ -1129,7 +1299,9 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if let Some(var_type) = var_type { self.visit_var(location, var_type, name, None, &mut scoped_locals); } - self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals); + state.end_loop(); + return state }, Statement::ForRange { var_type, name, start, end, step, block } => { let mut scoped_locals = local_vars.clone(); @@ -1140,7 +1312,21 @@ impl<'o, 's> AnalyzeProc<'o, 's> { if let Some(var_type) = var_type { self.visit_var(location, var_type, name, Some(start), &mut scoped_locals); } - self.visit_block(block, &mut scoped_locals); + let mut state = self.visit_block(block, &mut scoped_locals); + if let Some(startterm) = start.as_term() { + if let Some(endterm) = end.as_term() { + if let Some(validity) = startterm.valid_for_range(endterm, step) { + if !validity { + error(location,"for range loop body is never reached due to invalid range") + .register(self.context); + } else { + return state + } + } + } + } + state.end_loop(); + return state }, Statement::Var(var) => self.visit_var_stmt(location, var, local_vars), Statement::Vars(vars) => { @@ -1150,7 +1336,7 @@ impl<'o, 's> AnalyzeProc<'o, 's> { }, Statement::Setting { name, mode: SettingMode::Assign, value } => { if name != "waitfor" { - return + return ControlFlow::allfalse() } match match value.as_term() { Some(Term::Int(0)) => Some(true), @@ -1172,6 +1358,8 @@ impl<'o, 's> AnalyzeProc<'o, 's> { self.inside_newcontext = self.inside_newcontext.wrapping_sub(1); }, Statement::Switch { input, cases, default } => { + let mut allterm = ControlFlow::alltrue(); + self.visit_control_condition(location, input); self.visit_expression(location, input, None, local_vars); for &(ref case, ref block) in cases.iter() { let mut scoped_locals = local_vars.clone(); @@ -1184,11 +1372,18 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } } } - self.visit_block(block, &mut scoped_locals); + let state = self.visit_block(block, &mut scoped_locals); + allterm.merge_false(state); } if let Some(default) = default { - self.visit_block(default, &mut local_vars.clone()); + let state = self.visit_block(default, &mut local_vars.clone()); + allterm.merge_false(state); + } else { + allterm.no_else(); + return allterm } + allterm.finalize(); + return allterm }, Statement::TryCatch { try_block, catch_params, catch_block } => { self.visit_block(try_block, &mut local_vars.clone()); @@ -1212,12 +1407,13 @@ impl<'o, 's> AnalyzeProc<'o, 's> { } self.visit_block(catch_block, &mut catch_locals); }, - Statement::Continue(_) => {}, - Statement::Break(_) => {}, + Statement::Continue(_) => { return ControlFlow { returns: false, continues: true, breaks: false, fuzzy: true } }, + Statement::Break(_) => { return ControlFlow { returns: false, continues: false, breaks: true, fuzzy: true } }, Statement::Goto(_) => {}, - Statement::Label { name: _, block } => self.visit_block(block, &mut local_vars.clone()), + Statement::Label { name: _, block } => { self.visit_block(block, &mut local_vars.clone()); }, Statement::Del(expr) => { self.visit_expression(location, expr, None, local_vars); }, } + return ControlFlow::allfalse() } fn visit_var_stmt(&mut self, location: Location, var: &'o VarStatement, local_vars: &mut HashMap>) { diff --git a/src/dreamchecker/test_helpers.rs b/src/dreamchecker/test_helpers.rs index 119c32731..64af9a7c7 100644 --- a/src/dreamchecker/test_helpers.rs +++ b/src/dreamchecker/test_helpers.rs @@ -1,13 +1,12 @@ -use dm::Context; use dm::objtree::Code; +use dm::Context; use std::borrow::Cow; -use crate::{AnalyzeObjectTree, check_var_defs}; +use crate::{check_var_defs, AnalyzeObjectTree}; pub const NO_ERRORS: &[(u32, u16, &str)] = &[]; pub fn parse_a_file_for_test>>(buffer: S) -> Context { - let context = Context::default(); let pp = dm::preprocessor::Preprocessor::from_buffer(&context, "unit_tests.rs".into(), buffer); @@ -36,9 +35,11 @@ pub fn parse_a_file_for_test>>(buffer: S) -> Context { Code::Present(ref code) => { analyzer.check_proc(proc, code); } - Code::Invalid(_) => {}, - Code::Builtin => {}, - Code::Disabled => panic!("proc parsing was enabled, but also disabled. this is a bug"), + Code::Invalid(_) => {} + Code::Builtin => {} + Code::Disabled => { + panic!("proc parsing was enabled, but also disabled. this is a bug") + } } } }); @@ -61,10 +62,19 @@ pub fn check_errors_match>>(buffer: S, errorlist: &[(u let mut iter = errors.iter(); for (line, column, desc) in errorlist { let nexterror = iter.next().unwrap(); - if nexterror.location().line != *line || nexterror.location().column != *column || nexterror.description() != *desc { - panic!(format!("possible feature regression in dreamchecker, expected {}:{}:{}, found {}:{}:{}", - *line, *column, *desc, - nexterror.location().line, nexterror.location().column, nexterror.description())); + if nexterror.location().line != *line + || nexterror.location().column != *column + || nexterror.description() != *desc + { + panic!(format!( + "possible feature regression in dreamchecker, expected {}:{}:{}, found {}:{}:{}", + *line, + *column, + *desc, + nexterror.location().line, + nexterror.location().column, + nexterror.description() + )); } } if iter.next().is_some() { diff --git a/src/dreamchecker/tests/branch_eval_tests.rs b/src/dreamchecker/tests/branch_eval_tests.rs new file mode 100644 index 000000000..4b12cb4ac --- /dev/null +++ b/src/dreamchecker/tests/branch_eval_tests.rs @@ -0,0 +1,74 @@ + +extern crate dreamchecker as dc; + +use dc::test_helpers::*; + +pub const CONST_EVAL_ERRORS: &[(u32, u16, &str)] = &[ + (2, 7, "control flow condition is a static term"), + (2, 7, "if condition is always true"), +]; + +#[test] +fn const_eval() { + let code = r##" +/proc/test() + if(1) + return + return +"##.trim(); + check_errors_match(code, CONST_EVAL_ERRORS); +} + +pub const IF_ELSE_ERRORS: &[(u32, u16, &str)] = &[ + (6, 5, "possible unreachable code here"), +]; + +#[test] +fn if_else() { + let code = r##" +/proc/test() + if(prob(50)) + return + else + return + return +"##.trim(); + check_errors_match(code, IF_ELSE_ERRORS); +} + +pub const IF_ARMS_ERRORS: &[(u32, u16, &str)] = &[ + (2, 7, "control flow condition is a static term"), + (2, 7, "if condition is always true"), + (4, 12, "unreachable if block, preceeding if/elseif condition(s) are always true"), + // TODO: fix location reporting on this + (2, 5, "unreachable else block, preceeding if/elseif condition(s) are always true"), +]; + +#[test] +fn if_arms() { + let code = r##" +/proc/test() + if(1) + return + else if(prob(50)) + return + else + return +"##.trim(); + check_errors_match(code, IF_ARMS_ERRORS); +} + +pub const DO_WHILE_ERRORS: &[(u32, u16, &str)] = &[ + (2, 5, "do while terminates without ever reaching condition"), +]; + +#[test] +fn do_while() { + let code = r##" +/proc/test() + do + return + while(prob(50)) +"##.trim(); + check_errors_match(code, DO_WHILE_ERRORS); +} diff --git a/src/dreamchecker/type_expr.rs b/src/dreamchecker/type_expr.rs index 0bf5d18cf..867f4aa48 100644 --- a/src/dreamchecker/type_expr.rs +++ b/src/dreamchecker/type_expr.rs @@ -3,17 +3,17 @@ use std::collections::HashMap; -use dm::{Location, DMError}; -use dm::objtree::{ObjectTree, ProcRef}; -use dm::constants::Constant; use dm::ast::*; +use dm::constants::Constant; +use dm::objtree::{ObjectTree, ProcRef}; +use dm::{DMError, Location}; -use crate::{StaticType, Analysis}; +use crate::{Analysis, StaticType}; pub struct TypeExprContext<'o, 't> { pub objtree: &'o ObjectTree, pub param_name_map: HashMap<&'t str, Analysis<'o>>, - pub param_idx_map: HashMap> + pub param_idx_map: HashMap>, } impl<'o, 't> TypeExprContext<'o, 't> { @@ -58,11 +58,23 @@ pub enum TypeExpr<'o> { } impl<'o> TypeExpr<'o> { - pub fn compile(proc: ProcRef<'o>, location: Location, expression: &Expression) -> Result, DMError> { - TypeExprCompiler { objtree: proc.tree(), proc }.visit_expression(location, expression) + pub fn compile( + proc: ProcRef<'o>, + location: Location, + expression: &Expression, + ) -> Result, DMError> { + TypeExprCompiler { + objtree: proc.tree(), + proc, + } + .visit_expression(location, expression) } - pub fn evaluate(&self, location: Location, ec: &TypeExprContext<'o, '_>) -> Result, DMError> { + pub fn evaluate( + &self, + location: Location, + ec: &TypeExprContext<'o, '_>, + ) -> Result, DMError> { match self { TypeExpr::Static(st) => Ok(st.clone()), @@ -72,9 +84,13 @@ impl<'o> TypeExpr<'o> { } else { else_.evaluate(location, ec) } - }, + } - TypeExpr::ParamTypepath { name, p_idx, index_ct: _ } => { + TypeExpr::ParamTypepath { + name, + p_idx, + index_ct: _, + } => { if let Some(analysis) = ec.get(name, *p_idx) { if let Some(Constant::Prefab(ref pop)) = analysis.value { crate::static_type(ec.objtree, location, &pop.path) @@ -84,15 +100,19 @@ impl<'o> TypeExpr<'o> { } else { Ok(StaticType::None) } - }, + } - TypeExpr::ParamStaticType { name, p_idx, index_ct } => { + TypeExpr::ParamStaticType { + name, + p_idx, + index_ct, + } => { if let Some(analysis) = ec.get(name, *p_idx) { Ok(analysis.static_ty.clone().strip_lists(*index_ct)) } else { Ok(StaticType::None) } - }, + } } } } @@ -109,11 +129,22 @@ struct TypeExprCompiler<'o> { } impl<'o> TypeExprCompiler<'o> { - fn visit_expression(&mut self, location: Location, expr: &Expression) -> Result, DMError> { + fn visit_expression( + &mut self, + location: Location, + expr: &Expression, + ) -> Result, DMError> { match expr { - Expression::Base { unary, term, follow } => { + Expression::Base { + unary, + term, + follow, + } => { if let Some(op) = unary.first() { - return Err(DMError::new(location, format!("type expr: bad unary {}", op.name()))); + return Err(DMError::new( + location, + format!("type expr: bad unary {}", op.name()), + )); } let mut ty = self.visit_term(term.location, &term.elem)?; @@ -121,8 +152,12 @@ impl<'o> TypeExprCompiler<'o> { ty = self.visit_follow(each.location, ty, &each.elem)?; } Ok(ty) - }, - Expression::BinaryOp { op: BinaryOp::Or, lhs, rhs } => { + } + Expression::BinaryOp { + op: BinaryOp::Or, + lhs, + rhs, + } => { // `A || B` => `A ? A : B` let lty = self.visit_expression(location, lhs)?; let rty = self.visit_expression(location, rhs)?; @@ -131,8 +166,12 @@ impl<'o> TypeExprCompiler<'o> { if_: Box::new(lty), else_: Box::new(rty), }) - }, - Expression::BinaryOp { op: BinaryOp::And, lhs, rhs } => { + } + Expression::BinaryOp { + op: BinaryOp::And, + lhs, + rhs, + } => { // `A && B` => `A ? B : A` let lty = self.visit_expression(location, lhs)?; let rty = self.visit_expression(location, rhs)?; @@ -141,14 +180,12 @@ impl<'o> TypeExprCompiler<'o> { if_: Box::new(rty), else_: Box::new(lty), }) - }, - Expression::TernaryOp { cond, if_, else_ } => { - Ok(TypeExpr::Condition { - cond: Box::new(self.visit_expression(location, cond)?), - if_: Box::new(self.visit_expression(location, if_)?), - else_: Box::new(self.visit_expression(location, else_)?), - }) - }, + } + Expression::TernaryOp { cond, if_, else_ } => Ok(TypeExpr::Condition { + cond: Box::new(self.visit_expression(location, cond)?), + if_: Box::new(self.visit_expression(location, if_)?), + else_: Box::new(self.visit_expression(location, else_)?), + }), _ => Err(DMError::new(location, "type expr: bad expression node")), } } @@ -160,11 +197,18 @@ impl<'o> TypeExprCompiler<'o> { Term::Ident(unscoped_name) => { for (i, param) in self.proc.parameters.iter().enumerate() { if *unscoped_name == param.name { - return Ok(TypeExpr::ParamTypepath { name: unscoped_name.to_owned(), p_idx: i, index_ct: 0 }); + return Ok(TypeExpr::ParamTypepath { + name: unscoped_name.to_owned(), + p_idx: i, + index_ct: 0, + }); } } - Err(DMError::new(location, format!("type expr: no such parameter {:?}", unscoped_name))) - }, + Err(DMError::new( + location, + format!("type expr: no such parameter {:?}", unscoped_name), + )) + } Term::Expr(expr) => self.visit_expression(location, expr), @@ -172,29 +216,57 @@ impl<'o> TypeExprCompiler<'o> { let bits: Vec<_> = fab.path.iter().map(|(_, name)| name.to_owned()).collect(); let ty = crate::static_type(self.objtree, location, &bits)?; Ok(TypeExpr::from(ty)) - }, + } _ => Err(DMError::new(location, "type expr: bad term node")), } } - fn visit_follow(&mut self, location: Location, lhs: TypeExpr<'o>, rhs: &Follow) -> Result, DMError> { + fn visit_follow( + &mut self, + location: Location, + lhs: TypeExpr<'o>, + rhs: &Follow, + ) -> Result, DMError> { match rhs { // X[_] => static type of argument X with one /list stripped Follow::Index(expr) => match expr.as_term() { Some(Term::Ident(name)) if name == "_" => match lhs { - TypeExpr::ParamTypepath { name, p_idx, index_ct } => - Ok(TypeExpr::ParamTypepath { name, p_idx, index_ct: index_ct + 1 }), - _ => Err(DMError::new(location, "type expr: cannot index non-parameters")), + TypeExpr::ParamTypepath { + name, + p_idx, + index_ct, + } => Ok(TypeExpr::ParamTypepath { + name, + p_idx, + index_ct: index_ct + 1, + }), + _ => Err(DMError::new( + location, + "type expr: cannot index non-parameters", + )), }, - _ => Err(DMError::new(location, "type expr: cannot index by anything but `_`")), + _ => Err(DMError::new( + location, + "type expr: cannot index by anything but `_`", + )), }, // X.type => static type of argument X Follow::Field(_, name) if name == "type" => match lhs { - TypeExpr::ParamTypepath { name, p_idx, index_ct } => - Ok(TypeExpr::ParamStaticType { name, p_idx, index_ct }), - _ => Err(DMError::new(location, "type expr: cannot take .type of non-parameters")), + TypeExpr::ParamTypepath { + name, + p_idx, + index_ct, + } => Ok(TypeExpr::ParamStaticType { + name, + p_idx, + index_ct, + }), + _ => Err(DMError::new( + location, + "type expr: cannot take .type of non-parameters", + )), }, _ => Err(DMError::new(location, "type expr: bad follow node")), diff --git a/src/dreammaker/ast.rs b/src/dreammaker/ast.rs index 49d6a2a56..4910d090a 100644 --- a/src/dreammaker/ast.rs +++ b/src/dreammaker/ast.rs @@ -390,6 +390,91 @@ impl Expression { _ => None, } } + + pub fn is_const_eval(&self) -> bool { + match self { + Expression::BinaryOp { op, lhs, rhs } => { + guard!(let Some(lhterm) = lhs.as_term() else { + return false + }); + guard!(let Some(rhterm) = rhs.as_term() else { + return false + }); + if !lhterm.is_static() { + return false + } + if !rhterm.is_static() { + return false + } + match op { + BinaryOp::Eq | + BinaryOp::NotEq | + BinaryOp::Less | + BinaryOp::Greater | + BinaryOp::LessEq | + BinaryOp::GreaterEq | + BinaryOp::And | + BinaryOp::Or => return true, + _ => return false, + } + }, + _ => false, + } + } + + pub fn is_truthy(&self) -> Option { + match self { + Expression::Base { unary, term, follow: _ } => { + guard!(let Some(truthy) = term.elem.is_truthy() else { + return None + }); + let mut negation = false; + for u in unary { + if let UnaryOp::Not = u { + negation = !negation; + } + } + if negation { + return Some(!truthy) + } else { + return Some(truthy) + } + }, + Expression::BinaryOp { op, lhs, rhs } => { + guard!(let Some(lhtruth) = lhs.is_truthy() else { + return None + }); + guard!(let Some(rhtruth) = rhs.is_truthy() else { + return None + }); + return match op { + BinaryOp::And => Some(lhtruth && rhtruth), + BinaryOp::Or => Some(lhtruth || rhtruth), + _ => None, + } + }, + Expression::AssignOp { op, lhs: _, rhs } => { + if let AssignOp::Assign = op { + return match rhs.as_term() { + Some(term) => term.is_truthy(), + _ => None, + } + } else { + return None + } + }, + Expression::TernaryOp { cond, if_, else_ } => { + guard!(let Some(condtruth) = cond.is_truthy() else { + return None + }); + if condtruth { + return if_.is_truthy() + } else { + return else_.is_truthy() + } + } + } + } } impl From for Expression { @@ -465,6 +550,65 @@ pub enum Term { DynamicCall(Vec, Vec), } +impl Term { + pub fn is_static(&self) -> bool { + return match self { + Term::Null | + Term::Int(_) | + Term::Float(_) | + Term::String(_) | + Term::Prefab(_) => true, + _ => false, + } + } + + pub fn is_truthy(&self) -> Option { + return match self { + // null is always false + Term::Null => Some(false), + // number literals are false if they're 0 + Term::Int(i) => Some(*i != 0), + Term::Float(i) => Some(*i != 0f32), + // empty strings are false + Term::String(s) => Some(s.len() > 0), + // recurse + Term::Expr(e) => e.is_truthy(), + // paths/prefabs are true + Term::Prefab(_) => Some(true), + // these always have a length therefore true + Term::InterpString(_, _) => Some(true), + // new is true if it succeeds, assume it does + Term::New{type_: _, args: _} => Some(true), + // since it returns a reference its true + Term::List(_) => Some(true), + + _ => None, + }; + } + + pub fn valid_for_range(&self, other: &Term, step: &Option) -> Option { + if let Term::Int(i) = self { + if let Term::Int(o) = other { + // edge case + if *i == 0 && *o == 0 { + return Some(false) + } + if let Some(stepexp) = step { + if let Some(stepterm) = stepexp.as_term() { + if let Term::Int(_s) = stepterm { + return Some(true) + } + } else { + return Some(true) + } + } + return Some(*i <= *o) + } + } + None + } +} + impl From for Term { fn from(expr: Expression) -> Term { match expr { @@ -862,6 +1006,7 @@ pub enum Statement { block: Block, }, Del(Expression), + Crash(Expression), } #[derive(Debug, Clone, PartialEq)] diff --git a/src/dreammaker/parser.rs b/src/dreammaker/parser.rs index 9507eb869..af2b20550 100644 --- a/src/dreammaker/parser.rs +++ b/src/dreammaker/parser.rs @@ -1491,6 +1491,11 @@ impl<'ctx, 'an, 'inp> Parser<'ctx, 'an, 'inp> { // statement :: 'return' expression ';' let expression = self.expression()?; success(Statement::Return(expression)) + } else if let Some(()) = self.exact_ident("CRASH")? { + require!(self.exact(Token::Punct(Punctuation::LParen))); + let expression = require!(self.expression()); + require!(self.exact(Token::Punct(Punctuation::RParen))); + success(Statement::Crash(expression)) } else if let Some(()) = self.exact_ident("throw")? { // statement :: 'throw' expression ';' let expression = require!(self.expression()); diff --git a/src/langserver/debugger/local_names.rs b/src/langserver/debugger/local_names.rs index 63ed17f68..ebd42c7cd 100644 --- a/src/langserver/debugger/local_names.rs +++ b/src/langserver/debugger/local_names.rs @@ -24,6 +24,7 @@ impl<'o> WalkProc<'o> { match statement { Statement::Expr(_) => {}, Statement::Return(_) => {}, + Statement::Crash(_) => {}, Statement::Throw(_) => {}, Statement::While { block, .. } => self.visit_block(block), Statement::DoWhile { block, .. } => self.visit_block(block), diff --git a/src/langserver/find_references.rs b/src/langserver/find_references.rs index 1bbe93d68..a5fd55d23 100644 --- a/src/langserver/find_references.rs +++ b/src/langserver/find_references.rs @@ -294,6 +294,7 @@ impl<'o> WalkProc<'o> { Statement::Continue(_) => {}, Statement::Break(_) => {}, Statement::Goto(_) => {}, + Statement::Crash(_) => {}, Statement::Label { name: _, block } => self.visit_block(block), Statement::Del(expr) => { self.visit_expression(location, expr, None); }, }