Refactor everything
Huge dump of refactored code. Still in the middle of the changes that are to be squashed later in a one huge monster commit, because there is no value in anything atomic here.
This commit is contained in:
parent
5bd9aadc55
commit
588790b9b4
72 changed files with 13654 additions and 3960 deletions
|
|
@ -3,22 +3,21 @@ name = "dev_tests"
|
|||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "dump_tokens"
|
||||
path = "src/dump_tokens.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "uc_lexer_verify"
|
||||
path = "src/uc_lexer_verify.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "temp"
|
||||
path = "src/temp.rs"
|
||||
name = "verify_expr"
|
||||
path = "src/verify_expr.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
rottlib = { version = "0", path = "../rottlib", features = ["debug"] }
|
||||
is-terminal = "0.4"
|
||||
libc = "0.2"
|
||||
sysinfo = "0.30"
|
||||
walkdir="2.5"
|
||||
encoding_rs="0.8"
|
||||
chardet="0.2"
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use encoding_rs::{Encoding, UTF_8};
|
||||
use rottlib::lexer::{DebugTools, TokenizedFile};
|
||||
|
||||
/// Recursively search `root` for the first file whose *basename* matches
|
||||
/// `needle` (case-sensitive).
|
||||
///
|
||||
/// Returns the absolute path.
|
||||
fn find_file(root: &Path, needle: &str) -> Option<PathBuf> {
|
||||
for entry in walkdir::WalkDir::new(root)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let path = entry.path();
|
||||
if path.is_file() && (path.file_name().and_then(|name| name.to_str()) == Some(needle)) {
|
||||
return fs::canonicalize(path).ok();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// CLI: `dump_tokens <root_dir> <file_name>` - searches for `<file_name>`
|
||||
/// recursively inside `<root_dir>`.
|
||||
///
|
||||
/// This utility takes *root directory* and *file name* instead of the full path
|
||||
/// to help us avoid searching for them typing names out:
|
||||
///
|
||||
/// - We know where all the sources are;
|
||||
/// - We usually just know the name of the file that is being problematic.
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let root_dir = args.next().unwrap_or_else(|| {
|
||||
eprintln!("Usage: inspect_uc <root_dir> <file_name>");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let file_name = args.next().unwrap_or_else(|| {
|
||||
eprintln!("Usage: inspect_uc <root_dir> <file_name>");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let root = PathBuf::from(&root_dir);
|
||||
if !root.exists() {
|
||||
eprintln!("Root directory '{root_dir}' does not exist.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
let found_path = find_file(&root, &file_name).map_or_else(
|
||||
|| {
|
||||
eprintln!("File '{file_name}' not found under '{root_dir}'.");
|
||||
std::process::exit(1);
|
||||
},
|
||||
|path| path,
|
||||
);
|
||||
|
||||
// Read & decode
|
||||
let raw_bytes = match fs::read(&found_path) {
|
||||
Ok(sources) => sources,
|
||||
Err(error) => {
|
||||
eprintln!("Could not read {}: {error}", found_path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (encoding_label, _, _) = chardet::detect(&raw_bytes);
|
||||
let encoding = Encoding::for_label(encoding_label.as_bytes()).unwrap_or(UTF_8);
|
||||
let (decoded_str, _, _) = encoding.decode(&raw_bytes);
|
||||
|
||||
let source_text = decoded_str.to_string();
|
||||
let tokenized_file = TokenizedFile::from_str(&source_text);
|
||||
|
||||
tokenized_file.dump_debug_layout();
|
||||
}
|
||||
14
dev_tests/src/pretty.rs
Normal file
14
dev_tests/src/pretty.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// diagnostics_render.rs
|
||||
|
||||
use rottlib::diagnostics::{Diagnostic};
|
||||
use rottlib::lexer::TokenizedFile;
|
||||
|
||||
pub fn render_diagnostic(
|
||||
diag: &Diagnostic,
|
||||
_file: &TokenizedFile,
|
||||
file_name: Option<&str>,
|
||||
colors: bool,
|
||||
) -> String {
|
||||
diag.render(_file, file_name.unwrap_or("<default>"));
|
||||
"fuck it".to_string()
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
//! src/main.rs
|
||||
//! --------------------------------------------
|
||||
//! Build & run:
|
||||
//! cargo run
|
||||
//! --------------------------------------------
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use rottlib::arena::Arena;
|
||||
use rottlib::lexer::TokenizedFile;
|
||||
use rottlib::parser::{ParseError, Parser, pretty::ExprTree};
|
||||
|
||||
/*
|
||||
- Convenient array definitions: [1, 3, 5, 2, 4]
|
||||
- Boolean dynamic arrays
|
||||
- Structures in default properties
|
||||
- Auto conversion of arrays into strings
|
||||
- Making 'var' and 'local' unnecessary
|
||||
- Allowing variable creation in 'for' loops
|
||||
- Allowing variable creation at any place inside a function
|
||||
- Default parameters for functions
|
||||
- Function overloading?
|
||||
- repeat/until
|
||||
- The syntax of the default properties block is pretty strict for an arcane reason. Particularly adding spaces before or after the "=" will lead to errors in pre-UT2003 versions.
|
||||
- Scopes
|
||||
- different names for variables and in config file
|
||||
- anonymous pairs (objects?) and value destruction
|
||||
>>> AST > HIR > MIR > byte code
|
||||
*/
|
||||
|
||||
/// Closest plan:
|
||||
/// - Add top-level declaration parsing
|
||||
/// - Handle pretty.rs shit somehow
|
||||
/// - COMMITS
|
||||
/// ---------------------------------------
|
||||
/// - Add fancy error reporting
|
||||
/// - Make a fancy REPL
|
||||
/// - Add evaluation
|
||||
///
|
||||
/// WARNINGS:
|
||||
/// - Empty code/switch blocks
|
||||
|
||||
fn parse_and_print(src: &str) -> Result<(), ParseError> {
|
||||
let tokenized = TokenizedFile::from_str(src);
|
||||
let arena = Arena::new();
|
||||
let mut parser = Parser::new(&tokenized, &arena);
|
||||
|
||||
let expr = parser.parse_expression(); // ArenaNode<Expression>
|
||||
println!("{}", ExprTree(&*expr)); // if ArenaNode<Deref>
|
||||
// or: println!("{}", ExprTree(expr.as_ref())); // if no Deref
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repl_once() -> Result<(), ParseError> {
|
||||
print!("Enter an statement > ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut input = String::new();
|
||||
if io::stdin().read_line(&mut input).is_err() {
|
||||
eprintln!("failed to read input");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if input.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
parse_and_print(&input)
|
||||
}
|
||||
|
||||
fn read_stdin_all() -> io::Result<String> {
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_to_string(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn read_file_to_string(path: &Path) -> io::Result<String> {
|
||||
fs::read_to_string(path)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), ParseError> {
|
||||
// Accept a single positional arg as the input path.
|
||||
// "-" means read all of stdin.
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
if let Some(arg1) = args.next() {
|
||||
if arg1 == "-h" || arg1 == "--help" {
|
||||
println!("Usage:");
|
||||
println!(
|
||||
" {} # REPL",
|
||||
env::args().next().unwrap_or_else(|| "prog".into())
|
||||
);
|
||||
println!(
|
||||
" {} <file> # parse file",
|
||||
env::args().next().unwrap_or_else(|| "prog".into())
|
||||
);
|
||||
println!(
|
||||
" {} - # read source from stdin",
|
||||
env::args().next().unwrap_or_else(|| "prog".into())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if arg1 == "-" {
|
||||
match read_stdin_all() {
|
||||
Ok(src) => return parse_and_print(&src),
|
||||
Err(e) => {
|
||||
eprintln!("stdin read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let path = Path::new(&arg1);
|
||||
match read_file_to_string(path) {
|
||||
Ok(src) => return parse_and_print(&src),
|
||||
Err(e) => {
|
||||
eprintln!("file read error ({}): {}", path.display(), e);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No filename provided -> keep REPL behavior
|
||||
repl_once()
|
||||
}
|
||||
|
|
@ -1,122 +1,341 @@
|
|||
use std::{collections::HashSet, fs, path::PathBuf};
|
||||
#![allow(
|
||||
clippy::all,
|
||||
clippy::pedantic,
|
||||
clippy::nursery,
|
||||
clippy::cargo,
|
||||
clippy::restriction
|
||||
)]
|
||||
|
||||
use rottlib::lexer::{DebugTools, TokenizedFile};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
/// Read `ignore.txt` (one path per line, `#` for comments) from root directory
|
||||
/// and turn it into a canonicalized [`HashSet<PathBuf>`].
|
||||
use encoding_rs::Encoding;
|
||||
use rottlib::diagnostics::Diagnostic as Diag;
|
||||
use rottlib::lexer::TokenizedFile;
|
||||
use rottlib::parser::Parser;
|
||||
|
||||
mod pretty;
|
||||
|
||||
// ---------- CONFIG ----------
|
||||
const FILE_LIMIT: usize = 10000; // cap on files scanned
|
||||
const DIAG_SHOW_FIRST: usize = 12; // show first N diagnostics
|
||||
const DIAG_SHOW_LAST: usize = 12; // show last N diagnostics
|
||||
/// If true, print the old debug struct dump after each pretty diagnostic.
|
||||
const ALSO_PRINT_DEBUG_AFTER_PRETTY: bool = true;
|
||||
|
||||
// Cargo.toml additions:
|
||||
// is-terminal = "0.4"
|
||||
// sysinfo = { version = "0.30", features = ["multithread"] }
|
||||
// walkdir = "2"
|
||||
// chardet = "0.2"
|
||||
// encoding_rs = "0.8"
|
||||
|
||||
// Linux-only accurate RSS in MB. Fallback uses sysinfo.
|
||||
fn rss_mb() -> u64 {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
if let Ok(mut f) = std::fs::File::open("/proc/self/statm")
|
||||
&& f.read_to_string(&mut s).is_ok()
|
||||
&& let Some(rss_pages) = s
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|x| x.parse::<u64>().ok())
|
||||
{
|
||||
let page = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as u64 };
|
||||
return (rss_pages * page) / (1024 * 1024);
|
||||
}
|
||||
}
|
||||
use sysinfo::{System, get_current_pid};
|
||||
let mut sys = System::new();
|
||||
sys.refresh_processes();
|
||||
let Ok(pid) = get_current_pid() else { return 0 };
|
||||
sys.process(pid).map_or(0, |p| p.memory() / 1024)
|
||||
}
|
||||
|
||||
fn mark(label: &str, t0: Instant) {
|
||||
println!(
|
||||
"[{:>14}] t={:>8.2?} rss={} MB",
|
||||
label,
|
||||
t0.elapsed(),
|
||||
rss_mb()
|
||||
);
|
||||
}
|
||||
|
||||
/// Read `ignore.txt` next to `root` and build a canonicalized set.
|
||||
fn load_ignore_set(root: &std::path::Path) -> HashSet<PathBuf> {
|
||||
let ignore_file = root.join("ignore.txt");
|
||||
if !ignore_file.exists() {
|
||||
return HashSet::new();
|
||||
}
|
||||
|
||||
let content = match fs::read_to_string(&ignore_file) {
|
||||
Ok(content) => content,
|
||||
Err(error) => {
|
||||
eprintln!("Could not read {}: {error}", ignore_file.display());
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Could not read {}: {e}", ignore_file.display());
|
||||
return HashSet::new();
|
||||
}
|
||||
};
|
||||
|
||||
content
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.filter(|l| !l.is_empty() && !l.starts_with('#'))
|
||||
.filter_map(|line| {
|
||||
let next_path = PathBuf::from(line);
|
||||
let absolute_path = if next_path.is_absolute() {
|
||||
next_path
|
||||
} else {
|
||||
root.join(next_path)
|
||||
};
|
||||
fs::canonicalize(absolute_path).ok()
|
||||
let p = PathBuf::from(line);
|
||||
let abs = if p.is_absolute() { p } else { root.join(p) };
|
||||
fs::canonicalize(abs).ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// CLI: `verify_uc <root_dir>` - find all `.uc` files in the provided directory
|
||||
/// (except those listed in `ignore.txt` in the root) and test them all.
|
||||
///
|
||||
/// Reported execution time is the tokenization time, without considering time
|
||||
/// it takes to read files from disk.
|
||||
///
|
||||
/// `ignore.txt` is for listing specific files, not directories.
|
||||
fn main() {
|
||||
let root_dir = std::env::args().nth(1).unwrap(); // it is fine to crash debug utility
|
||||
let root = PathBuf::from(&root_dir);
|
||||
/// Wait for Enter if running in a TTY, shown before printing errors.
|
||||
fn wait_before_errors(msg: &str) {
|
||||
let _ = io::stdout().flush();
|
||||
if is_terminal::is_terminal(io::stdin()) {
|
||||
eprint!("{msg}");
|
||||
let _ = io::stderr().flush();
|
||||
let mut s = String::new();
|
||||
let _ = io::stdin().read_line(&mut s);
|
||||
}
|
||||
}
|
||||
|
||||
/// CLI: `verify_uc <root_dir> [file_name]`
|
||||
///
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let root_dir = args.next().unwrap_or_else(|| {
|
||||
eprintln!("Usage: verify_uc <root_dir> [file_name]");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let target_raw = args.next(); // optional file name hint
|
||||
let target_ci = target_raw.as_ref().map(|s| s.to_ascii_lowercase());
|
||||
let single_mode = target_ci.is_some();
|
||||
|
||||
let root = PathBuf::from(&root_dir);
|
||||
if !root.exists() {
|
||||
eprintln!("Root directory '{root_dir}' does not exist.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Load files
|
||||
let ignored_paths = load_ignore_set(&root);
|
||||
let t0 = Instant::now();
|
||||
mark("baseline", t0);
|
||||
|
||||
// Stage 0: discover + read, bounded by FILE_LIMIT or first match in single_mode
|
||||
let ignored = load_ignore_set(&root);
|
||||
let mut uc_files: Vec<(PathBuf, String)> = Vec::new();
|
||||
let mut seen = 0usize;
|
||||
let mut picked_any = false;
|
||||
|
||||
for entry in walkdir::WalkDir::new(&root)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok) // for debug tool this is ok
|
||||
.filter(|entry| {
|
||||
let path = entry.path();
|
||||
// Skip anything explicitly ignored
|
||||
if let Ok(absolute_path) = fs::canonicalize(path) {
|
||||
if ignored_paths.contains(&absolute_path) {
|
||||
return false;
|
||||
}
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| {
|
||||
let path = e.path();
|
||||
if let Ok(abs) = fs::canonicalize(path)
|
||||
&& ignored.contains(&abs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Must be *.uc
|
||||
path.is_file()
|
||||
&& path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("uc"))
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|e| e.eq_ignore_ascii_case("uc"))
|
||||
})
|
||||
{
|
||||
if !single_mode && seen >= FILE_LIMIT {
|
||||
break;
|
||||
}
|
||||
|
||||
// If in single-file mode, keep only the first whose file name matches.
|
||||
if let Some(needle) = target_ci.as_deref() {
|
||||
let fname = entry
|
||||
.path()
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
let fname_lc = fname.to_ascii_lowercase();
|
||||
if !(fname_lc == needle || fname_lc.contains(needle)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
seen += 1;
|
||||
|
||||
let path = entry.path();
|
||||
match fs::read(path) {
|
||||
Ok(raw_bytes) => {
|
||||
// Auto-detect encoding for old Unreal script sources
|
||||
let (encoding_label, _, _) = chardet::detect(&raw_bytes);
|
||||
let encoding = encoding_rs::Encoding::for_label(encoding_label.as_bytes())
|
||||
.unwrap_or(encoding_rs::UTF_8);
|
||||
let (decoded_text, _, _) = encoding.decode(&raw_bytes);
|
||||
uc_files.push((path.to_path_buf(), decoded_text.into_owned()));
|
||||
Ok(raw) => {
|
||||
let (label, _, _) = chardet::detect(&raw);
|
||||
let enc = Encoding::for_label(label.as_bytes()).unwrap_or(encoding_rs::UTF_8);
|
||||
let (txt, _, _) = enc.decode(&raw);
|
||||
uc_files.push((path.to_path_buf(), txt.into_owned()));
|
||||
picked_any = true;
|
||||
if single_mode {
|
||||
// Only the first match.
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("Failed to read `{}`: {error}", path.display());
|
||||
Err(e) => {
|
||||
wait_before_errors("Read error detected. Press Enter to print details...");
|
||||
eprintln!("Failed to read `{}`: {e}", path.display());
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("Loaded {} .uc files into memory.", uc_files.len());
|
||||
|
||||
// Tokenize and measure performance
|
||||
let start_time = std::time::Instant::now();
|
||||
let tokenized_files: Vec<(PathBuf, TokenizedFile)> = uc_files
|
||||
.iter()
|
||||
.map(|(path, source_code)| {
|
||||
let tokenized_file = TokenizedFile::from_str(source_code);
|
||||
if tokenized_file.has_errors() {
|
||||
println!("TK: {}", path.display());
|
||||
}
|
||||
(path.clone(), tokenized_file)
|
||||
})
|
||||
.collect();
|
||||
let elapsed_time = start_time.elapsed();
|
||||
if single_mode && !picked_any {
|
||||
let needle = target_raw.as_deref().unwrap();
|
||||
eprintln!(
|
||||
"No .uc file matching '{needle}' found under '{}'.",
|
||||
root.display()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!(
|
||||
"Loaded {} .uc files into memory (cap={}, reached={}).",
|
||||
uc_files.len(),
|
||||
FILE_LIMIT,
|
||||
if !single_mode && uc_files.len() >= FILE_LIMIT {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
}
|
||||
);
|
||||
mark("after_read", t0);
|
||||
|
||||
// Stage 1: tokenize all
|
||||
let t_tok = Instant::now();
|
||||
let mut tokenized: Vec<(PathBuf, TokenizedFile)> = Vec::with_capacity(uc_files.len());
|
||||
let mut tk_error_idx: Option<usize> = None;
|
||||
|
||||
for (i, (path, source)) in uc_files.iter().enumerate() {
|
||||
let tf = TokenizedFile::tokenize(source);
|
||||
if tk_error_idx.is_none() && tf.has_errors() {
|
||||
tk_error_idx = Some(i);
|
||||
}
|
||||
tokenized.push((path.clone(), tf));
|
||||
}
|
||||
println!(
|
||||
"Tokenized {} files in {:.2?}",
|
||||
tokenized_files.len(),
|
||||
elapsed_time
|
||||
tokenized.len(),
|
||||
t_tok.elapsed()
|
||||
);
|
||||
mark("after_tokenize", t0);
|
||||
|
||||
// Round-trip check
|
||||
for ((path, original), (_, tokenized_file)) in uc_files.iter().zip(tokenized_files.iter()) {
|
||||
let reconstructed = tokenized_file.reconstruct_source();
|
||||
if original != &reconstructed {
|
||||
eprintln!("Reconstruction mismatch in `{}`!", path.display());
|
||||
std::process::exit(1);
|
||||
// If tokenization error: wait, dump tokens for the first failing file, then exit.
|
||||
if let Some(idx) = tk_error_idx {
|
||||
let (bad_path, _) = &tokenized[idx];
|
||||
wait_before_errors("Tokenization error found. Press Enter to dump tokens...");
|
||||
eprintln!("--- Tokenization error in: {}", bad_path.display());
|
||||
//bad_tf.dump_debug_layout(); // from DebugTools
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Stage 2: parse all with ONE arena kept alive
|
||||
let arena = rottlib::arena::Arena::new();
|
||||
let t_parse = Instant::now();
|
||||
|
||||
// First failing parse: (tokenized_index, diagnostics, fatal)
|
||||
let mut first_fail: Option<(usize, Vec<Diag>, Option<String>)> = None;
|
||||
|
||||
for (i, (path, tk)) in tokenized.iter().enumerate() {
|
||||
// --- progress line BEFORE parsing this file ---
|
||||
{
|
||||
use std::io::Write;
|
||||
eprint!(
|
||||
"Parsing [{}/{}] {} | rss={} MB\r\n",
|
||||
i + 1,
|
||||
tokenized.len(),
|
||||
path.display(),
|
||||
rss_mb()
|
||||
);
|
||||
let _ = io::stderr().flush();
|
||||
}
|
||||
|
||||
let mut parser = Parser::new(tk, &arena);
|
||||
|
||||
match parser.parse_source_file() {
|
||||
Ok(_) => {
|
||||
if !parser.diagnostics.is_empty() && first_fail.is_none() {
|
||||
first_fail = Some((i, parser.diagnostics.clone(), None));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if first_fail.is_none() {
|
||||
first_fail = Some((i, parser.diagnostics.clone(), Some(format!("{e:?}"))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("All .uc files matched successfully.");
|
||||
println!(
|
||||
"Parsed {} files in {:.2?}",
|
||||
tokenized.len(),
|
||||
t_parse.elapsed()
|
||||
);
|
||||
mark("after_parse", t0);
|
||||
|
||||
// Summary
|
||||
println!("--- Summary ---");
|
||||
println!("Files processed: {}", tokenized.len());
|
||||
println!("File cap: {FILE_LIMIT}");
|
||||
|
||||
if let Some((idx, diags, fatal)) = first_fail {
|
||||
wait_before_errors("Parse issues detected. Press Enter to print diagnostics...");
|
||||
let (path, tf) = &tokenized[idx];
|
||||
eprintln!("--- Parse issues in first failing file ---");
|
||||
eprintln!("File: {}", path.display());
|
||||
if let Some(f) = &fatal {
|
||||
eprintln!("Fatal parse error: {f}");
|
||||
}
|
||||
if diags.is_empty() && fatal.is_none() {
|
||||
eprintln!("(no diagnostics captured)");
|
||||
} else {
|
||||
let use_colors = is_terminal::is_terminal(io::stderr());
|
||||
let fname = path.display().to_string();
|
||||
let total = diags.len();
|
||||
let first_n = DIAG_SHOW_FIRST.min(total);
|
||||
let last_n = DIAG_SHOW_LAST.min(total.saturating_sub(first_n));
|
||||
|
||||
if total > first_n + last_n {
|
||||
// first window
|
||||
for (k, d) in diags.iter().take(first_n).enumerate() {
|
||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||
eprintln!("{s}");
|
||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||
eprintln!("#{}: {:#?}", k + 1, d);
|
||||
}
|
||||
}
|
||||
eprintln!("... {} diagnostics omitted ...", total - (first_n + last_n));
|
||||
// last window
|
||||
let start = total - last_n;
|
||||
for (offset, d) in diags.iter().skip(start).enumerate() {
|
||||
let idx_global = start + offset + 1;
|
||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||
eprintln!("{s}");
|
||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||
eprintln!("#{idx_global}: {d:#?}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (k, d) in diags.iter().enumerate() {
|
||||
let s = pretty::render_diagnostic(d, tf, Some(&fname), use_colors);
|
||||
eprintln!("{s}");
|
||||
if ALSO_PRINT_DEBUG_AFTER_PRETTY {
|
||||
eprintln!("#{}: {:#?}", k + 1, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("All files parsed without diagnostics.");
|
||||
}
|
||||
|
|
|
|||
85
dev_tests/src/verify_expr.rs
Normal file
85
dev_tests/src/verify_expr.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#![allow(
|
||||
clippy::all,
|
||||
clippy::pedantic,
|
||||
clippy::nursery,
|
||||
clippy::cargo,
|
||||
clippy::restriction
|
||||
)]
|
||||
|
||||
use rottlib::arena::Arena;
|
||||
use rottlib::lexer::TokenizedFile;
|
||||
use rottlib::parser::Parser;
|
||||
|
||||
mod pretty;
|
||||
|
||||
/// Expressions to test.
|
||||
///
|
||||
/// Add, remove, or edit entries here.
|
||||
/// Using `(&str, &str)` gives each case a human-readable label.
|
||||
const TEST_CASES: &[(&str, &str)] = &[
|
||||
("simple_add", "1 + 2 * 3"),
|
||||
("member_call", "Foo.Bar(1, 2)"),
|
||||
("index_member", "arr[5].X"),
|
||||
("tagged_name", "Class'MyPackage.MyThing'"),
|
||||
("broken_expr", "a + (]\n//AAA\n//BBB\n//CCC\n//DDD\n//EEE\n//FFF"),
|
||||
];
|
||||
|
||||
/// If true, print the parsed expression using Debug formatting.
|
||||
const PRINT_PARSED_EXPR: bool = false;
|
||||
|
||||
/// If true, print diagnostics even when parsing returned a value.
|
||||
const ALWAYS_PRINT_DIAGNOSTICS: bool = true;
|
||||
|
||||
fn main() {
|
||||
let arena = Arena::new();
|
||||
|
||||
println!("Running {} expression test case(s)...", TEST_CASES.len());
|
||||
println!();
|
||||
|
||||
let mut had_any_problem = false;
|
||||
|
||||
for (idx, (label, source)) in TEST_CASES.iter().enumerate() {
|
||||
println!("============================================================");
|
||||
println!("Case #{:02}: {}", idx + 1, label);
|
||||
println!("Source: {}", source);
|
||||
println!("------------------------------------------------------------");
|
||||
|
||||
let tf = TokenizedFile::tokenize(source);
|
||||
|
||||
let mut parser = Parser::new(&tf, &arena);
|
||||
let expr = parser.parse_expression();
|
||||
|
||||
println!("parse_expression() returned.");
|
||||
|
||||
if PRINT_PARSED_EXPR {
|
||||
println!("Parsed expression:");
|
||||
println!("{expr:#?}");
|
||||
}
|
||||
|
||||
if parser.diagnostics.is_empty() {
|
||||
println!("Diagnostics: none");
|
||||
} else {
|
||||
had_any_problem = true;
|
||||
println!("Diagnostics: {}", parser.diagnostics.len());
|
||||
|
||||
if ALWAYS_PRINT_DIAGNOSTICS {
|
||||
let use_colors = false;
|
||||
for (k, diag) in parser.diagnostics.iter().enumerate() {
|
||||
let rendered = pretty::render_diagnostic(diag, &tf, Some(label), use_colors);
|
||||
println!("Diagnostic #{}:", k + 1);
|
||||
println!("{rendered}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("============================================================");
|
||||
if had_any_problem {
|
||||
println!("Done. At least one case had tokenization or parse diagnostics.");
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
println!("Done. All cases completed without diagnostics.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue