nmhit 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nmhit-0.1.3 → nmhit-0.1.4}/CMakeLists.txt +1 -1
- {nmhit-0.1.3 → nmhit-0.1.4}/PKG-INFO +1 -1
- {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/Node.h +10 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/pyproject.toml +1 -1
- {nmhit-0.1.3 → nmhit-0.1.4}/src/Node.cpp +40 -19
- {nmhit-0.1.3 → nmhit-0.1.4}/tests/test_hit.cpp +66 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/.clang-format +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/.github/workflows/ci.yml +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/.github/workflows/release.yml +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/.gitignore +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/.pre-commit-config.yaml +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/CONTRIBUTING.md +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/README.md +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/cmake/nmhit.pc.in +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/cmake/nmhitConfig.cmake.in +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/generated/Lexer.cpp +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/generated/Lexer.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/generated/Parser.cpp +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/generated/Parser.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/generated/location.hh +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/BraceExpr.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/TypeRegistry.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/nmhit.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/python/nmhit/__init__.py +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/python/nmhit/py.typed +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/python/src/_nmhit.cpp +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/python/tests/test_nmhit.py +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/src/BraceExpr.cpp +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/src/Lexer.l +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/src/ParseDriver.h +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/src/Parser.y +0 -0
- {nmhit-0.1.3 → nmhit-0.1.4}/tests/CMakeLists.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
cmake_minimum_required(VERSION 3.20)
|
|
2
2
|
# Keep this version in sync with [project] version in pyproject.toml.
|
|
3
|
-
project(neml2-hit VERSION 0.1.
|
|
3
|
+
project(neml2-hit VERSION 0.1.4 LANGUAGES CXX)
|
|
4
4
|
|
|
5
5
|
set(CMAKE_CXX_STANDARD 17)
|
|
6
6
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
@@ -244,8 +244,18 @@ public:
|
|
|
244
244
|
std::string render(int indent = 0, const std::string & indent_text = " ") const override;
|
|
245
245
|
std::unique_ptr<Node> clone() const override;
|
|
246
246
|
|
|
247
|
+
/// True if this section was synthesized by the parser to wrap a path-split
|
|
248
|
+
/// key (e.g. the "Models" in `Models/a/foo = 1`), rather than being written
|
|
249
|
+
/// explicitly by the user as `[Models] ... []`. Wrapper sections are merged
|
|
250
|
+
/// with same-name siblings at parse time; explicit sections are not.
|
|
251
|
+
bool is_path_wrapper() const { return _is_wrapper; }
|
|
252
|
+
|
|
253
|
+
// ── internal setter (used by the parser) ──────────────────────────────────
|
|
254
|
+
void _set_path_wrapper(bool v) { _is_wrapper = v; }
|
|
255
|
+
|
|
247
256
|
private:
|
|
248
257
|
std::string _name;
|
|
258
|
+
bool _is_wrapper = false;
|
|
249
259
|
};
|
|
250
260
|
|
|
251
261
|
/// A key-value field, e.g. dim = 3 or values = '1 2 3'.
|
|
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nmhit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4" # Keep in sync with VERSION in CMakeLists.txt.
|
|
8
8
|
description = "Python bindings for the nmhit NEML2-flavored HIT parser"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -501,6 +501,7 @@ Section::clone() const
|
|
|
501
501
|
{
|
|
502
502
|
auto s = std::make_unique<Section>(_name);
|
|
503
503
|
s->_set_location(filename(), line(), column());
|
|
504
|
+
s->_is_wrapper = _is_wrapper;
|
|
504
505
|
for (const auto & c : children())
|
|
505
506
|
s->add_child(c->clone());
|
|
506
507
|
return s;
|
|
@@ -705,12 +706,16 @@ ParseDriver::parse()
|
|
|
705
706
|
|
|
706
707
|
/// Apply last-override-wins semantics to items, merging same-name sections.
|
|
707
708
|
///
|
|
708
|
-
/// Pass 1 — section merge:
|
|
709
|
-
///
|
|
710
|
-
///
|
|
711
|
-
/// (
|
|
712
|
-
/// wrapper
|
|
713
|
-
///
|
|
709
|
+
/// Pass 1 — path-wrapper section merge: a "path wrapper" is a section the
|
|
710
|
+
/// parser synthesized to represent intermediate segments of a path-split
|
|
711
|
+
/// key (e.g. the "Models" in `Models/a/foo = 1`), flagged via
|
|
712
|
+
/// Section::is_path_wrapper(). When a wrapper meets a preceding same-name
|
|
713
|
+
/// sibling (wrapper or explicit), its children are folded into that
|
|
714
|
+
/// sibling and the duplicate node is dropped. The surviving section
|
|
715
|
+
/// becomes "explicit" if either input was explicit.
|
|
716
|
+
///
|
|
717
|
+
/// Two explicit `[Name] ... []` sections written by the user are NEVER
|
|
718
|
+
/// merged — they round-trip as two distinct top-level blocks.
|
|
714
719
|
///
|
|
715
720
|
/// Pass 2 — field duplicate / override check (per level):
|
|
716
721
|
/// - If the later occurrence was built with ':=', the earlier one is removed.
|
|
@@ -722,9 +727,11 @@ ParseDriver::parse()
|
|
|
722
727
|
void
|
|
723
728
|
ParseDriver::apply_overrides(std::vector<std::unique_ptr<nmhit::Node>> & items)
|
|
724
729
|
{
|
|
725
|
-
// ── Pass 1:
|
|
730
|
+
// ── Pass 1: fold path-wrapper sections into same-name siblings ───────────
|
|
726
731
|
{
|
|
727
|
-
|
|
732
|
+
// For each section name, the index of the most recent surviving
|
|
733
|
+
// same-name section that a subsequent wrapper may fold into.
|
|
734
|
+
std::unordered_map<std::string, std::size_t> latest;
|
|
728
735
|
std::vector<bool> merged(items.size(), false);
|
|
729
736
|
|
|
730
737
|
for (std::size_t i = 0; i < items.size(); ++i)
|
|
@@ -733,19 +740,26 @@ ParseDriver::apply_overrides(std::vector<std::unique_ptr<nmhit::Node>> & items)
|
|
|
733
740
|
if (!sec)
|
|
734
741
|
continue;
|
|
735
742
|
|
|
736
|
-
auto it =
|
|
737
|
-
if (it !=
|
|
738
|
-
{
|
|
739
|
-
auto * first = dynamic_cast<nmhit::Section *>(items[it->second].get());
|
|
740
|
-
auto raw_kids = sec->children();
|
|
741
|
-
for (auto * k : raw_kids)
|
|
742
|
-
first->add_child(sec->remove_child(k));
|
|
743
|
-
merged[i] = true;
|
|
744
|
-
}
|
|
745
|
-
else
|
|
743
|
+
auto it = latest.find(sec->path());
|
|
744
|
+
if (it != latest.end())
|
|
746
745
|
{
|
|
747
|
-
|
|
746
|
+
auto * prev = dynamic_cast<nmhit::Section *>(items[it->second].get());
|
|
747
|
+
const bool either_wrapper = sec->is_path_wrapper() || prev->is_path_wrapper();
|
|
748
|
+
if (either_wrapper)
|
|
749
|
+
{
|
|
750
|
+
auto raw_kids = sec->children();
|
|
751
|
+
for (auto * k : raw_kids)
|
|
752
|
+
prev->add_child(sec->remove_child(k));
|
|
753
|
+
// Surviving section is explicit if either input was explicit.
|
|
754
|
+
if (!sec->is_path_wrapper())
|
|
755
|
+
prev->_set_path_wrapper(false);
|
|
756
|
+
merged[i] = true;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// Both explicit: leave as separate siblings, fall through to update
|
|
760
|
+
// `latest` so a later wrapper folds into the most recent block.
|
|
748
761
|
}
|
|
762
|
+
latest[sec->path()] = i;
|
|
749
763
|
}
|
|
750
764
|
|
|
751
765
|
if (std::any_of(merged.begin(), merged.end(), [](bool b) { return b; }))
|
|
@@ -842,6 +856,12 @@ split_path(const std::string & path)
|
|
|
842
856
|
}
|
|
843
857
|
|
|
844
858
|
/// Wrap a node in a chain of Section nodes for a/b/c-style paths.
|
|
859
|
+
///
|
|
860
|
+
/// Every wrapper created here is marked via `_set_path_wrapper(true)` so that
|
|
861
|
+
/// apply_overrides() can distinguish synthesized wrappers (which must merge
|
|
862
|
+
/// with same-name siblings to make `Models/a/foo = 1` + `Models/b/bar = 2`
|
|
863
|
+
/// resolve through a single "Models") from explicit `[Models] ... []` blocks
|
|
864
|
+
/// written by the user (which are preserved verbatim for round-tripping).
|
|
845
865
|
static std::unique_ptr<nmhit::Node>
|
|
846
866
|
wrap_in_sections(std::vector<std::string> segs,
|
|
847
867
|
std::unique_ptr<nmhit::Node> inner_node,
|
|
@@ -856,6 +876,7 @@ wrap_in_sections(std::vector<std::string> segs,
|
|
|
856
876
|
{
|
|
857
877
|
auto wrapper = std::make_unique<nmhit::Section>(segs[i]);
|
|
858
878
|
wrapper->_set_location(fname, line, col);
|
|
879
|
+
wrapper->_set_path_wrapper(true);
|
|
859
880
|
wrapper->add_child(std::move(inner_node));
|
|
860
881
|
inner_node = std::move(wrapper);
|
|
861
882
|
}
|
|
@@ -730,6 +730,72 @@ main()
|
|
|
730
730
|
std::string::npos);
|
|
731
731
|
});
|
|
732
732
|
|
|
733
|
+
// ── 20. Explicit duplicate sections preserved (round-trip) ────────────────
|
|
734
|
+
|
|
735
|
+
run("two_explicit_same_name_sections_preserved", []() {
|
|
736
|
+
// Two user-written [Models] blocks must remain as two distinct siblings
|
|
737
|
+
// — not collapsed into one — so the document round-trips intact.
|
|
738
|
+
auto root = p("[Models]\n a = 1\n[]\n[Models]\n b = 2\n[]");
|
|
739
|
+
auto secs = root->children(nmhit::NodeType::Section);
|
|
740
|
+
EXPECT(secs.size() == 2);
|
|
741
|
+
EXPECT(secs[0]->path() == "Models");
|
|
742
|
+
EXPECT(secs[1]->path() == "Models");
|
|
743
|
+
// The first block owns `a`, the second owns `b`; they are not merged.
|
|
744
|
+
EXPECT(secs[0]->children(nmhit::NodeType::Field).size() == 1);
|
|
745
|
+
EXPECT(secs[1]->children(nmhit::NodeType::Field).size() == 1);
|
|
746
|
+
EXPECT(secs[0]->children(nmhit::NodeType::Field)[0]->path() == "a");
|
|
747
|
+
EXPECT(secs[1]->children(nmhit::NodeType::Field)[0]->path() == "b");
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
run("two_explicit_sections_round_trip", []() {
|
|
751
|
+
// Rendering then re-parsing keeps both blocks distinct.
|
|
752
|
+
auto root = p("[Models]\n a = 1\n[]\n[Models]\n b = 2\n[]");
|
|
753
|
+
auto rendered = root->render();
|
|
754
|
+
auto root2 = p(rendered);
|
|
755
|
+
EXPECT(root2->children(nmhit::NodeType::Section).size() == 2);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
run("explicit_then_path_split_merges", []() {
|
|
759
|
+
// A path-split fragment whose root name matches an existing explicit
|
|
760
|
+
// section folds into that explicit block — backward-compatible behavior.
|
|
761
|
+
auto root = p("[Models]\n a = 1\n[]\nModels/b = 2");
|
|
762
|
+
auto secs = root->children(nmhit::NodeType::Section);
|
|
763
|
+
EXPECT(secs.size() == 1);
|
|
764
|
+
EXPECT(root->param<int>("Models/a") == 1);
|
|
765
|
+
EXPECT(root->param<int>("Models/b") == 2);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
run("path_split_then_explicit_merges", []() {
|
|
769
|
+
// Reverse order: a leading path-split fragment is folded into the
|
|
770
|
+
// following explicit block of the same name.
|
|
771
|
+
auto root = p("Models/a = 1\n[Models]\n b = 2\n[]");
|
|
772
|
+
auto secs = root->children(nmhit::NodeType::Section);
|
|
773
|
+
EXPECT(secs.size() == 1);
|
|
774
|
+
EXPECT(root->param<int>("Models/a") == 1);
|
|
775
|
+
EXPECT(root->param<int>("Models/b") == 2);
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
run("two_explicit_blocks_then_path_split_targets_latest", []() {
|
|
779
|
+
// [Models]a[] [Models]b[] Models/c=3 → the wrapper folds into the
|
|
780
|
+
// most recent same-name explicit block (the second [Models]).
|
|
781
|
+
auto root = p("[Models]\n a = 1\n[]\n[Models]\n b = 2\n[]\nModels/c = 3");
|
|
782
|
+
auto secs = root->children(nmhit::NodeType::Section);
|
|
783
|
+
EXPECT(secs.size() == 2);
|
|
784
|
+
EXPECT(secs[0]->children(nmhit::NodeType::Field).size() == 1); // just a
|
|
785
|
+
EXPECT(secs[1]->children(nmhit::NodeType::Field).size() == 2); // b + c
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
run("explicit_duplicate_field_in_first_block_no_error", []() {
|
|
789
|
+
// Same field name in two SEPARATE explicit blocks of the same name is
|
|
790
|
+
// not a duplicate-field error: the blocks are independent containers.
|
|
791
|
+
auto root = p("[Models]\n k = 1\n[]\n[Models]\n k = 2\n[]");
|
|
792
|
+
auto secs = root->children(nmhit::NodeType::Section);
|
|
793
|
+
EXPECT(secs.size() == 2);
|
|
794
|
+
// find() walks the first match; HIT semantics for duplicate explicit
|
|
795
|
+
// sections leave value resolution document-order dependent.
|
|
796
|
+
EXPECT(root->param<int>("Models/k") == 1);
|
|
797
|
+
});
|
|
798
|
+
|
|
733
799
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
734
800
|
|
|
735
801
|
std::cerr << "\n=== Results: " << g_passed << " passed, " << g_failed << " failed ===\n";
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|