Selaa lähdekoodia

Parse pragma solidity version numbers (#1593)

Many Solidity features depend on the version of the compiler. So, parse
the version pragma and in another PR we will use this information.

This PR adds pragmas to the ast and we test it with a graphviz dot file.

Signed-off-by: Sean Young <sean@mess.org>
Sean Young 2 vuotta sitten
vanhempi
sitoutus
4a1833f8e8

+ 135 - 8
solang-parser/src/helpers/fmt.rs

@@ -803,14 +803,79 @@ impl Display for pt::SourceUnitPart {
             Self::TypeDefinition(inner) => inner.fmt(f),
             Self::Annotation(inner) => inner.fmt(f),
             Self::Using(inner) => inner.fmt(f),
-            Self::PragmaDirective(_, ident, lit) => {
+            Self::PragmaDirective(inner) => inner.fmt(f),
+            Self::StraySemicolon(_) => f.write_char(';'),
+        }
+    }
+}
+
+impl Display for pt::PragmaDirective {
+    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+        match self {
+            Self::Identifier(_, ident, val) => {
                 f.write_str("pragma")?;
                 write_opt!(f, ' ', ident);
-                // this isn't really a string literal, it's just parsed as one by the lexer
-                write_opt!(f, ' ', lit.as_ref().map(|lit| &lit.string));
+                write_opt!(f, ' ', val);
                 f.write_char(';')
             }
-            Self::StraySemicolon(_) => f.write_char(';'),
+            Self::StringLiteral(_, ident, lit) => {
+                f.write_str("pragma ")?;
+                ident.fmt(f)?;
+                f.write_char(' ')?;
+                lit.fmt(f)?;
+                f.write_char(';')
+            }
+            Self::Version(_, ident, versions) => {
+                f.write_str("pragma ")?;
+                ident.fmt(f)?;
+                f.write_char(' ')?;
+                write_separated(versions, f, " ")?;
+                f.write_char(';')
+            }
+        }
+    }
+}
+
+impl Display for pt::VersionComparator {
+    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+        match self {
+            Self::Plain { version, .. } => write_separated(version, f, "."),
+            Self::Operator { op, version, .. } => {
+                op.fmt(f)?;
+                write_separated(version, f, ".")
+            }
+            Self::Range { from, to, .. } => {
+                write_separated(from, f, ".")?;
+                f.write_str(" - ")?;
+                write_separated(to, f, ".")
+            }
+            Self::Or { left, right, .. } => {
+                left.fmt(f)?;
+                f.write_str(" || ")?;
+                right.fmt(f)
+            }
+        }
+    }
+}
+
+impl Display for pt::VersionOp {
+    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
+        f.write_str(self.as_str())
+    }
+}
+
+impl pt::VersionOp {
+    /// Returns the string representation of this type.
+    pub const fn as_str(&self) -> &'static str {
+        match self {
+            Self::Exact => "=",
+            Self::Greater => ">",
+            Self::GreaterEq => ">=",
+            Self::Less => "<",
+            Self::LessEq => "<=",
+            Self::Tilde => "~",
+            Self::Caret => "^",
+            Self::Wildcard => "*",
         }
     }
 }
@@ -1047,6 +1112,7 @@ impl Display for pt::UserDefinedOperator {
         f.write_str(self.as_str())
     }
 }
+
 impl pt::UserDefinedOperator {
     /// Returns the string representation of this type.
     pub const fn as_str(&self) -> &'static str {
@@ -1376,6 +1442,52 @@ mod tests {
         };
     }
 
+    /// VersionComparsion
+    macro_rules! version {
+        ($($l:literal),+) => {
+            <[_]>::into_vec(Box::new([ $( $l.into() ),+ ]))
+        }
+    }
+
+    macro_rules! plain_version {
+        ($($l:literal),+) => {
+            pt::VersionComparator::Plain {
+                loc: loc!(),
+                version: <[_]>::into_vec(Box::new([ $( $l.into() ),+ ])),
+            }
+        };
+    }
+
+    macro_rules! op_version {
+        ($op:expr, $($l:literal),+) => {
+            pt::VersionComparator::Operator {
+                loc: loc!(),
+                op: $op,
+                version: <[_]>::into_vec(Box::new([ $( $l.into() ),+ ])),
+            }
+        };
+    }
+
+    macro_rules! range_version {
+        ($from:expr, $to:expr) => {
+            pt::VersionComparator::Range {
+                loc: loc!(),
+                from: $from,
+                to: $to,
+            }
+        };
+    }
+
+    macro_rules! or_version {
+        ($left:expr, $right:expr) => {
+            pt::VersionComparator::Or {
+                loc: loc!(),
+                left: $left.into(),
+                right: $right.into(),
+            }
+        };
+    }
+
     /// Statement
     macro_rules! stmt {
         ( {} ) => {
@@ -2232,12 +2344,27 @@ mod tests {
             pt::SourceUnitPart: {
                 // rest tested individually
 
-                pt::SourceUnitPart::PragmaDirective(loc!(), None, None) => "pragma;",
-                pt::SourceUnitPart::PragmaDirective(loc!(), Some(id("solidity")), None)
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Identifier(loc!(), None, None).into()) => "pragma;",
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Identifier(loc!(), Some(id("solidity")), None).into())
                     => "pragma solidity;",
-                pt::SourceUnitPart::PragmaDirective(loc!(), Some(id("solidity")), Some(lit!("0.8.0")))
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::StringLiteral(loc!(), id("abi"), lit!("v2")).into())
+                    => "pragma abi \"v2\";",
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Version(loc!(), id("solidity"), vec![plain_version!("0", "8", "0")]).into())
                     => "pragma solidity 0.8.0;",
-
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Version(loc!(), id("solidity"), vec![
+                    op_version!(pt::VersionOp::Exact, "0", "5", "16"),
+                    op_version!(pt::VersionOp::GreaterEq, "0", "5"),
+                    op_version!(pt::VersionOp::Greater, "0"),
+                    op_version!(pt::VersionOp::Less, "1"),
+                    op_version!(pt::VersionOp::LessEq, "1"),
+                    op_version!(pt::VersionOp::Caret, "0", "5", "16"),
+                    op_version!(pt::VersionOp::Wildcard, "5", "5")]
+                ).into())
+                    => "pragma solidity =0.5.16 >=0.5 >0 <1 <=1 ^0.5.16 *5.5;",
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Version(loc!(), id("solidity"), vec![or_version!(plain_version!("0"), op_version!(pt::VersionOp::Caret, "1", "0"))]).into())
+                    => "pragma solidity 0 || ^1.0;",
+                pt::SourceUnitPart::PragmaDirective(pt::PragmaDirective::Version(loc!(), id("solidity"), vec![range_version!(version!["0"], version!["1", "0"])]).into())
+                    => "pragma solidity 0 - 1.0;",
                 pt::SourceUnitPart::StraySemicolon(loc!()) => ";",
             }
 

+ 8 - 2
solang-parser/src/helpers/loc.rs

@@ -393,8 +393,8 @@ impl_for_enums! {
         Self::TypeDefinition(ref l, ..) => l.loc(),
         Self::Annotation(ref l, ..) => l.loc(),
         Self::Using(ref l, ..) => l.loc(),
-        Self::PragmaDirective(l, ..)
-        | Self::StraySemicolon(l, ..) => l,
+        Self::PragmaDirective(ref l, ..) => l.loc(),
+        Self::StraySemicolon(l, ..) => l,
     }
 
     pt::Statement: match self {
@@ -467,6 +467,12 @@ impl_for_enums! {
         | Self::Default(l, ..) => l,
     }
 
+    pt::PragmaDirective: match self {
+        Self::Identifier(l, ..)
+        | Self::StringLiteral(l, ..)
+        | Self::Version(l, ..) => l,
+    }
+
     // other
     LexicalError: match self {
         Self::EndOfFileInComment(l)

+ 47 - 51
solang-parser/src/lexer.rs

@@ -348,6 +348,8 @@ pub struct Lexer<'input> {
     chars: PeekNth<CharIndices<'input>>,
     comments: &'input mut Vec<Comment>,
     file_no: usize,
+    /// While parsing version semver, do not parse rational numbers
+    parse_semver: bool,
     last_tokens: [Option<Token<'input>>; 2],
     /// The mutable reference to the error vector.
     pub errors: &'input mut Vec<LexicalError>,
@@ -577,6 +579,7 @@ impl<'input> Lexer<'input> {
             chars: peek_nth(input.char_indices()),
             comments,
             file_no,
+            parse_semver: false,
             last_tokens: [None, None],
             errors,
         }
@@ -632,6 +635,14 @@ impl<'input> Lexer<'input> {
             end = *i;
             self.chars.next();
         }
+
+        if self.parse_semver {
+            let integer = &self.input[start..=end];
+            let exp = &self.input[0..0];
+
+            return Ok((start, Token::Number(integer, exp), end + 1));
+        }
+
         let mut rational_end = end;
         let mut end_before_rational = end + 1;
         let mut rational_start = end;
@@ -971,7 +982,10 @@ impl<'input> Lexer<'input> {
                         return Some((start, Token::Annotation(&id[1..]), end));
                     };
                 }
-                Some((i, ';')) => return Some((i, Token::Semicolon, i + 1)),
+                Some((i, ';')) => {
+                    self.parse_semver = false;
+                    return Some((i, Token::Semicolon, i + 1));
+                }
                 Some((i, ',')) => return Some((i, Token::Comma, i + 1)),
                 Some((i, '(')) => return Some((i, Token::OpenParenthesis, i + 1)),
                 Some((i, ')')) => return Some((i, Token::CloseParenthesis, i + 1)),
@@ -1124,7 +1138,7 @@ impl<'input> Lexer<'input> {
                 }
                 Some((i, '.')) => {
                     if let Some((_, a)) = self.chars.peek() {
-                        if a.is_ascii_digit() {
+                        if a.is_ascii_digit() && !self.parse_semver {
                             return match self.parse_number(i + 1, '.') {
                                 Err(lex_error) => {
                                     self.errors.push(lex_error);
@@ -1175,46 +1189,6 @@ impl<'input> Lexer<'input> {
         }
     }
 
-    /// Next token is pragma value. Return it
-    fn pragma_value(&mut self) -> Option<Spanned<'input>> {
-        // special parser for pragma solidity >=0.4.22 <0.7.0;
-        let mut start = None;
-        let mut end = 0;
-
-        // solc will include anything upto the next semicolon, whitespace
-        // trimmed on left and right
-        loop {
-            match self.chars.peek() {
-                Some((_, ';')) | None => {
-                    return if let Some(start) = start {
-                        Some((
-                            start,
-                            Token::StringLiteral(false, &self.input[start..end]),
-                            end,
-                        ))
-                    } else {
-                        self.next()
-                    };
-                }
-                Some((_, ch)) if ch.is_whitespace() => {
-                    self.chars.next();
-                }
-                Some((i, _)) => {
-                    if start.is_none() {
-                        start = Some(*i);
-                    }
-                    self.chars.next();
-
-                    // end should point to the byte _after_ the character
-                    end = match self.chars.peek() {
-                        Some((i, _)) => *i,
-                        None => self.input.len(),
-                    }
-                }
-            }
-        }
-    }
-
     fn match_identifier(&mut self, start: usize) -> (&'input str, usize) {
         let end;
         loop {
@@ -1241,11 +1215,11 @@ impl<'input> Iterator for Lexer<'input> {
         // Lexer should be aware of whether the last two tokens were
         // pragma followed by identifier. If this is true, then special parsing should be
         // done for the pragma value
-        let token = if let [Some(Token::Pragma), Some(Token::Identifier(_))] = self.last_tokens {
-            self.pragma_value()
-        } else {
-            self.next()
-        };
+        if let [Some(Token::Pragma), Some(Token::Identifier(_))] = self.last_tokens {
+            self.parse_semver = true;
+        }
+
+        let token = self.next();
 
         self.last_tokens = [
             self.last_tokens[1],
@@ -1412,7 +1386,18 @@ mod tests {
             vec!(
                 (0, Token::Pragma, 6),
                 (7, Token::Identifier("solidity"), 15),
-                (16, Token::StringLiteral(false, ">=0.5.0 <0.7.0"), 30),
+                (16, Token::MoreEqual, 18),
+                (18, Token::Number("0", ""), 19),
+                (19, Token::Member, 20),
+                (20, Token::Number("5", ""), 21),
+                (21, Token::Member, 22),
+                (22, Token::Number("0", ""), 23),
+                (24, Token::Less, 25),
+                (25, Token::Number("0", ""), 26),
+                (26, Token::Member, 27),
+                (27, Token::Number("7", ""), 28),
+                (28, Token::Member, 29),
+                (29, Token::Number("0", ""), 30),
                 (30, Token::Semicolon, 31),
             )
         );
@@ -1430,7 +1415,18 @@ mod tests {
             vec!(
                 (0, Token::Pragma, 6),
                 (7, Token::Identifier("solidity"), 15),
-                (17, Token::StringLiteral(false, ">=0.5.0 <0.7.0"), 31),
+                (17, Token::MoreEqual, 19),
+                (19, Token::Number("0", ""), 20),
+                (20, Token::Member, 21),
+                (21, Token::Number("5", ""), 22),
+                (22, Token::Member, 23),
+                (23, Token::Number("0", ""), 24),
+                (25, Token::Less, 26),
+                (26, Token::Number("0", ""), 27),
+                (27, Token::Member, 28),
+                (28, Token::Number("7", ""), 29),
+                (29, Token::Member, 30),
+                (30, Token::Number("0", ""), 31),
                 (34, Token::Semicolon, 35),
             )
         );
@@ -1443,7 +1439,7 @@ mod tests {
             vec!(
                 (0, Token::Pragma, 6),
                 (7, Token::Identifier("solidity"), 15),
-                (16, Token::StringLiteral(false, "赤"), 19),
+                (16, Token::Identifier("赤"), 19),
                 (19, Token::Semicolon, 20)
             )
         );
@@ -1533,7 +1529,7 @@ mod tests {
             vec!(
                 (0, Token::Pragma, 6),
                 (7, Token::Identifier("foo"), 10),
-                (11, Token::StringLiteral(false, "bar"), 14),
+                (11, Token::Identifier("bar"), 14),
             )
         );
 

+ 75 - 1
solang-parser/src/pt.rs

@@ -346,7 +346,7 @@ pub enum SourceUnitPart {
     /// `pragma <1> <2>;`
     ///
     /// `1` and `2` are `None` only if an error occurred during parsing.
-    PragmaDirective(Loc, Option<Identifier>, Option<StringLiteral>),
+    PragmaDirective(Box<PragmaDirective>),
 
     /// An import directive.
     ImportDirective(Import),
@@ -575,6 +575,80 @@ pub enum ContractPart {
     StraySemicolon(Loc),
 }
 
+/// A pragma directive
+#[derive(Debug, PartialEq, Eq, Clone)]
+#[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))]
+pub enum PragmaDirective {
+    /// pragma a b;
+    Identifier(Loc, Option<Identifier>, Option<Identifier>),
+    /// pragma a "b";
+    StringLiteral(Loc, Identifier, StringLiteral),
+    /// pragma version =0.5.16;
+    Version(Loc, Identifier, Vec<VersionComparator>),
+}
+
+/// A `version` list
+#[derive(Debug, PartialEq, Eq, Clone)]
+#[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))]
+pub enum VersionComparator {
+    /// 0.8.22
+    Plain {
+        /// The code location.
+        loc: Loc,
+        /// List of versions: major, minor, patch. minor and patch are optional
+        version: Vec<String>,
+    },
+    /// =0.5.16
+    Operator {
+        /// The code location.
+        loc: Loc,
+        /// Semver comparison operator
+        op: VersionOp,
+        /// version number
+        version: Vec<String>,
+    },
+    /// foo || bar
+    Or {
+        /// The code location.
+        loc: Loc,
+        /// left part
+        left: Box<VersionComparator>,
+        /// right part
+        right: Box<VersionComparator>,
+    },
+    /// 0.7.0 - 0.8.22
+    Range {
+        /// The code location.
+        loc: Loc,
+        /// start of range
+        from: Vec<String>,
+        /// end of range
+        to: Vec<String>,
+    },
+}
+
+/// Comparison operator
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+#[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))]
+pub enum VersionOp {
+    /// `=`
+    Exact,
+    /// `>`
+    Greater,
+    /// `>=`
+    GreaterEq,
+    /// `<`
+    Less,
+    /// `<=`
+    LessEq,
+    /// `~`
+    Tilde,
+    /// `^`
+    Caret,
+    /// `*`
+    Wildcard,
+}
+
 /// A `using` list. See [Using].
 #[derive(Debug, PartialEq, Eq, Clone)]
 #[cfg_attr(feature = "pt-serde", derive(Serialize, Deserialize))]

+ 59 - 5
solang-parser/src/solidity.lalrpop

@@ -10,7 +10,7 @@ pub SourceUnit: SourceUnit = {
 
 SourceUnitPart: SourceUnitPart = {
     ContractDefinition => SourceUnitPart::ContractDefinition(<>),
-    PragmaDirective => <>,
+    PragmaDirective => SourceUnitPart::PragmaDirective(<>.into()),
     ImportDirective => <>,
     EnumDefinition => SourceUnitPart::EnumDefinition(<>),
     StructDefinition => SourceUnitPart::StructDefinition(<>),
@@ -63,13 +63,67 @@ ImportRename: (Identifier, Option<Identifier>) = {
     <from:SolIdentifier> "as" <to:SolIdentifier> => (from, Some(to)),
 }
 
-PragmaDirective: SourceUnitPart = {
-    // The lexer does special parsing for String literal; it isn't really a string literal
-    <l:@L> "pragma" <i:SolIdentifier> <s:StringLiteral> <r:@R> ";" => SourceUnitPart::PragmaDirective(Loc::File(file_no, l, r), Some(i), Some(s)) ,
+PragmaDirective: PragmaDirective = {
+    <l:@L> "pragma" <i:SolIdentifier> <s:Identifier> <r:@R> ";" => {
+        PragmaDirective::Identifier(Loc::File(file_no, l, r), Some(i), Some(s))
+    },
+    <l:@L> "pragma" <i:SolIdentifier> <s:StringLiteral> <r:@R> ";" => {
+        PragmaDirective::StringLiteral(Loc::File(file_no, l, r), i, s)
+    },
+    <l:@L> "pragma" <i:SolIdentifier> <comp:VersionComparator1+> <r:@R> ";" => {
+        PragmaDirective::Version(Loc::File(file_no, l, r), i, comp)
+    },
     <l:@L> "pragma" <false_token:!> <r:@R> ";" => {
         parser_errors.push(false_token);
-        SourceUnitPart::PragmaDirective(Loc::File(file_no, l, r), None, None)
+        PragmaDirective::Identifier(Loc::File(file_no, l, r), None, None)
+    }
+}
+
+VersionComparator1: VersionComparator = {
+    <l:@L> <left:VersionComparator1> "||" <right:VersionComparator> <r:@R> => VersionComparator::Or {
+        loc: Loc::File(file_no, l, r),
+        left: left.into(),
+        right: right.into()
+    },
+    VersionComparator
+}
+
+VersionComparator: VersionComparator = {
+    <l:@L> <version:Version> <r:@R> => VersionComparator::Plain {
+        loc: Loc::File(file_no, l, r),
+        version
     },
+    <l:@L> <op:VersionOp> <version:Version> <r:@R> => VersionComparator::Operator {
+        loc: Loc::File(file_no, l, r),
+        op,
+        version
+    },
+    <l:@L> <from:Version> "-" <to:Version> <r:@R> => VersionComparator::Range {
+        loc: Loc::File(file_no, l, r),
+        from,
+        to
+    },
+}
+
+Version: Vec<String> = {
+    <major:number> <minors:("." <number>)*> => {
+        let mut v = minors;
+
+        v.insert(0, major);
+
+        v.into_iter().map(|(b,_)| b.to_string()).collect()
+    }
+}
+
+VersionOp: VersionOp = {
+    "=" => VersionOp::Exact,
+    ">" => VersionOp::Greater,
+    ">=" => VersionOp::GreaterEq,
+    "<" => VersionOp::Less,
+    "<=" => VersionOp::LessEq,
+    "~" => VersionOp::Tilde,
+    "^" => VersionOp::Caret,
+    "*" => VersionOp::Wildcard,
 }
 
 Type: Type = {

+ 1 - 1
solang-parser/src/tests.rs

@@ -44,7 +44,7 @@ contract 9c {
             errors,
             vec![
                 Diagnostic { loc: File(0, 17, 21), level: Error, ty: ParserError, message: "'frum' found where 'from' expected".to_string(), notes: vec![]},
-                Diagnostic { loc: File(0, 48, 49), level: Error, ty: ParserError, message: r#"unrecognised token ';', expected string"#.to_string(), notes: vec![] },
+                Diagnostic { loc: File(0, 48, 49), level: Error, ty: ParserError, message: "unrecognised token ';', expected \"*\", \"<\", \"<=\", \"=\", \">\", \">=\", \"^\", \"~\", identifier, number, string".to_string(), notes: vec![] },
                 Diagnostic { loc: File(0, 62, 65), level: Error, ty: ParserError, message: r#"unrecognised token 'for', expected "(", ";", "=""#.to_string(), notes: vec![] },
                 Diagnostic { loc: File(0, 78, 79), level: Error, ty: ParserError, message: r#"unrecognised token '9', expected "case", "default", "leave", "revert", "switch", identifier"#.to_string(), notes: vec![] },
                 Diagnostic { loc: File(0, 95, 96), level: Error, ty: ParserError, message: "unrecognised token '0', expected \"(\", \"++\", \"--\", \".\", \"[\", \"case\", \"constant\", \"default\", \"external\", \"immutable\", \"internal\", \"leave\", \"override\", \"private\", \"public\", \"revert\", \"switch\", \"{\", identifier".to_string(), notes: vec![] },

+ 65 - 0
src/sema/ast.rs

@@ -16,6 +16,7 @@ pub use solang_parser::diagnostics::*;
 use solang_parser::pt;
 use solang_parser::pt::{CodeLocation, FunctionTy, OptionalCodeLocation};
 use std::cell::RefCell;
+use std::fmt::Write;
 use std::{
     collections::HashSet,
     collections::{BTreeMap, HashMap},
@@ -662,6 +663,7 @@ pub struct File {
 #[derive(Debug)]
 pub struct Namespace {
     pub target: Target,
+    pub pragmas: Vec<Pragma>,
     pub files: Vec<File>,
     pub enums: Vec<EnumDecl>,
     pub structs: Vec<StructDecl>,
@@ -696,6 +698,69 @@ pub struct Namespace {
     pub hover_overrides: HashMap<pt::Loc, String>,
 }
 
+#[derive(Debug)]
+pub enum Pragma {
+    Identifier {
+        loc: pt::Loc,
+        name: pt::Identifier,
+        value: pt::Identifier,
+    },
+    StringLiteral {
+        loc: pt::Loc,
+        name: pt::Identifier,
+        value: pt::StringLiteral,
+    },
+    SolidityVersion {
+        loc: pt::Loc,
+        versions: Vec<VersionReq>,
+    },
+}
+
+#[derive(Debug)]
+pub enum VersionReq {
+    Plain {
+        loc: pt::Loc,
+        version: Version,
+    },
+    Operator {
+        loc: pt::Loc,
+        op: pt::VersionOp,
+        version: Version,
+    },
+    Range {
+        loc: pt::Loc,
+        from: Version,
+        to: Version,
+    },
+    Or {
+        loc: pt::Loc,
+        left: Box<VersionReq>,
+        right: Box<VersionReq>,
+    },
+}
+
+#[derive(Debug)]
+pub struct Version {
+    pub major: u64,
+    pub minor: Option<u64>,
+    pub patch: Option<u64>,
+}
+
+impl fmt::Display for Version {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.major.fmt(f)?;
+        if let Some(minor) = self.minor {
+            f.write_char('.')?;
+            minor.fmt(f)?
+        }
+        if let Some(patch) = self.patch {
+            f.write_char('.')?;
+            patch.fmt(f)?;
+        }
+        Ok(())
+    }
+}
+
 #[derive(Debug)]
 pub struct Layout {
     pub slot: BigInt,

+ 87 - 0
src/sema/dotgraphviz.rs

@@ -2406,6 +2406,53 @@ impl Dot {
 
         node
     }
+
+    fn add_version(
+        &mut self,
+        version: &VersionReq,
+        parent: usize,
+        parent_rel: String,
+        ns: &Namespace,
+    ) {
+        match version {
+            VersionReq::Plain { loc, version } => {
+                let labels = vec![
+                    format!("version: {version}"),
+                    ns.loc_to_string(PathDisplay::FullPath, loc),
+                ];
+
+                self.add_node(Node::new("plain", labels), Some(parent), Some(parent_rel));
+            }
+            VersionReq::Operator { loc, op, version } => {
+                let labels = vec![
+                    format!("version: {op}{version}"),
+                    ns.loc_to_string(PathDisplay::FullPath, loc),
+                ];
+
+                self.add_node(
+                    Node::new("operator", labels),
+                    Some(parent),
+                    Some(parent_rel),
+                );
+            }
+            VersionReq::Range { loc, from, to } => {
+                let labels = vec![
+                    format!("version: {from} - {to}"),
+                    ns.loc_to_string(PathDisplay::FullPath, loc),
+                ];
+
+                self.add_node(Node::new("range", labels), Some(parent), Some(parent_rel));
+            }
+            VersionReq::Or { loc, left, right } => {
+                let labels = vec![format!("||"), ns.loc_to_string(PathDisplay::FullPath, loc)];
+
+                let node = self.add_node(Node::new("or", labels), Some(parent), Some(parent_rel));
+
+                self.add_version(left, node, "left".into(), ns);
+                self.add_version(right, node, "right".into(), ns);
+            }
+        }
+    }
 }
 
 impl Namespace {
@@ -2684,6 +2731,46 @@ impl Namespace {
             }
         }
 
+        // pragmas
+        if !self.pragmas.is_empty() {
+            let pragmas = dot.add_node(Node::new("pragmas", Vec::new()), None, None);
+
+            for pragma in &self.pragmas {
+                match pragma {
+                    Pragma::Identifier { loc, name, value } => {
+                        let labels = vec![
+                            format!("name: {}", name.name),
+                            format!("value: {}", value.name),
+                            self.loc_to_string(PathDisplay::FullPath, loc),
+                        ];
+
+                        dot.add_node(Node::new("pragma", labels), Some(pragmas), None);
+                    }
+                    Pragma::StringLiteral { loc, name, value } => {
+                        let labels = vec![
+                            format!("name: {}", name.name),
+                            format!("value: '{}'", value.string),
+                            self.loc_to_string(PathDisplay::FullPath, loc),
+                        ];
+
+                        dot.add_node(Node::new("pragma", labels), Some(pragmas), None);
+                    }
+                    Pragma::SolidityVersion { loc, versions } => {
+                        let labels = vec![
+                            "name: solidity".into(),
+                            self.loc_to_string(PathDisplay::FullPath, loc),
+                        ];
+
+                        let node = dot.add_node(Node::new("pragma", labels), Some(pragmas), None);
+
+                        for (no, version) in versions.iter().enumerate() {
+                            dot.add_version(version, node, format!("version {no}"), self);
+                        }
+                    }
+                }
+            }
+        }
+
         // diagnostics
         if !self.diagnostics.is_empty() {
             let diagnostics = dot.add_node(Node::new("diagnostics", Vec::new()), None, None);

+ 115 - 20
src/sema/mod.rs

@@ -119,9 +119,9 @@ fn sema_file(file: &ResolvedFile, resolver: &mut FileResolver, ns: &mut ast::Nam
     // resolve pragmas and imports
     for item in &tree.items {
         match &item.part {
-            pt::SourceUnitPart::PragmaDirective(loc, name, value) => {
+            pt::SourceUnitPart::PragmaDirective(pragma) => {
                 annotions_not_allowed(&item.annotations, "pragma", ns);
-                resolve_pragma(loc, name.as_ref().unwrap(), value.as_ref().unwrap(), ns);
+                resolve_pragma(pragma, ns);
             }
             pt::SourceUnitPart::ImportDirective(import) => {
                 annotions_not_allowed(&item.annotations, "import", ns);
@@ -417,28 +417,65 @@ fn resolve_import(
 }
 
 /// Resolve pragma. We don't do anything with pragmas for now
-fn resolve_pragma(
-    loc: &pt::Loc,
-    name: &pt::Identifier,
-    value: &pt::StringLiteral,
-    ns: &mut ast::Namespace,
-) {
-    if name.name == "solidity" {
-        ns.diagnostics.push(ast::Diagnostic::debug(
-            *loc,
-            "pragma 'solidity' is ignored".to_string(),
-        ));
-    } else if name.name == "experimental" && value.string == "ABIEncoderV2" {
+fn resolve_pragma(pragma: &pt::PragmaDirective, ns: &mut ast::Namespace) {
+    match pragma {
+        pt::PragmaDirective::Identifier(loc, Some(ident), Some(value)) => {
+            plain_pragma(loc, &ident.name, &value.name, ns);
+
+            ns.pragmas.push(ast::Pragma::Identifier {
+                loc: *loc,
+                name: ident.clone(),
+                value: value.clone(),
+            });
+        }
+        pt::PragmaDirective::StringLiteral(loc, ident, value) => {
+            plain_pragma(loc, &ident.name, &value.string, ns);
+
+            ns.pragmas.push(ast::Pragma::StringLiteral {
+                loc: *loc,
+                name: ident.clone(),
+                value: value.clone(),
+            });
+        }
+        pt::PragmaDirective::Version(loc, ident, versions) => {
+            if ident.name != "solidity" {
+                ns.diagnostics.push(ast::Diagnostic::error(
+                    ident.loc,
+                    format!("unknown pragma '{}'", ident.name),
+                ));
+            } else {
+                // parser versions
+                let mut res = Vec::new();
+
+                for version in versions {
+                    let Ok(v) = parse_version_comparator(version, ns) else {
+                        return;
+                    };
+                    res.push(v);
+                }
+
+                ns.pragmas.push(ast::Pragma::SolidityVersion {
+                    loc: *loc,
+                    versions: res,
+                });
+            }
+        }
+        pt::PragmaDirective::Identifier { .. } => (),
+    }
+}
+
+fn plain_pragma(loc: &pt::Loc, name: &str, value: &str, ns: &mut ast::Namespace) {
+    if name == "experimental" && value == "ABIEncoderV2" {
         ns.diagnostics.push(ast::Diagnostic::debug(
             *loc,
             "pragma 'experimental' with value 'ABIEncoderV2' is ignored".to_string(),
         ));
-    } else if name.name == "experimental" && value.string == "solidity" {
+    } else if name == "experimental" && value == "solidity" {
         ns.diagnostics.push(ast::Diagnostic::error(
             *loc,
             "experimental solidity features are not supported".to_string(),
         ));
-    } else if name.name == "abicoder" && value.string == "v2" {
+    } else if name == "abicoder" && value == "v2" {
         ns.diagnostics.push(ast::Diagnostic::debug(
             *loc,
             "pragma 'abicoder' with value 'v2' is ignored".to_string(),
@@ -446,12 +483,70 @@ fn resolve_pragma(
     } else {
         ns.diagnostics.push(ast::Diagnostic::warning(
             *loc,
-            format!(
-                "unknown pragma '{}' with value '{}' ignored",
-                name.name, value.string
-            ),
+            format!("unknown pragma '{}' with value '{}' ignored", name, value),
+        ));
+    }
+}
+
+fn parse_version_comparator(
+    version: &pt::VersionComparator,
+    ns: &mut ast::Namespace,
+) -> Result<ast::VersionReq, ()> {
+    match version {
+        pt::VersionComparator::Plain { loc, version } => Ok(ast::VersionReq::Plain {
+            loc: *loc,
+            version: parse_version(loc, version, ns)?,
+        }),
+        pt::VersionComparator::Operator { loc, op, version } => Ok(ast::VersionReq::Operator {
+            loc: *loc,
+            op: *op,
+            version: parse_version(loc, version, ns)?,
+        }),
+        pt::VersionComparator::Range { loc, from, to } => Ok(ast::VersionReq::Range {
+            loc: *loc,
+            from: parse_version(loc, from, ns)?,
+            to: parse_version(loc, to, ns)?,
+        }),
+        pt::VersionComparator::Or { loc, left, right } => Ok(ast::VersionReq::Or {
+            loc: *loc,
+            left: parse_version_comparator(left, ns)?.into(),
+            right: parse_version_comparator(right, ns)?.into(),
+        }),
+    }
+}
+
+fn parse_version(
+    loc: &pt::Loc,
+    version: &[String],
+    ns: &mut ast::Namespace,
+) -> Result<ast::Version, ()> {
+    let mut res = Vec::new();
+
+    for v in version {
+        if let Ok(v) = v.parse() {
+            res.push(v);
+        } else {
+            ns.diagnostics.push(ast::Diagnostic::error(
+                *loc,
+                format!("'{v}' is not a valid number"),
+            ));
+            return Err(());
+        }
+    }
+
+    if version.len() > 3 {
+        ns.diagnostics.push(ast::Diagnostic::error(
+            *loc,
+            "no more than three numbers allowed - major.minor.patch".into(),
         ));
+        return Err(());
     }
+
+    Ok(ast::Version {
+        major: res[0],
+        minor: res.get(1).cloned(),
+        patch: res.get(2).cloned(),
+    })
 }
 
 /// Walk through the parse tree and collect all the annotations and doccomments for

+ 5 - 8
src/sema/namespace.rs

@@ -8,20 +8,18 @@ use super::{
     builtin,
     diagnostics::Diagnostics,
     eval::eval_const_number,
-    expression::{ExprContext, ResolveTo},
+    expression::{resolve_expression::expression, ExprContext, ResolveTo},
     resolve_params, resolve_returns,
     symtable::Symtable,
     ArrayDimension,
 };
-use crate::sema::expression::resolve_expression::expression;
 use crate::Target;
+use itertools::Itertools;
 use num_bigint::BigInt;
-use num_traits::Signed;
-use num_traits::Zero;
-use solang_parser::pt::FunctionTy;
+use num_traits::{Signed, Zero};
 use solang_parser::{
     pt,
-    pt::{CodeLocation, OptionalCodeLocation},
+    pt::{CodeLocation, FunctionTy, OptionalCodeLocation},
 };
 use std::collections::HashMap;
 
@@ -47,6 +45,7 @@ impl Namespace {
 
         let mut ns = Namespace {
             target,
+            pragmas: Vec::new(),
             files: Vec::new(),
             enums: Vec::new(),
             structs: Vec::new(),
@@ -1545,7 +1544,6 @@ impl Namespace {
             diagnostics,
             ResolveTo::Type(&Type::Uint(256)),
         )?;
-        context.drop();
 
         match size_expr.ty() {
             Type::Uint(_) | Type::Int(_) => {}
@@ -1573,7 +1571,6 @@ impl Namespace {
             params
                 .iter()
                 .map(|p| p.ty.to_signature_string(false, self))
-                .collect::<Vec<String>>()
                 .join(",")
         )
     }

+ 34 - 2
tests/contract.rs

@@ -89,7 +89,7 @@ fn parse_file(path: PathBuf, target: Target) -> io::Result<()> {
         }
     }
 
-    check_diagnostics(&path, &ns);
+    check_diagnostics(&path, &ns)?;
 
     if !ns.diagnostics.any_errors() {
         // let's try and emit
@@ -144,7 +144,7 @@ fn add_file(cache: &mut FileResolver, path: &Path, target: Target) -> io::Result
     Ok(filename.to_string())
 }
 
-fn check_diagnostics(path: &Path, ns: &Namespace) {
+fn check_diagnostics(path: &Path, ns: &Namespace) -> io::Result<()> {
     let mut expected = "// ---- Expect: diagnostics ----\n".to_owned();
 
     for diag in ns.diagnostics.iter() {
@@ -172,6 +172,10 @@ fn check_diagnostics(path: &Path, ns: &Namespace) {
     for line in BufReader::new(file).lines() {
         let line = line.unwrap();
 
+        if line.starts_with("// ---- Expect: dot ----") {
+            check_dot(path, ns)?;
+        }
+
         if found.is_empty() && !line.starts_with("// ---- Expect: diagnostics ----") {
             continue;
         }
@@ -183,4 +187,32 @@ fn check_diagnostics(path: &Path, ns: &Namespace) {
     assert!(!found.is_empty());
 
     assert_eq!(found, expected, "source: {}", path.display());
+
+    Ok(())
+}
+
+fn check_dot(path: &Path, ns: &Namespace) -> io::Result<()> {
+    let mut path = path.to_path_buf();
+
+    path.set_extension("dot");
+
+    let generated_dot = ns.dotgraphviz();
+
+    // uncomment the next three lines to regenerate the test data
+    // use std::io::Write;
+    // let mut file = File::create(&path)?;
+    // file.write_all(generated_dot.as_bytes())?;
+
+    let mut file = File::open(&path)?;
+
+    let mut test_dot = String::new();
+
+    file.read_to_string(&mut test_dot)?;
+
+    // The dot files may have had their end of lines mangled on Windows
+    let test_dot = test_dot.replace("\r\n", "\n");
+
+    pretty_assertions::assert_eq!(generated_dot, test_dot);
+
+    Ok(())
 }

+ 4 - 0
tests/contract_testcases/evm/bad_pragmas.sol

@@ -0,0 +1,4 @@
+pragma solidity 1.2.3.4;
+
+// ---- Expect: diagnostics ----
+// error: 1:17-24: no more than three numbers allowed - major.minor.patch

+ 36 - 0
tests/contract_testcases/evm/pragmas.dot

@@ -0,0 +1,36 @@
+strict digraph "tests/contract_testcases/evm/pragmas.sol" {
+	contract [label="contract C\ntests/contract_testcases/evm/pragmas.sol:6:1-14"]
+	pragma [label="name: foo\nvalue: bar\ntests/contract_testcases/evm/pragmas.sol:1:1-15"]
+	pragma_4 [label="name: abicode\nvalue: v2\ntests/contract_testcases/evm/pragmas.sol:2:1-18"]
+	pragma_5 [label="name: abicode\nvalue: 'v2'\ntests/contract_testcases/evm/pragmas.sol:3:1-20"]
+	pragma_6 [label="name: solidity\ntests/contract_testcases/evm/pragmas.sol:4:1-65"]
+	operator [label="version: ^0.5.16\ntests/contract_testcases/evm/pragmas.sol:4:17-24"]
+	range [label="version: 0.4 - 1\ntests/contract_testcases/evm/pragmas.sol:4:25-32"]
+	or [label="||\ntests/contract_testcases/evm/pragmas.sol:4:33-52"]
+	operator_10 [label="version: =0.8.22\ntests/contract_testcases/evm/pragmas.sol:4:33-40"]
+	operator_11 [label="version: >=0.8.21\ntests/contract_testcases/evm/pragmas.sol:4:44-52"]
+	operator_12 [label="version: <=2\ntests/contract_testcases/evm/pragmas.sol:4:53-56"]
+	operator_13 [label="version: ~1\ntests/contract_testcases/evm/pragmas.sol:4:57-59"]
+	plain [label="version: 0.6.2\ntests/contract_testcases/evm/pragmas.sol:4:60-65"]
+	diagnostic [label="unknown pragma 'foo' with value 'bar' ignored\nlevel Warning\ntests/contract_testcases/evm/pragmas.sol:1:1-15"]
+	diagnostic_17 [label="unknown pragma 'abicode' with value 'v2' ignored\nlevel Warning\ntests/contract_testcases/evm/pragmas.sol:2:1-18"]
+	diagnostic_18 [label="unknown pragma 'abicode' with value 'v2' ignored\nlevel Warning\ntests/contract_testcases/evm/pragmas.sol:3:1-20"]
+	diagnostic_19 [label="found contract 'C'\nlevel Debug\ntests/contract_testcases/evm/pragmas.sol:6:1-14"]
+	contracts -> contract
+	pragmas -> pragma
+	pragmas -> pragma_4
+	pragmas -> pragma_5
+	pragmas -> pragma_6
+	pragma_6 -> operator [label="version 0"]
+	pragma_6 -> range [label="version 1"]
+	pragma_6 -> or [label="version 2"]
+	or -> operator_10 [label="left"]
+	or -> operator_11 [label="right"]
+	pragma_6 -> operator_12 [label="version 3"]
+	pragma_6 -> operator_13 [label="version 4"]
+	pragma_6 -> plain [label="version 5"]
+	diagnostics -> diagnostic [label="Warning"]
+	diagnostics -> diagnostic_17 [label="Warning"]
+	diagnostics -> diagnostic_18 [label="Warning"]
+	diagnostics -> diagnostic_19 [label="Debug"]
+}

+ 12 - 0
tests/contract_testcases/evm/pragmas.sol

@@ -0,0 +1,12 @@
+pragma foo bar;
+pragma abicode v2;
+pragma abicode "v2";
+pragma solidity ^0.5.16 0.4 - 1 =0.8.22 || >=0.8.21 <=2 ~1 0.6.2;
+
+contract C {}
+
+// ---- Expect: dot ----
+// ---- Expect: diagnostics ----
+// warning: 1:1-15: unknown pragma 'foo' with value 'bar' ignored
+// warning: 2:1-18: unknown pragma 'abicode' with value 'v2' ignored
+// warning: 3:1-20: unknown pragma 'abicode' with value 'v2' ignored

+ 1 - 1
tests/contract_testcases/solana/annotations/annotations_bad.sol

@@ -67,7 +67,7 @@ abstract contract c {
 // ---- Expect: diagnostics ----
 // error: 2:1-19: annotations not allowed on pragma
 // error: 3:1-10: annotations not allowed on pragma
-// warning: 4:1-19: unknown pragma 'version' with value '1.1' ignored
+// error: 4:8-15: unknown pragma 'version'
 // error: 6:1-8: annotations not allowed on struct
 // error: 9:1-15: annotations not allowed on event
 // error: 12:1-14: annotations not allowed on enum

+ 1 - 1
tests/evm.rs

@@ -253,7 +253,7 @@ fn ethereum_solidity_tests() {
         })
         .sum();
 
-    assert_eq!(errors, 946);
+    assert_eq!(errors, 936);
 }
 
 fn set_file_contents(source: &str, path: &Path) -> (FileResolver, Vec<String>) {