diff --git a/Changelog.md b/Changelog.md index 651b5d6..19253da 100644 --- a/Changelog.md +++ b/Changelog.md @@ -4,6 +4,13 @@ - Fixed some formatting issues when converting Markdown to Text Mesh Pro for Stationpedia +- Added support for ternary expressions + - `let i = someValue ? 4 : 5;` + - `i = someValue ? 4 : 5;` + - This greedily evaluates both sides, so side effects like calling functions + is not recommended i.e. + - `i = someValue : doSomething() : doSomethingElse();` + - Both sides will be evaluated before calling the `select` instruction [0.2.1] diff --git a/csharp_mod/Patches.cs b/csharp_mod/Patches.cs index a96809e..7f09926 100644 --- a/csharp_mod/Patches.cs +++ b/csharp_mod/Patches.cs @@ -12,6 +12,7 @@ public static class SlangPatches { private static ProgrammableChipMotherboard? _currentlyEditingMotherboard; private static AsciiString? _motherboardCachedCode; + private static Guid? _currentlyEditingGuid; [HarmonyPatch( typeof(ProgrammableChipMotherboard), @@ -32,11 +33,13 @@ public static class SlangPatches return; } - var thisRef = Guid.NewGuid(); + var thisRef = _currentlyEditingGuid ?? Guid.NewGuid(); // Ensure we cache this compiled code for later retreival. GlobalCode.SetSource(thisRef, result); + _currentlyEditingGuid = null; + // Append REF to the bottom compiled += $"\n{GlobalCode.SLANG_REF}{thisRef}"; result = compiled; @@ -77,6 +80,7 @@ public static class SlangPatches return; } + _currentlyEditingGuid = sourceRef; var slangSource = GlobalCode.GetSource(sourceRef); if (string.IsNullOrEmpty(slangSource)) @@ -223,6 +227,7 @@ public static class SlangPatches _currentlyEditingMotherboard = null; _motherboardCachedCode = null; + _currentlyEditingGuid = null; } [HarmonyPatch(typeof(Stationpedia), nameof(Stationpedia.Regenerate))] diff --git a/rust_compiler/libs/compiler/src/test/binary_expression.rs b/rust_compiler/libs/compiler/src/test/binary_expression.rs index 757a0e0..f3a1258 100644 --- a/rust_compiler/libs/compiler/src/test/binary_expression.rs +++ b/rust_compiler/libs/compiler/src/test/binary_expression.rs @@ -1,9 +1,10 @@ use crate::compile; +use anyhow::Result; use indoc::indoc; use pretty_assertions::assert_eq; #[test] -fn simple_binary_expression() -> anyhow::Result<()> { +fn simple_binary_expression() -> Result<()> { let compiled = compile! { debug " @@ -26,7 +27,7 @@ fn simple_binary_expression() -> anyhow::Result<()> { } #[test] -fn nested_binary_expressions() -> anyhow::Result<()> { +fn nested_binary_expressions() -> Result<()> { let compiled = compile! { debug " @@ -71,7 +72,7 @@ fn nested_binary_expressions() -> anyhow::Result<()> { } #[test] -fn stress_test_constant_folding() -> anyhow::Result<()> { +fn stress_test_constant_folding() -> Result<()> { let compiled = compile! { debug " @@ -94,7 +95,7 @@ fn stress_test_constant_folding() -> anyhow::Result<()> { } #[test] -fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> { +fn test_constant_folding_with_variables_mixed_in() -> Result<()> { let compiled = compile! { debug r#" @@ -120,3 +121,55 @@ fn test_constant_folding_with_variables_mixed_in() -> anyhow::Result<()> { Ok(()) } + +#[test] +fn test_ternary_expression() -> Result<()> { + let compiled = compile! { + debug + r#" + let i = 1 > 2 ? 15 : 20; + "# + }; + + assert_eq!( + compiled, + indoc! { + " + j main + main: + sgt r1 1 2 + select r2 r1 15 20 + move r8 r2 #i + " + } + ); + + Ok(()) +} + +#[test] +fn test_ternary_expression_assignment() -> Result<()> { + let compiled = compile! { + debug + r#" + let i = 0; + i = 1 > 2 ? 15 : 20; + "# + }; + + assert_eq!( + compiled, + indoc! { + " + j main + main: + move r8 0 #i + sgt r1 1 2 + select r2 r1 15 20 + move r8 r2 #i + " + } + ); + + Ok(()) +} diff --git a/rust_compiler/libs/compiler/src/v1.rs b/rust_compiler/libs/compiler/src/v1.rs index aab52ff..7df2875 100644 --- a/rust_compiler/libs/compiler/src/v1.rs +++ b/rust_compiler/libs/compiler/src/v1.rs @@ -8,7 +8,7 @@ use parser::{ AssignmentExpression, BinaryExpression, BlockExpression, ConstDeclarationExpression, DeviceDeclarationExpression, Expression, FunctionExpression, IfExpression, InvocationExpression, Literal, LiteralOr, LiteralOrVariable, LogicalExpression, - LoopExpression, MemberAccessExpression, Span, Spanned, WhileExpression, + LoopExpression, MemberAccessExpression, Span, Spanned, TernaryExpression, WhileExpression, }, }; use std::{ @@ -145,6 +145,7 @@ pub struct CompilerConfig { pub debug: bool, } +#[derive(Debug)] struct CompilationResult<'a> { location: VariableLocation<'a>, /// If Some, this is the name of the temporary variable that holds the result. @@ -313,6 +314,7 @@ impl<'a, 'w, W: std::io::Write> Compiler<'a, 'w, W> { self.expression_assignment(assign_expr.node, scope)?; Ok(None) } + Expression::Ternary(tern) => Ok(Some(self.expression_ternary(tern.node, scope)?)), Expression::Invocation(expr_invoke) => { self.expression_function_invocation(expr_invoke, scope)?; // Invocation returns result in r15 (RETURN_REGISTER). @@ -740,6 +742,23 @@ impl<'a, 'w, W: std::io::Write> Compiler<'a, 'w, W> { (var_loc, None) } + Expression::Ternary(ternary) => { + let res = self.expression_ternary(ternary.node, scope)?; + println!("{res:?}"); + let var_loc = scope.add_variable( + name_str.clone(), + LocationRequest::Persist, + Some(name_span), + )?; + + let res_register = self.resolve_register(&res.location)?; + self.emit_variable_assignment(name_str, &var_loc, res_register)?; + + if let Some(name) = res.temp_name { + scope.free_temp(name, None)?; + } + (var_loc, None) + } _ => { return Err(Error::Unknown( format!("`{name_str}` declaration of this type is not supported/implemented."), @@ -1208,6 +1227,45 @@ impl<'a, 'w, W: std::io::Write> Compiler<'a, 'w, W> { } } + fn expression_ternary( + &mut self, + expr: TernaryExpression<'a>, + scope: &mut VariableScope<'a, '_>, + ) -> Result, Error<'a>> { + let TernaryExpression { + condition, + true_value, + false_value, + } = expr; + + let (cond, cond_clean) = self.compile_operand(*condition, scope)?; + let (true_val, true_clean) = self.compile_operand(*true_value, scope)?; + let (false_val, false_clean) = self.compile_operand(*false_value, scope)?; + + let result_name = self.next_temp_name(); + let result_loc = scope.add_variable(result_name.clone(), LocationRequest::Temp, None)?; + let result_reg = self.resolve_register(&result_loc)?; + + self.write_output(format!( + "select {} {} {} {}", + result_reg, cond, true_val, false_val + ))?; + + if let Some(clean) = cond_clean { + scope.free_temp(clean, None)?; + } + if let Some(clean) = true_clean { + scope.free_temp(clean, None)?; + } + if let Some(clean) = false_clean { + scope.free_temp(clean, None)?; + } + Ok(CompilationResult { + location: result_loc, + temp_name: Some(result_name), + }) + } + /// Helper to resolve a location to a register string (e.g., "r0"). /// Note: This does not handle Stack locations automatically, as they require /// instruction emission to load. Use `compile_operand` for general handling. diff --git a/rust_compiler/libs/parser/src/lib.rs b/rust_compiler/libs/parser/src/lib.rs index bcba95d..b81e73f 100644 --- a/rust_compiler/libs/parser/src/lib.rs +++ b/rust_compiler/libs/parser/src/lib.rs @@ -293,12 +293,12 @@ impl<'a> Parser<'a> { // Handle Infix operators (Binary, Logical, Assignment) if self_matches_peek!( self, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question) ) { return Ok(Some(self.infix(lhs)?)); } else if self_matches_current!( self, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question) ) { self.tokenizer.seek(SeekFrom::Current(-1))?; return Ok(Some(self.infix(lhs)?)); @@ -769,6 +769,7 @@ impl<'a> Parser<'a> { | Expression::Priority(_) | Expression::Literal(_) | Expression::Variable(_) + | Expression::Ternary(_) | Expression::Negation(_) | Expression::MemberAccess(_) | Expression::MethodCall(_) => {} @@ -788,7 +789,7 @@ impl<'a> Parser<'a> { // Include Assign in the operator loop while token_matches!( temp_token, - TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign) + TokenType::Symbol(s) if s.is_operator() || s.is_comparison() || s.is_logical() || matches!(s, Symbol::Assign | Symbol::Question | Symbol::Colon) ) { let operator = match temp_token.token_type { TokenType::Symbol(s) => s, @@ -1019,7 +1020,52 @@ impl<'a> Parser<'a> { } operators.retain(|symbol| !matches!(symbol, Symbol::LogicalOr)); - // --- PRECEDENCE LEVEL 8: Assignment (=) --- + // -- PRECEDENCE LEVEL 8: Ternary (x ? 1 : 2) + for i in (0..operators.len()).rev() { + if matches!(operators[i], Symbol::Question) { + // Ensure next operator is a colon + if i + 1 >= operators.len() || !matches!(operators[i + 1], Symbol::Colon) { + return Err(Error::InvalidSyntax( + self.current_span(), + "Ternary operator '?' missing matching ':'".to_string(), + )); + } + + let false_branch = expressions.remove(i + 2); + let true_branch = expressions.remove(i + 1); + let condition = expressions.remove(i); + + let span = Span { + start_line: condition.span.start_line, + end_line: false_branch.span.end_line, + start_col: condition.span.start_col, + end_col: false_branch.span.end_col, + }; + + let ternary_node = Spanned { + span, + node: TernaryExpression { + condition: Box::new(condition), + true_value: Box::new(true_branch), + false_value: Box::new(false_branch), + }, + }; + + expressions.insert( + i, + Spanned { + node: Expression::Ternary(ternary_node), + span, + }, + ); + + // Remove the `?` and the `:` from the operators list + operators.remove(i); + operators.remove(i); + } + } + + // --- PRECEDENCE LEVEL 9: Assignment (=) --- // Assignment is Right Associative: a = b = c => a = (b = c) // We iterate Right to Left for (i, operator) in operators.iter().enumerate().rev() { diff --git a/rust_compiler/libs/parser/src/test/mod.rs b/rust_compiler/libs/parser/src/test/mod.rs index c78111a..7c7c7f5 100644 --- a/rust_compiler/libs/parser/src/test/mod.rs +++ b/rust_compiler/libs/parser/src/test/mod.rs @@ -160,3 +160,37 @@ fn test_negative_literal_const() -> Result<()> { Ok(()) } + +#[test] +fn test_ternary_expression() -> Result<()> { + let expr = parser!(r#"let i = x ? 1 : 2;"#).parse()?.unwrap(); + + assert_eq!("(let i = (x ? 1 : 2))", expr.to_string()); + Ok(()) +} + +#[test] +fn test_complex_binary_with_ternary() -> Result<()> { + let expr = parser!("let i = (x ? 1 : 3) * 2;").parse()?.unwrap(); + + assert_eq!("(let i = ((x ? 1 : 3) * 2))", expr.to_string()); + + Ok(()) +} + +#[test] +fn test_operator_prescedence_with_ternary() -> Result<()> { + let expr = parser!("let x = x ? 1 : 3 * 2;").parse()?.unwrap(); + + assert_eq!("(let x = (x ? 1 : (3 * 2)))", expr.to_string()); + + Ok(()) +} + +#[test] +fn test_nested_ternary_right_associativity() -> Result<()> { + let expr = parser!("let i = a ? b : c ? d : e;").parse()?.unwrap(); + + assert_eq!("(let i = (a ? b : (c ? d : e)))", expr.to_string()); + Ok(()) +} diff --git a/rust_compiler/libs/parser/src/tree_node.rs b/rust_compiler/libs/parser/src/tree_node.rs index 8e221a8..60b4311 100644 --- a/rust_compiler/libs/parser/src/tree_node.rs +++ b/rust_compiler/libs/parser/src/tree_node.rs @@ -277,6 +277,23 @@ pub struct WhileExpression<'a> { pub body: BlockExpression<'a>, } +#[derive(Debug, PartialEq, Eq)] +pub struct TernaryExpression<'a> { + pub condition: Box>>, + pub true_value: Box>>, + pub false_value: Box>>, +} + +impl<'a> std::fmt::Display for TernaryExpression<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({} ? {} : {})", + self.condition, self.true_value, self.false_value + ) + } +} + impl<'a> std::fmt::Display for WhileExpression<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "(while {} {})", self.condition, self.body) @@ -366,6 +383,7 @@ pub enum Expression<'a> { Priority(Box>>), Return(Box>>), Syscall(Spanned>), + Ternary(Spanned>), Variable(Spanned>), While(Spanned>), } @@ -393,6 +411,7 @@ impl<'a> std::fmt::Display for Expression<'a> { Expression::Priority(e) => write!(f, "({})", e), Expression::Return(e) => write!(f, "(return {})", e), Expression::Syscall(e) => write!(f, "{}", e), + Expression::Ternary(e) => write!(f, "{}", e), Expression::Variable(id) => write!(f, "{}", id), Expression::While(e) => write!(f, "{}", e), } diff --git a/rust_compiler/libs/tokenizer/src/token.rs b/rust_compiler/libs/tokenizer/src/token.rs index 7127d95..2b14066 100644 --- a/rust_compiler/libs/tokenizer/src/token.rs +++ b/rust_compiler/libs/tokenizer/src/token.rs @@ -225,6 +225,7 @@ pub enum TokenType<'a> { #[token(".", symbol!(Dot))] #[token("^", symbol!(Caret))] #[token("%", symbol!(Percent))] + #[token("?", symbol!(Question))] #[token("==", symbol!(Equal))] #[token("!=", symbol!(NotEqual))] #[token("&&", symbol!(LogicalAnd))] @@ -535,6 +536,8 @@ pub enum Symbol { Caret, /// Represents the `%` symbol Percent, + /// Represents the `?` symbol + Question, // Double Character Symbols /// Represents the `==` symbol @@ -601,6 +604,7 @@ impl std::fmt::Display for Symbol { Self::Asterisk => write!(f, "*"), Self::Slash => write!(f, "/"), Self::LessThan => write!(f, "<"), + Self::Question => write!(f, "?"), Self::LessThanOrEqual => write!(f, "<="), Self::GreaterThan => write!(f, ">"), Self::GreaterThanOrEqual => write!(f, ">="),