feat(domain-and-input-skeleton): step 2 — abaqus-scoped-syntax-parser

This commit is contained in:
KOKO\Mimi
2026-07-30 18:01:05 +09:00
parent ceaf92f2a9
commit abbc408aec
9 changed files with 807 additions and 1 deletions
+411
View File
@@ -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 = &current_part->records.back();
break;
case Scope::assembly:
current_assembly->records.push_back(
std::move(next_record));
current_record = &current_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