From e0ff44b34afb84fd74e99f78fd710b7e5cc2f515 Mon Sep 17 00:00:00 2001 From: Jochen Lillich Date: Thu, 25 Dec 2025 22:01:49 +0000 Subject: [PATCH 1/2] feat: Memory management The library was missing memory management. This change introduces the Controlled Type `Document` that'll trigger a cleanup when it goes out of scope. See [README](README.md) and the example programs for how this works. --- README.md | 41 +++++++++++++++++++++--- examples/basic_parsing.adb | 25 +++++++++------ examples/error_handling.adb | 34 ++++++++++++-------- src/tinyaml-documents.adb | 31 ++++++++++++++++++ src/tinyaml-documents.ads | 39 +++++++++++++++++++++++ src/tinyaml-nodes-navigation.adb | 40 ++++++++++++----------- src/tinyaml-nodes.adb | 54 ++++++++++++++++++++++++++++++++ src/tinyaml-nodes.ads | 4 +++ src/tinyaml-parser.adb | 7 +++++ src/tinyaml-parser.ads | 9 ++++++ src/tinyaml-schemas-map.adb | 16 ++++++++++ src/tinyaml-schemas-map.ads | 3 ++ src/tinyaml-schemas-seq.adb | 7 +++++ src/tinyaml-schemas-seq.ads | 3 ++ src/tinyaml-schemas.adb | 35 +++++++++++++++++++++ src/tinyaml-schemas.ads | 4 +++ tests/test_parser.adb | 42 +++++++++++++++++++++++++ 17 files changed, 348 insertions(+), 46 deletions(-) create mode 100644 src/tinyaml-documents.adb create mode 100644 src/tinyaml-documents.ads diff --git a/README.md b/README.md index 0aa8f96..1c8aa15 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ alr with tinyaml ```ada with Ada.Text_IO; +with Tinyaml.Documents; use Tinyaml.Documents; with Tinyaml.Parser; with Tinyaml.Nodes; use Tinyaml.Nodes; with Tinyaml.Nodes.Prelude; use Tinyaml.Nodes.Prelude; @@ -29,10 +30,10 @@ procedure Example is " host: localhost" & ASCII.LF & " port: 5432"; - Doc : Node_Access; + -- Document automatically frees memory when it goes out of scope + Doc : constant Document := Tinyaml.Parser.Parse_Document (Config); begin - Doc := Tinyaml.Parser.Parse (Config); - Ada.Text_IO.Put_Line (Get_String (Doc, "database.host")); -- "localhost" + Ada.Text_IO.Put_Line (Get_String (Root (Doc), "database.host")); -- "localhost" end Example; ``` @@ -96,7 +97,15 @@ These YAML features are intentionally rejected to keep configurations simple and ### Parsing ```ada -Doc := Tinyaml.Parser.Parse (Input : String) return Node_Access; +-- Recommended: automatic memory management with Document wrapper +Doc : Document := Tinyaml.Parser.Parse_Document (Input); +Root_Node : Node_Access := Root (Doc); -- Access the root node +-- Memory is freed when Doc goes out of scope + +-- Alternative: manual memory management +Doc : Node_Access := Tinyaml.Parser.Parse (Input); +-- ... use Doc ... +Free_Node (Doc); -- Must call explicitly to avoid memory leak ``` ### Node Types and Navigation @@ -156,6 +165,30 @@ Schema.Seq ("tags", To_Seq_Item (Str_Schema'(null record))); Schema.Map ("database", Db_Schema); -- Nested map ``` +### Memory Management + +TinyAML provides two approaches for memory management: + +**Automatic (recommended):** Use `Document` which is a controlled type that automatically frees the entire node tree when it goes out of scope: + +```ada +declare + Doc : constant Document := Tinyaml.Parser.Parse_Document (Config); +begin + Put_Line (Get_String (Root (Doc), "database.host")); +end; -- All nodes freed automatically here +``` + +**Manual:** Use `Parse` and call `Free_Node` explicitly: + +```ada +Doc : Node_Access := Tinyaml.Parser.Parse (Config); +-- ... use Doc ... +Free_Node (Doc); -- Recursively frees all nodes; sets Doc to null +``` + +For schemas, `Free_Schema` is available for cleanup when schemas are created dynamically, though schemas are typically static and live for the program's lifetime. + ## Building from Source Requires Ada 2022 compiler (FSF GNAT 13+). diff --git a/examples/basic_parsing.adb b/examples/basic_parsing.adb index d3379d0..6702d1e 100644 --- a/examples/basic_parsing.adb +++ b/examples/basic_parsing.adb @@ -2,14 +2,17 @@ -- -- Demonstrates parsing YAML without schema validation. -- All values are strings until explicitly converted. +-- Uses Document wrapper for automatic memory management. with Ada.Text_IO; +with Tinyaml.Documents; with Tinyaml.Parser; with Tinyaml.Nodes; with Tinyaml.Nodes.Prelude; procedure Basic_Parsing is use Ada.Text_IO; + use Tinyaml.Documents; use Tinyaml.Nodes; use Tinyaml.Nodes.Prelude; @@ -22,22 +25,22 @@ procedure Basic_Parsing is " - logging" & ASCII.LF & " - metrics"; - Doc : Node_Access; + -- Document automatically frees memory when it goes out of scope + Doc : constant Document := Tinyaml.Parser.Parse_Document (Config); begin Put_Line ("Parsing configuration..."); New_Line; - Doc := Tinyaml.Parser.Parse (Config); - - -- Access nested values using path notation - Put_Line ("Database host: " & Get_String (Doc, "database.host")); - Put_Line ("Database port: " & Get_String (Doc, "database.port")); - Put_Line ("Database name: " & Get_String (Doc, "database.name")); + -- Access nested values using path notation (Root returns the root node) + Put_Line ("Database host: " & Get_String (Root (Doc), "database.host")); + Put_Line ("Database port: " & Get_String (Root (Doc), "database.port")); + Put_Line ("Database name: " & Get_String (Root (Doc), "database.name")); New_Line; -- Check for optional fields using Navigate (returns null if missing) declare - Timeout : constant Node_Access := Navigate (Doc, "database.timeout"); + Timeout : constant Node_Access := + Navigate (Root (Doc), "database.timeout"); begin if Timeout /= null then Put_Line ("Database timeout: " & Value (Timeout)); @@ -49,7 +52,7 @@ begin -- Alternative: use Contains on Map_Node declare - Database : constant Node_Access := Navigate (Doc, "database"); + Database : constant Node_Access := Navigate (Root (Doc), "database"); begin if Is_Map (Database.all) then if Map_Node (Database.all).Contains ("port") then @@ -65,7 +68,7 @@ begin -- Access sequence items declare - Features : constant Node_Access := Navigate (Doc, "features"); + Features : constant Node_Access := Navigate (Root (Doc), "features"); Seq : Sequence_Node; begin Put_Line ("Features:"); @@ -76,4 +79,6 @@ begin end loop; end if; end; + + -- When Doc goes out of scope here, the entire tree is automatically freed end Basic_Parsing; diff --git a/examples/error_handling.adb b/examples/error_handling.adb index e5078ff..c7ccb13 100644 --- a/examples/error_handling.adb +++ b/examples/error_handling.adb @@ -1,16 +1,19 @@ -- Error Handling Example -- -- Demonstrates handling parse errors and validation failures. +-- Shows both Document-based API and manual memory management patterns. with Ada.Text_IO; with Ada.Exceptions; with Tinyaml; +with Tinyaml.Documents; with Tinyaml.Parser; with Tinyaml.Nodes; with Tinyaml.Nodes.Prelude; procedure Error_Handling is use Ada.Text_IO; + use Tinyaml.Documents; use Tinyaml.Nodes; use Tinyaml.Nodes.Prelude; @@ -21,15 +24,16 @@ procedure Error_Handling is -- Invalid YAML: flow style (not supported) Flow_Style : constant String := "{key: value}"; - - Doc : Node_Access; - pragma Unreferenced (Doc); begin - -- Example 1: Duplicate key error + -- Example 1: Duplicate key error (exception handling with Document) Put_Line ("Parsing YAML with duplicate keys..."); begin - Doc := Tinyaml.Parser.Parse (Bad_Yaml); - Put_Line (" Unexpected success!"); + declare + Doc : constant Document := Tinyaml.Parser.Parse_Document (Bad_Yaml); + pragma Unreferenced (Doc); + begin + Put_Line (" Unexpected success!"); + end; exception when E : Tinyaml.Parse_Error => Put_Line (" Parse error: " & Ada.Exceptions.Exception_Message (E)); @@ -39,8 +43,12 @@ begin -- Example 2: Unsupported syntax Put_Line ("Parsing flow-style YAML (not supported)..."); begin - Doc := Tinyaml.Parser.Parse (Flow_Style); - Put_Line (" Unexpected success!"); + declare + Doc : constant Document := Tinyaml.Parser.Parse_Document (Flow_Style); + pragma Unreferenced (Doc); + begin + Put_Line (" Unexpected success!"); + end; exception when E : Tinyaml.Parse_Error => Put_Line (" Parse error: " & Ada.Exceptions.Exception_Message (E)); @@ -48,24 +56,24 @@ begin New_Line; -- Example 3: Safe navigation with null checks + -- Document automatically frees memory when it goes out of scope Put_Line ("Safe navigation with null checks..."); declare Config : constant String := "database:" & ASCII.LF & " host: localhost"; - Root : Node_Access; + Doc : constant Document := Tinyaml.Parser.Parse_Document (Config); Found : Node_Access; begin - Root := Tinyaml.Parser.Parse (Config); - -- This path exists - Found := Navigate (Root, "database.host"); + Found := Navigate (Root (Doc), "database.host"); if Found /= null then Put_Line (" Found: database.host = " & Value (Found)); end if; -- This path doesn't exist - Found := Navigate (Root, "database.port"); + Found := Navigate (Root (Doc), "database.port"); if Found = null then Put_Line (" Not found: database.port"); end if; end; + -- Doc is finalized here, freeing all nodes end Error_Handling; diff --git a/src/tinyaml-documents.adb b/src/tinyaml-documents.adb new file mode 100644 index 0000000..e9adb4d --- /dev/null +++ b/src/tinyaml-documents.adb @@ -0,0 +1,31 @@ +package body Tinyaml.Documents is + + use type Nodes.Node_Access; + + overriding procedure Finalize (Doc : in out Document) is + begin + if Doc.Root_Node /= null then + Nodes.Free_Node (Doc.Root_Node); + end if; + end Finalize; + + function Is_Empty (Doc : Document) return Boolean is + begin + return Doc.Root_Node = null; + end Is_Empty; + + function Root (Doc : Document) return Nodes.Node_Access is + begin + return Doc.Root_Node; + end Root; + + procedure Set_Root (Doc : in out Document; N : Nodes.Node_Access) is + begin + -- If there was a previous root, free it first + if Doc.Root_Node /= null then + Nodes.Free_Node (Doc.Root_Node); + end if; + Doc.Root_Node := N; + end Set_Root; + +end Tinyaml.Documents; diff --git a/src/tinyaml-documents.ads b/src/tinyaml-documents.ads new file mode 100644 index 0000000..0b7e44a --- /dev/null +++ b/src/tinyaml-documents.ads @@ -0,0 +1,39 @@ +-- TinyAML Document - RAII wrapper for parsed YAML documents +-- +-- Document owns the root node and automatically deallocates the entire +-- document tree when it goes out of scope. + +with Ada.Finalization; + +with Tinyaml.Nodes; + +package Tinyaml.Documents is + + --------------------------------------------------------------------------- + -- Document Type (RAII wrapper) + --------------------------------------------------------------------------- + + type Document is new Ada.Finalization.Limited_Controlled with private; + + -- Check if document has a valid root node + function Is_Empty (Doc : Document) return Boolean; + + -- Get the root node for navigation (null if empty) + function Root (Doc : Document) return Nodes.Node_Access; + + --------------------------------------------------------------------------- + -- Internal: Used by Parser to construct Document + --------------------------------------------------------------------------- + + procedure Set_Root (Doc : in out Document; N : Nodes.Node_Access); + +private + + type Document is new Ada.Finalization.Limited_Controlled with record + Root_Node : Nodes.Node_Access := null; + end record; + + overriding procedure Finalize (Doc : in out Document); + -- Recursively frees all nodes in the document tree + +end Tinyaml.Documents; diff --git a/src/tinyaml-nodes-navigation.adb b/src/tinyaml-nodes-navigation.adb index c7f194a..103fc32 100644 --- a/src/tinyaml-nodes-navigation.adb +++ b/src/tinyaml-nodes-navigation.adb @@ -1,13 +1,10 @@ with Ada.Strings.Fixed; -with Ada.Strings.Unbounded; with Tinyaml.Nodes.Map; with Tinyaml.Nodes.Scalar; package body Tinyaml.Nodes.Navigation is - use Ada.Strings.Unbounded; - function Get_String (N : YAML_Node'Class; Path : String) return String @@ -20,7 +17,7 @@ package body Tinyaml.Nodes.Navigation is if Target.all not in Scalar.Scalar_Node'Class then raise Access_Error with "Value at path is not a scalar: " & Path; end if; - return To_String (Scalar.Scalar_Node (Target.all).Value); + return Scalar.Scalar_Node (Target.all).Value; end Get_String; function Get_String (N : Node_Access; Path : String) return String is @@ -37,9 +34,10 @@ package body Tinyaml.Nodes.Navigation is is use Ada.Strings.Fixed; - Current : Node_Access; - Start : Positive := Path'First; - Dot_Pos : Natural; + Current : Node_Access := null; + Start : Positive := Path'First; + Dot_Pos : Natural; + First_Iter : Boolean := True; begin -- Handle empty path if Path'Length = 0 then @@ -51,9 +49,6 @@ package body Tinyaml.Nodes.Navigation is return null; -- Can't navigate from non-map end if; - -- Create an access to N (we need to work with access types) - Current := new YAML_Node'Class'(N); - -- Parse path components separated by dots loop Dot_Pos := Index (Path (Start .. Path'Last), "."); @@ -65,17 +60,24 @@ package body Tinyaml.Nodes.Navigation is else Path (Start .. Dot_Pos - 1)); begin - -- Navigate to next component - if Current.all not in Map.Map_Node'Class then - return null; -- Can't navigate through non-map + if First_Iter then + -- First iteration: use N directly (avoid allocation) + First_Iter := False; + if not Map.Map_Node (N).Contains (Key) then + return null; + end if; + Current := Map.Map_Node (N).Get (Key); + else + -- Subsequent iterations: use Current + if Current.all not in Map.Map_Node'Class then + return null; + end if; + if not Map.Map_Node (Current.all).Contains (Key) then + return null; + end if; + Current := Map.Map_Node (Current.all).Get (Key); end if; - if not Map.Map_Node (Current.all).Contains (Key) then - return null; -- Key not found - end if; - - Current := Map.Map_Node (Current.all).Get (Key); - -- Check if we're done if Dot_Pos = 0 then return Current; diff --git a/src/tinyaml-nodes.adb b/src/tinyaml-nodes.adb index e40e26e..7d6599c 100644 --- a/src/tinyaml-nodes.adb +++ b/src/tinyaml-nodes.adb @@ -1,5 +1,59 @@ +with Ada.Unchecked_Deallocation; + +with Tinyaml.Nodes.Map; +with Tinyaml.Nodes.Sequence; + package body Tinyaml.Nodes is + procedure Deallocate is new Ada.Unchecked_Deallocation + (Object => YAML_Node'Class, + Name => Node_Access); + + procedure Free_Sequence_Children (Seq : Sequence.Sequence_Node); + procedure Free_Map_Children (M : Map.Map_Node); + + procedure Free_Sequence_Children (Seq : Sequence.Sequence_Node) is + begin + for I in 1 .. Seq.Length loop + declare + Child : Node_Access := Seq.Element (I); + begin + Free_Node (Child); + end; + end loop; + end Free_Sequence_Children; + + procedure Free_Map_Children (M : Map.Map_Node) is + use Ada.Strings.Unbounded; + Keys : constant String_Array := M.Keys; + begin + for K of Keys loop + declare + Child : Node_Access := M.Get (To_String (K)); + begin + Free_Node (Child); + end; + end loop; + end Free_Map_Children; + + procedure Free_Node (N : in out Node_Access) is + begin + if N = null then + return; + end if; + + case N.Kind is + when Scalar_Kind => + null; + when Sequence_Kind => + Free_Sequence_Children (Sequence.Sequence_Node (N.all)); + when Map_Kind => + Free_Map_Children (Map.Map_Node (N.all)); + end case; + + Deallocate (N); + end Free_Node; + function Is_Map (N : YAML_Node'Class) return Boolean is begin return N.Kind = Map_Kind; diff --git a/src/tinyaml-nodes.ads b/src/tinyaml-nodes.ads index f2b1fac..6770c4a 100644 --- a/src/tinyaml-nodes.ads +++ b/src/tinyaml-nodes.ads @@ -28,6 +28,10 @@ package Tinyaml.Nodes is function Is_Sequence (N : YAML_Node'Class) return Boolean; function Is_Map (N : YAML_Node'Class) return Boolean; + -- Memory management: recursively free a node and all its children + procedure Free_Node (N : in out Node_Access) + with Post => N = null; + --------------------------------------------------------------------------- -- Common Types --------------------------------------------------------------------------- diff --git a/src/tinyaml-parser.adb b/src/tinyaml-parser.adb index 59098bb..6fa2879 100644 --- a/src/tinyaml-parser.adb +++ b/src/tinyaml-parser.adb @@ -328,4 +328,11 @@ package body Tinyaml.Parser is end case; end Parse_Value; + function Parse_Document (Input : String) return Documents.Document is + begin + return Doc : Documents.Document do + Documents.Set_Root (Doc, Parse (Input)); + end return; + end Parse_Document; + end Tinyaml.Parser; diff --git a/src/tinyaml-parser.ads b/src/tinyaml-parser.ads index 6f299ac..da4e07f 100644 --- a/src/tinyaml-parser.ads +++ b/src/tinyaml-parser.ads @@ -1,12 +1,21 @@ -- TinyAML Parser - Builds document tree from YAML input -- +with Tinyaml.Documents; with Tinyaml.Nodes; package Tinyaml.Parser is -- Parse a YAML string and return the document root. -- Raises Parse_Error if the input is invalid. + -- + -- NOTE: The returned Node_Access must be manually freed with Free_Node, + -- or use Parse_Document instead for automatic memory management. function Parse (Input : String) return Nodes.Node_Access; + -- Parse a YAML string and return a Document wrapper. + -- The Document automatically frees the node tree when it goes out of scope. + -- Raises Parse_Error if the input is invalid. + function Parse_Document (Input : String) return Documents.Document; + end Tinyaml.Parser; diff --git a/src/tinyaml-schemas-map.adb b/src/tinyaml-schemas-map.adb index a3540c5..a5de1c3 100644 --- a/src/tinyaml-schemas-map.adb +++ b/src/tinyaml-schemas-map.adb @@ -9,6 +9,22 @@ with Tinyaml.Schemas.Any; package body Tinyaml.Schemas.Map is + procedure Free_Fields (M : in out Map_Schema) is + use Field_Maps; + begin + for C in M.Fields.Iterate loop + declare + Info : Field_Info := Element (C); + S_Acc : Schema_Access := Info.Field_Schema; + begin + if S_Acc /= null then + Free_Schema (S_Acc); + end if; + end; + end loop; + M.Fields.Clear; + end Free_Fields; + procedure Add_Field (M : in out Map_Schema'Class; Name : String; diff --git a/src/tinyaml-schemas-map.ads b/src/tinyaml-schemas-map.ads index f7be7ad..77ee63a 100644 --- a/src/tinyaml-schemas-map.ads +++ b/src/tinyaml-schemas-map.ads @@ -76,6 +76,9 @@ package Tinyaml.Schemas.Map is Schema : Map_Schema; Optional : Boolean := False); + -- Memory management: free all field schemas (used by Free_Schema) + procedure Free_Fields (M : in out Map_Schema); + private type Field_Info is record diff --git a/src/tinyaml-schemas-seq.adb b/src/tinyaml-schemas-seq.adb index 9fb6946..638c3b8 100644 --- a/src/tinyaml-schemas-seq.adb +++ b/src/tinyaml-schemas-seq.adb @@ -14,6 +14,13 @@ package body Tinyaml.Schemas.Seq is return Item.Schema_Ref.all; end Get_Schema; + procedure Free_Item_Schema (Item : in out Seq_Item) is + begin + if Item.Schema_Ref /= null then + Free_Schema (Item.Schema_Ref); + end if; + end Free_Item_Schema; + overriding function Is_Valid (S : Seq_Schema; N : Nodes.Node_Access) return Boolean is diff --git a/src/tinyaml-schemas-seq.ads b/src/tinyaml-schemas-seq.ads index 5a498b0..6360793 100644 --- a/src/tinyaml-schemas-seq.ads +++ b/src/tinyaml-schemas-seq.ads @@ -10,6 +10,9 @@ package Tinyaml.Schemas.Seq is function To_Seq_Item (S : Schema'Class) return Seq_Item; function Get_Schema (Item : Seq_Item) return Schema'Class; + -- Memory management: free the schema inside a Seq_Item + procedure Free_Item_Schema (Item : in out Seq_Item); + -- Seq_Schema validates sequences with items matching a schema type Seq_Schema is new Schema with record Item : Seq_Item; diff --git a/src/tinyaml-schemas.adb b/src/tinyaml-schemas.adb index b293b79..8e3b839 100644 --- a/src/tinyaml-schemas.adb +++ b/src/tinyaml-schemas.adb @@ -1,9 +1,44 @@ with Ada.Strings.Unbounded; use Ada.Strings.Unbounded; +with Ada.Unchecked_Deallocation; + with Tinyaml.Nodes; use Tinyaml.Nodes; with Tinyaml.Nodes.Scalar; use Tinyaml.Nodes.Scalar; +with Tinyaml.Schemas.Map; +with Tinyaml.Schemas.Seq; package body Tinyaml.Schemas is + procedure Deallocate is new Ada.Unchecked_Deallocation + (Object => Schema'Class, + Name => Schema_Access); + + procedure Free_Schema (S : in out Schema_Access) is + begin + if S = null then + return; + end if; + + -- Recursively free nested schemas based on schema type + if S.all in Map.Map_Schema'Class then + -- Map_Schema has fields that contain Schema_Access + declare + M : Map.Map_Schema renames Map.Map_Schema (S.all); + begin + Map.Free_Fields (M); + end; + elsif S.all in Seq.Seq_Schema'Class then + -- Seq_Schema has a Seq_Item containing Schema_Access + declare + Sq : Seq.Seq_Schema renames Seq.Seq_Schema (S.all); + begin + Seq.Free_Item_Schema (Sq.Item); + end; + end if; + + -- Deallocate this schema + Deallocate (S); + end Free_Schema; + function Validate_Node (S : Schema; N : Nodes.Node_Access) return Validation_Info diff --git a/src/tinyaml-schemas.ads b/src/tinyaml-schemas.ads index ca51bdc..5e8e60a 100644 --- a/src/tinyaml-schemas.ads +++ b/src/tinyaml-schemas.ads @@ -41,4 +41,8 @@ package Tinyaml.Schemas is type Schema_Access is access all Schema'Class; + -- Memory management: recursively free a schema and any nested schemas + procedure Free_Schema (S : in out Schema_Access) + with Post => S = null; + end Tinyaml.Schemas; diff --git a/tests/test_parser.adb b/tests/test_parser.adb index 4ebc35d..a3c791d 100644 --- a/tests/test_parser.adb +++ b/tests/test_parser.adb @@ -1,5 +1,6 @@ with Test_Harness; with Tinyaml; +with Tinyaml.Documents; with Tinyaml.Parser; with Tinyaml.Nodes; with Tinyaml.Nodes.Map; @@ -10,6 +11,7 @@ with Tinyaml.Nodes.Sequence; package body Test_Parser is use Test_Harness; + use Tinyaml.Documents; use Tinyaml.Nodes; use Tinyaml.Nodes.Map; use Tinyaml.Nodes.Navigation; @@ -180,6 +182,44 @@ package body Test_Parser is Pass; end Test_Get_String; + procedure Test_Parse_Document is + Input : constant String := + "database:" & ASCII.LF & + " host: localhost" & ASCII.LF & + " port: 5432"; + begin + Start_Test ("Parse_Document with automatic memory management"); + declare + Doc : constant Document := Tinyaml.Parser.Parse_Document (Input); + begin + Assert (not Is_Empty (Doc), "Document should not be empty"); + Assert (Root (Doc) /= null, "Root should not be null"); + Assert (Is_Map (Root (Doc).all), "Root should be a map"); + Assert_Equal ("localhost", Get_String (Root (Doc), "database.host")); + Assert_Equal ("5432", Get_String (Root (Doc), "database.port")); + end; + -- Document goes out of scope here, memory is automatically freed + Pass; + end Test_Parse_Document; + + procedure Test_Free_Node is + Input : constant String := + "items:" & ASCII.LF & + " - one" & ASCII.LF & + " - two" & ASCII.LF & + " - three"; + Doc : Node_Access; + begin + Start_Test ("Free_Node deallocates tree"); + Doc := Tinyaml.Parser.Parse (Input); + Assert (Doc /= null, "Parse should return non-null"); + Assert (Is_Map (Doc.all), "Root should be a map"); + -- Manually free the tree + Free_Node (Doc); + Assert (Doc = null, "After Free_Node, access should be null"); + Pass; + end Test_Free_Node; + --------------- -- Run_Tests -- --------------- @@ -198,6 +238,8 @@ package body Test_Parser is Test_Duplicate_Key_Rejected; Test_Navigate_Path; Test_Get_String; + Test_Parse_Document; + Test_Free_Node; end Run_Tests; end Test_Parser; From b175257c23c4d0214855ae04fe42e935c7352acb Mon Sep 17 00:00:00 2001 From: Jochen Lillich Date: Thu, 25 Dec 2025 22:02:49 +0000 Subject: [PATCH 2/2] feat: Changelog file Refer to [CHANGELOG.md](CHANGELOG.md) for the upcoming and prior releases. --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bdaa0b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +> [!NOTE] +> Until the release of version 1.0, this project will be in development +> mode. In consequence, even minor versions might contain breaking changes. +> Starting with version 1.0, we'll follow [Semantic +> Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed + +- The library was missing memory management. This release introduces the + Controlled Type `Document` that'll trigger a cleanup when it goes out of scope. + See [README](README.md) and the example programs for how this works. + +## [0.1.0] - 2025-12-24 + +### Added + +- First release of the library. It's going to be in development mode until + release 1.0. The practical consequence is that until then, even minor versions + might well contain _breaking changes_.