Compare commits

...

14 Commits

  1. 116
      README.md
  2. 66
      crsn/crsn-sexp/src/error.rs
  3. 6
      crsn/crsn-sexp/src/lib.rs
  4. 72
      crsn/crsn-sexp/src/position.rs
  5. 20
      crsn/crsn-sexp/src/test.rs
  6. 28
      crsn/src/asm/error.rs
  7. 35
      crsn/src/asm/instr/flatten.rs
  8. 68
      crsn/src/asm/mod.rs
  9. 2
      crsn/src/asm/parse/arg_parser.rs
  10. 26
      crsn/src/asm/parse/mod.rs
  11. 2
      crsn/src/asm/parse/parse_data.rs
  12. 98
      crsn/src/asm/parse/parse_instr.rs
  13. 21
      crsn/src/asm/patches/mod.rs
  14. 2
      crsn/src/asm/read_file.rs
  15. 6
      crsn/src/builtin/defs.rs
  16. 33
      crsn/src/builtin/exec.rs
  17. 123
      crsn/src/builtin/mod.rs
  18. 77
      crsn/src/builtin/parse.rs
  19. 12
      crsn/src/runtime/fault.rs
  20. 11
      crsn/src/runtime/program.rs
  21. 193
      crsn/src/runtime/run_thread.rs
  22. 3
      crsn/src/runtime/run_thread/info.rs
  23. 30
      crsn/src/runtime/run_thread/state.rs
  24. 169
      crsn_stdio/src/lib.rs
  25. 0
      examples/coroutines2-gen.csn
  26. 44
      examples/coroutines3-crit.csn
  27. 17
      examples/coroutines4-io.csn
  28. 18
      examples/include/main.csn
  29. 3
      examples/include/other.csn
  30. 14
      examples/include/utils/itoa.csn
  31. 17
      examples/include/utils/printnum.csn
  32. 43
      examples/loop.csn
  33. 34
      launcher/src/main.rs

@ -136,6 +136,29 @@ The same program can be written in a compact form:
((ld r0 100)(:again)(sub r0 1 (nz? (j :again))))
```
### Includes
Croissant allows program composition using includes.
```
(include path)
```
Path can be a filesystem absolute or relative path (with slashes). Relative path is always resolved from the current source file.
Use double quotes if needed.
If the included file is missing an extension, `.csn` is appended automatically.
Each included file must contain a list of instructions or procedures, just like the main file.
Includes work at the S-expression level. The included file is parsed to instructions and inserted
in place of the include directive. Error reports will always show which file the line and column refer to.
There are no special scoping rules. It is possible to define a label in a file and jump to it from another.
Defines and register aliases (sym) also work cross-file.
Don't go too crazy with includes, it can get messy. Or do, I'm not your mom.
## Instruction
Instructions are written like this:
@ -187,7 +210,8 @@ The different ways to specify a value can be grouped as "reads" and "writes":
- `RW` - intersection of the two sets, capable of reading and writing: `REG`, `SYM`, `@REG`, `@SYM`
Objects (`@reg`, `@sym`) can be read or written as if they were a register, but only if the referenced object supports it.
Other objects may produce a runtime fault or set the INVALID flag.
Other objects may produce a runtime fault or set the INVALID flag.
The object syntax is also used to read values yielded by a generator-like coroutine.
In the instruction lists below, I will use the symbols `Rd` for reads, `Wr` for writes, `RW` for read-writes, and `@Obj` for object handles,
with optional description after an apostrophe, such as: `(add Wr'dst Rd'a Rd'b)`.
@ -325,6 +349,29 @@ You can also use one register for up to 64 mutexes.
Remember to only use global registers (or buffer items) as mutexes: `g0`-`g15`. Each coroutine has its own set of *regular* registers.
Another way to avoid race conditions is to use **critical sections**.
Context switch (switching between active coroutines) is forbidden in a critical section. Try to keep critical sections as short as possible,
since they can distort sleep times and cause other similar problems.
```
(crit-begin)
...
(crit-end)
```
A safer way is to use the "critical block", which expands to the same, but also detects some common bugs at compile time,
like trying to jump out of the critical section.
```
(crit
...
)
```
Critical section nesting is allowed, but probably a bug.
**Beware deadlocks!**
### Using coroutines as generators
A coroutine can "yield a value" by invoking the `yield` instruction with an operand. This can be done any number of times.
@ -341,6 +388,11 @@ The coroutine is blocked until the value is consumed by someone. To consume a yi
(ld r0 @r5) ; read a yielded value
```
**Caution!!!** Due to the way this is implemented, the instruction that tries to use a yielded value might partially execute
before detecting that it is blocked, and will be retried when the thread is next scheduled to run. This has the consequence
that if something like a object handle is read by the same instruction, it may be read multiple times while waiting for the
yielded value. It is recommended to *never combine reads of a yielded value with reads of other object handles*.
### Joining a coroutine
Use the `join` instruction with a coroutine object handle to wait for its completion.
@ -353,7 +405,7 @@ The returned values are placed in the result registers, just like with the `call
(spawn r5 foo)
; ...
(join @r0)
(join @r5)
; res0-res15 now contain return values
```
@ -379,6 +431,36 @@ to a varying number of skips and conditional instructions by the assembler. Only
Jumping to a label is always safer than a manual skip.
### Loop syntactic sugar
Infinite loops are a very common construct, so there is special syntax added for them.
These are converted by the assembler to an anonymous label and a jump to it.
```
(loop
...ops
)
```
If you want to have a convenient jump target, give the loop a name. This lets you easily "break" and "continue"
by jumping to the labels.
```
(loop :label
...ops
)
```
becomes
```
(:label)
...ops
(j :label)
(:label-end)
```
## Built-in Instructions
...and pseudo-instructions
@ -473,9 +555,34 @@ Jumping to a label is always safer than a manual skip.
; The arguments are passed as argX. Return values are stored in resX registers.
(call PROC Rd...)
; Exit the current routine with return values
; Spawn a coroutine. The handle is stored in the output register.
(spawn Wr'handle PROC Rd...)
; Exit the current routine (or coroutine) with return values
(ret Rd...)
; Yield control from a coroutine (use when waiting for a mutex to give control early to
; other coroutines that might be holding it)
(yield)
; Yield a value generated by a coroutine. Gives up control and blocks until the value is consumed.
; Conversely, an instruction that tries to read a yielded value using the object handle is blocked
; until such value becomes available.
(yield Rd'value)
; Wait for a coroutine to complete, read its return values and delete it.
(join @Obj)
; Begin a critical section (no context switch allowed)
(crit-begin)
; End a critical section
(crit-end)
; Shortcut to define a critical section
(crit
...ops
)
; Generate a run-time fault with a debugger message
(fault)
(fault message)
@ -491,6 +598,9 @@ Jumping to a label is always safer than a manual skip.
; The label can be a numeric or string label, its sole purpose is tying the two together. They must be unique in the program.
(barrier-open LABEL)
(barrier-close LABEL)
; Set coroutine scheduler timeslice (in microseconds). Set to zero to disable preemption.
(rt-opt RT_TIMESLICE Rd'usec)
```
## Arithmetic Module

@ -1,5 +1,6 @@
use std::{cmp, fmt};
use std::fmt::{Formatter, Debug};
use std::{fmt};
use super::SourcePosition;
use position::get_line_and_column;
/// The representation of an s-expression parse error.
pub struct Error {
@ -9,38 +10,6 @@ pub struct Error {
pub pos: SourcePosition,
}
/// Position in the input string
#[derive(PartialEq, Clone, Default)]
pub struct SourcePosition {
/// The line number on which the error occurred.
pub line: u32,
/// The column number on which the error occurred.
pub column: u32,
/// The index in the given string which caused the error.
pub index: u32,
}
impl fmt::Display for SourcePosition {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{}:{}", self.line, self.column)
}
}
impl Debug for SourcePosition {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("SourcePosition")
.field("line", &self.line)
.field("column", &self.column)
.field("index", &self.index)
.finish()
} else {
// shorter version
write!(f, "Pos({}:{})", self.line, self.column)
}
}
}
/// Since errors are the uncommon case, they're boxed. This keeps the size of
/// structs down, which helps performance in the common case.
///
@ -54,7 +23,7 @@ pub(crate) type ERes<T> = Result<T, Err>;
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{}:{}: {}", self.pos.line, self.pos.column, self.message)
write!(f, "{}: {}", self.pos, self.message)
}
}
@ -66,24 +35,6 @@ impl fmt::Debug for Error {
impl std::error::Error for Error {}
pub(crate) fn get_line_and_column(s: &str, pos: usize) -> SourcePosition {
let mut line: usize = 1;
let mut col: isize = -1;
for c in s.chars().take(pos + 1) {
if c == '\n' {
line += 1;
col = -1;
} else {
col += 1;
}
}
SourcePosition {
line: line as u32,
column: cmp::max(col, 0) as u32,
index: pos as u32,
}
}
#[cold]
fn err_impl(message: &'static str, s: &str, pos: usize) -> Err {
Box::new(Error {
@ -96,12 +47,3 @@ fn err_impl(message: &'static str, s: &str, pos: usize) -> Err {
pub(crate) fn err<T>(message: &'static str, s: &str, pos: usize) -> ERes<T> {
Err(err_impl(message, s, pos))
}
/// Build a span
pub(crate) fn spos(s: &str, pos: usize) -> SourcePosition {
if pos >= s.len() {
Default::default()
} else {
get_line_and_column(s, pos)
}
}

@ -8,14 +8,16 @@ use std::borrow::Cow;
use std::fmt;
use std::str::{self, FromStr};
use error::{ERes, err, spos};
use error::{ERes, err};
pub use error::Error;
pub use error::SourcePosition;
pub use position::SourcePosition;
use position::spos;
#[cfg(test)]
mod test;
mod error;
mod position;
/// A single data element in an s-expression. Floats are excluded to ensure
/// atoms may be used as keys in ordered and hashed data structures.

@ -0,0 +1,72 @@
use std::{fmt, cmp};
use std::fmt::{Formatter, Debug};
/// Position in the input string
#[derive(PartialEq, Clone, Default)]
pub struct SourcePosition {
/// The line number on which the error occurred.
pub line: u32,
/// The column number on which the error occurred.
pub column: u32,
/// The index in the given string which caused the error.
pub index: u32,
/// File index if there are multiple files
pub file: u32,
}
impl fmt::Display for SourcePosition {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "{}:{}", self.line, self.column)
}
}
impl SourcePosition {
pub fn with_file(mut self, file: u32) -> Self {
self.file = file;
self
}
}
impl Debug for SourcePosition {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_struct("SourcePosition")
.field("line", &self.line)
.field("column", &self.column)
.field("index", &self.index)
.field("file", &self.file)
.finish()
} else {
// shorter version
write!(f, "Pos({}|{}:{})", self.file, self.line, self.column)
}
}
}
pub(crate) fn get_line_and_column(s: &str, pos: usize) -> SourcePosition {
let mut line: usize = 1;
let mut col: isize = -1;
for c in s.chars().take(pos + 1) {
if c == '\n' {
line += 1;
col = -1;
} else {
col += 1;
}
}
SourcePosition {
line: line as u32,
column: cmp::max(col, 0) as u32,
index: pos as u32,
file: 0
}
}
/// Build a span
pub(crate) fn spos(s: &str, pos: usize) -> SourcePosition {
if pos >= s.len() {
Default::default()
} else {
get_line_and_column(s, pos)
}
}

@ -1,5 +1,5 @@
use super::*;
use super::error::get_line_and_column;
use super::position::get_line_and_column;
#[test]
fn test_hello_world() {
@ -67,23 +67,23 @@ fn show_an_error() {
#[test]
fn line_and_col_test() {
let s = "0123456789\n0123456789\n\n6";
assert_eq!(get_line_and_column(s, 4), SourcePosition { line: 1, column: 4, index: 4 });
assert_eq!(get_line_and_column(s, 4), SourcePosition { line: 1, column: 4, index: 4, file: 0 });
assert_eq!(get_line_and_column(s, 10), SourcePosition { line: 2, column: 0, index: 10 });
assert_eq!(get_line_and_column(s, 11), SourcePosition { line: 2, column: 0, index: 11 });
assert_eq!(get_line_and_column(s, 15), SourcePosition { line: 2, column: 4, index: 15 });
assert_eq!(get_line_and_column(s, 10), SourcePosition { line: 2, column: 0, index: 10, file: 0 });
assert_eq!(get_line_and_column(s, 11), SourcePosition { line: 2, column: 0, index: 11, file: 0 });
assert_eq!(get_line_and_column(s, 15), SourcePosition { line: 2, column: 4, index: 15, file: 0 });
assert_eq!(get_line_and_column(s, 21), SourcePosition { line: 3, column: 0, index: 21 });
assert_eq!(get_line_and_column(s, 22), SourcePosition { line: 4, column: 0, index: 22 });
assert_eq!(get_line_and_column(s, 23), SourcePosition { line: 4, column: 0, index: 23 });
assert_eq!(get_line_and_column(s, 500), SourcePosition { line: 4, column: 0, index: 500 });
assert_eq!(get_line_and_column(s, 21), SourcePosition { line: 3, column: 0, index: 21, file: 0 });
assert_eq!(get_line_and_column(s, 22), SourcePosition { line: 4, column: 0, index: 22, file: 0 });
assert_eq!(get_line_and_column(s, 23), SourcePosition { line: 4, column: 0, index: 23, file: 0 });
assert_eq!(get_line_and_column(s, 500), SourcePosition { line: 4, column: 0, index: 500, file: 0 });
}
#[test]
fn sexp_size() {
// I just want to see when this changes, in the diff.
use std::mem;
assert_eq!(mem::size_of::<Sexp>(), mem::size_of::<isize>() * 6);
assert_eq!(mem::size_of::<Sexp>(), mem::size_of::<isize>() * 7);
}
#[test]

@ -8,18 +8,34 @@ use sexp::SourcePosition;
use crate::asm::data::{Register};
use crate::asm::data::literal::Label;
use crate::asm::instr::Cond;
use std::io;
/// csn_asm unified error type
#[derive(Error, Debug)]
pub enum CrsnError {
#[error("S-expression parsing error: {0:?}")]
#[error("S-expression parsing error: {0}")]
Sexp(#[from] Box<sexp::Error>),
#[error("Parse error: {0:?} at {1:?}")]
#[error("Parse error: {0} at {1}")]
Parse(Cow<'static, str>, SourcePosition),
#[error("Parse error: {0:?} at {1:?}")]
#[error("Parse error: {0} at {1}")]
ParseOther(Box<dyn Error + Send + Sync>, SourcePosition),
#[error("Assembler error: {0:?} at {1:?}")]
#[error("Assembler error: {0} at {1}")]
Asm(AsmError, SourcePosition),
#[error("IO error: {0}")]
IOError(#[from] io::Error),
}
impl CrsnError {
/// Get error pos
pub fn pos(&self) -> Option<&SourcePosition> {
match self {
CrsnError::Parse(_, p) => Some(p),
CrsnError::ParseOther(_, p) => Some(p),
CrsnError::Asm(_, p) => Some(p),
CrsnError::Sexp(se) => Some(&se.pos),
CrsnError::IOError(_) =>None,
}
}
}
/// Error from the assembler stage (after parsing S-expressions and basic validation)
@ -33,8 +49,10 @@ pub enum AsmError {
DiscardAsValue,
#[error("Conditional branch already defined for \"{0}\"")]
ConditionalAlreadyUsed(Cond),
#[error("Label \"{0:?}\" not defined")]
#[error("Label \"{0}\" not defined")]
LabelNotDefined(Label),
#[error("Label \"{0}\" already defined in this scope")]
LabelDuplicate(Label),
#[error("Bad register type: {0}")]
BadRegisterType(Register),
}

@ -29,6 +29,16 @@ impl Flatten for () {
}
}
impl Flatten for Op {
fn flatten(self: Box<Self>, _label_num: &AtomicU32) -> Result<Vec<Op>, CrsnError> {
Ok(vec![*self])
}
fn pos(&self) -> SourcePosition {
self.pos.clone()
}
}
impl Flatten for InstrWithBranches {
fn pos(&self) -> SourcePosition {
self.pos.clone()
@ -111,6 +121,23 @@ impl Flatten for Vec<Box<dyn Flatten>> {
}
}
impl Flatten for Vec<Op> {
fn pos(&self) -> SourcePosition {
match self.first() {
None => {
Default::default()
}
Some(f) => {
f.pos()
}
}
}
fn flatten(self: Box<Self>, _label_num: &AtomicU32) -> Result<Vec<Op>, CrsnError> {
Ok(*self)
}
}
impl Flatten for Routine {
fn pos(&self) -> SourcePosition {
self.pos.clone()
@ -138,15 +165,19 @@ impl Flatten for Routine {
}.into_op(self_pos)
);
labels_to_skips(ops)
jumps_to_skips(ops)
}
}
/// Convert jumps to relative skips
pub fn labels_to_skips(ops: Vec<Op>) -> Result<Vec<Op>, CrsnError> {
pub fn jumps_to_skips(ops: Vec<Op>) -> Result<Vec<Op>, CrsnError> {
let mut label_positions = HashMap::<Label, usize>::new();
for (n, op) in ops.iter().enumerate() {
if let OpKind::BuiltIn(BuiltinOp::Label(name)) = &op.kind {
if label_positions.contains_key(name) {
return Err(CrsnError::Asm(AsmError::LabelDuplicate(name.clone()), op.pos.clone()));
}
label_positions.insert(name.clone(), n - label_positions.len());
}
}

@ -3,33 +3,33 @@ use std::sync::Arc;
use sexp::SourcePosition;
use crate::asm::instr::flatten::labels_to_skips;
use crate::asm::instr::flatten::jumps_to_skips;
use crate::asm::parse::{ParserContext, ParserState};
use crate::module::{CrsnExtension, CrsnUniq};
use crate::runtime::program::Program;
use crate::builtin::BuiltinOps;
use crate::runtime::run_thread::{RunState, ThreadInfo, ThreadToken};
use crate::runtime::frame::REG_COUNT;
use std::sync::atomic::AtomicU32;
use std::path::{Path};
pub mod data;
pub mod error;
pub mod instr;
pub mod parse;
pub mod patches;
mod read_file;
use read_file::read_file;
/// Parse a program from string and assemble a low level instruction sequence from it.
pub fn assemble(source: &str, uniq : &CrsnUniq, mut parsers: Vec<Box<dyn CrsnExtension>>) -> Result<Arc<Program>, error::CrsnError> {
parsers.insert(0, BuiltinOps::new());
for p in &mut parsers {
p.init(uniq);
}
pub(crate) fn read_source_file(path: impl AsRef<Path>) -> Result<String, error::CrsnError> {
trace!("Read source file: {}", path.as_ref().display());
let parsers_arc = Arc::new(parsers);
let source = read_file(path)?;
// remove first line if it looks like a shebang
let source = if source.starts_with("#!") {
let s = if source.starts_with("#!") {
if let Some(nl) = source.find('\n') {
&source[nl + 1..]
(&source[nl + 1..]).to_string()
} else {
source
}
@ -37,15 +37,34 @@ pub fn assemble(source: &str, uniq : &CrsnUniq, mut parsers: Vec<Box<dyn CrsnExt
source
};
Ok(s)
}
/// Parse a program from string and assemble a low level instruction sequence from it.
pub fn assemble(path: impl AsRef<Path>, uniq : &CrsnUniq, mut parsers: Vec<Box<dyn CrsnExtension>>) -> Result<Arc<Program>, error::CrsnError> {
parsers.insert(0, BuiltinOps::new());
for p in &mut parsers {
p.init(uniq);
}
let path = path.as_ref().canonicalize()?;
let source = read_source_file(&path)?;
let parsers_arc = Arc::new(parsers);
let ti = Arc::new(ThreadInfo {
id: ThreadToken(0),
uniq: Default::default(),
program: Program::new(vec![], parsers_arc.clone()).unwrap(),
program: Program::new(vec![], parsers_arc.clone(), vec![path.clone()]).unwrap(),
cycle_time: Default::default(),
scheduler_interval: Default::default(),
extensions: parsers_arc.clone(),
});
/* numbered labels start with a weird high number
to avoid conflicts with user-defined numbered labels */
let label_num = Arc::new(AtomicU32::new(0x7890_0000));
let pcx = ParserContext {
parsers: &parsers_arc,
state: RefCell::new(ParserState {
@ -62,15 +81,32 @@ pub fn assemble(source: &str, uniq : &CrsnUniq, mut parsers: Vec<Box<dyn CrsnExt
parked: Default::default(),
global_regs: [0; REG_COUNT],
ext_data: Default::default(),
cr_deadline: None
cr_deadline: None,
critical_section: 0,
},
const_eval_ti: ti,
parsing_expr: false
parsing_expr: false,
label_num: label_num.clone(),
files: vec![
path,
],
active_file: 0
}),
};
let ops = parse::parse(source, &SourcePosition::default(), &pcx)?;
let ops = labels_to_skips(ops)?;
let res = do_parse(&source, &pcx, parsers_arc.clone());
if let Err(e) = &res {
if let Some(pos) = e.pos() {
let f = pcx.state.borrow().files[pos.file as usize].clone();
eprintln!("Error in source file: {}", f.display());
}
}
res
}
fn do_parse(source: &str, pcx : &ParserContext, parsers_arc : Arc<Vec<Box<dyn CrsnExtension>>>) -> Result<Arc<Program>, error::CrsnError> {
let ops = parse::parse(source, &SourcePosition::default(), pcx)?;
let ops = jumps_to_skips(ops)?;
Ok(Program::new(ops, parsers_arc)?)
Ok(Program::new(ops, parsers_arc, pcx.state.borrow_mut().files.split_off(0))?)
}

@ -109,7 +109,7 @@ impl<'a> TokenParser<'a> {
}
/// Look at the next entry
pub fn peek(&mut self) -> Option<&Sexp> {
pub fn peek(&self) -> Option<&Sexp> {
self.args.last()
}

@ -13,6 +13,7 @@ use crate::asm::parse::sexp_expect::expect_list;
use crate::module::CrsnExtension;
use crate::runtime::run_thread::{ThreadInfo, RunState};
use std::sync::Arc;
use std::path::PathBuf;
pub mod parse_cond;
pub mod parse_instr;
@ -51,14 +52,31 @@ pub struct ParserState {
/// True if we are in an expression parser context
pub parsing_expr : bool,
/// Label numberer
pub label_num : Arc<AtomicU32>,
/// Numbered files
pub files : Vec<PathBuf>,
/// Active file number. Is backed up before descending into a sub-file, and restored afterwards.
pub active_file: usize,
}
impl ParserState {
pub fn reset(&mut self) {
self.global_reg_aliases.clear();
self.reg_aliases.clear();
self.reg_alias_stack.clear();
self.constants.clear();
self.parsing_expr = false;
self.const_eval.reset();
}
}
pub fn parse(source: &str, pos: &SourcePosition, parsers: &ParserContext) -> Result<Vec<Op>, CrsnError> {
let (items, _pos) = expect_list(sexp::parse(source)?, true)?;
/* numbered labels start with a weird high number
to avoid conflicts with user-defined numbered labels */
let label_num = AtomicU32::new(0x7890_0000);
parse_instructions(items.into_iter(), pos, parsers)?
.flatten(&label_num)
.flatten(&parsers.state.borrow().label_num)
}

@ -124,7 +124,7 @@ pub fn parse_data_disp(tok: Sexp, pcx: &ParserContext) -> Result<DataDisp, CrsnE
}
let mut state_mut = pcx.state.borrow_mut();
state_mut.const_eval.clear_all();
state_mut.const_eval.reset();
let ticl = state_mut.const_eval.thread_info.clone();
op.execute(&ticl, &mut state_mut.const_eval)

@ -6,15 +6,20 @@ use crate::asm::parse::arg_parser::TokenParser;
use crate::asm::parse::parse_cond::parse_cond_branch;
use crate::asm::parse::parse_routine::parse_routine;
use crate::asm::parse::ParserContext;
use crate::asm::parse::sexp_expect::{expect_list, expect_string_atom};
use crate::asm::parse::sexp_expect::{expect_list, expect_string_atom, expect_any_string_atom};
use crate::asm::patches::NextOrErr;
use crate::module::ParseRes;
use crate::asm::patches::ErrSetFile;
use super::parse_op::parse_op;
use std::path::{PathBuf};
use std::convert::TryFrom;
use crate::asm::read_source_file;
pub fn parse_instructions(items: impl Iterator<Item=Sexp>, pos: &SourcePosition, pcx: &ParserContext) -> Result<Box<dyn Flatten>, CrsnError> {
let mut parsed = vec![];
for expr in items {
let mut parsed: Vec<Box<dyn Flatten>> = vec![];
'exprs: for expr in items {
let (tokens, listpos) = expect_list(expr, false)?;
let mut toki = tokens.into_iter();
@ -28,6 +33,84 @@ pub fn parse_instructions(items: impl Iterator<Item=Sexp>, pos: &SourcePosition,
}
}
if name == "include" {
// TODO move this to a separate function and get rid of err_file()
if parsing_expr {
return Err(CrsnError::Parse("Illegal syntax in const expression".into(), pos.clone()));
}
let (mut path, _namepos) = expect_any_string_atom(toki.next_or_err(listpos.clone(), "Expected file path or name to include")?)?;
// add extension if missing
let last_piece = path.split('/').rev().next().unwrap();
if !last_piece.contains('.') {
path.push_str(".csn");
}
trace!("*** include, raw path: {}", path);
let state = pcx.state.borrow();
let this_file = &state.files[state.active_file];
let new_file = PathBuf::try_from(path).unwrap();
let new_pb = if !new_file.is_absolute() {
let parent = this_file.parent().expect("file has parent");
let fixed = parent.join(&new_file);
trace!("Try to resolve: {}", fixed.display());
fixed.canonicalize()?
} else {
new_file.to_owned()
};
drop(state);
drop(new_file);
let (old_af, af) = {
let mut state = pcx.state.borrow_mut();
let old_af = state.active_file;
let af = state.files.len();
state.active_file = af;
state.files.push(new_pb.clone());
(old_af, af as u32)
};
let loaded = read_source_file(&new_pb)
.err_file(af)?;
let (items, _pos) =
expect_list(
sexp::parse(&loaded)
.map_err(|mut e| {
e.pos.file = af;
e
})?,
true)
.err_file(af)?;
let sub_parsed = parse_instructions(items.into_iter(), pos, pcx)
.err_file(af)?;
let mut flat = sub_parsed.flatten(&pcx.state.borrow().label_num)
.err_file(af)?;
flat.iter_mut().for_each(|op| {
op.pos.file = af as u32;
});
trace!("inner falt {:#?}", flat);
parsed.push(Box::new(flat));
{
let mut state = pcx.state.borrow_mut();
state.active_file = old_af;
}
continue;
}
if name == "proc" {
if parsing_expr {
return Err(CrsnError::Parse("Illegal syntax in const expression".into(), pos.clone()));
@ -40,8 +123,13 @@ pub fn parse_instructions(items: impl Iterator<Item=Sexp>, pos: &SourcePosition,
let mut token_parser = TokenParser::new(toki.collect(), &listpos, pcx);
for p in pcx.parsers {
token_parser = match p.parse_syntax(pos, &name, token_parser) {
Ok(ParseRes::Parsed(op)) => return Ok(op),
Ok(ParseRes::ParsedNone) => return Ok(Box::new(())),
Ok(ParseRes::Parsed(op)) => {
parsed.push(op);
continue 'exprs;
},
Ok(ParseRes::ParsedNone) => {
continue 'exprs;
},
Ok(ParseRes::Unknown(to_reuse)) => {
if to_reuse.parsing_started() {
panic!("Module \"{}\" started parsing syntax, but returned Unknown!", p.name());

@ -57,3 +57,24 @@ impl<T, E: std::error::Error + Send + Sync + 'static> ErrWithPos<T> for Result<T
}
}
}
pub trait ErrSetFile<T> {
fn err_file(self, file: u32) -> Result<T, CrsnError>;
}
impl<T> ErrSetFile<T> for Result<T, CrsnError> {
/// Set file context in the error
fn err_file(self, file: u32) -> Result<T, CrsnError> {
match self {
Ok(v) => Ok(v),
Err(CrsnError::Parse(a, p)) => Err(CrsnError::Parse(a, p.with_file(file))),
Err(CrsnError::ParseOther(a, p)) => Err(CrsnError::ParseOther(a, p.with_file(file))),
Err(CrsnError::Asm(a, p)) => Err(CrsnError::Asm(a, p.with_file(file))),
Err(CrsnError::Sexp(mut se)) => {
se.pos.file = file;
Err(CrsnError::Sexp(se))
},
Err(other @ CrsnError::IOError(_)) => Err(other),
}
}
}

@ -4,7 +4,7 @@ use std::io::Read;
use std::path::Path;
/// Read a file to string
pub fn read_file<P: AsRef<Path>>(path: P) -> io::Result<String> {
pub fn read_file(path: impl AsRef<Path>) -> io::Result<String> {
let path = path.as_ref();
let mut file = File::open(path)?;

@ -151,6 +151,12 @@ pub enum BuiltinOp {
/// Yield control, optionally yielding a value that must be consumed (by reading the task handle)
/// before execution can resume
Yield { value: Option<Rd> },
/// Set runtime option
RuntimeOpt { opt: Rd, value: Rd },
/// Begin critical section
CriticalBegin,
/// End critical section
CriticalEnd,
/// Deny jumps, skips and run across this address, producing a run-time fault.
Barrier {
kind: Barrier,

@ -10,6 +10,8 @@ use crate::runtime::frame::StackFrame;
use crate::runtime::run_thread::{state::RunState, ThreadInfo};
use crate::asm::instr::cond::Flag;
use super::RT_OPT_TIMESLICE;
impl OpTrait for BuiltinOp {
fn execute(&self, info: &ThreadInfo, state: &mut RunState) -> Result<EvalRes, Fault> {
let program = &info.program;
@ -89,7 +91,7 @@ impl OpTrait for BuiltinOp {
values.push(state.read(arg)?);
}
let mut frame2 = StackFrame::new(pos, &values);
let frame2 = StackFrame::new(pos, &values);
let handle_val = state.add_coroutine(frame2);
state.write(handle, handle_val)?;
@ -252,6 +254,35 @@ impl OpTrait for BuiltinOp {
BuiltinOp::Join(obj) => {
res.sched = SchedSignal::Join(obj.read(state)?);
}
BuiltinOp::RuntimeOpt { opt, value } => {
let opt = state.read(opt)?;
let val = state.read(value)?;
match opt {
RT_OPT_TIMESLICE => {
*state.thread_info.scheduler_interval.write() = Duration::from_micros(val);
}
_ => {
warn!("Invalid rt-opt {}", opt);
state.set_flag(Flag::Invalid, true);
}
}
}
BuiltinOp::CriticalBegin => {
if state.critical_section == Value::MAX {
return Err(Fault::UnbalancedCriticalSection);
}
state.critical_section += 1;
res.cycles = 0;
}
BuiltinOp::CriticalEnd => {
if state.critical_section == 0 {
return Err(Fault::UnbalancedCriticalSection);
}
state.critical_section -= 1;
res.cycles = 0;
}
}
Ok(res)

@ -1,14 +1,23 @@
use sexp::SourcePosition;
use sexp::{SourcePosition, Sexp, Atom};
use crate::asm::error::CrsnError;
use crate::asm::instr::op::OpKind;
use crate::asm::parse::arg_parser::TokenParser;
use crate::module::{CrsnExtension, ParseRes};
use crate::asm::data::literal::{Value, Label};
use crate::asm::instr::{Flatten, Op};
use crate::asm::parse::parse_instructions;
use crate::builtin::defs::BuiltinOp;
use crate::asm::instr::flatten::jumps_to_skips;
use crate::asm::data::{Rd, RdData};
use crate::asm::parse::parse_data::parse_label;
pub mod defs;
pub mod exec;
pub mod parse;
pub(crate) const RT_OPT_TIMESLICE : u64 = 1;
#[derive(Debug, Clone)]
pub struct BuiltinOps;
@ -26,4 +35,116 @@ impl CrsnExtension for BuiltinOps {
fn parse_op<'a>(&self, pos: &SourcePosition, keyword: &str, args: TokenParser<'a>) -> Result<ParseRes<'a, OpKind>, CrsnError> {
parse::parse_op(pos, keyword, args)
}
/// Get value of an extension-provided constant.
/// This constant may be an object handle, or a constant value used as argument in some other instruction.
fn get_constant_value<'a>(&self, name: &str) -> Option<Value> {
match name {
"RT_TIMESLICE" => Some(RT_OPT_TIMESLICE),
_ => None,
}
}
/// Parse a generic S-expression (non-op) that started with the given keyword
///
/// pcx is available on the arg_tokens parser
fn parse_syntax<'a>(&self, pos: &SourcePosition, keyword: &str, mut tokens: TokenParser<'a>)
-> Result<ParseRes<'a, Box<dyn Flatten>>, CrsnError>
{
if keyword == "crit" || keyword == "critical" {
let pcx = tokens.pcx;
let opts = parse_instructions(tokens.into_iter(), pos, pcx)?;
let flattened = jumps_to_skips(opts.flatten(&pcx.state.borrow_mut().label_num)?)?;
let len = flattened.len();
for (n, op) in flattened.iter().enumerate() {
match op.kind {
OpKind::BuiltIn(BuiltinOp::Skip(Rd(RdData::Immediate(skip)))) => {
let signed = i64::from_ne_bytes(skip.to_ne_bytes());
let target = n as i64 + signed;
if target < 0 || target > len as i64 {
return Err(CrsnError::Parse("Cannot jump out of a critical section!".into(), op.pos()));
}
}
/* Non-constant skip cannot be validated */
OpKind::BuiltIn(BuiltinOp::Skip(_)) => {
return Err(CrsnError::Parse("Variable skips are not allowed in a critical section".into(), op.pos()));
}
/* Yield in critical makes zero sense */
OpKind::BuiltIn(BuiltinOp::Yield { .. }) => {
return Err(CrsnError::Parse("Yield in a critical section!".into(), op.pos()));
}
/* This is likely a bug */
OpKind::BuiltIn(BuiltinOp::Ret(_)) => {
return Err(CrsnError::Parse("Ret in a critical section!".into(), op.pos()));
}
/* Probably also a bug. If someone really wants this, they can start and end the critical section manually. */
OpKind::BuiltIn(BuiltinOp::FarJump(_)) => {
return Err(CrsnError::Parse("Far jump a critical section!".into(), op.pos()));
}
_ => {}
}
}
let vec : Vec<Box<dyn Flatten>> = vec![
Box::new(Op {
kind: OpKind::BuiltIn(BuiltinOp::CriticalBegin),
cond: None,
pos: pos.clone(),
}),
Box::new(flattened),
Box::new(Op {
kind: OpKind::BuiltIn(BuiltinOp::CriticalEnd),
cond: None,
pos: pos.clone(),
}),
];
return Ok(ParseRes::Parsed(Box::new(vec)));
}
if keyword == "loop" {
let mut end_label = None;
let label = if let Some(Sexp::Atom(Atom::S(_), _)) = tokens.peek() {
let label = parse_label(tokens.next().unwrap())?;
if let Label::Named(n) = &label {
end_label = Some(Label::Named(format!("{}-end", n)));
}
label
} else {
Label::unique(&tokens.pcx.state.borrow().label_num)
};
let pcx = tokens.pcx;
let inner = parse_instructions(tokens.into_iter(), pos, pcx)?;
let mut vec : Vec<Box<dyn Flatten>> = vec![
Box::new(Op {
kind: OpKind::BuiltIn(BuiltinOp::Label(label.clone())),
cond: None,
pos: pos.clone(),
}),
inner,
Box::new(Op {
kind: OpKind::BuiltIn(BuiltinOp::Jump(label)),
cond: None,
pos: pos.clone(),
}),
];
if let Some(el) = end_label {
vec.push(Box::new(Op {
kind: OpKind::BuiltIn(BuiltinOp::Label(el)),
cond: None,
pos: pos.clone(),
}));
}
return Ok(ParseRes::Parsed(Box::new(vec)))
}
Ok(ParseRes::Unknown(tokens))
}
}

@ -1,4 +1,4 @@
use sexp::{Atom, Sexp, SourcePosition, atom_qs, atom_s};
use sexp::{Atom, Sexp, SourcePosition, atom_qs};
use crate::asm::data::literal::{RoutineName};
use crate::asm::data::reg::parse_reg;
@ -183,6 +183,21 @@ pub(crate) fn parse_op<'a>(op_pos: &SourcePosition, keyword: &str, mut args: Tok
})
}
"crit-begin" => {
BuiltinOp::CriticalBegin
}
"crit-end" => {
BuiltinOp::CriticalEnd
}
"rt-opt" => {
BuiltinOp::RuntimeOpt {
opt: args.next_rd()?,
value: args.next_rd()?,
}
}
"ld" => {
BuiltinOp::Load {
dst: args.next_wr()?,
@ -349,6 +364,9 @@ pub(crate) fn parse_routine_name(name: String, pos: &SourcePosition) -> Result<R
pub(crate) fn to_sexp(op: &BuiltinOp) -> Sexp {
match op {
BuiltinOp::CriticalBegin => sexp::list(&[A("crit-begin")]),
BuiltinOp::CriticalEnd => sexp::list(&[A("crit-end")]),
BuiltinOp::RuntimeOpt { opt, value } => sexp::list(&[A("rt-opt"), A(opt), A(value)]),
BuiltinOp::Nop => sexp::list(&[A("nop")]),
BuiltinOp::Halt => sexp::list(&[A("halt")]),
BuiltinOp::Sleep { count: micros, unit_us: SleepUnit::Sec } => sexp::list(&[A("sslp"), A(micros)]),
@ -452,7 +470,7 @@ pub(crate) fn to_sexp(op: &BuiltinOp) -> Sexp {
if args.is_empty() {
sexp::list(&[A("spawn"), A(handle), A(proc)])
} else {
let mut v = vec![A("spawn"), A(handle), A(proc)];
let mut v = vec![A("spawn"), A(handle), A(&proc.name)];
v.extend(args.iter().map(|r| A(r)));
sexp::list(&v)
}
@ -473,10 +491,15 @@ mod test {
use sexp::SourcePosition;
use crate::asm::parse::{parse_instructions, ParserContext};
use crate::asm::parse::{parse_instructions, ParserContext, ParserState};
use crate::asm::parse::sexp_expect::expect_list;
use crate::builtin::BuiltinOps;
use crate::module::OpTrait;
use std::cell::RefCell;
use std::sync::Arc;
use crate::runtime::run_thread::{ThreadInfo, ThreadToken, RunState};
use crate::runtime::program::Program;
use crate::runtime::frame::REG_COUNT;
#[test]
fn roundtrip() {
@ -562,13 +585,46 @@ mod test {
let parser = BuiltinOps::new();
let parsers = &[parser];
let parsers = Arc::new(vec![parser]);
let ti = Arc::new(ThreadInfo {
id: ThreadToken(0),
uniq: Default::default(),
program: Program::new(vec![], parsers.clone(), vec![]).unwrap(),
cycle_time: Default::default(),
scheduler_interval: Default::default(),
extensions: parsers.clone(),
});
let pcx = ParserContext {
parsers: &parsers,
state: RefCell::new(ParserState {
reg_aliases: Default::default(),
reg_alias_stack: vec![],
global_reg_aliases: Default::default(),
constants: Default::default(),
// This is a fake thread to pass to constant expressions when evaluating them.
// This allows to evaluate nearly all instructions at compile time.
const_eval: RunState {
thread_info: ti.clone(),
cr: Default::default(),
parked: Default::default(),
global_regs: [0; REG_COUNT],
ext_data: Default::default(),
cr_deadline: None,
critical_section: 0,
},
const_eval_ti: ti.clone(),
parsing_expr: false,
label_num: Default::default(),
files: vec![],
active_file: 0
}),
};
for (sample, expected) in samples {
let pcx = ParserContext {
parsers,
state: Default::default(),
};
pcx.state.borrow_mut().reset();
println!("Parse: {}", sample);
@ -587,10 +643,7 @@ mod test {
assert_eq!(expected, exported);
println!(" - 2nd cycle");
let pcx = ParserContext {
parsers,
state: Default::default(),
};
pcx.state.borrow_mut().reset();
/* second cycle, nothing should change */
let s = sexp::parse(&format!("({})", exported))

@ -77,6 +77,18 @@ pub enum Fault {
#[error("Attempt to read undefined extension data store")]
ExtDataNotDefined,
#[error("Unbalanced critical sections")]
UnbalancedCriticalSection,
#[error("Deadlock detected")]
Deadlock,
#[error("Root routine returned or yielded a value")]
RootReturned,
#[error("System IO error: {0}")]
IOError(#[from] std::io::Error),
}
#[derive(Error, Debug)]

@ -10,6 +10,7 @@ use crate::asm::instr::op::OpKind;
use crate::builtin::defs::{Barrier, BuiltinOp};
use crate::module::CrsnExtension;
use crate::runtime::fault::Fault;
use std::path::PathBuf;
#[derive(Debug)]
pub struct Program {
@ -20,16 +21,23 @@ pub struct Program {
/// Barriers from-to (inclusive).
/// Standalone barriers have both addresses the same.
barriers: Vec<(Addr, Addr)>,
/// This is used at runtime to better report error locations when included files are used
pub(crate) file_names: Vec<PathBuf>,
}
impl Program {
pub fn new(ops: Vec<Op>, extensions: Arc<Vec<Box<dyn CrsnExtension>>>) -> Result<Arc<Self>, CrsnError> {
pub fn new(
ops: Vec<Op>,
extensions: Arc<Vec<Box<dyn CrsnExtension>>>,
file_names: Vec<PathBuf>,
) -> Result<Arc<Self>, CrsnError> {
let mut p = Self {
ops,
extensions,
routines: Default::default(),
far_labels: Default::default(),
barriers: Default::default(),
file_names,
};
p.scan()?;
Ok(Arc::new(p))
@ -100,6 +108,7 @@ impl Program {
line: 0,
column: 0,
index: 0,
file: 0,
},
cond: None,
}

@ -1,6 +1,5 @@
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
pub use info::ThreadInfo;
@ -14,6 +13,8 @@ use crate::runtime::program::Program;
use crate::runtime::run_thread::state::{CoroutineContext, CoroutineState};
use std::{mem, thread};
use crate::asm::instr::cond::Flag;
use parking_lot::RwLock;
use crate::asm::instr::Flatten;
#[derive(Clone, Copy, Eq, PartialEq, Debug, Ord, PartialOrd)]
pub struct ThreadToken(pub u32);
@ -40,27 +41,30 @@ impl RunThread {
pub fn new(params: ThreadParams) -> Self {
let extensions = params.program.extensions.clone();
// TODO investigate if this division to 2 structs is still needed
let ti = Arc::new(ThreadInfo {
id: params.id,
uniq: params.uniq,
program: params.program,
cycle_time: params.cycle_time,
scheduler_interval: params.scheduler_interval,
scheduler_interval: RwLock::new(params.scheduler_interval),
extensions,
});
let rs = RunState {
thread_info: ti.clone(),
cr: CoroutineContext {
cr: Box::new(CoroutineContext {
handle: 0, // this is the root
frame: StackFrame::new(params.pc, params.args),
call_stack: vec![],
cr_state: Default::default(),
},
}),
parked: Default::default(),
global_regs: [0; REG_COUNT],
ext_data: Default::default(),
cr_deadline: None
cr_deadline: None,
critical_section: 0,
};
Self {
@ -69,21 +73,17 @@ impl RunThread {
}
}
/// Spawn as a thread
pub fn spawn(self) -> JoinHandle<()> {
std::thread::spawn(move || {
self.run();
})
}
/// Start synchronously
pub fn run(mut self) {
let mut loop_helper = spin_sleep::LoopHelper::builder()
.build_with_target_rate(1.0/self.info.cycle_time.as_secs_f64());
loop_helper.loop_start();
'run: loop {
let mut orig_pc;
let fault = 'run: loop {
let mut want_switch = false;
orig_pc = self.state.cr.frame.pc;
match self.eval_op() {
Ok(EvalRes { cycles, advance, sched }) => {
for _ in 0..cycles {
@ -91,7 +91,6 @@ impl RunThread {
loop_helper.loop_start();
}
trace!("Step {}; Status = {}", advance, self.state.cr.frame.status);
let orig_pc = self.state.cr.frame.pc;
self.state.cr.frame.pc.advance(advance);
if let Some(dl) = self.state.cr_deadline {
@ -126,7 +125,7 @@ impl RunThread {
'find: for (n, th) in self.state.parked.iter_mut().enumerate() {
if th.handle == handle {
if let CoroutineState::Finished(_) = &th.cr_state {
let mut crs = mem::replace(&mut th.cr_state, CoroutineState::Ready);
let crs = mem::replace(&mut th.cr_state, CoroutineState::Ready);
self.state.parked.remove(n); // delete it
found = true;
@ -154,83 +153,141 @@ impl RunThread {
}
}
Err(Fault::Blocked) => {
// Do not advance, the last instruction will be re-tried
trace!("Thread reports being blocked!");
self.state.cr.frame.pc = orig_pc; // Retry
self.state.cr.cr_state = CoroutineState::Ready;
want_switch = true;
}
Err(Fault::Halt) => {
// TODO implement coordinated shutdown when more threads are running!
break 'run;
}
Err(e) => {
error!("Fault: {:?}", e);
error!("Core dump: {:?}", self.state);
break 'run;
break 'run e;
}
}
if want_switch {
trace!("Switch requested");
// Resolve the next coroutine to run, or wait a bit...
'next: loop {
if want_switch {
let now = Instant::now();
let now = Instant::now();
let mut candidate = None;
let mut closest_due = None;
for _ in 0..self.state.parked.len() {
if let Some(mut rt) = self.state.parked.pop_front() {
match rt.cr_state {
CoroutineState::Ready => {
candidate = Some(rt);
break;
}
if self.state.critical_section > 0 {
match self.state.cr.cr_state {
CoroutineState::Ready => {}
CoroutineState::Sleep { due } => {
if due <= now {
rt.cr_state = CoroutineState::Ready;
let time = due.saturating_duration_since(now);
trace!("Sleep in critical: {:?}", time);
thread::sleep(time);
continue 'run;
}
CoroutineState::Finished(_) | CoroutineState::YieldedValue(_) => {
// This is not good
break 'run Fault::Deadlock;
}
}
// This is either a bug, or waiting for IO.
// If it is the latter, try to give the OS a bit of breathing room..
std::thread::yield_now();
continue 'run;
}
trace!("Switch requested");
let mut candidate = None;
let mut closest_due = None;
'findbest: for _ in 0..self.state.parked.len() {
if let Some(mut rt) = self.state.parked.pop_front() {
match rt.cr_state {
CoroutineState::Ready => {
trace!("Found READY thread to run next");
candidate = Some(rt);
break;
} else {
match closest_due {
Some(d) => {
if d > due {
break 'findbest;
}
CoroutineState::Sleep { due } => {
if due <= now {
trace!("Found DUE sleeping thread to run next");
rt.cr_state = CoroutineState::Ready;
candidate = Some(rt);
break 'findbest;
} else {
match closest_due {
Some(d) => {
if d > due {
closest_due = Some(due);
}
},
None => {
closest_due = Some(due);
}
},
None => {
closest_due = Some(due);
}
self.state.parked.push_back(rt);
}
}
_ => {
self.state.parked.push_back(rt);
}
}
_ => {
self.state.parked.push_back(rt);
}
}
}
}
if let Some(cr) = candidate {
trace!("Context switch to {:?}", cr);
// Do switch
let old = mem::replace(&mut self.state.cr, cr);
self.state.parked.push_back(old);
self.state.cr_deadline = Some(now + self.info.scheduler_interval);
} else if let Some(due) = closest_due {
let time = due.saturating_duration_since(now);
trace!("No thread to switch to, sleep {:?}", time);
thread::sleep(time);
if let Some(cr) = candidate {
trace!("Context switch to {:?}", cr);
// Do switch
let old = mem::replace(&mut self.state.cr, cr);
self.state.parked.push_back(old);
self.state.start_task_switching();
} else if let Some(due) = closest_due {
if self.state.cr.cr_state == CoroutineState::Ready {
trace!("No candidate found to run, retry same thread.");
// Let it run another quantum, maybe it was just blocked
continue 'run;
}
let time = due.saturating_duration_since(now);
trace!("No thread to switch to, sleep {:?}", time);
thread::sleep(time);
continue 'next;
} else {
// Nothing to run?
thread::yield_now();
trace!("No thread to switch to!");
}
let n_alive = self.state.parked.iter()
.filter(|p| p.cr_state.is_alive()).count();
if n_alive == 0 {
trace!("Stop task switching, no parked threads are alive");
self.state.stop_task_switching(); // This should improve performance in single-threaded mode
}
match self.state.cr.cr_state {
CoroutineState::Finished(_) | CoroutineState::YieldedValue(_) => {
break 'run Fault::RootReturned;
}
_ => {}
}
}
let n_alive = self.state.parked.iter()
.filter(|p| p.cr_state.is_alive()).count();
break 'next;
}
};
if n_alive == 0 {
trace!("Stop task switching, no parked threads are alive");
self.state.cr_deadline = None;
match fault {
Fault::Halt => {
debug!("Thread ended.");
}
e => {
eprintln!("*** Program failed: {} ***", e);
if let Some(instr) = self.info.program.ops.get(orig_pc.0 as usize) {
eprintln!("Source location: {}, line {}, column {}", self.info.program.file_names[instr.pos().file as usize].display(), instr.pos().line, instr.pos().column);
} else {
eprintln!("Instruction address: {}", orig_pc);
}
debug!("\nCore dump: {:?}", self.state);
}
}
debug!("Thread ended.");
}
}

@ -6,6 +6,7 @@ use crate::asm::data::literal::Value;
use crate::runtime::program::Program;
use crate::runtime::run_thread::ThreadToken;
use crate::module::{CrsnExtension, CrsnUniq};
use parking_lot::RwLock;
#[derive(Debug)]
pub struct ThreadInfo {
@ -18,7 +19,7 @@ pub struct ThreadInfo {
/// Program to run
pub(crate) cycle_time: Duration,
/// Interval one thread/coroutine is let to run before the context switches
pub(crate) scheduler_interval: Duration,
pub(crate) scheduler_interval: RwLock<Duration>,
/// Extensions
pub extensions: Arc<Vec<Box<dyn CrsnExtension>>>,
}

@ -12,20 +12,22 @@ use nudge::{likely};
use crate::asm::instr::cond::Flag;
use std::fmt::{Debug, Formatter};
use std::fmt;
use std::time::Instant;
use std::time::{Instant, Duration};
pub struct RunState {
pub thread_info: Arc<ThreadInfo>,
/// The active coroutine
pub cr: CoroutineContext,
pub cr: Box<CoroutineContext>,
/// Parked coroutines
pub parked: VecDeque<CoroutineContext>,
pub parked: VecDeque<Box<CoroutineContext>>,
/// General purpose registers that stay valid for the entire run-time of the thread
pub global_regs: [Value; REG_COUNT],
/// Extension data
pub ext_data: ExtensionDataStore,
/// Execution deadline, if multi-tasked
pub cr_deadline: Option<Instant>,
/// Nonzero if inside a critical section
pub critical_section: Value,
}
#[derive(Debug,Default)]
@ -72,7 +74,7 @@ impl Default for CoroutineState {
impl RunState {
/// Clear everything. Caution: when done at runtime, this effectively reboots the thread
pub fn clear_all(&mut self) {
pub fn reset(&mut self) {
self.cr = Default::default();
self.parked = Default::default();
self.global_regs = Default::default();
@ -85,18 +87,32 @@ impl RunState {
trace!("Spawn cr {:#}", handle);
// front - so it runs ASAP
self.parked.push_front(CoroutineContext {
self.parked.push_front(Box::new(CoroutineContext {
handle,
frame,
call_stack: vec![],
cr_state: Default::default()
});
}));
if self.cr_deadline.is_none() {
// start context switching
self.cr_deadline = Some(Instant::now() + self.thread_info.scheduler_interval);
self.start_task_switching();
}
handle
}
pub(crate) fn start_task_switching(&mut self) {
let ival = *self.thread_info.scheduler_interval.read();
if ival > Duration::default() {
self.cr_deadline = Some(Instant::now() + ival);
} else {
// Disabled
self.cr_deadline = None;
}
}
pub(crate) fn stop_task_switching(&mut self) {
self.cr_deadline = None;
}
}
impl Debug for RunState {

@ -21,13 +21,29 @@ mod console {
use std::ffi::c_void;
use std::mem::{self, MaybeUninit};
use crsn::runtime::fault::Fault;
use std::time::{Instant};
struct ReadCharState {
bytes: [u8; 4],
cursor: usize,
len: usize,
last_timeout : u8,
}
static mut READ_CHAR_STATE: ReadCharState = ReadCharState {
bytes: [0; 4],
cursor: 0,
len: 0,
last_timeout : 0,
};
fn setup_fd(fd: RawFd) -> io::Result<libc::termios> {
fn setup_fd(fd: RawFd) -> Result<libc::termios, Fault> {
use libc::*;
let mut tio = MaybeUninit::uninit();
if 0 != unsafe { tcgetattr(fd, tio.as_mut_ptr()) } {
return Err(io::Error::last_os_error());
return Err(Fault::IOError(io::Error::last_os_error()));
}
let mut tio = unsafe { MaybeUninit::assume_init(tio) };
let old_tio : termios = unsafe { mem::transmute_copy(&tio) };
@ -39,36 +55,76 @@ mod console {
tio.c_cc[VMIN] = 1;
tio.c_cc[VTIME] = 0;
if 0 != unsafe { tcsetattr(fd, TCSANOW, &tio) } {
return Err(io::Error::last_os_error());
return Err(Fault::IOError(io::Error::last_os_error()));
}
Ok(old_tio)
}
pub fn init_io() -> io::Result<libc::termios> {
pub fn init_io() -> Result<libc::termios, Fault> {
setup_fd(libc::STDIN_FILENO)
}
pub fn read_byte() -> io::Result<u8> {
pub fn read_byte(deadline : Option<Instant>) -> Result<u8, Fault> {
// Set TIO timeout
let state = unsafe { &mut READ_CHAR_STATE };
if (state.last_timeout == 0) && deadline.is_none() {
// Keep it like that
} else {
let vtime = if let Some(dl) = deadline {
let timeout = dl.saturating_duration_since(Instant::now());
((timeout.as_secs_f32() * 10.0).round() as u32).min(255).max(1) as u8
} else {
0
};
if state.last_timeout != vtime {
// vtime changes
state.last_timeout = vtime;
let mut tio = MaybeUninit::uninit();
if 0 != unsafe { libc::tcgetattr(libc::STDIN_FILENO, tio.as_mut_ptr()) } {
return Err(Fault::IOError(io::Error::last_os_error()));
}
let mut tio = unsafe { MaybeUninit::assume_init(tio) };
if vtime > 0 {
tio.c_cc[libc::VTIME] = vtime; /* unit = 0.1 */
tio.c_cc[libc::VMIN] = 0;
} else {
tio.c_cc[libc::VTIME] = 0; // no counting
tio.c_cc[libc::VMIN] = 1; // want at least one character
}
unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &tio) };
}
}
let mut buf = 0u8;
let len = unsafe { libc::read(libc::STDIN_FILENO, &mut buf as *mut u8 as *mut c_void, 1) };
if len == 0 && state.last_timeout != 0 {
return Err(Fault::IOError(io::Error::new(std::io::ErrorKind::TimedOut, "")));
}
if len <= 0 {
Err(io::Error::last_os_error())
Err(Fault::IOError(io::Error::last_os_error()))
} else {
Ok(buf as u8)
}
}
pub fn write_byte(b : u8) -> io::Result<()> {
pub fn write_byte(b : u8) -> Result<(), Fault> {
let len = unsafe { libc::write(libc::STDOUT_FILENO, &b as *const u8 as *const c_void, 1) };
if len <= 0 {
Err(io::Error::last_os_error())
Err(Fault::IOError(io::Error::last_os_error()))
} else {
Ok(())
}
}
pub fn write_char(c : char) -> io::Result<()> {
pub fn write_char(c : char) -> Result<(), Fault> {
let mut buf = [0u8; 4];
for b in c.encode_utf8(&mut buf).as_bytes() {
write_byte(*b)?;
@ -76,30 +132,43 @@ mod console {
Ok(())
}
pub fn read_char() -> io::Result<char> {
let first = read_byte()?;
pub fn read_char(deadline : Option<Instant>) -> Result<char, Fault> {
let state = unsafe { &mut READ_CHAR_STATE };
if first & 0x80 == 0 {
return Ok(first as char);
}
if state.cursor == 0 {
let first = read_byte(deadline)?;
let mut bytes = [first, 0, 0, 0];
if first & 0x80 == 0 {
return Ok(first as char);
}
let remain = if first & 0b1110_0000 == 0b1100_0000 {
1
} else if first & 0b1111_0000 == 0b1110_0000 {
2
} else /*if first & 0b1111_1000 == 0b1111_0000*/ {
3
};
state.bytes[0] = first;
state.cursor = 1;
state.len = if first & 0b1110_0000 == 0b1100_0000 {
2
} else if first & 0b1111_0000 == 0b1110_0000 {
3
} else /*if first & 0b1111_1000 == 0b1111_0000*/ {
4
};
}
for n in 1..=remain {
bytes[n] = read_byte()?;
let len = state.len;
while state.cursor < len {
let b = read_byte(deadline)?;
state.bytes[state.cursor] = b;
state.cursor += 1;
}
std::str::from_utf8(&bytes[..=remain])
let rv = std::str::from_utf8(&state.bytes[..=len])
.map(|s| s.chars().nth(0).unwrap())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
.map_err(|e| Fault::IOError(io::Error::new(io::ErrorKind::InvalidData, e)));
state.cursor = 0;
state.len = 0;
rv
}
}
@ -183,35 +252,47 @@ impl CrsnExtension for StdioOps {
fn read_obj(&self, state: &mut RunState, handle: Value)
-> Result<Option<Value>, Fault>
{
let deadline = state.cr_deadline;
if handle == self.hdl_stdin {
match console::read_char() {
match console::read_char(deadline) {
Ok(c) => {
return Ok(Some(c as Value));
}
Err(e) => {
Err(Fault::IOError(e)) => {
if e.kind() == io::ErrorKind::TimedOut {
return Err(Fault::Blocked);
}
state.set_flag(Flag::Invalid, true);
if e.kind() != io::ErrorKind::InvalidData {
state.set_flag(Flag::Eof, true);
}
return Ok(Some(0));
}
Err(other) => {
return Err(other);
}
}
}
if handle == self.hdl_stdin_raw {
match console::read_byte() {
} else if handle == self.hdl_stdin_raw {
match console::read_byte(deadline) {
Ok(b) => {
return Ok(Some(b as Value));
}
Err(_e) => {
Err(Fault::IOError(e)) => {
if e.kind() == io::ErrorKind::TimedOut {
return Err(Fault::Blocked);
}
state.set_flag(Flag::Invalid, true);
state.set_flag(Flag::Eof, true);
return Ok(Some(0));
}
Err(other) => {
return Err(other);
}
}
}
if handle == self.hdl_stdout || handle == self.hdl_stdout_raw {
} else if handle == self.hdl_stdout || handle == self.hdl_stdout_raw {
state.set_flag(Flag::Invalid, true);
return Ok(Some(0));
}
@ -254,13 +335,15 @@ impl CrsnExtension for StdioOps {
fn read_obj_all(&self, state: &mut RunState, whandle: Wr, rhandle: Value)
-> Result<Option<()>, Fault>
{
// XXX This is blocking, there is no sensible way to split it up.
if rhandle == self.hdl_stdin {
loop {
match console::read_char() {
match console::read_char(None) {
Ok(c) => {
state.write(whandle, c as Value)?;
}
Err(e) => {
Err(Fault::IOError(e)) => {
if e.kind() != io::ErrorKind::InvalidData {
state.set_flag(Flag::Eof, true);
} else {
@ -268,17 +351,20 @@ impl CrsnExtension for StdioOps {
}
return Ok(Some(()));
}
Err(other) => {
return Err(other);
}
}
}
}
if rhandle == self.hdl_stdin_raw {
loop {
match console::read_byte() {
match console::read_byte(None) {
Ok(c) => {
state.write(whandle, c as Value)?;
}
Err(e) => {
Err(Fault::IOError(e)) => {
if e.kind() != io::ErrorKind::InvalidData {
state.set_flag(Flag::Eof, true);
} else {
@ -286,6 +372,9 @@ impl CrsnExtension for StdioOps {
}
return Ok(Some(()));
}
Err(other) => {
return Err(other);
}
}
}
}

@ -0,0 +1,44 @@
(
; This example shows the use of critical sections.
; Set short timeslice (50us) to make the effect more pronounced
(rt-opt RT_TIMESLICE 50)
(spawn _ unsafe 'A' 'Z')
(spawn _ unsafe 'a' 'z')
(spawn _ safe '0' '9') ; Notice that the sequence 0-9 is always printed in its entirety - because it is in a critical section
(msleep 200)
(halt)
(proc unsafe start end
; This can be interrupted any time
(:x)
(yield)
(ld r0 start)
(:l)
(ld @cout r0)
(cmp r0 end)
(j.eq :x)
(inc r0)
(j :l)
)
(proc safe start end
(:again)
(ld r0 start)
; The sequence will always be complete
(crit
(ld @cout ' ') ; space to make it easier to read
(:l)
(ld @cout r0)
(cmp r0 end)
(j.eq :x)
(inc r0)
(j :l)
(:x)
(ld @cout ' ') ; space to make it easier to read
)
(yield)
(j :again)
)
)

@ -0,0 +1,17 @@
(
; This demo shows that stdin is nearly non-blocking
(spawn _ ReadAndEcho)
(:a)
(mslp 2500)
(lds @cout "Tick\n")
(j :a)
(proc ReadAndEcho
(:a)
(ld @cout '?')
(ld @cout @cin)
(j :a)
)
)

@ -0,0 +1,18 @@
(
(include other)
(def BAR (=add FOO 1000))
(cmp BAR 1123 (ne? (fault)))
(include utils/itoa.csn)
(include "utils/printnum")
(mkbf r0)
(call itoa r0 BAR)
(lds @cout @r0)
(del @r0)
(ld @cout '\n')
(call printnum BAR)
(ld @cout '\n')
)

@ -0,0 +1,3 @@
(
(def FOO 123)
)

@ -0,0 +1,14 @@
(
(proc itoa buf num
(ld r1 num)
(tst r1 (<0? (mul r1 -1)))
(:next)
(mod r0 r1 10)
(add r0 '0')
(bfrpush @buf r0)
(div r1 10 (z?
(tst num (<0? (bfrpush @buf '-')))
(ret)))
(j :next)
)
)

@ -0,0 +1,17 @@
(
(proc printnum num
(mkbf r15)
(ld r1 num)
(tst r1 (<0? (mul r1 -1)))
(:next)
(mod r0 r1 10)
(add r0 '0')
(bfrpush @r15 r0)
(div r1 10 (z?
(tst num (<0? (bfrpush @r15 '-')))
(lds @cout @r15)
(del @r15)
(ret)))
(j :next)
)
)

@ -0,0 +1,43 @@
(
; Unlabeled loop. Can be exited by jumping out
(loop
(nop)
(nop)
(nop)
)
(barrier)
; The above is equivalent to:
(:label)
(nop)
(nop)
(nop)
(j :label)
(barrier)
; The label name can be specified.
; This adds a start label and ":label-end" at the end of the loop:
(loop :repeat
(nop)
(nop)
(j :repeat-end)
(nop)
)
(barrier)
; The above is equivalent to: (labels changed to avoid a compile error)
(:repeat2)
(nop)
(nop)
(j :repeat2-end)
(nop)
(j :repeat2)
(:repeat2-end)
)

@ -17,7 +17,6 @@ use crsn_screen::ScreenOps;
use crsn_stdio::StdioOps;
use crsn_buf::BufOps;
mod read_file;
mod serde_duration_millis;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -108,6 +107,14 @@ impl AppConfig for Config {
.help("Cycle time (us, append \"s\" or \"ms\" for coarser time)")
.takes_value(true),
)
.arg(
clap::Arg::with_name("sched")
.long("sched")
.short("S")
.value_name("MILLIS")
.help("Scheduler switch time (ms, append \"u\", \"s\" or \"ms\" for other unit)")
.takes_value(true),
)
}
fn configure(mut self, clap: &ArgMatches) -> anyhow::Result<Self> {
@ -126,11 +133,28 @@ impl AppConfig for Config {
} else if t.ends_with("s") {
(&t[..(t.len()-1)], 1000_000)
} else {
(t, 1)
(t, 1) // us default
};
self.cycle_time = Duration::from_micros(t.parse::<u64>().expect("parse -C value") * mul);
println!("ct = {:?}", self.cycle_time);
}
if let Some(t) = clap.value_of("sched") {
let (t, mul) = if t.ends_with("us") {
(&t[..(t.len()-2)], 1)
} else if t.ends_with("ms") {
(&t[..(t.len()-2)], 1000)
} else if t.ends_with("m") {
(&t[..(t.len()-1)], 1000)
} else if t.ends_with("u") {
(&t[..(t.len()-1)], 1)
} else if t.ends_with("s") {
(&t[..(t.len()-1)], 1000_000)
} else {
(t, 1000) // ms default
};
self.switch_time = Duration::from_micros(t.parse::<u64>().expect("parse -S value") * mul);
}
Ok(self)
}
@ -142,11 +166,9 @@ fn main() -> anyhow::Result<()> {
debug!("Loading {}", config.program_file);
let source = read_file::read_file(&config.program_file)?;
let uniq = CrsnUniq::new();
let parsed = crsn::asm::assemble(&source, &uniq, vec![
let parsed = crsn::asm::assemble(&config.program_file, &uniq, vec![
ArithOps::new(),
BufOps::new(),
ScreenOps::new(),

Loading…
Cancel
Save