diff --git a/README.md b/README.md index a94c1d1..ad487a0 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,35 @@ Some instructions use bit offsets and widths. Width is directly attached to the offsets are attached at the respective arguments after a colon: `(ld16 r0:8 r1:32)` - load 16 bits, starting at offset 32 of `r1`, into `r0`, starting at offset 8. The rest if the register is not affected. +### Compile-time arithmetics + +Quite often you will want to do some calculations at compile time, for example to create a constant whose value depends on another, +or to compose a binary value using bit shifts and masking. This can be done in any place that expects an input operand (`Rd`). + +The syntax for this is as follows: + +``` +(ld r0 (=add 123 456)) +``` + +The expressions can be nested. The equals sign is not required in the inner levels, and the output operand must be omitted +(the compiler inserts a placeholder register there during evaluation): + +``` +(def WIDTH 1024) +(def HALFW_MAX (=sub (div WIDTH 2) 1)) +``` + +Almost any instruction can be evaluated this way, excluding instructions that perform IO, work with the screen, objects etc. +Instructions that are not compile-time evaluation safe will produce a compile error. + +There are several limitations to consider: + +- Only immediate values (literals) and constant names may be used. +- Registers, if used, will always read as zero and writes have no effect. (This should also produce an error) +- Only instructions that take the form `(op Wr ...)` can be used. The `Wr` operand is inserted automatically and must NOT be specified! +- Conditionals are not allowed inside expressions: branches, conditional suffixes + ### Conditional branches Conditonal branches are written like this: @@ -296,7 +325,9 @@ Jumping to a label is always safer than a manual skip. ; However, if the register is a global register, then the alias is valid everywhere. (sym SYM REG) -; Define a constant. These are valid in the whole program. +; Define a constant. These are valid in the entire program following the definition point, unless +; un-defined. Constants are evaluated and assigned at compile time, the program control flow has no +; effect. ; Value must be known at compile time. (def CONST VALUE) diff --git a/crsn/src/asm/instr/op.rs b/crsn/src/asm/instr/op.rs index b81799f..296c1ce 100644 --- a/crsn/src/asm/instr/op.rs +++ b/crsn/src/asm/instr/op.rs @@ -65,4 +65,11 @@ impl OpTrait for Op { se } + + fn is_compile_time_evaluable(&self) -> bool { + match &self.kind { + OpKind::BuiltIn(op) => op.is_compile_time_evaluable(), + OpKind::Ext(op) => op.is_compile_time_evaluable() + } + } } diff --git a/crsn/src/asm/parse/parse_data.rs b/crsn/src/asm/parse/parse_data.rs index c8251f6..3335a48 100644 --- a/crsn/src/asm/parse/parse_data.rs +++ b/crsn/src/asm/parse/parse_data.rs @@ -3,7 +3,7 @@ use std::convert::TryFrom; use sexp::{Atom, Sexp, SourcePosition}; -use crate::asm::data::{DataDisp, Rd, RdData, reg, Wr, WrData, RdWr}; +use crate::asm::data::{DataDisp, Rd, RdData, reg, Wr, WrData, RdWr, Register}; use crate::asm::data::literal::{ConstantName, Label, RegisterAlias, Value}; use crate::asm::error::CrsnError; use crate::asm::parse::{ParserContext, parse_instructions}; @@ -75,6 +75,7 @@ pub fn parse_label_str(name: &str, pos: &SourcePosition) -> Result Result { // trace!("parse data: {:?}", tok); match tok { + // immediates are okay in const eval - no tests needed Sexp::Atom(Atom::I(val), _pos) => { Ok(DataDisp::Immediate(unsafe { std::mem::transmute(val) })) } @@ -92,10 +93,12 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result { if let Some(Sexp::Atom(Atom::S(s), s_pos)) = list.first() { + // Const eval + let parsing_expr = pcx.state.borrow().parsing_expr; if !s.starts_with('=') && !parsing_expr { - return Err(CrsnError::Parse(format!("Invalid syntax, did you mean \"={}\"?", s).into(), s_pos.clone())); + return Err(CrsnError::Parse(format!("Invalid syntax, did you mean \"={}\" (a compile-time expression)?", s).into(), s_pos.clone())); } // start expr parsing mode @@ -116,6 +119,10 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result Result Result { +fn parse_data_disp_from_str(s: &str, pos: &SourcePosition, pcx: &ParserContext) -> Result { if s == "_" { return Ok(DataDisp::Discard); } if let Some(reference) = s.strip_prefix('@') { + if pcx.state.borrow().parsing_expr { + return Err(CrsnError::Parse("Object handles cannot be used in expressions!".into(), pos.clone())); + } + /* extension constants (pre-defined handles) */ for p in pcx.parsers { if let Some(val) = p.get_constant_value(reference) { @@ -188,7 +199,7 @@ pub fn parse_data_disp_from_str(s: &str, pos: &SourcePosition, pcx: &ParserConte return Ok(DataDisp::Immediate(*val)); } - /* register aliases */ + /* register aliases - no need to check for const_eval, the aliases could never be defined */ if let Some(val) = pstate.reg_aliases.get(s) { return Ok(DataDisp::Register(*val)); } else if let Some(val) = pstate.global_reg_aliases.get(s) { @@ -215,6 +226,14 @@ pub fn parse_data_disp_from_str(s: &str, pos: &SourcePosition, pcx: &ParserConte } } + if pcx.state.borrow().parsing_expr { + if s == "=result" { + return Ok(DataDisp::Register(Register::Gen(0))); + } else { + return Err(CrsnError::Parse("Registers cannot be used in expressions!".into(), pos.clone())); + } + } + /* register */ let reg = reg::parse_reg(&s, &pos)?; @@ -231,39 +250,13 @@ pub fn parse_data_disp_from_str(s: &str, pos: &SourcePosition, pcx: &ParserConte /// Parse immediate value pub fn parse_value(tok: Sexp, pcx: &ParserContext) -> Result<(Value, SourcePosition), CrsnError> { - match tok { - Sexp::Atom(Atom::I(val), pos) => { - Ok((unsafe { std::mem::transmute(val) }, pos)) - } - Sexp::Atom(Atom::U(val), pos) => { - Ok((val, pos)) - } - Sexp::Atom(Atom::C(val), pos) => { - Ok((val as u64, pos)) - } - Sexp::Atom(Atom::QS(_), pos) => { - Err(CrsnError::Parse("quoted string not expected here".into(), pos)) - } - Sexp::Atom(Atom::F(val), pos) => { - Ok((unsafe { std::mem::transmute(val) }, pos)) - } - Sexp::Atom(Atom::S(s), pos) => { - let pstate = pcx.state.borrow(); - if let Some(val) = pstate.constants.get(&s) { - return Ok((*val, pos)); - } - - /* extension constants */ - for p in pcx.parsers { - if let Some(val) = p.get_constant_value(&s) { - return Ok((val, pos)); - } - } + let pos = tok.pos().clone(); + let parsed = parse_rd(tok, pcx)?; - Err(CrsnError::Parse(format!("unknown constant: {}", s).into(), pos)) - } - Sexp::List(_, pos) => { - Err(CrsnError::Parse("expected a value".into(), pos)) + match parsed { + Rd(RdData::Immediate(value)) => Ok((value, pos)), + _ => { + Err(CrsnError::Parse("expected an immediate value".into(), pos)) } } } diff --git a/crsn/src/asm/parse/parse_instr.rs b/crsn/src/asm/parse/parse_instr.rs index 6fccd7e..adb3eb1 100644 --- a/crsn/src/asm/parse/parse_instr.rs +++ b/crsn/src/asm/parse/parse_instr.rs @@ -95,7 +95,7 @@ pub fn parse_instructions(items: impl Iterator, pos: &SourcePosition, if parsing_expr { // hackity hack for const expr eval - arg_parser.prepend(atom_s("r0")); + arg_parser.prepend(atom_s("=result")); } if let Some(op) = parse_op(name.as_str(), arg_parser, &namepos)? { diff --git a/crsn/src/builtin/exec.rs b/crsn/src/builtin/exec.rs index 80eaab8..34f9c83 100644 --- a/crsn/src/builtin/exec.rs +++ b/crsn/src/builtin/exec.rs @@ -204,6 +204,13 @@ impl OpTrait for BuiltinOp { Ok(res) } + fn is_compile_time_evaluable(&self) -> bool { + match self { + BuiltinOp::Load { .. } | BuiltinOp::LoadBits { .. } => true, + _ => false + } + } + fn to_sexp(&self) -> Sexp { super::parse::to_sexp(self) } diff --git a/crsn/src/module/mod.rs b/crsn/src/module/mod.rs index 07442f9..661aed5 100644 --- a/crsn/src/module/mod.rs +++ b/crsn/src/module/mod.rs @@ -48,6 +48,13 @@ pub trait OpTrait: Debug + Send + Sync + 'static { /// Turn into an S-expression that produces this instruction when parsed fn to_sexp(&self) -> Sexp; + + /// Check if an operation is safe to use in compile-time arithmetics - has no side effects + /// outside the input and output registers (and flags), has the form (op Wr ...), and does not + /// use object handles or extension data. + fn is_compile_time_evaluable(&self) -> bool { + false + } } /// CRSN initializer object. diff --git a/crsn_arith/src/exec.rs b/crsn_arith/src/exec.rs index 5625d5a..53cfc48 100644 --- a/crsn_arith/src/exec.rs +++ b/crsn_arith/src/exec.rs @@ -757,6 +757,15 @@ impl OpTrait for ArithOp { ArithOp::FloatHypAcot { dst, a } => to_sexp_1_or_2("facoth", dst, a), } } + + fn is_compile_time_evaluable(&self) -> bool { + match self { + ArithOp::Test { .. } | ArithOp::Compare { .. } | ArithOp::RangeTest { .. } + | ArithOp::FloatTest { .. } | ArithOp::FloatCompare { .. } | ArithOp::FloatRangeTest { .. } => false, + // rng is allowed... makes little sense, but it also has no side effects + _ => true, // a bold claim... + } + } } fn to_sexp_2_or_3(name: &str, dst: &Wr, a: &Rd, b: &Rd) -> Sexp { diff --git a/examples/expr.csn b/examples/expr.csn index 5368c93..2088779 100644 --- a/examples/expr.csn +++ b/examples/expr.csn @@ -1,12 +1,15 @@ ( - (def FOO 14) + (def A 10) + (def FOO (=add A (sub 10 5))) + + ; FOO is 15 (ld r0 (=add 14 1)) (cmp r0 15 (ne? (fault "1"))) (ld r0 (=add FOO 1)) - (cmp r0 15 (ne? (fault "2"))) + (cmp r0 16 (ne? (fault "2"))) (ld r0 (=add (sub FOO 2) 3)) - (cmp r0 15 (ne? (fault "3"))) + (cmp r0 16 (ne? (fault "3"))) )