From a05903df4dc2279235119962cc2d2b9d28cdb44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominic=20H=C3=B6glinger?= Date: Sat, 25 Apr 2026 01:08:54 +0200 Subject: [PATCH] Add stricter semicolon grammar, require() function, fix multi-rule xn To easily spot semicolon errors, they are now mandatory delimiters after each construct. Add require() to raise an error if a match is unsucessful. Multiple rules in one xn files now possible. --- Parser/XnParser.cs | 32 ++++++++++++++++++++---------- Program.cs | 25 +++++++++++++++++++---- Transformation/ContextFunctions.cs | 13 ++++++++++++ Transformation/RequireException.cs | 15 ++++++++++++++ Transformation/Rule.cs | 16 +++++++++------ 5 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 Transformation/RequireException.cs diff --git a/Parser/XnParser.cs b/Parser/XnParser.cs index f959107..57c282e 100644 --- a/Parser/XnParser.cs +++ b/Parser/XnParser.cs @@ -199,11 +199,13 @@ namespace XNeedle.Parser from ws1 in Parse.WhiteSpace.Many().Optional() from open in Parse.Char('{') from ws2 in Parse.WhiteSpace.Many().Optional() - from expressions in Parse.Ref(() => BangObject) - .Or(Parse.Ref(() => RuleObject)) - .Or(ExprPart(ctx).Select(e => (Part)e)) - .DelimitedBy(Parse.Char(';').Token()) - from tailingsemi in Parse.Char(';').Optional() + from expressions in ( + from expr in Parse.Ref(() => BangObject) + .Or(Parse.Ref(() => RuleObject)) + .Or(ExprPart(ctx).Select(e => (Part)e)) + from semi in Parse.Char(';').Token() + select expr + ).Many() from ws3 in Parse.WhiteSpace.Many().Optional() from close in Parse.Char('}') select new Bang{Name = new string([.. name]), Arguments = arguments, Body = expressions}; @@ -214,15 +216,23 @@ namespace XNeedle.Parser from ws1 in Parse.WhiteSpace.Many().Optional() from open in Parse.Char('{') from ws2 in Parse.WhiteSpace.Many().Optional() - from expressions in Parse.Ref(() => BangObject) - .Or(Parse.Ref(() => RuleObject)) - .Or(ExprPart(ctx).Select(e => (Part)e)) - .DelimitedBy(Parse.Char(';').Token()) - from tailingsemi in Parse.Char(';').Optional() + from expressions in ( + from expr in Parse.Ref(() => BangObject) + .Or(Parse.Ref(() => RuleObject)) + .Or(ExprPart(ctx).Select(e => (Part)e)) + from semi in Parse.Char(';').Token() + select expr + ).Many() from ws3 in Parse.WhiteSpace.Many().Optional() from close in Parse.Char('}') select new Rule{XPathSelector = selector, Body = expressions}; - + + public static readonly Parser> Rules = + (from rule in RuleObject.Token() + from semi in Parse.Char(';').Token() + select rule) + .Many() + .End(); static public string StripComments(string source) { diff --git a/Program.cs b/Program.cs index ae34d3e..36a84f6 100644 --- a/Program.cs +++ b/Program.cs @@ -40,10 +40,10 @@ namespace XNeedle var rawSource = File.ReadAllText(opts.XnDefinition); var xnSource = XnParser.StripComments(rawSource); - Rule masterRule; + IEnumerable rules; try { - masterRule = (Rule)XnParser.RuleObject.Parse(xnSource); + rules = XnParser.Rules.Parse(xnSource); } catch (ParseException ex) { @@ -54,11 +54,19 @@ namespace XNeedle if (showPosition) { - Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {ex.Message}"); + var msg = ex.Message; + var hint = (msg.Contains("expected }") && !msg.Contains("expected ';'")) + ? "Hint: a missing ';' terminator may be located just after this position" + : ""; + Console.Error.WriteLine($"{opts.XnDefinition}({line},{col}): parse error: {msg}"); var srcLine = line <= lines.Length ? lines[line - 1].TrimEnd() : ""; var pointer = new string(' ', Math.Max(0, col - 1)) + "^"; Console.Error.WriteLine($" {srcLine}"); Console.Error.WriteLine($" {pointer}"); + if (!string.IsNullOrEmpty(hint)) + { + Console.Error.WriteLine($"\n{hint}"); + } } else { @@ -69,7 +77,16 @@ namespace XNeedle var doc = XDocument.Load(opts.Input, LoadOptions.PreserveWhitespace); var encoding = XmlRoundtripWriter.DetectEncoding(File.ReadAllBytes(opts.Input)); var sloc = new SourceLocation(opts.Input, encoding); - masterRule.Transform(doc, sloc); + try + { + foreach (var rule in rules.OfType()) + rule.Transform(doc, sloc); + } + catch (RequireException ex) + { + Console.Error.WriteLine($"{ex.SLoc.FilePath}({ex.SLoc.LineNumber}): require error: {ex.UserMessage}"); + return; + } using var writer = XmlRoundtripWriter.Create(opts.Input, opts.Output); doc.WriteTo(writer); Console.WriteLine($"Applied \"{opts.XnDefinition}\" to \"{opts.Input}\" and saved result to \"{opts.Output}\""); diff --git a/Transformation/ContextFunctions.cs b/Transformation/ContextFunctions.cs index 008b16f..8bf926d 100644 --- a/Transformation/ContextFunctions.cs +++ b/Transformation/ContextFunctions.cs @@ -187,5 +187,18 @@ namespace XNeedle.Transformation } return ""; } + + public static string require(Context ctx, string message = "Required node not present") + { + if (ctx.Node == null) + { + throw new RequireException{SLoc=ctx.SLoc, UserMessage=message}; + } + else + { + Console.WriteLine(ctx.Node.ToString()); + } + return ""; + } } } \ No newline at end of file diff --git a/Transformation/RequireException.cs b/Transformation/RequireException.cs new file mode 100644 index 0000000..64485c1 --- /dev/null +++ b/Transformation/RequireException.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using XNeedle; +using XNeedle.Parser.Elements; + +namespace XNeedle.Transformation +{ + public class RequireException : Exception + { + public SourceLocation SLoc { get; set; } + public required string UserMessage { get; set; } + } +} \ No newline at end of file diff --git a/Transformation/Rule.cs b/Transformation/Rule.cs index a51e897..7eabc60 100644 --- a/Transformation/Rule.cs +++ b/Transformation/Rule.cs @@ -22,15 +22,19 @@ namespace XNeedle.Transformation public override void Operate(Context ctx) { - if(ctx.Node is null) - { - return; - } - var nodes = ctx.Node.XPathSelectElements(XPathSelector); - if(nodes is null) + var nodes = ctx.Node?.XPathSelectElements(XPathSelector); + if(nodes is null || nodes.Count() == 0) { + var nullCtx = ctx.Clone(); + nullCtx.Node = null; + nullCtx.SLoc = ctx.SLoc.WithLineNumber(-1); + foreach (var b in Body) + { + b.Operate(nullCtx); + } return; } + foreach (XElement n in nodes) { var loopCtx = ctx.Clone();