fix: enforce phase1 parser subset

This commit is contained in:
NINI
2026-05-04 13:03:20 +09:00
parent 99445d43bb
commit c0f668754d
6 changed files with 329 additions and 23 deletions
+4 -4
View File
@@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
## Current Objective
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-03 parser/domain subset revalidation. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-04 validation and singular diagnostic coverage. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
## Required Reading For New Agents
1. `AGENTS.md`
@@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase
## Active Phase Files
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
- Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step15.md` is the independent evaluator closeout.
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step15.md` is the independent evaluator closeout.
- Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements.
- Historical phase directory: `phases/1-linear-static-mitc4`
- Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase.
@@ -74,7 +74,7 @@ Each gate should be satisfied before moving to the next implementation band unle
|---|---|---|---|
| G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md |
| G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output |
| G2 - Parser and domain | pending rebaseline | Must be revalidated through steps 3 and 4 against the current parser subset and stored-reference compatibility policy. | Future parser and validation tests |
| G2 - Parser and domain | partial | Parser subset revalidated in step 3; validation and singular diagnostics remain for step 4. | Parser acceptance/rejection tests and validation output |
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, and model diagnostic context were revalidated in step 2; DofManager/results/assembly remain for steps 5, 6, and 12. | P1R-02 validation output |
| G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests |
| G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output |
@@ -84,7 +84,7 @@ All milestones are intended to become one or more self-contained sprint contract
| ID | Status | Owner | Objective | Depends On | Acceptance Focus |
|---|---|---|---|---|---|
| P1R-03 | pending | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
| P1R-03 | completed | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
| P1R-04 | pending | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests |
| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests |
| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
+29 -1
View File
@@ -13,10 +13,38 @@ Every new agent session must read this file together with `PLAN.md` before plann
- Do not remove history unless the user explicitly asks for archival cleanup.
## Current Status
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 2 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, and model diagnostic context have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 3 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, and the Phase 1 parser/domain subset have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
## Completed Work
### 2026-05-04 - P1R-03 parser/domain subset completed
Author: Codex
Changed files:
- `include/fesa/fesa.hpp`
- `tests/test_main.cpp`
- `docs/ABAQUS_INPUT_SUBSET.md`
- `phases/1-linear-static-mitc4-rebaseline/index.json`
- `PLAN.md`
- `PROGRESS.md`
Summary:
- Added parser acceptance coverage for repeated explicit sets, generated `*Nset` and `*Elset`, `D` exponent numeric input, keyword-line continuation, and every Phase 1 structural input keyword.
- Expanded unsupported-feature coverage for `S4R`, `Part`, `Assembly`, `Instance`, `*Include`, `*Density`, and `NLGEOM=YES`.
- Added negative coverage for unsupported parameters and flags on otherwise supported keywords, unsupported shell-section data modes, malformed numeric fields, and invalid DOF fields with file, line, and keyword diagnostics.
- Tightened the parser so supported keywords reject unknown parameters/flags, fixed-width data rows reject extra non-empty fields, generated sets require exactly `first,last,increment`, and keyword continuation lines are assembled before keyword parsing.
- Clarified `docs/ABAQUS_INPUT_SUBSET.md` so the strict parameter and fixed-width data-row rules are explicit.
Verification:
- First ran `python scripts/validate_workspace.py` after adding unsupported-parameter tests; it failed as expected because the parser still accepted supported keywords with unknown controls.
- After implementing strict keyword-control rejection, validation passed.
- Added keyword-continuation tests and first observed the expected failure; after implementing continuation handling, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully.
- CTest result: 1 test executable passed.
Follow-up:
- Continue with P1R-04 validation and singular diagnostic coverage.
- Keep `Part/Assembly/Instance`, `S4R`, `*Density`, and `NLGEOM=YES` outside the Phase 1 parser until an ADR and parser contract intentionally broaden the subset.
### 2026-05-04 - P1R-02 core harness guardrails completed
Author: Codex
+2
View File
@@ -42,6 +42,8 @@ FESA parser rules:
- Keyword continuation with a trailing comma should be supported for keyword lines.
- Data continuation should be supported only where this document explicitly allows it.
- Abbreviated Abaqus keywords are not supported in Phase 1. Require exact keyword names after case normalization.
- Supported keywords may use only the parameters and flags listed in this document. Unknown parameters and flags are errors, even when the keyword itself is supported.
- Fixed-width data rows for `*Node`, `*Element`, `*Elastic`, `*Shell Section`, `*Boundary`, and `*Cload` must not contain extra non-empty fields beyond the supported form.
- Include files through `INPUT=` are not supported in Phase 1.
- Part/Assembly/Instance syntax is not supported in Phase 1 unless added by ADR.
- Normalized derivative inputs such as `references/quad_02_phase1.inp` may be used for Phase 1 parser and solver tests when the original stored Abaqus file contains unsupported Abaqus/CAE scaffolding.
+128 -17
View File
@@ -308,6 +308,9 @@ inline KeywordLine parseKeywordLine(const std::string& line) {
keyword.name = lower(trim(pieces.front()));
for (std::size_t i = 1; i < pieces.size(); ++i) {
const std::string piece = trim(pieces[i]);
if (piece.empty()) {
continue;
}
const auto eq = piece.find('=');
if (eq == std::string::npos) {
keyword.flags.insert(lower(piece));
@@ -337,9 +340,38 @@ class AbaqusInputParser {
std::string current_material_key;
KeywordLine current_shell_section;
LocalIndex line_number = 0;
LocalIndex current_keyword_line = 0;
auto add_error = [&](const std::string& code, const std::string& message) {
result.diagnostics.push_back({Severity::Error, code, message, {file_name, line_number, current.name}});
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
result.diagnostics.push_back({Severity::Error, code, message, {file_name, source_line, current.name}});
};
auto is_allowed = [](const std::string& value, std::initializer_list<const char*> allowed_values) {
return std::any_of(allowed_values.begin(), allowed_values.end(), [&](const char* allowed) {
return value == allowed;
});
};
auto reject_unsupported_controls = [&](std::initializer_list<const char*> allowed_parameters,
std::initializer_list<const char*> allowed_flags) {
for (const auto& [parameter, value] : current.parameters) {
(void)value;
if (!is_allowed(parameter, allowed_parameters)) {
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
result.diagnostics.push_back({Severity::Error,
"FESA-PARSE-UNSUPPORTED-PARAMETER",
"Unsupported *" + current.name + " parameter: " + parameter,
{file_name, source_line, current.name}});
}
}
for (const std::string& flag : current.flags) {
if (!is_allowed(flag, allowed_flags)) {
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
result.diagnostics.push_back({Severity::Error,
"FESA-PARSE-UNSUPPORTED-PARAMETER",
"Unsupported *" + current.name + " flag: " + flag,
{file_name, source_line, current.name}});
}
}
};
while (std::getline(stream, line)) {
@@ -349,16 +381,52 @@ class AbaqusInputParser {
continue;
}
if (!line.empty() && line.front() == '*') {
current = parseKeywordLine(line);
if (current.name == "node" || current.name == "element" || current.name == "nset" ||
current.name == "elset" || current.name == "elastic" || current.name == "shell section" ||
current.name == "boundary" || current.name == "cload" || current.name == "static") {
if (current.name == "shell section") {
current_shell_section = current;
current_keyword_line = line_number;
std::string keyword_line = line;
while (!keyword_line.empty() && keyword_line.back() == ',') {
std::string continuation;
if (!std::getline(stream, continuation)) {
break;
}
++line_number;
continuation = trim(continuation);
if (continuation.empty() || continuation.rfind("**", 0) == 0) {
continue;
}
keyword_line += continuation;
}
current = parseKeywordLine(keyword_line);
if (current.name == "node") {
reject_unsupported_controls({}, {});
continue;
}
if (current.name == "element") {
reject_unsupported_controls({"type", "elset"}, {});
continue;
}
if (current.name == "nset") {
reject_unsupported_controls({"nset"}, {"generate"});
continue;
}
if (current.name == "elset") {
reject_unsupported_controls({"elset"}, {"generate"});
continue;
}
if (current.name == "elastic") {
reject_unsupported_controls({}, {});
continue;
}
if (current.name == "shell section") {
reject_unsupported_controls({"elset", "material"}, {});
current_shell_section = current;
continue;
}
if (current.name == "boundary" || current.name == "cload" || current.name == "static") {
reject_unsupported_controls({}, {});
continue;
}
if (current.name == "material") {
reject_unsupported_controls({"name"}, {});
auto name_it = current.parameters.find("name");
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
@@ -376,6 +444,7 @@ class AbaqusInputParser {
continue;
}
if (current.name == "step") {
reject_unsupported_controls({"name", "nlgeom"}, {});
auto nlgeom = current.parameters.find("nlgeom");
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
@@ -389,6 +458,7 @@ class AbaqusInputParser {
continue;
}
if (current.name == "end step") {
reject_unsupported_controls({}, {});
continue;
}
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
@@ -433,8 +503,16 @@ class AbaqusInputParser {
}
private:
static std::size_t effectiveFieldCount(const std::vector<std::string>& fields) {
std::size_t count = fields.size();
while (count > 0 && trim(fields[count - 1]).empty()) {
--count;
}
return count;
}
static void parseNode(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
if (fields.size() < 4) {
if (effectiveFieldCount(fields) != 4) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}});
return;
}
@@ -464,7 +542,7 @@ class AbaqusInputParser {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-ELEMENT", "Unsupported element type: " + type_it->second, {file_name, line, "element"}});
return;
}
if (fields.size() < 5) {
if (effectiveFieldCount(fields) != 5) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT", "S4 element requires id,n1,n2,n3,n4", {file_name, line, "element"}});
return;
}
@@ -524,7 +602,8 @@ class AbaqusInputParser {
static void parseSetData(const std::vector<std::string>& fields, bool generate, std::vector<GlobalId>& output,
std::vector<Diagnostic>& diagnostics, const std::string& file_name, LocalIndex line, const std::string& keyword) {
if (generate) {
if (fields.size() < 3) {
const std::size_t field_count = effectiveFieldCount(fields);
if (field_count != 3) {
diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Generated set requires first,last,increment", {file_name, line, keyword}});
return;
}
@@ -540,7 +619,9 @@ class AbaqusInputParser {
}
return;
}
for (const std::string& field : fields) {
const std::size_t field_count = effectiveFieldCount(fields);
for (std::size_t i = 0; i < field_count; ++i) {
const std::string& field = fields[i];
if (trim(field).empty()) {
continue;
}
@@ -558,10 +639,16 @@ class AbaqusInputParser {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}});
return;
}
if (fields.size() < 2) {
const std::size_t field_count = effectiveFieldCount(fields);
if (field_count < 2) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}});
return;
}
if (field_count > 2) {
result.diagnostics.push_back(
{Severity::Error, "FESA-PARSE-ELASTIC-UNSUPPORTED", "Only isotropic E,nu elastic data is supported", {file_name, line, "elastic"}});
return;
}
auto e = parseReal(fields[0]);
auto nu = parseReal(fields[1]);
if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) {
@@ -579,10 +666,18 @@ class AbaqusInputParser {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-PARAM", "*Shell Section requires ELSET and MATERIAL", {file_name, line, "shell section"}});
return;
}
if (fields.empty()) {
const std::size_t field_count = effectiveFieldCount(fields);
if (field_count == 0) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION", "*Shell Section requires thickness", {file_name, line, "shell section"}});
return;
}
if (field_count > 1) {
result.diagnostics.push_back({Severity::Error,
"FESA-PARSE-SHELL-SECTION-UNSUPPORTED",
"Only homogeneous shell thickness data is supported",
{file_name, line, "shell section"}});
return;
}
auto thickness = parseReal(fields[0]);
if (!thickness || *thickness <= 0.0) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-THICKNESS", "Shell thickness must be positive", {file_name, line, "shell section"}});
@@ -592,13 +687,21 @@ class AbaqusInputParser {
}
static void parseBoundary(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
if (fields.size() < 2) {
const std::size_t field_count = effectiveFieldCount(fields);
if (field_count < 2) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}});
return;
}
if (field_count > 4) {
result.diagnostics.push_back({Severity::Error,
"FESA-PARSE-BOUNDARY-UNSUPPORTED",
"Only direct zero-valued boundary data is supported",
{file_name, line, "boundary"}});
return;
}
auto first = parseInt64(fields[1]);
auto last = fields.size() >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
auto magnitude = fields.size() >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
auto last = field_count >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
auto magnitude = field_count >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) || !dofFromAbaqus(static_cast<int>(*last)) || *first > *last) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-DOF", "Invalid boundary DOF range", {file_name, line, "boundary"}});
return;
@@ -611,10 +714,18 @@ class AbaqusInputParser {
}
static void parseLoad(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
if (fields.size() < 3) {
const std::size_t field_count = effectiveFieldCount(fields);
if (field_count < 3) {
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}});
return;
}
if (field_count > 3) {
result.diagnostics.push_back({Severity::Error,
"FESA-PARSE-CLOAD-UNSUPPORTED",
"Only direct concentrated load data is supported",
{file_name, line, "cload"}});
return;
}
auto dof = parseInt64(fields[1]);
auto magnitude = parseReal(fields[2]);
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
@@ -5,7 +5,7 @@
{ "step": 0, "name": "rebaseline-audit", "status": "completed" },
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
{ "step": 2, "name": "core-harness-guardrails", "status": "completed" },
{ "step": 3, "name": "parser-domain-subset", "status": "pending" },
{ "step": 3, "name": "parser-domain-subset", "status": "completed" },
{ "step": 4, "name": "validation-singular-diagnostics", "status": "pending" },
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
+165
View File
@@ -105,6 +105,25 @@ fesa::Domain parsedPhase1Domain() {
return parsed.domain;
}
const fesa::Diagnostic* findDiagnostic(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
for (const auto& diagnostic : diagnostics) {
if (diagnostic.code == code) {
return &diagnostic;
}
}
return nullptr;
}
std::size_t diagnosticCount(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
std::size_t count = 0;
for (const auto& diagnostic : diagnostics) {
if (diagnostic.code == code) {
++count;
}
}
return count;
}
} // namespace
FESA_TEST(core_types_and_dof_mapping_are_stable) {
@@ -144,24 +163,170 @@ FESA_TEST(parser_accepts_phase1_subset) {
FESA_CHECK(parsed.domain.loads.size() == 2);
}
FESA_TEST(parser_accepts_repeated_and_generated_sets) {
const std::string text = R"inp(
*Node
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element, type=S4, elset=EALL
1, 1, 2, 3, 4
*Nset, nset=FIXED
1, 2, 2
3
*Nset, nset=FIXED, generate
3, 4, 1
*Nset, nset=LOADS, generate
2, 4, 2
*Elset, elset=EALL
1, 1
*Elset, elset=CHECK, generate
1, 5, 2
*Material, name=MAT
*Elastic
2.0D5, 0.25
*Shell Section, elset=EALL, material=MAT
0.2
*Boundary
FIXED, 1, 6
*Cload
LOADS, 3, -2.5
*Step, name=Step-A, nlgeom=NO
*Static
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 2, 3, 4}));
FESA_CHECK(parsed.domain.node_sets.at("loads").node_ids == std::vector<fesa::GlobalId>({2, 4}));
FESA_CHECK(parsed.domain.element_sets.at("eall").element_ids == std::vector<fesa::GlobalId>({1}));
FESA_CHECK(parsed.domain.element_sets.at("check").element_ids == std::vector<fesa::GlobalId>({1, 3, 5}));
FESA_CHECK(parsed.domain.materials.at("mat").elastic_modulus == 2.0e5);
FESA_CHECK(parsed.domain.steps.front().name == "Step-A");
}
FESA_TEST(parser_accepts_keyword_line_continuation) {
const std::string text = R"inp(
*Node
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element,
type=S4, elset=EALL
1, 1, 2, 3, 4
*Nset,
nset=FIXED, generate
1, 4, 3
*Elset,
elset=EALL
1
*Material,
name=MAT
*Elastic
2.0e5, 0.25
*Shell Section,
elset=EALL, material=MAT
0.2
*Boundary
FIXED, 1, 6
*Cload
2, 3, -1.0
*Step,
name=Step-1
*Static
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.elements.at(1).source_elset == "EALL");
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 4}));
FESA_CHECK(parsed.domain.materials.count("mat") == 1);
FESA_CHECK(parsed.domain.shell_sections.front().material == "MAT");
}
FESA_TEST(parser_rejects_unsupported_features) {
const std::string text = R"inp(
*Part, name=P1
*Assembly, name=A1
*Instance, name=I1, part=P1
*Include, input=other.inp
*Node
1, 0, 0, 0
*Element, type=S4R
1, 1, 2, 3, 4
*Density
7850
*Step, nlgeom=YES
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(!parsed.ok());
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD") >= 4);
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"));
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-NLGEOM"));
}
FESA_TEST(parser_rejects_unsupported_keyword_parameters_and_modes) {
const std::string text = R"inp(
*Node, input=nodes.csv
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element, type=S4, elset=EALL, orientation=OR1
1, 1, 2, 3, 4
*Nset, nset=FIXED, unsorted
1, 4
*Material, name=MAT, description=bad
*Elastic, type=ENGINEERING CONSTANTS
2.0e5, 0.25
*Shell Section, elset=EALL, material=MAT, offset=SPOS
0.2, 5
*Boundary, op=NEW
FIXED, 1, 6
*Cload, amplitude=A1
2, 3, -1.0
*Step, name=Step-1, inc=100
*Static, stabilize
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text, "unsupported_modes.inp");
FESA_CHECK(!parsed.ok());
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-PARAMETER") >= 8);
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-SHELL-SECTION-UNSUPPORTED"));
}
FESA_TEST(parser_diagnostics_include_file_line_and_keyword) {
const std::string text = R"inp(
*Node
1, bad, 0, 0
*Boundary
FIXED, 7, 7
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text, "malformed.inp");
FESA_CHECK(!parsed.ok());
const fesa::Diagnostic* node = findDiagnostic(parsed.diagnostics, "FESA-PARSE-NODE-NUMERIC");
FESA_CHECK(node != nullptr);
FESA_CHECK(node->source.file == "malformed.inp");
FESA_CHECK(node->source.line == 3);
FESA_CHECK(node->source.keyword == "node");
const fesa::Diagnostic* boundary = findDiagnostic(parsed.diagnostics, "FESA-PARSE-BOUNDARY-DOF");
FESA_CHECK(boundary != nullptr);
FESA_CHECK(boundary->source.file == "malformed.inp");
FESA_CHECK(boundary->source.line == 5);
FESA_CHECK(boundary->source.keyword == "boundary");
}
FESA_TEST(quad01_reference_input_remains_unsupported) {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_01.inp");