#![doc(html_root_url = "https://docs.rs/rucc-session/0.2.10")]
mod fs;
pub use crate::fs::{Dir, FileSystem, Found, IncludeForm, MemoryFileSystem, SearchPath};
use std::fmt;
use std::str::FromStr;
use rucc_base::Interner;
use rucc_diag::{Diagnostic, Severity, SourceMap};
use rucc_target::{TargetInfo, Triple};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum OptLevel {
#[default]
O0,
O1,
O2,
O3,
Os,
Oz,
}
impl OptLevel {
pub const fn as_flag(self) -> &'static str {
match self {
OptLevel::O0 => "-O0",
OptLevel::O1 => "-O1",
OptLevel::O2 => "-O2",
OptLevel::O3 => "-O3",
OptLevel::Os => "-Os",
OptLevel::Oz => "-Oz",
}
}
pub const fn is_size(self) -> bool {
matches!(self, OptLevel::Os | OptLevel::Oz)
}
pub const fn runs_optimizer(self) -> bool {
!matches!(self, OptLevel::O0)
}
}
impl fmt::Display for OptLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_flag())
}
}
impl FromStr for OptLevel {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
Ok(match s {
"0" => OptLevel::O0,
"" | "1" => OptLevel::O1,
"2" => OptLevel::O2,
"3" | "4" | "5" | "6" | "7" | "8" | "9" => OptLevel::O3,
"s" => OptLevel::Os,
"z" => OptLevel::Oz,
_ => return Err(()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum EmitKind {
#[default]
Executable,
Object,
Asm,
Preprocessed,
Tast,
Ir,
MirFinal,
}
impl EmitKind {
pub const fn as_str(self) -> &'static str {
match self {
EmitKind::Executable => "exe",
EmitKind::Object => "obj",
EmitKind::Asm => "asm",
EmitKind::Preprocessed => "preprocessed",
EmitKind::Tast => "tast",
EmitKind::Ir => "ir",
EmitKind::MirFinal => "mir-final",
}
}
}
impl FromStr for EmitKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
Ok(match s {
"exe" => EmitKind::Executable,
"obj" => EmitKind::Object,
"asm" => EmitKind::Asm,
"preprocessed" => EmitKind::Preprocessed,
"tast" => EmitKind::Tast,
"ir" => EmitKind::Ir,
"mir-final" => EmitKind::MirFinal,
_ => return Err(()),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum Std {
C89,
C99,
C11,
C17,
#[default]
C23,
}
impl Std {
pub const fn stdc_version(self) -> Option<&'static str> {
match self {
Std::C89 => None,
Std::C99 => Some("199901L"),
Std::C11 => Some("201112L"),
Std::C17 => Some("201710L"),
Std::C23 => Some("202311L"),
}
}
pub const fn as_str(self) -> &'static str {
match self {
Std::C89 => "c89",
Std::C99 => "c99",
Std::C11 => "c11",
Std::C17 => "c17",
Std::C23 => "c23",
}
}
pub const fn has_c11(self) -> bool {
matches!(self, Std::C11 | Std::C17 | Std::C23)
}
#[must_use]
pub fn from_flag(name: &str) -> Option<(Std, bool)> {
let gnu = name.starts_with("gnu");
let std = match name {
"c89" | "c90" | "gnu89" | "gnu90" | "iso9899:1990" | "iso9899:199409" => Std::C89,
"c99" | "c9x" | "gnu99" | "gnu9x" | "iso9899:1999" | "iso9899:199x" => Std::C99,
"c11" | "c1x" | "gnu11" | "gnu1x" | "iso9899:2011" => Std::C11,
"c17" | "c18" | "gnu17" | "gnu18" | "iso9899:2017" | "iso9899:2018" => Std::C17,
"c23" | "c2x" | "gnu23" | "gnu2x" => Std::C23,
_ => return None,
};
Some((std, gnu))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GnucVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Default for GnucVersion {
fn default() -> GnucVersion {
GnucVersion { major: 4, minor: 2, patch: 1 }
}
}
impl FromStr for GnucVersion {
type Err = String;
fn from_str(text: &str) -> Result<GnucVersion, String> {
let mut parts = text.split('.');
let mut next = |what: &str| -> Result<u32, String> {
match parts.next() {
None => Ok(0),
Some(field) => {
field.parse().map_err(|_| format!("`{text}` has a {what} that is not a number"))
}
}
};
let major = next("major")?;
let minor = next("minor")?;
let patch = next("patchlevel")?;
if parts.next().is_some() {
return Err(format!("`{text}` has more than three components"));
}
Ok(GnucVersion { major, minor, patch })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Dumps {
pub macros: bool,
}
impl Dumps {
const LETTERS: &'static str = "MDNIU";
#[must_use]
pub fn is_family(arg: &str) -> bool {
match arg.strip_prefix("-d") {
Some("") | None => false,
Some(letters) => letters.chars().all(|c| Dumps::LETTERS.contains(c)),
}
}
pub fn add(&mut self, letters: &str) {
for letter in letters.chars() {
if letter == 'M' {
self.macros = true;
}
}
}
#[must_use]
pub const fn any(self) -> bool {
self.macros
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Options {
pub target: Triple,
pub opt_level: OptLevel,
pub emit: EmitKind,
pub debug_info: bool,
pub warnings_are_errors: bool,
pub error_limit: u32,
pub std: Std,
pub gnu_extensions: bool,
pub gnuc: GnucVersion,
pub hosted: bool,
pub defines: Vec<String>,
pub undefines: Vec<String>,
pub search: SearchPath,
pub line_markers: bool,
pub dumps: Dumps,
}
impl Options {
pub fn new(target: Triple) -> Self {
Self {
target,
opt_level: OptLevel::default(),
emit: EmitKind::default(),
debug_info: false,
warnings_are_errors: false,
error_limit: 20,
std: Std::default(),
gnu_extensions: true,
gnuc: GnucVersion::default(),
hosted: true,
defines: Vec::new(),
undefines: Vec::new(),
search: SearchPath::new(),
line_markers: true,
dumps: Dumps::default(),
}
}
}
#[derive(Debug)]
pub struct Session {
pub opts: Options,
pub target: TargetInfo,
pub interner: Interner,
pub sources: SourceMap,
diagnostics: Vec<Diagnostic>,
error_count: u32,
warning_count: u32,
}
impl Session {
pub fn new(opts: Options) -> Self {
let target = TargetInfo::new(opts.target);
Self {
opts,
target,
interner: Interner::with_capacity(1024),
sources: SourceMap::new(),
diagnostics: Vec::new(),
error_count: 0,
warning_count: 0,
}
}
pub fn emit(&mut self, mut diag: Diagnostic) {
if self.opts.warnings_are_errors && diag.severity == Severity::Warning {
diag.severity = Severity::Error;
}
match diag.severity {
Severity::Error | Severity::Ice => self.error_count += 1,
Severity::Warning => self.warning_count += 1,
Severity::Note | Severity::Help => {}
}
self.diagnostics.push(diag);
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
pub fn error_count(&self) -> u32 {
self.error_count
}
pub fn warning_count(&self) -> u32 {
self.warning_count
}
pub fn error_limit_reached(&self) -> bool {
self.opts.error_limit != 0 && self.error_count >= self.opts.error_limit
}
}
#[cfg(test)]
mod tests {
use super::*;
fn session() -> Session {
Session::new(Options::new("x86_64-unknown-linux-gnu".parse().unwrap()))
}
#[test]
fn a_version_claim_reads_the_way_gcc_prints_one() {
let all = |v: &str| v.parse::<GnucVersion>().unwrap();
assert_eq!(all("15.1.0"), GnucVersion { major: 15, minor: 1, patch: 0 });
assert_eq!(all("15"), GnucVersion { major: 15, minor: 0, patch: 0 });
assert_eq!(all("4.2"), GnucVersion { major: 4, minor: 2, patch: 0 });
assert!("".parse::<GnucVersion>().is_err());
assert!("15.".parse::<GnucVersion>().is_err(), "a trailing dot is a typo, not a zero");
assert!("1.2.3.4".parse::<GnucVersion>().is_err());
}
#[test]
fn optimisation_levels_parse_the_way_gcc_spells_them() {
assert_eq!("".parse::<OptLevel>().unwrap(), OptLevel::O1);
assert_eq!("0".parse::<OptLevel>().unwrap(), OptLevel::O0);
assert_eq!("2".parse::<OptLevel>().unwrap(), OptLevel::O2);
assert_eq!("9".parse::<OptLevel>().unwrap(), OptLevel::O3);
assert_eq!("s".parse::<OptLevel>().unwrap(), OptLevel::Os);
assert!("q".parse::<OptLevel>().is_err());
}
#[test]
fn only_o0_skips_the_optimizer() {
assert!(!OptLevel::O0.runs_optimizer());
assert!(OptLevel::O1.runs_optimizer());
assert!(OptLevel::Oz.runs_optimizer());
}
#[test]
fn emit_kinds_round_trip_through_their_names() {
for k in [
EmitKind::Executable,
EmitKind::Object,
EmitKind::Asm,
EmitKind::Preprocessed,
EmitKind::Tast,
EmitKind::Ir,
EmitKind::MirFinal,
] {
assert_eq!(k.as_str().parse::<EmitKind>().unwrap(), k);
}
}
#[test]
fn errors_are_counted_and_warnings_are_not() {
let mut s = session();
s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
assert_eq!(s.error_count(), 1);
assert_eq!(s.warning_count(), 1);
assert!(s.has_errors());
assert_eq!(s.diagnostics().len(), 2);
}
#[test]
fn werror_promotes_once_at_the_sink() {
let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
opts.warnings_are_errors = true;
let mut s = Session::new(opts);
s.emit(Diagnostic::warning("hmm", rucc_diag::Span::DUMMY));
assert_eq!(s.error_count(), 1);
assert_eq!(s.warning_count(), 0);
assert_eq!(s.diagnostics()[0].severity, Severity::Error);
}
#[test]
fn the_error_limit_can_be_switched_off() {
let mut opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
opts.error_limit = 0;
let mut s = Session::new(opts);
for _ in 0..100 {
s.emit(Diagnostic::error("no", rucc_diag::Span::DUMMY));
}
assert!(!s.error_limit_reached());
}
#[test]
fn the_session_carries_the_source_map_spans_are_resolved_against() {
let mut s = session();
let file = s.sources.add("a.c", b"int x;\n".to_vec()).unwrap();
let start = s.sources.file(file).start;
assert_eq!(s.sources.render_position(start + 4), "a.c:1:5");
}
#[test]
fn the_session_carries_the_resolved_target() {
let s = session();
assert_eq!(s.target.pointer_width, 64);
assert!(s.target.char_is_signed);
}
}