diff --git a/Coder.Test/Ast/UnfilledNodeCloneTests.cs b/Coder.Test/Ast/UnfilledNodeCloneTests.cs
new file mode 100644
index 0000000..fa54519
--- /dev/null
+++ b/Coder.Test/Ast/UnfilledNodeCloneTests.cs
@@ -0,0 +1,92 @@
+// Copyright (c) 2023-2026 ktsu-dev contributors
+
+namespace ktsu.Coder.Test.Ast;
+
+using System.Reflection;
+using ktsu.Coder.Ast;
+using ktsu.Coder.Languages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Tests that a node nobody has filled in yet can still be cloned.
+///
+///
+/// A node dropped in the editor, or read from YAML with a key missing, holds whatever its
+/// parameterless constructor gave it. The generators clone whole types before they write them, so a
+/// clone that validates what it copies turns a half-built AST into an exception out of generation.
+///
+[TestClass]
+public class UnfilledNodeCloneTests
+{
+ ///
+ /// Every node type built with its parameterless constructor clones without throwing.
+ ///
+ [TestMethod]
+ public void EveryNodeType_ClonesFromItsParameterlessConstructor()
+ {
+ IEnumerable nodeTypes = typeof(AstNode).Assembly.GetTypes()
+ .Where(t => typeof(AstNode).IsAssignableFrom(t) && !t.IsAbstract && !t.ContainsGenericParameters)
+ .Where(t => t.GetConstructor(BindingFlags.Public | BindingFlags.Instance, Type.EmptyTypes) is not null);
+
+ List failures = [];
+ foreach (Type nodeType in nodeTypes)
+ {
+ AstNode node = (AstNode)Activator.CreateInstance(nodeType)!;
+ try
+ {
+ _ = node.Clone();
+ }
+ catch (ArgumentException ex)
+ {
+ failures.Add($"{nodeType.Name}: {ex.Message}");
+ }
+ }
+
+ Assert.IsEmpty(failures, string.Join(Environment.NewLine, failures));
+ }
+
+ ///
+ /// A variable reference with no name yet clones to another with no name.
+ ///
+ [TestMethod]
+ public void VariableReference_WithNoName_ClonesToNoName()
+ {
+ VariableReference original = new() { ExpectedType = "int" };
+
+ VariableReference clone = (VariableReference)original.Clone();
+
+ Assert.AreEqual(string.Empty, clone.Name);
+ Assert.AreEqual("int", clone.ExpectedType?.ToString());
+ }
+
+ ///
+ /// A class with a property and an assignment that has no target yet still generates in the
+ /// languages that clone the class before writing it.
+ ///
+ /// The language to generate.
+ [TestMethod]
+ [DataRow("c")]
+ [DataRow("cpp")]
+ [DataRow("rust")]
+ [DataRow("go")]
+ public void ClassWithAnUnfilledAssignment_Generates(string languageId)
+ {
+ ClassDeclaration box = new("Box");
+ box.Members.Add(new PropertyDeclaration("Size", "int"));
+ FunctionDeclaration reset = new("reset");
+ reset.Body.Add(new AssignmentStatement());
+ box.Members.Add(reset);
+
+ ILanguageGenerator generator = languageId switch
+ {
+ "c" => new CGenerator(),
+ "cpp" => new CppGenerator(),
+ "rust" => new RustGenerator(),
+ _ => new GoGenerator(),
+ };
+
+ string output = generator.Generate(box);
+
+ Assert.Contains("Box", output);
+ }
+}
diff --git a/Coder/Ast/VariableReference.cs b/Coder/Ast/VariableReference.cs
index 0503605..38d9b8f 100644
--- a/Coder/Ast/VariableReference.cs
+++ b/Coder/Ast/VariableReference.cs
@@ -37,8 +37,11 @@ public class VariableReference : Expression
/// A new instance with the same property values.
public override AstNode Clone()
{
- VariableReference clone = new(Name)
+ // The parameterless constructor and an assignment, because the validating one would refuse
+ // the empty name an unfilled node legitimately holds, and a clone must not judge what it copies.
+ VariableReference clone = new()
{
+ Name = Name,
ExpectedType = ExpectedType
};