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.
Files changed (32) hide show
  1. {nmhit-0.1.3 → nmhit-0.1.4}/CMakeLists.txt +1 -1
  2. {nmhit-0.1.3 → nmhit-0.1.4}/PKG-INFO +1 -1
  3. {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/Node.h +10 -0
  4. {nmhit-0.1.3 → nmhit-0.1.4}/pyproject.toml +1 -1
  5. {nmhit-0.1.3 → nmhit-0.1.4}/src/Node.cpp +40 -19
  6. {nmhit-0.1.3 → nmhit-0.1.4}/tests/test_hit.cpp +66 -0
  7. {nmhit-0.1.3 → nmhit-0.1.4}/.clang-format +0 -0
  8. {nmhit-0.1.3 → nmhit-0.1.4}/.github/workflows/ci.yml +0 -0
  9. {nmhit-0.1.3 → nmhit-0.1.4}/.github/workflows/release.yml +0 -0
  10. {nmhit-0.1.3 → nmhit-0.1.4}/.gitignore +0 -0
  11. {nmhit-0.1.3 → nmhit-0.1.4}/.pre-commit-config.yaml +0 -0
  12. {nmhit-0.1.3 → nmhit-0.1.4}/CONTRIBUTING.md +0 -0
  13. {nmhit-0.1.3 → nmhit-0.1.4}/README.md +0 -0
  14. {nmhit-0.1.3 → nmhit-0.1.4}/cmake/nmhit.pc.in +0 -0
  15. {nmhit-0.1.3 → nmhit-0.1.4}/cmake/nmhitConfig.cmake.in +0 -0
  16. {nmhit-0.1.3 → nmhit-0.1.4}/generated/Lexer.cpp +0 -0
  17. {nmhit-0.1.3 → nmhit-0.1.4}/generated/Lexer.h +0 -0
  18. {nmhit-0.1.3 → nmhit-0.1.4}/generated/Parser.cpp +0 -0
  19. {nmhit-0.1.3 → nmhit-0.1.4}/generated/Parser.h +0 -0
  20. {nmhit-0.1.3 → nmhit-0.1.4}/generated/location.hh +0 -0
  21. {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/BraceExpr.h +0 -0
  22. {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/TypeRegistry.h +0 -0
  23. {nmhit-0.1.3 → nmhit-0.1.4}/include/nmhit/nmhit.h +0 -0
  24. {nmhit-0.1.3 → nmhit-0.1.4}/python/nmhit/__init__.py +0 -0
  25. {nmhit-0.1.3 → nmhit-0.1.4}/python/nmhit/py.typed +0 -0
  26. {nmhit-0.1.3 → nmhit-0.1.4}/python/src/_nmhit.cpp +0 -0
  27. {nmhit-0.1.3 → nmhit-0.1.4}/python/tests/test_nmhit.py +0 -0
  28. {nmhit-0.1.3 → nmhit-0.1.4}/src/BraceExpr.cpp +0 -0
  29. {nmhit-0.1.3 → nmhit-0.1.4}/src/Lexer.l +0 -0
  30. {nmhit-0.1.3 → nmhit-0.1.4}/src/ParseDriver.h +0 -0
  31. {nmhit-0.1.3 → nmhit-0.1.4}/src/Parser.y +0 -0
  32. {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 LANGUAGES CXX)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: nmhit
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: Python bindings for the nmhit NEML2-flavored HIT parser
5
5
  License: MIT
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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.3" # Keep in sync with VERSION in CMakeLists.txt.
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: sections with the same name are collapsed into the
709
- /// first occurrence by moving all children of the duplicate into the first.
710
- /// This handles path-split fields that share a common ancestor section
711
- /// (e.g. "Models/a/foo = 1" and "Models/b/bar = 2" both create a "Models"
712
- /// wrapper; the two wrappers are merged so lookup through a single "Models"
713
- /// finds both).
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: merge same-name sections ─────────────────────────────────────
730
+ // ── Pass 1: fold path-wrapper sections into same-name siblings ───────────
726
731
  {
727
- std::unordered_map<std::string, std::size_t> section_first;
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 = section_first.find(sec->path());
737
- if (it != section_first.end())
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
- section_first[sec->path()] = i;
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