1
0
mirror of https://github.com/bdring/Grbl_Esp32.git synced 2025-09-02 10:53:01 +02:00

Implemented parser for configuration. A small x64 test can be found in ParserTest.cpp; haven't ran it on ESP32 yet.

This commit is contained in:
Stefan de Bruijn
2020-12-18 22:42:01 +01:00
parent db19135f0c
commit 68ff966c39
8 changed files with 602 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
#pragma once
namespace Configuration {
class ParseException {
int line_;
int column_;
const char* description_;
public:
ParseException() = default;
ParseException(const ParseException&) = default;
ParseException(const char* start, const char* current, const char* description) : description_(description) {
line_ = 1;
column_ = 1;
while (start != current) {
if (*start == '\n') {
++line_;
column_ = 1;
}
++start;
}
}
inline int LineNumber() const { return line_; }
inline int ColumnNumber() const { return column_; }
inline const char* What() const { return description_; }
};
}

View File

@@ -0,0 +1,112 @@
#include "Parser.h"
#include "ParseException.h"
namespace Configuration {
Parser::Parser(const char* start, const char* end) : Tokenizer(start, end), current_() {
Tokenize();
current_ = token_;
if (current_.kind_ != TokenKind::Eof) {
Tokenize();
}
}
void Parser::ParseError(const char* description) const {
// Attempt to use the correct position in the parser:
if (current_.keyEnd_) {
throw ParseException(start_, current_.keyEnd_, description);
}
else {
Tokenizer::ParseError(description);
}
}
/// <summary>
/// MoveNext: moves to the next entry in the current section. By default we're in the
/// root section.
/// </summary>
bool Parser::MoveNext() {
// While the indent of the token is > current indent, we have to skip it. This is a
// sub-section, that we're apparently not interested in.
while (token_.indent_ > current_.indent_) {
Tokenize();
}
// If the indent is the same, we're in the same section. Update current, move to next
// token.
if (token_.indent_ == current_.indent_) {
current_ = token_;
Tokenize();
} else {
// Apparently token_.indent < current_.indent_, which means we have no more items
// in our tokenizer that are relevant.
//
// Note that we want to preserve current_.indent_!
current_.kind_ = TokenKind::Eof;
}
return current_.kind_ != TokenKind::Eof;
}
void Parser::Enter() {
indentStack_.push(current_.indent_);
// If we can enter, token_.indent_ > current_.indent_:
if (token_.indent_ > current_.indent_) {
current_ = token_;
Tokenize();
} else {
current_ = TokenData();
current_.indent_ = INT_MAX;
}
}
void Parser::Leave() {
// While the indent of the tokenizer is >= current, we can ignore the contents:
while (token_.indent_ >= current_.indent_ && token_.kind_ != TokenKind::Eof) {
Tokenize();
}
// At this point, we just know the indent is smaller. We don't know if we're in
// the *right* section tho.
auto last = indentStack_.top();
indentStack_.pop();
if (last == token_.indent_) {
// Yes, the token continues where we left off:
current_ = token_;
// Tokenize(); --> No need, this is handled by MoveNext!
} else {
current_ = TokenData();
current_.indent_ = last;
}
}
std::string Parser::StringValue() const {
if (current_.kind_ != TokenKind::String) {
ParseError("Expected a string value (e.g. 'foo')");
}
return std::string(current_.sValueStart_, current_.sValueEnd_);
}
bool Parser::BoolValue() const {
if (current_.kind_ != TokenKind::Boolean) {
ParseError("Expected a boolean value (e.g. true or value)");
}
return current_.bValue_;
}
int Parser::IntValue() const {
if (current_.kind_ != TokenKind::Boolean) {
ParseError("Expected an integer value (e.g. 123456)");
}
return current_.iValue_;
}
double Parser::FloatValue() const {
if (current_.kind_ != TokenKind::Boolean) {
ParseError("Expected a float value (e.g. 123.456)");
}
return current_.fValue_;
}
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include "Tokenizer.h"
#include <stack>
#include <cstring>
namespace Configuration {
class Parser : public Tokenizer {
// Parsing here might be a bit confusing, because the state of the tokenizer is one step
// ahead of the parser. That way we always have 2 tokens at our disposal, so we know when
// we're entering or exiting a section.
std::stack<int> indentStack_;
TokenData current_;
void ParseError(const char* description) const;
public:
Parser(const char* start, const char* end);
/// <summary>
/// MoveNext: moves to the next entry in the current section. By default we're in the
/// root section.
/// </summary>
bool MoveNext();
inline bool IsEndSection() { return current_.kind_ == TokenKind::Eof || token_.indent_ < current_.indent_; }
// !!! Important !!! We cannot use a scoped variable for enter & leave, because 'leave' can throw,
// and it could be called using stack unrolling. Destructors by definition have to be 'nothrow',
// so forget it: it just Won't Work. In other words, if we leave the 'leave' call up to the
// destructor, we end up what we in C++ call 'undefined behavior'.
void Enter();
void Leave();
inline bool Is(const char* expected) const {
return !strncmp(expected, current_.keyStart_, size_t(current_.keyEnd_ - current_.keyStart_));
}
inline std::string Key() const { return std::string(current_.keyStart_, current_.keyEnd_); }
std::string StringValue() const;
bool BoolValue() const;
int IntValue() const;
double FloatValue() const;
};
}

View File

@@ -0,0 +1,93 @@
#ifndef ESP32
# include "Parser.h"
# include <string>
# include <fstream>
# include <iostream>
# include <streambuf>
namespace Configuration {
void ParseSpecificAxis(Parser& parser, int axis, int ganged) {
const char* allAxis = "xyzabc";
std::cout << "Parsing axis " << allAxis[axis] << ", ganged #" << ganged << std::endl;
for (; !parser.IsEndSection(); parser.MoveNext()) {
if (parser.Is("limit")) {
auto limitPin = parser.StringValue();
std::cout << "Limit pin: " << limitPin << std::endl;
}
// and so on...
}
}
void ParseAxis(Parser& parser) {
std::cout << "Parsing axis." << std::endl;
const char* allAxis = "xyzabc";
for (; !parser.IsEndSection(); parser.MoveNext()) {
auto str = parser.Key();
if (str.size() == 1) {
auto idx = strchr(allAxis, str[0]);
if (idx != nullptr) {
parser.Enter();
ParseSpecificAxis(parser, idx - allAxis, 0);
parser.Leave();
}
} else if (str.size() == 2) {
auto idx = strchr(allAxis, str[0]);
if (idx != nullptr && str[1] >= '1' && str[1] <= '9') {
int ganged = str[1] - '1';
parser.Enter();
ParseSpecificAxis(parser, idx - allAxis, ganged);
parser.Leave();
}
}
}
}
void ParseBus(Parser& parser) {
std::cout << "Parsing bus." << std::endl;
// TODO
}
void ParseRoot(Parser& parser) {
std::cout << "Parsing root." << std::endl;
for (; !parser.IsEndSection(); parser.MoveNext()) {
if (parser.Is("axis")) {
parser.Enter();
ParseAxis(parser);
parser.Leave();
} else if (parser.Is("bus")) {
parser.Enter();
ParseBus(parser);
parser.Leave();
}
}
}
int main() {
std::ifstream t("..\\Fiddling\\Test.yaml");
std::string str((std::istreambuf_iterator<char>(t)), std::istreambuf_iterator<char>());
const auto begin = str.c_str();
const auto end = begin + str.size();
try {
Parser parser(begin, end);
ParseRoot(parser);
} catch (ParseException ex) {
std::cout << "Parse error: " << ex.What() << " @ " << ex.LineNumber() << ":" << ex.ColumnNumber() << std::endl;
} catch (...) { std::cout << "Uncaught exception" << std::endl; }
std::string s;
std::getline(std::cin, s);
return 0;
}
}
#endif

View File

@@ -0,0 +1,28 @@
axis:
x:
limit: gpio.33:low
stepstick:
step: gpio.4
direction: gpio.16
y2:
limit: gpio.32:low
stepstick:
step: gpio.18
direction: gpio.18
z:
limit: gpio.34:low
dynamixel:
channel: 3
#yeah this rocks
bus:
rs485:
baud: 19200
rx: gpio.12
tx: gpio.13
spindle:
vfd:
channel: 1
unattached: true
vfd:
channel: 2

View File

@@ -0,0 +1,13 @@
#pragma once
namespace Configuration {
enum struct TokenKind {
Section,
Boolean,
String,
IntegerValue,
FloatingPoint,
Eof,
};
}

View File

@@ -0,0 +1,193 @@
#include "Tokenizer.h"
#include "ParseException.h"
#include <cstdlib>
namespace Configuration {
inline void Tokenizer::ParseError(const char* description) const { throw ParseException(start_, current_, description); }
void Tokenizer::Tokenize() {
// We parse 1 line at a time. Each time we get here, we can assume that the cursor
// is at the start of the line.
parseAgain:
int indent = 0;
while (!Eof() && IsSpace()) {
Inc();
++indent;
}
if (!Eof()) {
switch (Current()) {
case '\t':
// TODO FIXME: We can do tabs or spaces, not both. However, we *could* let the user decide.
ParseError("Indentation through tabs is not allowed. Convert all tabs to spaces please.");
break;
case '#': // Comment till end of line
Inc();
while (!Eof() && !IsEndLine()) {
Inc();
}
return;
case '\r':
case '\n':
Inc();
if (!Eof() && Current() == '\n') {
Inc();
} // \r\n
goto parseAgain;
default:
if (!IsAlpha()) {
ParseError("Expected identifier.");
}
token_.keyStart_ = current_;
Inc();
while (!Eof() && (IsAlpha() || IsDigit() || Current() == '_')) {
Inc();
}
token_.keyEnd_ = current_;
// Skip whitespaces:
while (!Eof() && IsWhiteSpace()) {
Inc();
}
if (Current() != ':') {
ParseError("After a key or section name, we expect a colon character ':'.");
}
Inc();
// Skip whitespaces after the colon:
while (!Eof() && IsWhiteSpace()) {
Inc();
}
token_.indent_ = indent;
if (IsEndLine()) {
token_.kind_ = TokenKind::Section;
Inc();
if (!Eof() && Current() == '\n') {
Inc();
} // \r\n
} else {
switch (Current()) {
case '"':
case '\'': {
auto delimiter = Current();
token_.kind_ = TokenKind::String;
Inc();
token_.sValueStart_ = current_;
while (!Eof() && Current() != delimiter && !IsEndLine()) {
Inc();
}
token_.sValueEnd_ = current_;
if (Current() != delimiter) {
ParseError("Could not find matching delimiter in string value.");
}
Inc();
} break;
default:
if (EqualsCaseInsensitive("true")) {
token_.kind_ = TokenKind::Boolean;
token_.bValue_ = true;
for (auto i = 0; i < 4; ++i) {
Inc();
}
} else if (EqualsCaseInsensitive("false")) {
token_.kind_ = TokenKind::Boolean;
token_.bValue_ = false;
for (auto i = 0; i < 5; ++i) {
Inc();
}
} else if (IsDigit() || Current() == '-') {
auto doubleOrIntStart = current_;
int intValue = 0;
bool negative = false;
if (Current() == '-') {
Inc();
negative = true;
}
while (IsDigit()) {
intValue = intValue * 10 + int(Current() - '0');
Inc();
}
if (Current() == 'e' || Current() == 'E' || Current() == '.' || // markers
(current_ - doubleOrIntStart) >= 9) { // liberal interpretation of 'out of int range'
char* floatEnd;
token_.fValue_ = strtod(doubleOrIntStart, &floatEnd);
token_.kind_ = TokenKind::FloatingPoint;
current_ = floatEnd;
} else {
if (negative) {
intValue = -intValue;
}
token_.iValue_ = intValue;
token_.kind_ = TokenKind::IntegerValue;
}
} else {
// If it's not 'true', not 'false', and not a digit, we have a string delimited by a whitespace
token_.kind_ = TokenKind::String;
token_.sValueStart_ = current_;
while (!Eof() && !IsWhiteSpace() && !IsEndLine()) {
Inc();
}
token_.sValueEnd_ = current_;
}
break;
}
// Skip more whitespaces
while (!Eof() && IsSpace()) {
Inc();
}
// A comment after a key-value pair is allowed.
if (Current() == '#') {
Inc();
while (!Eof() && !IsEndLine()) {
Inc();
}
}
// Should be EOL or EOF at this point.
if (!IsEndLine() && !Eof()) {
ParseError("Expected line end after key/value pair.");
}
}
}
} else {
token_.kind_ = TokenKind::Eof;
token_.indent_ = 0;
}
}
inline Tokenizer::Tokenizer(const char* start, const char* end) : start_(start), current_(start), end_(end), token_() {
// If start is a yaml document start ('---' [newline]), skip that first.
if (EqualsCaseInsensitive("---")) {
for (int i = 0; i < 3; ++i) {
Inc();
}
while (IsWhiteSpace()) {
Inc();
}
while (Current() == '\r' || Current() == '\n') {
Inc();
}
start_ = current_;
}
}
}

View File

@@ -0,0 +1,85 @@
#pragma once
#include "TokenKind.h"
#include <string>
namespace Configuration {
class Tokenizer {
const char* current_;
const char* end_;
inline void Inc() {
if (current_ != end_) {
++current_;
}
}
inline char Current() const { return Eof() ? '\0' : (*current_); }
inline bool IsAlpha() {
char c = Current();
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
}
inline bool IsSpace() { return Current() == ' '; }
inline bool IsWhiteSpace() {
char c = Current();
return c == ' ' || c == '\t' || c == '\f';
}
inline bool IsEndLine() { return Current() == '\n'; }
inline bool IsDigit() {
char c = Current();
return (c >= '0' && c <= '9');
}
static inline char ToLower(char c) { return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c; }
inline bool EqualsCaseInsensitive(const char* input) {
const char* tmp = current_;
while (ToLower(*input) == ToLower(Current()) && *input != '\0') {
Inc();
}
bool isSame = *input == '\0'; // Everything till the end of the input string is the same
current_ = tmp; // Restore situation
return isSame;
}
protected:
const char* start_;
// Results:
struct TokenData {
TokenData() :
keyStart_(nullptr), keyEnd_(nullptr), indent_(0), kind_(TokenKind::Eof), sValueStart_(nullptr), sValueEnd_(nullptr) {}
const char* keyStart_;
const char* keyEnd_;
int indent_;
TokenKind kind_;
union {
struct {
const char* sValueStart_;
const char* sValueEnd_;
};
int iValue_;
double fValue_;
bool bValue_;
};
} token_;
void ParseError(const char* description) const;
inline bool Eof() const { return current_ == end_; }
void Tokenize();
public:
Tokenizer(const char* start, const char* end);
};
}