diff --git a/Cargo.lock b/Cargo.lock index ddba9ff9..9825690e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,11 +167,61 @@ dependencies = [ "lazy_static", "macro-attr", "ntest", + "rayon", "serde", "serde_json", "soa_derive", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "enum_derive" version = "0.1.7" @@ -211,6 +261,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.1" @@ -233,7 +292,7 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "libc", "windows-sys", ] @@ -244,7 +303,7 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.1", "io-lifetimes", "rustix", "windows-sys", @@ -304,6 +363,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + [[package]] name = "memory_units" version = "0.4.0" @@ -343,6 +411,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -377,6 +455,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "rustix" version = "0.37.18" @@ -397,6 +497,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.160" diff --git a/Cargo.toml b/Cargo.toml index 01fe7e52..b898bb33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,9 @@ [workspace] -members = [ - "compiler", - "tci-web" -] +members = ["compiler", "tci-web"] [profile.dev] -opt-level = 0 +opt-level = 0 [profile.release] opt-level = "z" diff --git a/compiler/Cargo.toml b/compiler/Cargo.toml index f44e7db8..9fefae57 100644 --- a/compiler/Cargo.toml +++ b/compiler/Cargo.toml @@ -3,7 +3,7 @@ name = "compiler" version = "0.1.0" authors = ["Albert Liu "] edition = "2021" -description = "Teaching C Interpreter" +description = "C compiler for students" license = "MIT" [dependencies] @@ -16,6 +16,7 @@ clap = { version = "4.2.4", features = ["derive"] } bitfield-struct = "0.3" enum_derive = "0.1.7" macro-attr = "0.2.0" +rayon = "1.7.0" [dev-dependencies] ntest = "0.9.0" diff --git a/compiler/src/ast.rs b/compiler/src/ast.rs index 808f5f74..612e3a5a 100644 --- a/compiler/src/ast.rs +++ b/compiler/src/ast.rs @@ -4,6 +4,14 @@ This module describes the AST created by the parser. use crate::api::*; +pub trait AstInterpretData { + type Output; + + fn read(&self, field: &u64) -> Self::Output; + + fn as_mut_ref(&self, field: &mut u64) -> &mut Self::Output; +} + #[derive(Debug, Clone, Copy, StructOfArray)] pub struct AstNode { pub kind: AstNodeKind, @@ -21,7 +29,7 @@ pub struct AstNode { } macro_attr! { -#[derive(Serialize, Deserialize, Debug, Clone, Copy, EnumFromInner!)] +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, EnumFromInner!)] #[serde(tag = "kind", content = "data")] pub enum AstNodeKind { Expr(AstExpr), @@ -34,10 +42,7 @@ pub enum AstNodeKind { } } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct AstEof; - -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum AstExpr { IntLit, // data: i32 LongLit, // data: i64 @@ -62,7 +67,7 @@ pub enum AstExpr { BinOpAssign(BinOp), // children: expression being assigned to, expression being assigned } -#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Serialize, Deserialize)] pub enum BinOp { Add, Sub, @@ -87,7 +92,7 @@ pub enum BinOp { Comma, } -#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy, PartialOrd, Serialize, Deserialize)] pub enum UnaryOp { Neg, BoolNot, @@ -101,16 +106,20 @@ pub enum UnaryOp { } /// Handles struct and union declarations: +/// +/// ```text /// struct a { int b; } +/// ``` +/// /// In the above, it would have children for each field /// declaration, and a child for the identifier as well. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum StructDeclaration { Struct, Union, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum AstStatement { Labeled, // data: label ; children: statement that is being labelled CaseLabeled, // children: case value expression, statement that is being labelled @@ -133,7 +142,7 @@ pub enum AstStatement { /// `int *const a`, or the `[3]` part of `int b[3]` /// /// Children: AstSpecifer for each type qualifier -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum AstDerivedDeclarator { Pointer = 0, @@ -158,7 +167,7 @@ pub enum AstDerivedDeclarator { } /// children: a AstDerivedDeclarator for each derived declarator -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum AstDeclarator { Abstract, /// data: Symbol @@ -167,7 +176,7 @@ pub enum AstDeclarator { NestedWithChild, } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub enum AstSpecifier { Extern, Static, @@ -187,29 +196,87 @@ pub enum AstSpecifier { Int, Long, + Unsigned, + Signed, + Float, Double, - UChar, - UShort, - UInt, - ULong, - Struct(StructDeclaration), // children: ident declaration of struct, field declarations of struct Ident, // data: Symbol } /// A typical declaration; this is a stand-in for -/// int *i[1] = {NULL}; or something similar +/// `int *i[1] = {NULL};` or something similar /// /// Children: AstSpecifier for each specifier, AstStructDeclaration if necessary, an AstInitDeclarator for each declared variable -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub struct AstDeclaration; -/// A typical declaration; this is a stand-in for -/// int *i[1] = {NULL}; or something similar +/// A function definition /// /// Data: DeclarationSpecifiers /// Children: AstSpecifier for each specifier, san AstDeclarator, and all the statements associated with the function -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Serialize, Deserialize)] pub struct AstFunctionDefinition; + +/// Prints the tree in a text format, so that it's a lil easier to read. +/// Output right now looks like this: +/// +/// ```text +/// FunctionDefinition(AstFunctionDefinition) +/// └ Specifier(Int) +/// └ Declarator(Ident) +/// | └ DerivedDeclarator(Function) +/// | | └ Declaration(AstDeclaration) +/// | | | └ Specifier(Int) +/// | | | └ Declarator(Ident) +/// | | └ Declaration(AstDeclaration) +/// | | | └ Specifier(Char) +/// | | | └ Declarator(Ident) +/// | | | | └ DerivedDeclarator(Pointer) +/// | | | | └ DerivedDeclarator(Pointer) +/// └ Statement(Block) +/// | └ Statement(Ret) +/// | | └ Expr(StringLit) +/// ``` +pub fn display_tree(ast: &AstNodeVec) -> String { + let mut children = Vec::>::with_capacity(ast.len()); + children.resize_with(ast.len(), || Vec::new()); + + let mut roots = Vec::new(); + + for node in ast.as_slice().into_iter() { + if *node.post_order != *node.parent { + children[*node.parent as usize].push(*node.post_order as usize); + } else { + roots.push(*node.post_order); + } + } + + roots.reverse(); + + let mut parent_stack = Vec::with_capacity(roots.len()); + for root in roots.iter().rev() { + parent_stack.push((0u32, *root as usize)); + } + + let mut out = String::new(); + while let Some((depth, node_id)) = parent_stack.pop() { + if depth > 0 { + for _ in 0..(depth - 1) { + out += "| "; + } + + out += "└ "; + } + + out += &format!("{:?}\n", ast.as_slice().index(node_id).kind); + + for id in children[node_id].iter().rev() { + parent_stack.push((depth + 1, *id)); + } + } + + return out; +} diff --git a/compiler/src/bin/test_runner.rs b/compiler/src/bin/test_runner.rs index 10c70571..014da7e4 100644 --- a/compiler/src/bin/test_runner.rs +++ b/compiler/src/bin/test_runner.rs @@ -1,15 +1,45 @@ use clap::Parser; +use codespan_reporting::term::termcolor::*; +use codespan_reporting::term::*; +use compiler::{parse_test_case, StageOutput}; -/// Search for a pattern in a file and display the lines that contain it. +#[derive(clap::ValueEnum, Clone, Copy)] +enum Stage { + Lex, + Macro, + Parse, +} + +/// Run #[derive(Parser)] +#[clap(author = "Albert Liu", about = "Test runner for TCI.")] struct Cli { + #[clap(help = "a path to a test case")] test_case: std::path::PathBuf, - #[clap(short, long)] - write: bool, + #[clap( + short, + long, + value_delimiter = ',', + help = "a stage to ignore", + long_help = r#"A stage to ignore. This can be repeated, or you can pass +the stage names as a comma-separated list. - #[clap(short, long)] +Examples: +"lex,macro" skips the lexing and macro expansion stages."# + )] + #[arg(value_enum)] + ignore: Vec, + + #[clap( + short, + long, + help = "output the result to OUT_FILE. Overrides `--write`" + )] out_file: Option, + + #[clap(short, long, help = "write to the input file in-place")] + write: bool, } fn main() { @@ -18,9 +48,34 @@ fn main() { let test_case = std::fs::read_to_string(&args.test_case).expect("file should exist and be a valid string"); - let result = compiler::api::run_test_code(&*test_case); + let writer = StandardStream::stderr(ColorChoice::Always); + let config = Config::default(); + let print_err: compiler::PrintFunc = &|files, tu, err| { + let diagnostic = tu.diagnostic(err); + codespan_reporting::term::emit(&mut writer.lock(), &config, files, &diagnostic) + .expect("wtf"); + }; + + let (source, expected) = parse_test_case(&test_case); + + let print_err = if args.out_file.is_some() || args.write { + None + } else { + Some(print_err) + }; + + let mut result = compiler::run_compiler_for_testing(source.to_string(), print_err); + assert_eq!(result, expected); + + for stage in args.ignore { + match stage { + Stage::Lex => result.lexer = StageOutput::Ignore, + Stage::Macro => result.macro_expansion = StageOutput::Ignore, + Stage::Parse => result.parsed_ast = StageOutput::Ignore, + } + } - let text = result.test_case(); + let text = result.test_case(source); if let Some(out) = &args.out_file { std::fs::write(out, text).expect("failed to write file"); diff --git a/compiler/src/error.rs b/compiler/src/error.rs index adfab826..ce2aeaed 100644 --- a/compiler/src/error.rs +++ b/compiler/src/error.rs @@ -1,3 +1,5 @@ +use codespan_reporting::diagnostic::{Diagnostic, Label}; + // Book-keeping to track which ranges belong to which file, so that we can // compute file and line number from `start` #[derive(Debug, Clone, Copy)] @@ -15,25 +17,118 @@ pub struct TranslationUnitDebugInfo { pub file_starts: Vec, } -#[derive(Debug)] +pub struct FileRange { + pub file: u32, + pub start: usize, +} + +impl TranslationUnitDebugInfo { + pub fn diagnostic(&self, err: &Error) -> Diagnostic { + return Diagnostic::error() + .with_message(err.message()) + .with_code(err.code()) + .with_labels(err.kind.labels(self)); + } + + pub fn token_range(&self, start: u32) -> FileRange { + // TODO: binary search + let mut previous = self.file_starts[0]; + for file_start in &self.file_starts { + if file_start.index > start { + break; + } + + previous = *file_start; + } + + return FileRange { + file: previous.file, + start: previous.file_index + (start as usize - previous.index as usize), + }; + } +} + +#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ErrorKind { - Todo(&'static str), + Todo(String), - UnrecognizedCharacter { idx: u32 }, + DidntRun, + NotImplemented(String), - UnrecognizedToken { idx: u32 }, + InvalidCharacterSequence { seq: String, index: u32 }, } -macro_rules! throw { +macro_rules! error { + ($e:ident ( $str:literal )) => { + Error::new(crate::error::ErrorKind::$e ( $str.to_string() )) + }; + ($e:ident) => { + Error::new(crate::error::ErrorKind::$e) + }; ($e:ident $t:tt) => { - return Err(Error::new(crate::error::ErrorKind::$e $t)) + Error::new(crate::error::ErrorKind::$e $t) + }; +} + +macro_rules! throw { + ($($e:tt)*) => { + { return Err(error!($($e)*)); } }; } +impl ErrorKind { + pub fn message(&self) -> String { + use ErrorKind::*; + + match self { + Todo(message) => format!("{}", message), + + DidntRun => format!("compiler phase didn't run"), + NotImplemented(message) => format!("{}", message), + + InvalidCharacterSequence { seq, index } => format!("'{}' isn't valid", seq), + } + } + + pub fn code(&self) -> &'static str { + use ErrorKind::*; + + match self { + Todo(message) => "001", + + DidntRun => "000", + NotImplemented(message) => "002", + + InvalidCharacterSequence { seq, index } => "100", + } + } + + pub fn labels(&self, tu: &TranslationUnitDebugInfo) -> Vec> { + use ErrorKind::*; + + let mut labels = Vec::new(); + + match self { + Todo(message) => {} + DidntRun => {} + NotImplemented(message) => {} + + InvalidCharacterSequence { seq, index } => { + let range = tu.token_range(*index); + labels.push(Label::primary( + range.file, + range.start..(range.start + seq.len()), + )); + } + } + + return labels; + } +} + #[derive(Debug)] pub struct Error { - kind: ErrorKind, - + pub kind: ErrorKind, backtrace: Option, } @@ -50,18 +145,11 @@ impl Error { }; } - fn todo() -> Result<(), Self> { - throw!(Todo("hello")); - } - pub fn message(&self) -> String { - use ErrorKind::*; - - match self.kind { - Todo(message) => format!("{}", message), + return self.kind.message(); + } - UnrecognizedCharacter { idx } => format!("unrecognized character"), - UnrecognizedToken { idx } => format!("unrecognized token"), - } + pub fn code(&self) -> &'static str { + return self.kind.code(); } } diff --git a/compiler/src/filedb.rs b/compiler/src/filedb.rs index b6e9678c..13eea4f2 100644 --- a/compiler/src/filedb.rs +++ b/compiler/src/filedb.rs @@ -1,4 +1,5 @@ use crate::api::*; +use codespan_reporting::files::{line_starts, Error as SpanErr, Files}; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum FileType { @@ -12,6 +13,7 @@ pub struct File { pub ty: FileType, pub name: String, pub source: String, + pub line_starts: Vec, } struct FileStatic { @@ -70,6 +72,7 @@ impl FileDb { id, ty, name, + line_starts: line_starts(&source).collect(), source, }); } @@ -91,6 +94,7 @@ impl FileDb { id, ty: FileType::User, name, + line_starts: line_starts(&source).collect(), source, }; self.names.insert((FileType::User, file.name.clone()), id); @@ -141,6 +145,66 @@ impl FileDb { } } +/// Return the starting byte index of the line with the specified line index. +/// Convenience method that already generates errors if necessary. +/// +/// Copied from codespan_reporting +fn get_line_start(len: usize, line_starts: &[usize], line_index: usize) -> Result { + use std::cmp::Ordering; + + match line_index.cmp(&line_starts.len()) { + Ordering::Less => Ok(line_starts + .get(line_index) + .cloned() + .expect("failed despite previous check")), + Ordering::Equal => Ok(len), + Ordering::Greater => Err(SpanErr::LineTooLarge { + given: line_index, + max: line_starts.len() - 1, + }), + } +} + +impl<'a> Files<'a> for FileDb { + type FileId = u32; + + type Name = &'a str; + + type Source = &'a str; + + fn name(&'a self, id: Self::FileId) -> Result { + let f = self.files.get(id as usize).ok_or(SpanErr::FileMissing)?; + return Ok(&f.name); + } + + fn source(&'a self, id: Self::FileId) -> Result { + let f = self.files.get(id as usize).ok_or(SpanErr::FileMissing)?; + return Ok(&f.source); + } + + fn line_index(&'a self, id: Self::FileId, byte_index: usize) -> Result { + let f = self.files.get(id as usize).ok_or(SpanErr::FileMissing)?; + + return Ok(f + .line_starts + .binary_search(&byte_index) + .unwrap_or_else(|next_line| next_line - 1)); + } + + fn line_range( + &'a self, + id: Self::FileId, + line_index: usize, + ) -> Result, SpanErr> { + let f = self.files.get(id as usize).ok_or(SpanErr::FileMissing)?; + + let line_start = get_line_start(f.source.len(), &f.line_starts, line_index)?; + let next_line_start = get_line_start(f.source.len(), &f.line_starts, line_index + 1)?; + + return Ok(line_start..next_line_start); + } +} + #[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] #[non_exhaustive] #[repr(u32)] diff --git a/compiler/src/lexer.rs b/compiler/src/lexer.rs index 5be88f2d..3890d8c4 100644 --- a/compiler/src/lexer.rs +++ b/compiler/src/lexer.rs @@ -9,8 +9,9 @@ pub enum TokenKind { StringLit, CharLit, - EOF, + Comment, Newline, + EOF, Hashtag, Dot, @@ -183,6 +184,12 @@ struct IncludeEntry<'a> { index: usize, } +struct TokInfo<'a> { + begin: u32, + index: &'a mut usize, + data: &'a [u8], +} + pub struct LexResult { pub translation_unit: TranslationUnitDebugInfo, pub symbols: SymbolTable, @@ -245,7 +252,7 @@ pub fn lex(files: &FileDb, file: &File) -> Result { } let data = &input.contents[input.index..]; - let res = match lex_tok_from_bytes(data) { + let res = match lex_tok_from_bytes(index, data) { Ok(res) => res, Err(error) => { return Err(LexError { @@ -300,7 +307,7 @@ pub fn lex(files: &FileDb, file: &File) -> Result { Err(e) => { return Err(LexError { translation_unit: result.translation_unit, - error: Error::new(ErrorKind::Todo(e)), + error: Error::new(ErrorKind::Todo(e.to_string())), }) } }; @@ -341,7 +348,7 @@ struct LexedTok { /// Lex a token from the bytes given. Assumes that we're not at EOF, and /// theres no whitespace before the token. -fn lex_tok_from_bytes<'a>(data: &'a [u8]) -> Result { +fn lex_tok_from_bytes<'a>(global_index: u32, data: &'a [u8]) -> Result { let mut index: usize = 0; let first = data[index]; @@ -386,6 +393,38 @@ fn lex_tok_from_bytes<'a>(data: &'a [u8]) -> Result { (b'-', _) => (0, TokenKind::Dash), (b'/', Some(b'=')) => (1, TokenKind::SlashEq), + (b'/', Some(b'/')) => { + // we've consumed 1 extra character already from the second '/' + // ALSO though, index is already pushed forwards by one + // So this code leaves our index right before the newline we just found + let mut i = 1; + while let Some(&b) = data.get(index + i) { + // Consume until the newline + match b { + b'\n' | b'\r' => break, + _ => i += 1, + } + } + (i, TokenKind::Comment) + } + (b'/', Some(b'*')) => { + let mut i = 1; + let mut prev = 0u8; + loop { + let b = *data + .get(index + i) + .ok_or(error!(Todo("EOF while inside a block comment")))?; + i += 1; + + // Consume until we hit the suffix + match (prev, b) { + (b'*', b'/') => break, + _ => prev = b, + } + } + + (i, TokenKind::Comment) + } (b'/', _) => (0, TokenKind::Slash), (b'*', Some(b'=')) => (1, TokenKind::StarEq), @@ -479,7 +518,10 @@ fn lex_tok_from_bytes<'a>(data: &'a [u8]) -> Result { }); } - throw!(Todo("'..' isn't valid")); + throw!(InvalidCharacterSequence { + seq: "..".to_string(), + index: global_index, + }); } return Ok(LexedTok { @@ -516,27 +558,20 @@ fn lex_num(mut index: usize, data: &[u8]) -> Result { ‘p’ or ‘P’ are used for hexadecimal floating-point constants.) */ - while index < data.len() { - let lower = data[index].to_ascii_lowercase(); - - match lower { + while let Some(&c) = data.get(index) { + let c = c.to_ascii_lowercase(); + match c { b'a'..=b'z' => {} b'0'..=b'9' => {} - b'.' => {} - b'_' => {} + b'.' | b'_' => {} x => break, } index += 1; // Match against exponent - if lower == b'e' || lower == b'p' { - match data.get(index) { - Some(b'-') | Some(b'+') => index += 1, - _ => {} - } - - continue; + if let (b'e' | b'p', Some(b'-' | b'+')) = (c, data.get(index)) { + index += 1; } } @@ -567,7 +602,7 @@ fn lex_character( // handle early newline if cur == b'\n' || cur == b'\r' { - throw!(Todo("invalid character found when parsing string literal",)); + throw!(Todo("invalid character found when parsing string literal")); } // handle escape cases @@ -617,7 +652,7 @@ fn lex_include_line(data: &[u8]) -> Result { loop { match data.get(index) { None => { - throw!(Todo("file ended before include file string was done",)) + throw!(Todo("file ended before include file string was done")) } Some(b'\n') | Some(b'\r') => { throw!(Todo("line ended before file string was done")) diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs index de51a7cb..9d6901b4 100644 --- a/compiler/src/lib.rs +++ b/compiler/src/lib.rs @@ -2,8 +2,6 @@ #![allow(unused_variables)] #![allow(incomplete_features)] -use api::AstNodeKind; - #[macro_use] extern crate soa_derive; #[macro_use] @@ -23,6 +21,7 @@ pub mod filedb; pub mod lexer; pub mod macros; pub mod parser; +pub mod pass; #[cfg(test)] mod tests; @@ -38,7 +37,7 @@ pub mod api { pub use super::macros::expand_macros; pub use super::parser::parse; - pub use super::run_test_code; + pub use super::run_compiler_test_case; pub(crate) use serde::{Deserialize, Serialize}; pub(crate) use std::collections::HashMap; @@ -47,68 +46,110 @@ pub mod api { pub use ntest::*; } -#[derive(serde::Deserialize)] -pub struct PipelineInput { - lexer: Option>, - macro_expansion: Option>, +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum StageOutput { + Ok(Vec), + Err(crate::error::ErrorKind), + Ignore, +} + +impl Default for StageOutput { + fn default() -> Self { + Self::Ignore + } +} + +impl PartialEq> for StageOutput +where + T: PartialEq, +{ + fn eq(&self, other: &StageOutput) -> bool { + match (self, other) { + // If there's no stage, dw about it + (Self::Ignore, _) => return true, + (_, Self::Ignore) => return true, + + (Self::Ok(s), Self::Ok(o)) => return s == o, + (Self::Err(s), Self::Err(o)) => return s == o, + + _ => return false, + } + } } -#[derive(serde::Serialize)] -pub struct PipelineOutput<'a> { - #[serde(skip_serializing)] - source: &'a str, +#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] +pub struct PipelineData { + #[serde(default)] + pub lexer: StageOutput, + + #[serde(default)] + pub macro_expansion: StageOutput, - lexer: Vec, - macro_expansion: Vec, - parsed_ast: Vec, + #[serde(default)] + pub parsed_ast: StageOutput, } -#[derive(serde::Serialize)] +#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)] pub struct SimpleAstNode { - pub kind: AstNodeKind, + pub kind: ast::AstNodeKind, pub parent: u32, pub post_order: u32, pub height: u16, } const TEST_CASE_DELIMITER: &'static str = "// -- END TEST CASE --\n// "; +pub type PrintFunc<'a> = &'a dyn Fn(&filedb::FileDb, &error::TranslationUnitDebugInfo, &error::Error); -pub fn run_test_code(test_source: &str) -> PipelineOutput { +// NOTE: the "source" field is empty +pub fn run_compiler_for_testing(mut source: String, print_err: Option) -> PipelineData { use crate::api::*; - let (source, expected_str) = test_source - .split_once(TEST_CASE_DELIMITER) - .unwrap_or((test_source, "null")); - - let expected = serde_json::from_str::>(expected_str) - .expect("Test case expected value didn't parse") - .unwrap_or(PipelineInput { - lexer: None, - macro_expansion: None, - }); - - let mut source_string = source.to_string(); - if !source_string.ends_with("\n") { - source_string.push('\n'); + if !source.ends_with("\n") { + source.push('\n'); } let mut files = FileDb::new(); let file_id = files - .add_file("main.c".to_string(), source_string) + .add_file("main.c".to_string(), source) .expect("file should add properly"); let file = &files.files[file_id as usize]; - let lexer_res = lex(&files, file).expect("Expected lex to succeed"); - if let Some(expected) = &expected.lexer { - assert_eq!(&lexer_res.tokens.kind, expected, "Invalid token stream"); - } + let mut out = PipelineData { + lexer: StageOutput::Err(ErrorKind::DidntRun), + macro_expansion: StageOutput::Err(ErrorKind::DidntRun), + parsed_ast: StageOutput::Err(ErrorKind::DidntRun), + }; + + let lexer_res = match lex(&files, file) { + Ok(res) => res, + Err(e) => { + if let Some(print) = print_err { + print(&files, &e.translation_unit, &e.error); + } + + out.lexer = StageOutput::Err(e.error.kind); + return out; + } + }; + + let tu = lexer_res.translation_unit; + out.lexer = StageOutput::Ok(lexer_res.tokens.kind.clone()); let macro_expansion_res = expand_macros(lexer_res.tokens.as_slice()); - if let Some(expected) = &expected.macro_expansion { - assert_eq!(¯o_expansion_res.kind, expected, "Invalid token stream"); - } + out.macro_expansion = StageOutput::Ok(macro_expansion_res.kind.clone()); + + let parsed_ast = match parse(¯o_expansion_res) { + Ok(res) => res, + Err(e) => { + if let Some(print) = print_err { + print(&files, &tu, &e); + } + + out.parsed_ast = StageOutput::Err(e.kind); + return out; + } + }; - let parsed_ast = parse(¯o_expansion_res).expect("parsing failed"); let mut simple_ast = Vec::with_capacity(parsed_ast.len()); for node in parsed_ast.as_slice() { simple_ast.push(SimpleAstNode { @@ -119,17 +160,39 @@ pub fn run_test_code(test_source: &str) -> PipelineOutput { }); } - return PipelineOutput { - source, - lexer: lexer_res.tokens.kind, - macro_expansion: macro_expansion_res.kind, - parsed_ast: simple_ast, - }; + out.parsed_ast = StageOutput::Ok(simple_ast); + + return out; +} + +pub fn run_compiler_test_case<'a>(test_source: &'a str) -> (&'a str, PipelineData) { + let (source, expected) = parse_test_case(test_source); + + let output = run_compiler_for_testing(source.to_string(), None); + assert_eq!(output, expected); + + return (source, output); +} + +pub fn parse_test_case(test_source: &str) -> (&str, PipelineData) { + let (source, expected_str) = test_source + .split_once(TEST_CASE_DELIMITER) + .unwrap_or((test_source, "null")); + + let expected = serde_json::from_str::>(expected_str) + .expect("Test case expected value didn't parse") + .unwrap_or(PipelineData { + lexer: StageOutput::Ignore, + macro_expansion: StageOutput::Ignore, + parsed_ast: StageOutput::Ignore, + }); + + return (source, expected); } -impl<'a> PipelineOutput<'a> { - pub fn test_case(&self) -> String { - let mut output = self.source.to_string(); +impl PipelineData { + pub fn test_case(&self, source: &str) -> String { + let mut output = source.to_string(); let text = serde_json::to_string(self).expect("failed to serialize test output"); diff --git a/compiler/src/macros.rs b/compiler/src/macros.rs index 36c7005b..b5a3b21b 100644 --- a/compiler/src/macros.rs +++ b/compiler/src/macros.rs @@ -20,6 +20,7 @@ pub fn expand_macros(tokens: TokenSlice) -> TokenVec { for tok in &tokens { match *tok.kind { TokenKind::Newline => continue, + TokenKind::Comment => continue, _ => {} } diff --git a/compiler/src/parser.rs b/compiler/src/parser.rs index 49d42694..da43be43 100644 --- a/compiler/src/parser.rs +++ b/compiler/src/parser.rs @@ -166,7 +166,7 @@ fn parse_global(p: &mut Parser) -> Result<(), Error> { return Ok(()); }; - unimplemented!("a global that's not a declaration"); + throw!(NotImplemented("a global that's not a declaration")); } enum DeclarationKind { diff --git a/compiler/src/pass/mod.rs b/compiler/src/pass/mod.rs new file mode 100644 index 00000000..cdb7f37a --- /dev/null +++ b/compiler/src/pass/mod.rs @@ -0,0 +1,122 @@ +use core::ops::Range; + +use crate::api::*; + +pub struct ByKindAst<'a> { + pub nodes: &'a mut AstNodeVec, + pub by_kind: HashMap>, + pub by_kind_in_order: Vec<(AstNodeKind, Range)>, +} + +impl<'a> ByKindAst<'a> { + pub fn new(ast: &'a mut AstNodeVec) -> Self { + Self::sort_by_kind(ast); + + let mut by_kind = HashMap::new(); + let mut by_kind_in_order = Vec::new(); + + let mut prev = *ast.index(0).kind; + let mut begin: usize = 0; + let mut index: usize = 0; + while index < ast.len() { + let node = ast.index(index); + let kind = *node.kind; + if kind == prev { + index += 1; + continue; + } + + if let Some(_) = by_kind.insert(prev, begin..index) { + panic!("kind is somehow not sorted"); + } + + by_kind_in_order.push((prev, begin..index)); + + begin = index; + prev = kind; + index += 1; + } + + return ByKindAst { + nodes: ast, + by_kind, + by_kind_in_order, + }; + } + + // NOTE: Assumes that the input was originally sorted by post-order + fn sort_by_kind(ast: &mut AstNodeVec) { + let mut indices = Vec::with_capacity(ast.len()); + for _ in 0..ast.len() { + indices.push(u32::MAX); + } + + // TODO: Sort by kind,post_order + + for (index, &order) in ast.post_order.iter().enumerate() { + indices[order as usize] = index as u32; + } + + // Rebuild parent indices + for parent in &mut ast.parent { + *parent = indices[*parent as usize]; + } + } +} + +impl<'a> Drop for ByKindAst<'a> { + fn drop(&mut self) { + sort_by_postorder(self.nodes); + } +} + +pub fn sort_by_postorder(ast: &mut AstNodeVec) { + let mut indices = Vec::with_capacity(ast.len()); + + for &order in &ast.post_order { + indices.push(order); + } + + // TODO: Sort by post_order + + // Rebuild parent indices + for parent in &mut ast.parent { + *parent = indices[*parent as usize]; + } +} + +// TODO: set up "data" system so that it's possible to interpret the data field +// of an AST node using the node's `kind` field + +// validate declarations -> produce declaration types +// Declaration specifiers need to make sense for the kind of declaration theyre on +pub fn validate_declaration_nodes(ast: &mut ByKindAst) -> Result<(), Error> { + for (kind, range) in &ast.by_kind_in_order { + let kind = match kind { + AstNodeKind::Specifier(k) => *k, + _ => continue, + }; + } + + // Loop over all specifier nodes, and: + // 1. ensure their parent is a declaration of some kind + // 2. add them to their parent's data field + // 3. ensure the combined declaration specifiers are valid for each kind of declaration + + // 4. Loop over all derived declarators, and combine them into their declarator + // 5. Loop over all declarators, and fold them into parents + // 6. Combine type from declaration and derived declarators to produce types for each declarator + // 7. Validate that types make sense for function definitions + + return Ok(()); +} + +// validate declarators relative to their scopes +// -> produce scopes +// validate identifiers +// -> produce types for the identifiers +// -> track which identifiers are pointer-referenced, and when each declaration is last used +// produce global symbols? +pub fn validate_scopes(ast: &mut ByKindAst) -> Result<(), Error> { + return Ok(()); +} diff --git a/compiler/src/tests/lexer/dotdot.c b/compiler/src/tests/lexer/dotdot.c new file mode 100644 index 00000000..eff8677d --- /dev/null +++ b/compiler/src/tests/lexer/dotdot.c @@ -0,0 +1,5 @@ +int main() { + .. +} +// -- END TEST CASE -- +// {"lexer":{"Err":{"InvalidCharacterSequence":{"seq":"..","index":17}}},"macro_expansion":{"Err":"DidntRun"},"parsed_ast":{"Err":"DidntRun"}} diff --git a/compiler/src/tests/lexer/include.c b/compiler/src/tests/lexer/include.c index 0d9a519b..4a87a9aa 100644 --- a/compiler/src/tests/lexer/include.c +++ b/compiler/src/tests/lexer/include.c @@ -1,4 +1,4 @@ #include int main() {} // -- END TEST CASE -- -// {"lexer":["Typedef","Char","Ident","Semicolon","Newline","Hashtag","Ident","Ident","PreprocessingNum","Newline","Hashtag","Ident","Ident","PreprocessingNum","Newline","Int","Ident","LParen","RParen","LBrace","RBrace","Newline"]} +// {"lexer":{"Ok":["Typedef","Char","Ident","Semicolon","Newline","Hashtag","Ident","Ident","PreprocessingNum","Newline","Hashtag","Ident","Ident","PreprocessingNum","Newline","Int","Ident","LParen","RParen","LBrace","RBrace","Newline"]},"macro_expansion":"Ignore","parsed_ast":"Ignore"} diff --git a/compiler/src/tests/lexer/mod.rs b/compiler/src/tests/lexer/mod.rs index 6146dcd2..d26b9d58 100644 --- a/compiler/src/tests/lexer/mod.rs +++ b/compiler/src/tests/lexer/mod.rs @@ -1,6 +1,11 @@ #[test] fn simple() { - crate::run_test_code(include_str!("simple.c")); + crate::run_compiler_test_case(include_str!("simple.c")); +} + +#[test] +fn dotdot() { + crate::run_compiler_test_case(include_str!("dotdot.c")); } // #[test] diff --git a/compiler/src/tests/lexer/simple.c b/compiler/src/tests/lexer/simple.c index 939b5c96..61c1622b 100644 --- a/compiler/src/tests/lexer/simple.c +++ b/compiler/src/tests/lexer/simple.c @@ -1,3 +1,5 @@ int main(int argc, char** argv) { return "printf"; -} \ No newline at end of file +} +// -- END TEST CASE -- +// {"lexer":{"Ok":["Int","Ident","LParen","Int","Ident","Comma","Char","Star","Star","Ident","RParen","LBrace","Newline","Return","StringLit","Semicolon","Newline","RBrace","Newline"]},"macro_expansion":"Ignore","parsed_ast":"Ignore"} diff --git a/components/Ast.tsx b/components/Ast.tsx new file mode 100644 index 00000000..0d190ae6 --- /dev/null +++ b/components/Ast.tsx @@ -0,0 +1,106 @@ +import { AstNode } from "./compiler.schema"; +import React from "react"; +import styles from "Ast.module.css"; +import { ScrollWindow } from "./ScrollWindow"; + +const DisplayAstNode = ({ + kind, + children, +}: { + kind: string; + children?: React.ReactNode; +}) => { + return ( +
+
{kind}
+ +
+ {children} +
+
+ ); +}; + +const RecursiveAst = ({ ast }: { ast: AstNode[] }) => { + let rootStack = []; + + // Assumes that nodes are in post order + for (const node of ast) { + const data = node.kind.data ? `,${JSON.stringify(node.kind.data)}` : ""; + const kind = `${node.kind.kind}${data}`; + + let index = rootStack.length; + while (index > 0 && rootStack[index - 1].parent === node.post_order) { + index -= 1; + } + + rootStack.push({ + parent: node.parent, + node: ( + + {rootStack.splice(index).map((node) => node.node)} + + ), + }); + } + + return <>{rootStack.map((root) => root.node)}; +}; + +const FlatAst = ({ ast }: { ast: AstNode[] }) => { + return ( +
+ {ast.map((obj, index) => { + const data = obj.kind.data ? `,${JSON.stringify(obj.kind.data)}` : ""; + return ( +
+            kind: {`${obj.kind.kind}${data}`}
+            {"\n"}
+            parent: {obj.parent}
+          
+ ); + })} +
+ ); +}; + +export const Ast = ({ ast }: { ast: AstNode[] }) => { + const [recursive, setRecursive] = React.useState(true); + + return ( + + + +

Parsed AST

+ + } + > +
+ {recursive ? : } +
+
+ ); +}; diff --git a/components/ScrollWindow.module.css b/components/ScrollWindow.module.css new file mode 100644 index 00000000..34c53fea --- /dev/null +++ b/components/ScrollWindow.module.css @@ -0,0 +1,14 @@ +.wrapper { + position: relative; +} + +.inner { + position: absolute; + + top: 0; + bottom: 0; + left: 0; + right: 0; + + overflow-y: scroll; +} diff --git a/components/ScrollWindow.tsx b/components/ScrollWindow.tsx new file mode 100644 index 00000000..5ac4b28d --- /dev/null +++ b/components/ScrollWindow.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import styles from "./ScrollWindow.module.css"; +import cx from "classnames"; + +type ScrollWindowProps = { + className?: string; + style?: React.CSSProperties; + innerClassName?: string; + innerStyle?: React.CSSProperties; + title?: React.ReactNode; + children?: React.ReactNode; +}; + +export const ScrollWindow = ({ + className, + style, + innerClassName, + innerStyle, + title, + children, +}: ScrollWindowProps) => { + /* + The position relative/absolute stuff makes it so that the + inner div doesn't affect layout calculations of the surrounding div. + I found this very confusing at first, so here's the SO post that I got it from: + https://stackoverflow.com/questions/27433183/make-scrollable-div-take-up-remaining-height + */ + return ( +
+
+ {title} +
+ +
+ {children} +
+
+ ); +}; diff --git a/components/compiler.schema.ts b/components/compiler.schema.ts index 3ed65695..2106975e 100644 --- a/components/compiler.schema.ts +++ b/components/compiler.schema.ts @@ -5,19 +5,22 @@ export const CompileCommand = z.object({ source: z.string(), }); +export type AstNode = z.infer; +export const AstNode = z.object({ + post_order: z.number(), + parent: z.number(), + kind: z.object({ + kind: z.string(), + data: z.any().optional(), + }), +}); + export type CompileResult = z.infer; export const CompileResult = z.object({ - source: z.string(), - lexer: z.array(z.string()), - parsed_ast: z.array( - z.object({ - parent: z.number(), - kind: z.object({ - kind: z.string(), - data: z.any().optional(), - }), - }) - ), + lexer: z.array(z.string()).nullish(), + macro_expansion: z.array(z.string()).nullish(), + parsed_ast: z.array(AstNode).nullish(), + errors: z.any().nullish(), }); export type CompilerOutput = z.infer; diff --git a/components/lodash.tsx b/components/lodash.tsx new file mode 100644 index 00000000..96d5ff16 --- /dev/null +++ b/components/lodash.tsx @@ -0,0 +1,9 @@ +export function debounce(callback: () => void, wait: number) { + let timeoutId: number | undefined = undefined; + return () => { + window.clearTimeout(timeoutId); + timeoutId = window.setTimeout(() => { + callback(); + }, wait); + }; +} diff --git a/package.json b/package.json index 27879985..bd89aa9b 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "private": false, "scripts": { "start": "next start", - "build-wasm": "wasm-pack build tci-web && yarn install --check-files", - "build": "next build", + "link-wasm": "cd tci-web/pkg && yarn link && cd ../.. && yarn link tci-web", + "wasm": "wasm-pack build --debug tci-web", "deploy": "git push origin main:production", "dev": "next", "lint": "next lint" @@ -17,6 +17,7 @@ "dependencies": { "@monaco-editor/react": "^4.0.7", "autoprefixer": "10.4.14", + "classnames": "^2.3.2", "eslint": "8.39.0", "eslint-config-next": "13.3.1", "idb-keyval": "^5.0.2", @@ -34,6 +35,7 @@ "@types/react": "18.0.38", "@types/react-dom": "18.0.11", "postcss": "8.4.23", + "sass": "^1.62.1", "tailwindcss": "3.3.1", "typescript": "5.0.4", "wasm-pack": "^0.11.0" diff --git a/pages/_app.tsx b/pages/_app.tsx index ad6ddc17..51ebbd10 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,4 +1,4 @@ -import "./globals.css"; +import "./globals.scss"; import type { AppProps } from "next/app"; export default function App({ Component, pageProps }: AppProps) { diff --git a/pages/globals.css b/pages/globals.scss similarity index 74% rename from pages/globals.css rename to pages/globals.scss index c91e6909..a2a45528 100644 --- a/pages/globals.css +++ b/pages/globals.scss @@ -6,6 +6,9 @@ --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; --background-end-rgb: 255, 255, 255; + + --border-radius: 0.25rem; + --spacing: 1.25rem; } @media (prefers-color-scheme: dark) { @@ -18,11 +21,49 @@ html, body, +.full, div#__next { height: 100%; width: 100%; } +.row { + display: flex; + flex-direction: row; + align-items: center; + // flex-wrap: nowrap; Don't know whether or now rows should wrap yet + + & > * { + margin: 0px; + } +} + +.col { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + & > * { + margin: 0px; + } +} + +.rounded { + border-radius: var(--border-radius); +} + +.border { + border: 0.25rem solid black; +} + +.pad { + padding: var(--spacing); +} + +.gap { + gap: var(--spacing); +} + .Resizer { background: #000; opacity: 0.2; diff --git a/pages/index.tsx b/pages/index.tsx index 27019e86..e289e848 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,20 +1,23 @@ import Link from "next/link"; +import cx from "classnames"; import styles from "./tci.module.css"; -import Editor, { Monaco } from "@monaco-editor/react"; -import type monaco from "monaco-editor"; +import Editor from "@monaco-editor/react"; +import monaco from "monaco-editor"; import React from "react"; import { useCompilerWorker } from "@/components/hooks"; -import { CompileResult, CompilerOutput } from "@/components/compiler.schema"; +import { CompileResult } from "@/components/compiler.schema"; +import { Ast } from "@/components/Ast"; +import { ScrollWindow } from "@/components/ScrollWindow"; +import { debounce } from "@/components/lodash"; -const INITIAL_TEXT = ` -int main() { +const INITIAL_TEXT = `// Write C code here +int main(int argc, char** argv) { return 0; } `; export function App() { - const [result, setResult] = - React.useState>(); + const [result, setResult] = React.useState(); const worker = useCompilerWorker((res) => { switch (res.kind) { @@ -26,10 +29,11 @@ export function App() { console.log("message:", res.message); break; case "error": - setResult({ error: res.error }); + console.log("Error returned"); console.error(res.error); break; case "result": + console.log("Compiled ", res.result); setResult(res.result); break; } @@ -66,58 +70,62 @@ export function App() { -
-
+
+
{ editorRef.current = editor; + + editor.setValue( + localStorage.getItem("tciEditorValue") ?? INITIAL_TEXT + ); + + const writeToStorage = debounce( + () => localStorage.setItem("tciEditorValue", editor.getValue()), + 300 + ); + editor.getModel()?.onDidChangeContent((evt) => writeToStorage()); + + monaco.editor.addKeybindingRules([ + { + keybinding: monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, + // TODO: make this do something useful + command: "editor.action.formatDocument", + }, + ]); compile(); }} />
-
- {result?.lexer && ( -
-

Lexed Tokens

-
-                {JSON.stringify(result.lexer, undefined, 2)}
-              
-
- )} +
+
+ + {result?.lexer && ( +
+                  {JSON.stringify(result.lexer, undefined, 2)}
+                
+ )} +
- {result?.parsed_ast && ( -
-

Parsed AST

-
-                {JSON.stringify(
-                  result.parsed_ast.map((obj) => ({
-                    ...obj,
-                    kind: `${obj.kind.kind}${
-                      obj.kind.data ? `,${obj.kind.data}` : ""
-                    }`,
-                  })),
-                  undefined,
-                  2
-                )}
-              
+
+ {result?.parsed_ast && }
+
+ + {result?.errors && ( + +
{JSON.stringify(result.errors)}
+
)}
diff --git a/pages/tci.module.css b/pages/tci.module.css index 46501ee2..167462b9 100644 --- a/pages/tci.module.css +++ b/pages/tci.module.css @@ -6,13 +6,7 @@ padding-right: 0.5rem; padding-top: 0.5rem; padding-bottom: 0.5rem; - --bg-opacity: 1; - background-color: #424242; - background-color: rgba(66, 66, 66, var(--bg-opacity)); border-bottom-width: 1px; - --border-opacity: 1; - border-color: #757575; - border-color: rgba(117, 117, 117, var(--border-opacity)); } .name { @@ -28,11 +22,7 @@ } .scrollBox { - position: relative; - min-height: 33%; - width: 100%; - border-radius: 4px; border: 2px solid black; } @@ -40,6 +30,7 @@ position: absolute; top: 0; right: 2rem; + z-index: 3; } .scrollBox .text { diff --git a/tci-web/Cargo.toml b/tci-web/Cargo.toml index 583ed1aa..efb904b1 100644 --- a/tci-web/Cargo.toml +++ b/tci-web/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "tci-web" +description = "TCI compiler server for the web" +repository = "https://github.com/A1Liu/tci" version = "0.1.0" edition = "2021" +# Neither of these seem to be processed properly by wasm-pack, so I just disabled them. Whatever +# license-file = "../LICENSE" +# readme = "../README.md" + [lib] crate-type = ["cdylib"] diff --git a/tci-web/src/lib.rs b/tci-web/src/lib.rs index 8a296d6e..f11ee1d9 100644 --- a/tci-web/src/lib.rs +++ b/tci-web/src/lib.rs @@ -1,4 +1,4 @@ -use compiler::api::*; +use compiler::{api::*, run_compiler_for_testing, StageOutput}; use serde::Serialize; use wasm_bindgen::prelude::*; @@ -7,46 +7,45 @@ static GLOBAL: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[derive(Serialize)] pub struct PipelineOutput { - source: String, - lexer: Vec, - macro_expansion: Vec, - parsed_ast: Vec, + lexer: Option>, + macro_expansion: Option>, + parsed_ast: Option>, + errors: Option>, } #[wasm_bindgen] pub fn compile(source: String) -> Result { - let mut source_string = source.to_string(); - if !source_string.ends_with("\n") { - source_string.push('\n'); - } - - let mut files = FileDb::new(); - let file_id = files.add_file("main.c".to_string(), source_string)?; - let file = &files.files[file_id as usize]; - - let lexer_res = lex(&files, file).expect("Expected lex to succeed"); - - let macro_expansion_res = expand_macros(lexer_res.tokens.as_slice()); + let mut output = PipelineOutput { + lexer: None, + macro_expansion: None, + parsed_ast: None, + errors: None, + }; - let parsed_ast = parse(¯o_expansion_res).map_err(|_e| "parsing failed")?; - let mut simple_ast = Vec::with_capacity(parsed_ast.len()); - for node in parsed_ast.as_slice() { - simple_ast.push(compiler::SimpleAstNode { - kind: *node.kind, - parent: *node.parent, - post_order: *node.post_order, - height: *node.height, - }); + 'done: { + let data = run_compiler_for_testing(source, None); + + macro_rules! stage_transfer { + ($i:ident) => { + match data.$i { + StageOutput::Ok(l) => output.$i = Some(l), + StageOutput::Ignore => {} + StageOutput::Err(e) => { + let error = format!(concat!(stringify!($i), " error: {:?}"), e); + output.errors.get_or_insert(Vec::new()).push(error); + + break 'done; + } + } + }; + } + + stage_transfer!(lexer); + stage_transfer!(macro_expansion); + stage_transfer!(parsed_ast); } - let out = PipelineOutput { - source, - lexer: lexer_res.tokens.kind, - macro_expansion: macro_expansion_res.kind, - parsed_ast: simple_ast, - }; - - let out = serde_json::to_string(&out).map_err(|e| e.to_string())?; + let out = serde_json::to_string(&output).map_err(|e| e.to_string())?; return Ok(out); } diff --git a/yarn.lock b/yarn.lock index cc30659c..a0a864f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -533,7 +533,7 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.3: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -553,6 +553,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +classnames@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + client-only@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" @@ -1322,6 +1327,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immutable@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" + integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2177,6 +2187,15 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" +sass@^1.62.1: + version "1.62.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.62.1.tgz#caa8d6bf098935bc92fc73fa169fb3790cacd029" + integrity sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -2227,7 +2246,7 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== -source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==