// Copyright 2017-2022 F4PGA Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 #include "FasmLexer.h" #include "FasmParser.h" #include "FasmParserVisitor.h" #include "antlr4-runtime.h" /// This code parses FASM and produces a lightweight binary format that /// is fast and simple to unpack based on the tag/length/value (TLV) /// format. /// /// For example, for a fixed width integer, this can just be: /// /// and for a variable length string: /// /// /// Note that the length itself must be a fixed width value, which does /// impose a size limit, but this format is more efficient and easier /// to decode than a UTF-8 style variable length encoding where a bit /// is reserved per per byte to indicate the end of a variable length /// value. /// /// Each 4-bytes of a numeric value is in native endian order. This /// format is designed to be produced and consumed on the same machine. /// /// TLVs can be nested, with each level adding 5 bytes of header /// overhead. There is a choice to aggregate values under another header /// (using withHeader) or not. Even though this encodes redundant size /// information, it can make the result easier to parse. /// /// Example of a nested TLV: /// /// /// /// /// Note that there is no need for a closing tag. /// /// For example, the consumer can allocate space for results in larger /// chunks, in this case it can preallocate a line at a time, after /// reading the first 5 bytes, although this is mostly useful for /// consumers that can use the data directly without further /// manipulation. /// /// The format used here does not rely on knowing the size of the /// entire output, which allows streaming line by line, although /// ANTLR does not yet implement an incremental parser. /// /// For a concrete example, see the test case in ParseFasmTests.cpp /// External C Interface /// /// These functions serialize the FASM parse tree to an easy to parse /// tag/length/value binary format, where the tag is one byte and /// the length is 4 bytes, in native endianness (typically little.) extern "C" { void from_string(const char* in, bool hex, void (*ret)(const char* str, size_t), void (*err)(size_t, size_t, const char*)); void from_file(const char* path, bool hex, void (*ret)(const char* str, size_t), void (*err)(size_t, size_t, const char*)); } using namespace antlr4; using namespace antlrcpp; /// Hex mode is useful for debugging. /// In this mode, binary values are printed as hex values surrounded by < > bool hex_mode = false; /// The Num class provides an << operator that either dumps the raw value /// or prints the hex value based on the mode. It also can print a tag. class Num { public: /// Constructors, with optional tag character Num(char tag, uint32_t num) : num(num), tag(tag) {} Num(uint32_t num) : num(num), tag(0) {} uint32_t num; /// The value char tag; /// Tag character /// The bit width static constexpr int kWidth = sizeof(num) * 8; }; /// Output stream operator for Num. /// In hex mode, Nums are printed as , otherwise they are /// copied into the output stream in the underlying representation. /// As such, this will use the native endianness. std::ostream& operator<<(std::ostream& s, const Num& num) { if (num.tag) s << num.tag; if (hex_mode) { s << "<" << std::hex << num.num << ">"; } else { s.write(reinterpret_cast(&num.num), sizeof(num.num)); } return s; } /// The Str class wraps a std::string to provide an << operator /// that includes the tag and length. struct Str { /// Takes the tag and string data Str(char tag, std::string data) : tag(tag), data(data) {} char tag; std::string data; }; /// This output stream operator adds the header needed to decode /// a string of unknown length and type. /// Note that some characters are escaped in hex mode to /// avoid confusion. std::ostream& operator<<(std::ostream& s, const Str& str) { s << str.tag << Num(str.data.size()); if (hex_mode) { /// escape < \ > for (char c : str.data) { if (c == '<' || c == '>' || c == '\\') { s.put('\\'); } s.put(c); } } else { s << str.data; } return s; } /// Wraps a string in another header, used to aggregate data. std::string withHeader(char tag, std::string data) { std::ostringstream header; header << tag << Num(data.size()); return header.str() + data; } /// Counts characters that don't match the given character. /// Used to count digits skipping '_'. int count_without(std::string::iterator start, std::string::iterator end, char c) { int count = 0; auto it = start; while (it != end) { if (*it != c) { count++; } it++; } return count; } /// Calculates the number of leading pad bits are needed /// so that the rightmost bit will be the LSB of a Num. /// e.g. This would be 31 for 33'b0. int lead_bits(int bits) { return (Num::kWidth - (bits % Num::kWidth)) % Num::kWidth; } // clang-format off /// Decode a hex digit. int from_hex_digit(char c) { int result = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10: -1; assert(result >= 0 && result < 16); return result; } // clang-format on /// Makes tags easy to extract for documentation and code generation. /// Use at most once per line to allow simple grepping. #define TAG(c, long_name) (c) /// Raised on parse errors struct ParseException { size_t line; ///< Line number of error. size_t position; ///< Position in that line. std::string message; ///< A descriptive message. }; /// Helper macro to convert a rule context into a string /// For use inside FasmParserBaseVisitor #define GET(x) (context->x() ? visit(context->x()).as() : "") /// FasmParserBaseVisitor is a visitor for the parse tree /// generated by the ANTLR parser. /// It will encode the tree a line at a time and stream out of /// the given std::ostream class FasmParserBaseVisitor : public FasmParserVisitor { public: static constexpr size_t kHeaderSize = 5; /// The constructor requires a std::ostream to stream encoded lines. /// This is to avoid storing an entire copy of the parse tree in a /// different form. FasmParserBaseVisitor(std::ostream& out) : out(out) {} /// Stream out FASM lines. virtual Any visitFasmFile( FasmParser::FasmFileContext* context) override { for (auto& line : context->fasmLine()) { std::string str = visit(line).as(); if (!str.empty()) { out << str; if (hex_mode) out << std::endl; } } return {}; } /// This is called for each FASM line. /// Tag: comment (#) /// Tag: line (l) virtual Any visitFasmLine( FasmParser::FasmLineContext* context) override { std::ostringstream data; data << GET(setFasmFeature) << GET(annotations); if (context->COMMENT_CAP()) { std::string c = context->COMMENT_CAP()->getText(); c.erase(0, 1); /// Remove the leading # data << Str(TAG('#', comment), c); } if (!data.str().empty()) { return withHeader(TAG('l', line), data.str()); } else { return std::string(); /// Don't emit empty lines. } } /// The set feature portion of a line (before annotations and comment.) /// Tag: feature (f) /// Tag: set feature (s) virtual Any visitSetFasmFeature( FasmParser::SetFasmFeatureContext* context) override { std::ostringstream data; data << Str(TAG('f', feature), context->FEATURE()->getText()) << GET(featureAddress) << GET(value); return withHeader(TAG('s', set_feature), data.str()); } /// The bracketed address, where the second number is optional. /// Tag: address (:) virtual Any visitFeatureAddress( FasmParser::FeatureAddressContext* context) override { std::ostringstream data; data << Num(std::stoul(context->INT(0)->getText())); if (context->INT(1)) { data << Num(std::stoul(context->INT(1)->getText())); } return withHeader(TAG(':', address), data.str()); } /// A Verilog style number. It can be "plain" (no leading size and /// base), or hex (h), binary (b), decimal (d), or octal (o). Tag: bit /// width (') virtual Any visitVerilogValue( FasmParser::VerilogValueContext* context) override { std::ostringstream data; if (context->verilogDigits()) { if (context->INT()) { data << Num( TAG('\'', width), std::stoi(context->INT()->getText())); } data << visit(context->verilogDigits()) .as(); } return data.str(); } /// A "plain" decimal value. /// Tag: plain (p) virtual Any visitPlainDecimal( FasmParser::PlainDecimalContext* context) override { std::ostringstream data; try { data << Num(TAG('p', plain), std::stoi(context->INT()->getText())); } catch (...) { throw ParseException{ .line = context->start->getLine(), .position = context->start->getCharPositionInLine(), .message = "Could not decode decimal number."}; } return data.str(); } /// A Verilog hex value. /// Tag: hex (h) virtual Any visitHexValue( FasmParser::HexValueContext* context) override { std::ostringstream data; std::string value = context->HEXADECIMAL_VALUE()->getText(); auto it = value.begin(); it += 2; /// skip 'h /// Build up Nums 4 bits at a time, skipping '_'. int bits = lead_bits(count_without(it, value.end(), '_') * 4); uint32_t word = 0; while (it != value.end()) { if (*it != '_') { word = (word << 4) | from_hex_digit(*it); bits += 4; if (bits == Num::kWidth) { data << Num(word); word = 0; bits = 0; } } it++; } assert(!word); return withHeader(TAG('h', hex), data.str()); } /// A Verilog binary value. /// Tag: binary (b) virtual Any visitBinaryValue( FasmParser::BinaryValueContext* context) override { std::ostringstream data; std::string value = context->BINARY_VALUE()->getText(); auto it = value.begin(); it += 2; /// skip 'b /// Build up Nums a bit at a time, skipping '_'. int bits = lead_bits(count_without(it, value.end(), '_')); uint32_t word = 0; while (it != value.end()) { if (*it != '_') { word = (word << 1) | (*it == '1' ? 1 : 0); bits += 1; if (bits == Num::kWidth) { data << Num(word); word = 0; bits = 0; } } it++; } assert(!word); return withHeader(TAG('b', binary), data.str()); } /// A Verilog decimal value. /// Tags: decimal (d) virtual Any visitDecimalValue( FasmParser::DecimalValueContext* context) override { long long unsigned integer = 0; std::string value = context->DECIMAL_VALUE()->getText(); auto it = value.begin(); it += 2; /// skip 'd /// Build up a Num, skipping '_'. while (it != value.end()) { if (*it != '_') { int digit_value = *it - '0'; // Check for overflow if (integer > (std::numeric_limits< long long unsigned>::max() - digit_value) / 10) { throw ParseException{ .line = context->start->getLine(), .position = context->start ->getCharPositionInLine(), .message = "Could not decode decimal " "number."}; } integer = (integer * 10) + digit_value; } it++; } std::ostringstream data; data << Num(integer); return withHeader(TAG('d', decimal), data.str()); } /// A Verilog octal value. /// Tags: octal (o) virtual Any visitOctalValue( FasmParser::OctalValueContext* context) override { std::ostringstream data; std::string value = context->OCTAL_VALUE()->getText(); auto it = value.begin(); it += 2; /// skip 'b /// Build up a Num 3 bits at a time. /// Note that since the word size is not evenly divisible by 3, /// intermediate values can be greater than the word size. /// This is why the 'word' below is 64 bits wide. int bits = lead_bits(count_without(it, value.end(), '_') * 3); uint64_t word = 0; /// could temporarily overflow uint32_t while (it != value.end()) { if (*it != '_') { word = (word << 3) | (*it - '0'); bits += 3; if (bits >= Num::kWidth) { data << Num(word >> (bits - Num::kWidth)); word >>= Num::kWidth; bits -= Num::kWidth; } } it++; } assert(!word); return withHeader(TAG('o', octal), data.str()); } /// A collection of annotations. { ... } /// Tags: annotations ({) virtual Any visitAnnotations( FasmParser::AnnotationsContext* context) override { std::ostringstream data; for (auto& a : context->annotation()) { data << visit(a).as(); } return withHeader(TAG('{', annotations), data.str()); } /// An annotation: x = "y" /// Tags: annotation (a), annotation name (.), annotation value (=) virtual Any visitAnnotation( FasmParser::AnnotationContext* context) override { std::ostringstream data; data << Str(TAG('.', annotation_name), context->ANNOTATION_NAME()->getText()); if (context->ANNOTATION_VALUE()) { std::string value = context->ANNOTATION_VALUE()->getText(); value.erase(0, 1); /// Convert "value" -> value value.pop_back(); data << Str(TAG('=', annotation_value), value); } return withHeader(TAG('a', annotation), data.str()); } private: std::ostream& out; }; // Prevent use of the GET macro outside FasmParseBaseVisitor #undef GET class FasmErrorListener : public BaseErrorListener { public: virtual void syntaxError(Recognizer* recognizer, Token* token, size_t line, size_t position, const std::string& msg, std::exception_ptr e) override { throw ParseException{.line = line, .position = position, .message = std::string(msg)}; } }; /// Common portion of 'from_string' and 'from_file'. /// Consumes an input stream and produces an output stream. static void parse_fasm(std::istream& in, std::ostream& out) { ANTLRInputStream stream(in); FasmLexer lexer(&stream); FasmErrorListener errorListener; lexer.removeErrorListeners(); lexer.addErrorListener(&errorListener); CommonTokenStream tokens(&lexer); FasmParser parser(&tokens); parser.removeErrorListeners(); parser.addErrorListener(&errorListener); auto* tree = parser.fasmFile(); FasmParserBaseVisitor(out).visit(tree); } /// Parse the given input string, returning output. /// Use hex mode (see above) if hex is true. /// Use a callback to avoid copying the result. void from_string(const char* in, bool hex, void (*ret)(const char* str, size_t), void (*err)(size_t, size_t, const char*)) { hex_mode = hex; std::istringstream input(in); std::ostringstream output; try { parse_fasm(input, output); output.put(0); std::string result = output.str(); ret(result.c_str(), result.size()); } catch (ParseException e) { // Parse failure will throw this exception. err(e.line, e.position, e.message.c_str()); } } /// Parse the given input file, returning output. /// Use hex mode (see above) if hex is true. /// Use a callback to avoid copying the result. void from_file(const char* path, bool hex, void (*ret)(const char* str, size_t), void (*err)(size_t, size_t, const char*)) { hex_mode = hex; std::fstream input(std::string(path), input.in); std::ostringstream output; if (input.is_open()) { try { parse_fasm(input, output); output.put(0); std::string result = output.str(); ret(result.c_str(), result.size()); } catch (ParseException e) { // Parse failure will throw this exception. err(e.line, e.position, e.message.c_str()); } } else { err(0, 0, "Couldn't open file"); } }