Compare commits

...

3 Commits

  1. 23
      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. 17
      crsn/crsn-sexp/src/test.rs
  6. 16
      crsn/src/asm/error.rs
  7. 55
      crsn/src/asm/mod.rs
  8. 7
      crsn/src/asm/parse/mod.rs
  9. 87
      crsn/src/asm/parse/parse_instr.rs
  10. 21
      crsn/src/asm/patches/mod.rs
  11. 2
      crsn/src/asm/read_file.rs
  12. 11
      crsn/src/runtime/program.rs
  13. 12
      crsn/src/runtime/run_thread.rs
  14. 8
      crsn/src/runtime/run_thread/state.rs
  15. 18
      examples/include/main.csn
  16. 3
      examples/include/other.csn
  17. 14
      examples/include/utils/itoa.csn
  18. 17
      examples/include/utils/printnum.csn
  19. 5
      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:

@ -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,6 @@
use super::*;
use super::error::get_line_and_column;
use super::position::get_line_and_column;
#[test]
fn test_hello_world() {
@ -67,16 +68,16 @@ 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]

@ -8,6 +8,7 @@ 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)]
@ -20,6 +21,21 @@ pub enum CrsnError {
ParseOther(Box<dyn Error + Send + Sync>, SourcePosition),
#[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)

@ -11,26 +11,25 @@ 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
}
@ -38,10 +37,25 @@ 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(),
@ -72,12 +86,27 @@ pub fn assemble(source: &str, uniq : &CrsnUniq, mut parsers: Vec<Box<dyn CrsnExt
},
const_eval_ti: ti,
parsing_expr: false,
label_num: label_num.clone()
label_num: label_num.clone(),
files: vec![
path,
],
active_file: 0
}),
};
let ops = parse::parse(source, &SourcePosition::default(), &pcx)?;
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))?)
}

@ -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;
@ -54,6 +55,12 @@ pub struct ParserState {
/// 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 {

@ -6,14 +6,19 @@ 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;
use crate::asm::instr::flatten::jumps_to_skips;
pub fn parse_instructions(items: impl Iterator<Item=Sexp>, pos: &SourcePosition, pcx: &ParserContext) -> Result<Box<dyn Flatten>, CrsnError> {
let mut parsed = vec![];
let mut parsed: Vec<Box<dyn Flatten>> = vec![];
'exprs: for expr in items {
let (tokens, listpos) = expect_list(expr, false)?;
@ -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()));

@ -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)?;

@ -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,
}

@ -54,12 +54,12 @@ impl RunThread {
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(),
@ -279,12 +279,14 @@ impl RunThread {
debug!("Thread ended.");
}
e => {
eprintln!("*** Program failed: {} ***", e);
if let Some(instr) = self.info.program.ops.get(orig_pc.0 as usize) {
error!("Fault at {}: {}", instr.pos(), e);
eprintln!("Source location: {}, line {}, column {}", self.info.program.file_names[instr.pos().file as usize].display(), instr.pos().line, instr.pos().column);
} else {
error!("Fault at PC {}: {}", orig_pc, e);
eprintln!("Instruction address: {}", orig_pc);
}
warn!("Core dump: {:?}", self.state);
debug!("\nCore dump: {:?}", self.state);
}
}
}

@ -17,9 +17,9 @@ 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
@ -87,12 +87,12 @@ 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.start_task_switching();

@ -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)
)
)

@ -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)]
@ -167,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