feat(domain-and-input-skeleton): step 2 — abaqus-scoped-syntax-parser
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
#include <fesa/io/abaqus/parser.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
enum class Scope { global, part, assembly, instance };
|
||||
|
||||
struct KeywordLine final {
|
||||
std::string keyword;
|
||||
std::map<std::string, std::string, std::less<>> parameters;
|
||||
SourceLocation source;
|
||||
};
|
||||
|
||||
std::string_view trim(const std::string_view value) {
|
||||
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||
const std::size_t first = value.find_first_not_of(whitespace);
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
const std::size_t last = value.find_last_not_of(whitespace);
|
||||
return value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
std::string uppercase_ascii(std::string value) {
|
||||
std::ranges::transform(value, value.begin(), [](const char character) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
return static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> split_fields(const std::string_view value) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t first = 0;
|
||||
while (true) {
|
||||
const std::size_t comma = value.find(',', first);
|
||||
const std::string_view field = comma == std::string_view::npos
|
||||
? value.substr(first)
|
||||
: value.substr(first, comma - first);
|
||||
fields.emplace_back(trim(field));
|
||||
if (comma == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
first = comma + 1;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
ParseDeckResult failure(
|
||||
const DiagnosticStage stage,
|
||||
std::string code,
|
||||
std::string message,
|
||||
std::optional<SourceLocation> source) {
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
diagnostics.push_back({
|
||||
stage,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::move(source),
|
||||
});
|
||||
return {std::nullopt, std::move(diagnostics)};
|
||||
}
|
||||
|
||||
ParseDeckResult syntax_failure(
|
||||
std::string code,
|
||||
std::string message,
|
||||
SourceLocation source) {
|
||||
return failure(
|
||||
DiagnosticStage::syntax,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::move(source));
|
||||
}
|
||||
|
||||
bool is_supported_record(const std::string_view keyword) {
|
||||
constexpr std::array supported{
|
||||
std::string_view{"BEAM GENERAL SECTION"},
|
||||
std::string_view{"BOUNDARY"},
|
||||
std::string_view{"CLOAD"},
|
||||
std::string_view{"ELASTIC"},
|
||||
std::string_view{"ELEMENT"},
|
||||
std::string_view{"ELSET"},
|
||||
std::string_view{"END STEP"},
|
||||
std::string_view{"MATERIAL"},
|
||||
std::string_view{"NODE"},
|
||||
std::string_view{"NSET"},
|
||||
std::string_view{"STATIC"},
|
||||
std::string_view{"STEP"},
|
||||
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||
};
|
||||
return std::ranges::find(supported, keyword) != supported.end();
|
||||
}
|
||||
|
||||
std::optional<KeywordLine> parse_keyword_line(
|
||||
const std::string_view line,
|
||||
const SourceLocation& source,
|
||||
ParseDeckResult& error) {
|
||||
const std::vector<std::string> fields = split_fields(line.substr(1));
|
||||
if (fields.empty() || fields[0].empty()) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.empty_keyword",
|
||||
"Abaqus keyword name is empty.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
KeywordLine parsed{
|
||||
uppercase_ascii(fields[0]),
|
||||
{},
|
||||
source,
|
||||
};
|
||||
for (std::size_t index = 1; index < fields.size(); ++index) {
|
||||
const std::string_view field = fields[index];
|
||||
if (field.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string_view key_text =
|
||||
trim(field.substr(0, equals));
|
||||
const std::string_view value_text =
|
||||
equals == std::string_view::npos
|
||||
? std::string_view{}
|
||||
: trim(field.substr(equals + 1));
|
||||
if (key_text.empty()) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.invalid_parameter",
|
||||
"Abaqus keyword parameter name is empty.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const std::string key = uppercase_ascii(std::string{key_text});
|
||||
if (!parsed.parameters.emplace(key, value_text).second) {
|
||||
error = syntax_failure(
|
||||
"abaqus.syntax.duplicate_parameter",
|
||||
"Abaqus keyword parameter '" + key +
|
||||
"' is specified more than once.",
|
||||
source);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
const std::string* parameter(
|
||||
const KeywordLine& keyword,
|
||||
const std::string_view name) {
|
||||
const auto found = keyword.parameters.find(name);
|
||||
return found == keyword.parameters.end() ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
ParseDeckResult missing_parameter(
|
||||
const KeywordLine& keyword,
|
||||
const std::string_view parameter_name) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.missing_parameter",
|
||||
"Abaqus *" + keyword.keyword + " requires parameter " +
|
||||
std::string{parameter_name} + ".",
|
||||
keyword.source);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
if (!input) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.open_failed",
|
||||
"Unable to open Abaqus input file.",
|
||||
SourceLocation{path, 0U, 0U});
|
||||
}
|
||||
|
||||
ParsedDeck deck;
|
||||
Scope scope = Scope::global;
|
||||
std::optional<ParsedPart> current_part;
|
||||
std::optional<ParsedAssembly> current_assembly;
|
||||
std::optional<ParsedInstance> current_instance;
|
||||
DeckRecord* current_record = nullptr;
|
||||
|
||||
std::string line;
|
||||
std::size_t line_number = 0;
|
||||
while (std::getline(input, line)) {
|
||||
++line_number;
|
||||
if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
|
||||
line.erase(0, 3);
|
||||
}
|
||||
|
||||
const std::size_t first_nonspace =
|
||||
line.find_first_not_of(" \t\f\v\r");
|
||||
if (first_nonspace == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string_view content = trim(line);
|
||||
if (content.starts_with("**")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!content.starts_with('*')) {
|
||||
const std::vector<std::string> fields = split_fields(content);
|
||||
if (scope == Scope::instance) {
|
||||
current_instance->transform_data.push_back(fields);
|
||||
} else if (current_record != nullptr) {
|
||||
current_record->data.push_back(fields);
|
||||
} else {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.data_without_keyword",
|
||||
"Abaqus data line has no preceding keyword record.",
|
||||
SourceLocation{path, line_number, first_nonspace + 1U});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const SourceLocation source{
|
||||
path,
|
||||
line_number,
|
||||
first_nonspace + 1U,
|
||||
};
|
||||
ParseDeckResult keyword_error;
|
||||
std::optional<KeywordLine> parsed =
|
||||
parse_keyword_line(content, source, keyword_error);
|
||||
if (!parsed.has_value()) {
|
||||
return keyword_error;
|
||||
}
|
||||
KeywordLine& keyword = *parsed;
|
||||
current_record = nullptr;
|
||||
|
||||
if (keyword.keyword == "PART") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_part_scope",
|
||||
"*PART is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
current_part = ParsedPart{*name, {}, source};
|
||||
scope = Scope::part;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END PART") {
|
||||
if (scope != Scope::part || !current_part.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_part",
|
||||
"*END PART does not match an open *PART.",
|
||||
source);
|
||||
}
|
||||
deck.parts.push_back(std::move(*current_part));
|
||||
current_part.reset();
|
||||
scope = Scope::global;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "ASSEMBLY") {
|
||||
if (scope != Scope::global) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_assembly_scope",
|
||||
"*ASSEMBLY is only valid in global input scope.",
|
||||
source);
|
||||
}
|
||||
if (deck.assembly.has_value() ||
|
||||
current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.multiple_assemblies",
|
||||
"ParsedDeck can preserve only one *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
current_assembly = ParsedAssembly{*name, {}, {}, source};
|
||||
scope = Scope::assembly;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END ASSEMBLY") {
|
||||
if (scope != Scope::assembly ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_assembly",
|
||||
"*END ASSEMBLY does not match an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
deck.assembly = std::move(*current_assembly);
|
||||
current_assembly.reset();
|
||||
scope = Scope::global;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "INSTANCE") {
|
||||
if (scope != Scope::assembly ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.invalid_instance_scope",
|
||||
"*INSTANCE is only valid in an open *ASSEMBLY.",
|
||||
source);
|
||||
}
|
||||
const std::string* name = parameter(keyword, "NAME");
|
||||
if (name == nullptr || name->empty()) {
|
||||
return missing_parameter(keyword, "NAME");
|
||||
}
|
||||
const std::string* part_name = parameter(keyword, "PART");
|
||||
if (part_name == nullptr || part_name->empty()) {
|
||||
return missing_parameter(keyword, "PART");
|
||||
}
|
||||
current_instance =
|
||||
ParsedInstance{*name, *part_name, {}, source};
|
||||
scope = Scope::instance;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.keyword == "END INSTANCE") {
|
||||
if (scope != Scope::instance ||
|
||||
!current_instance.has_value() ||
|
||||
!current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unexpected_end_instance",
|
||||
"*END INSTANCE does not match an open *INSTANCE.",
|
||||
source);
|
||||
}
|
||||
current_assembly->instances.push_back(
|
||||
std::move(*current_instance));
|
||||
current_instance.reset();
|
||||
scope = Scope::assembly;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_supported_record(keyword.keyword)) {
|
||||
return syntax_failure(
|
||||
"abaqus.unsupported_keyword",
|
||||
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||
source);
|
||||
}
|
||||
|
||||
DeckRecord next_record{
|
||||
std::move(keyword.keyword),
|
||||
std::move(keyword.parameters),
|
||||
{},
|
||||
source,
|
||||
};
|
||||
switch (scope) {
|
||||
case Scope::global:
|
||||
deck.global_records.push_back(std::move(next_record));
|
||||
current_record = &deck.global_records.back();
|
||||
break;
|
||||
case Scope::part:
|
||||
current_part->records.push_back(std::move(next_record));
|
||||
current_record = ¤t_part->records.back();
|
||||
break;
|
||||
case Scope::assembly:
|
||||
current_assembly->records.push_back(
|
||||
std::move(next_record));
|
||||
current_record = ¤t_assembly->records.back();
|
||||
break;
|
||||
case Scope::instance:
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.instance_local_keyword",
|
||||
"Keyword records inside *INSTANCE are unsupported.",
|
||||
source);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.bad()) {
|
||||
return failure(
|
||||
DiagnosticStage::io,
|
||||
"abaqus.io.read_failed",
|
||||
"Failed while reading Abaqus input file.",
|
||||
SourceLocation{path, line_number, 0U});
|
||||
}
|
||||
|
||||
if (current_instance.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_instance",
|
||||
"Abaqus *INSTANCE is not closed by *END INSTANCE.",
|
||||
current_instance->source);
|
||||
}
|
||||
if (current_part.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_part",
|
||||
"Abaqus *PART is not closed by *END PART.",
|
||||
current_part->source);
|
||||
}
|
||||
if (current_assembly.has_value()) {
|
||||
return syntax_failure(
|
||||
"abaqus.syntax.unclosed_assembly",
|
||||
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||
current_assembly->source);
|
||||
}
|
||||
|
||||
return {std::move(deck), {}};
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
Reference in New Issue
Block a user