// TAME compiler // // Copyright (C) 2014-2022 Ryan Specialty Group, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! This is the TAME compiler. //! //! `tamec` compiles source code into object files that are later linked //! into a final executable using [`tameld`](../tameld). extern crate tamer; use getopts::{Fail, Options}; use std::{ env, error::Error, ffi::OsStr, fmt::{self, Display, Write}, fs, io, path::Path, }; use tamer::{ diagnose::{ AnnotatedSpan, Diagnostic, FsSpanResolver, Reporter, VisualReporter, }, nir::{XirfToNir, XirfToNirError}, parse::{Lower, ParseError, Parsed, ParsedObject, UnknownToken}, xir::{ self, flat::{RefinedText, XirToXirf, XirToXirfError, XirfToken}, writer::XmlWriter, Error as XirError, Token as XirToken, }, }; /// Types of commands enum Command { Compile(String, String, String), Usage, } /// Entrypoint for the compiler pub fn main() -> Result<(), TamecError> { let args: Vec = env::args().collect(); let program = &args[0]; let opts = get_opts(); let usage = opts.usage(&format!("Usage: {} [OPTIONS] INPUT", program)); match parse_options(opts, args) { Ok(Command::Compile(input, _, output)) => { let source = Path::new(&input); if source.extension() != Some(OsStr::new("xml")) { panic!("{}: file format not recognized", input); } let dest = Path::new(&output); let mut reporter = VisualReporter::new(FsSpanResolver); Ok(()) .and_then(|_| { use std::io::{BufReader, BufWriter}; use tamer::{ fs::{File, PathFile}, xir::{ reader::XmlXirReader, DefaultEscaper, }, }; let escaper = DefaultEscaper::default(); let mut ebuf = String::new(); let mut xmlwriter = Default::default(); let mut fout = BufWriter::new(fs::File::create(dest)?); let mut has_err = false; let PathFile(_, file, ctx): PathFile> = PathFile::open(source)?; // TODO: This will be progressively refactored as // lowering is finalized. // Parse into XIR and re-lower into XML, // which is similar to a copy but proves that we're able // to parse source files. let _ = Lower::< ParsedObject, XirToXirf<64, RefinedText>, >::lower::<_, TamecError>( // TODO: We're just echoing back out XIR, // which will be the same sans some formatting. &mut XmlXirReader::new(file, &escaper, ctx) .inspect(|tok_result| { match tok_result { Ok(Parsed::Object(tok)) => { xmlwriter = tok.write( &mut fout, xmlwriter, &escaper ).unwrap(); }, _ => () } }), |toks| { Lower::, XirfToNir>::lower( toks, |nir| { // TODO: These errors do not yet fail // compilation. nir.fold(Ok(()), |x, result| match result { Ok(_) => x, Err(e) => { has_err = true; // See below note about buffering. ebuf.clear(); writeln!( ebuf, "{}", reporter.render(&e) )?; println!("{ebuf}"); x } }) }, ) }, )?; // TODO: Proper error summary, // and roll into rest of lowering pipeline. match has_err { false => Ok(()), true => { println!("fatal: failed to compile `{}`", output); std::process::exit(1); } } }) .or_else(|e: TamecError| { // POC: Rendering to a string ensures buffering so that we don't // interleave output between processes. let report = reporter.render(&e).to_string(); println!("{report}\nfatal: failed to compile `{}`", output); std::process::exit(1); }) } Ok(Command::Usage) => { println!("{}", usage); std::process::exit(exitcode::OK); } Err(e) => { eprintln!("{}", e); println!("{}", usage); std::process::exit(exitcode::USAGE); } } } /// Get 'Options' /// /// ``` /// use getopts::Options; /// /// let opts = get_opts(); /// ``` fn get_opts() -> Options { let mut opts = Options::new(); opts.optopt("o", "output", "set output file name", "NAME"); opts.optopt("", "emit", "set output type", "xmlo"); opts.optflag("h", "help", "print this help menu"); opts } /// Option parser fn parse_options(opts: Options, args: Vec) -> Result { let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(f) => { return Err(f); } }; if matches.opt_present("h") { return Ok(Command::Usage); } let input = match matches.free.len() { 0 => return Err(Fail::OptionMissing(String::from("INPUT"))), 1 => matches.free[0].clone(), _ => return Err(Fail::UnrecognizedOption(matches.free[1].clone())), }; let emit = match matches.opt_str("emit") { Some(m) => match &m[..] { "xmlo" => m, _ => { return Err(Fail::ArgumentMissing(String::from("--emit xmlo"))) } }, None => { return Err(Fail::OptionMissing(String::from("--emit xmlo"))); } }; let output = match matches.opt_str("o") { Some(m) => m, // we check that the extension is "xml" later None => format!("{}o", input), }; Ok(Command::Compile(input, emit, output)) } /// Compiler (`tamec`) error. /// /// This represents the aggregation of all possible errors that can occur /// during compile-time. /// This cannot include panics, /// but efforts have been made to reduce panics to situations that /// represent the equivalent of assertions. #[derive(Debug)] pub enum TamecError { Io(io::Error), XirParseError(ParseError), XirfParseError(ParseError), NirParseError(ParseError, XirfToNirError>), XirWriterError(xir::writer::Error), Fmt(fmt::Error), } impl From for TamecError { fn from(e: io::Error) -> Self { Self::Io(e) } } impl From> for TamecError { fn from(e: ParseError) -> Self { Self::XirParseError(e) } } impl From> for TamecError { fn from(e: ParseError) -> Self { Self::XirfParseError(e) } } impl From, XirfToNirError>> for TamecError { fn from(e: ParseError, XirfToNirError>) -> Self { Self::NirParseError(e) } } impl From for TamecError { fn from(e: xir::writer::Error) -> Self { Self::XirWriterError(e) } } impl From for TamecError { fn from(e: fmt::Error) -> Self { Self::Fmt(e) } } impl Display for TamecError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Io(e) => Display::fmt(e, f), Self::XirParseError(e) => Display::fmt(e, f), Self::XirfParseError(e) => Display::fmt(e, f), Self::NirParseError(e) => Display::fmt(e, f), Self::XirWriterError(e) => Display::fmt(e, f), Self::Fmt(e) => Display::fmt(e, f), } } } impl Error for TamecError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { Self::Io(e) => Some(e), Self::XirParseError(e) => Some(e), Self::XirfParseError(e) => Some(e), Self::NirParseError(e) => Some(e), Self::XirWriterError(e) => Some(e), Self::Fmt(e) => Some(e), } } } impl Diagnostic for TamecError { fn describe(&self) -> Vec { match self { Self::XirParseError(e) => e.describe(), Self::XirfParseError(e) => e.describe(), Self::NirParseError(e) => e.describe(), // TODO (will fall back to rendering just the error `Display`) _ => vec![], } } } #[cfg(test)] mod test { use super::*; #[test] fn parse_options_help() { let opts = get_opts(); let result = parse_options( opts, vec![String::from("program"), String::from("-h")], ); match result { Ok(Command::Usage) => {} _ => panic!("Long help option did not parse"), } } #[test] fn parse_options_help_long() { let opts = get_opts(); let result = parse_options( opts, vec![String::from("program"), String::from("--help")], ); match result { Ok(Command::Usage) => {} _ => panic!("Help option did not parse"), } } #[test] fn parse_options_invalid() { let opts = get_opts(); let result = parse_options( opts, vec![String::from("program"), String::from("-q")], ); match result { Err(Fail::UnrecognizedOption(_)) => {} _ => panic!("Invalid option not caught"), } } #[test] fn parse_options_missing_input() { let opts = get_opts(); let result = parse_options(opts, vec![String::from("program")]); match result { Err(Fail::OptionMissing(message)) => { assert_eq!("INPUT", message); } _ => panic!("Missing input not caught"), } } #[test] fn parse_options_missing_emit() { let opts = get_opts(); let result = parse_options( opts, vec![String::from("program"), String::from("filename")], ); match result { Err(Fail::OptionMissing(message)) => { assert_eq!("--emit xmlo", message); } _ => panic!("Missing emit not caught"), } } #[test] fn parse_options_invalid_emit() { let opts = get_opts(); let result = parse_options( opts, vec![ String::from("program"), String::from("filename.xml"), String::from("--emit"), String::from("foo"), ], ); match result { Err(Fail::ArgumentMissing(message)) => { assert_eq!("--emit xmlo", message); } _ => panic!("Invalid emit not caught"), } } #[test] fn parse_options_too_many_args() { let opts = get_opts(); let result = parse_options( opts, vec![ String::from("program"), String::from("foo"), String::from("--emit"), String::from("bar"), String::from("baz"), ], ); match result { Err(Fail::UnrecognizedOption(message)) => { assert_eq!("baz", message); } _ => panic!("Extra option not caught"), } } #[test] fn parse_options_valid() { let opts = get_opts(); let xmlo = String::from("xmlo"); let result = parse_options( opts, vec![ String::from("program"), String::from("foo.xml"), String::from("--emit"), xmlo, ], ); match result { Ok(Command::Compile(infile, xmlo, outfile)) => { assert_eq!("foo.xml", infile); assert_eq!("foo.xmlo", outfile); assert_eq!("xmlo", xmlo); } _ => panic!("Unexpected result"), } } #[test] fn parse_options_valid_custom_out() { let opts = get_opts(); let xmlo = String::from("xmlo"); let result = parse_options( opts, vec![ String::from("program"), String::from("foo.xml"), String::from("--emit"), xmlo, String::from("-o"), String::from("foo.xmli"), ], ); match result { Ok(Command::Compile(infile, xmlo, outfile)) => { assert_eq!("foo.xml", infile); assert_eq!("foo.xmli", outfile); assert_eq!("xmlo", xmlo); } _ => panic!("Unexpected result"), } } #[test] fn parse_options_valid_custom_out_long() { let opts = get_opts(); let xmlo = String::from("xmlo"); let result = parse_options( opts, vec![ String::from("program"), String::from("foo.xml"), String::from("--emit"), xmlo, String::from("--output"), String::from("foo.xmli"), ], ); match result { Ok(Command::Compile(infile, xmlo, outfile)) => { assert_eq!("foo.xml", infile); assert_eq!("foo.xmli", outfile); assert_eq!("xmlo", xmlo); } _ => panic!("Unexpected result"), } } }