Update grammar, expand CRC32 function (#2)

XN grammar and transformation engine are updated, including support for bitwise operations, improved function parameter handling, and enhanced error reporting.

Reviewed-on: dominic/XNeedle#2
This commit is contained in:
Dominic Höglinger 2026-04-25 10:26:05 +00:00
parent 0de7edee70
commit 01e08f1902
5 changed files with 167 additions and 46 deletions

View File

@ -30,12 +30,15 @@ namespace XNeedle.Parser
static readonly Parser<ExpressionType> Multiply = Operator("*", ExpressionType.MultiplyChecked);
static readonly Parser<ExpressionType> Divide = Operator("/", ExpressionType.Divide);
static readonly Parser<ExpressionType> Modulo = Operator("%", ExpressionType.Modulo);
static readonly Parser<ExpressionType> Power = Operator("^", ExpressionType.Power);
static readonly Parser<ExpressionType> Power = Operator("**", ExpressionType.Power);
static readonly Parser<ExpressionType> BitwiseXor = Operator("^", ExpressionType.ExclusiveOr);
static readonly Parser<ExpressionType> BitwiseAnd = Operator("&", ExpressionType.And);
static readonly Parser<ExpressionType> BitwiseOr = Operator("|", ExpressionType.Or);
static Parser<Expression> Function(Context ctx)
{
return
from name in Parse.Letter.AtLeastOnce().Text()
from name in Identifier
from lparen in Parse.Char('(')
from expr in Parse.Ref(() => Expr(ctx)).DelimitedBy(Parse.Char(',').Token()).Optional()
from rparen in Parse.Char(')')
@ -46,23 +49,44 @@ namespace XNeedle.Parser
{
var mathMethodInfo = typeof(Math).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).ToArray());
if (mathMethodInfo != null)
{
return Expression.Call(mathMethodInfo, parameters);
}
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo().GetMethod(name, parameters.Select(e => e.Type).Prepend(typeof(Context)).ToArray());
// Find by name and fill in optional parameters with their default values
var ctxMethodInfo = typeof(ContextFunctions).GetTypeInfo()
.GetMethods()
.FirstOrDefault(m =>
{
if (m.Name != name) return false;
var mp = m.GetParameters();
if (mp.Length == 0 || mp[0].ParameterType != typeof(Context)) return false;
int required = mp.Skip(1).Count(p => !p.HasDefaultValue);
return parameters.Length >= required && parameters.Length <= mp.Length - 1;
});
if (ctxMethodInfo != null)
{
return Expression.Call(ctxMethodInfo, parameters.Prepend(CtxParam).ToArray());
var mp = ctxMethodInfo.GetParameters();
var args = mp.Skip(1).Select((p, i) =>
i < parameters.Length
? parameters[i]
: Expression.Constant(p.DefaultValue, p.ParameterType)
).ToArray();
return Expression.Call(ctxMethodInfo, args.Prepend(CtxParam).ToArray());
}
throw new ParseException(string.Format("Function '{0}({1})' does not exist.", name,
string.Join(",", parameters.Select(e => e.Type.Name))));
}
public static readonly Parser<string> HexNumber =
from prefix in Parse.String("0x").Token()
from number in Parse.Char(char.IsAsciiHexDigit, "hexadecimal digit").AtLeastOnce().Text()
select number;
static readonly Parser<Expression> NumberConstant =
Parse.Decimal
.Select(x => Expression.Constant(double.Parse(x)))
.Named("number");
HexNumber.Select(x => Expression.Constant(int.Parse(x, System.Globalization.NumberStyles.HexNumber)))
.XOr(Parse.Decimal.Select(x => Expression.Constant(double.Parse(x))))
.Named("number");
private static readonly Parser<Expression> StringConstant =
from open in Parse.Char('"')
@ -95,7 +119,7 @@ namespace XNeedle.Parser
static Parser<Expression> Term(Context ctx)
{
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo), InnerTerm(ctx), Expression.MakeBinary);
return Parse.ChainOperator(Multiply.Or(Divide).Or(Modulo).Or(BitwiseXor).Or(BitwiseAnd).Or(BitwiseOr), InnerTerm(ctx), Expression.MakeBinary);
}
static Parser<Expression> ConcatExpr(Context ctx)
@ -183,10 +207,6 @@ namespace XNeedle.Parser
from arguments in Argument.DelimitedBy(Parse.Char(',').Token())
from close in Parse.Char(')')
select new Call{Identifier = new string(identifier.ToArray()), Arguments = arguments};
/*public static readonly Parser<Expression> ExpressionObject =
from expression in CallObject
*/
public static CommentParser Comment = new CommentParser { Single = "#", NewLine = Environment.NewLine };
@ -199,11 +219,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 +236,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<IEnumerable<Part>> Rules =
(from rule in RuleObject.Token()
from semi in Parse.Char(';').Token()
select rule)
.Many()
.End();
static public string StripComments(string source)
{

View File

@ -40,10 +40,10 @@ namespace XNeedle
var rawSource = File.ReadAllText(opts.XnDefinition);
var xnSource = XnParser.StripComments(rawSource);
Rule masterRule;
IEnumerable<Part> 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>())
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}\"");

View File

@ -12,10 +12,20 @@ namespace XNeedle.Transformation
{
public class ContextFunctions
{
public static string hex(Context ctx, double value)
public static string hexlit(Context ctx, int value)
{
uint v = (uint)value;
return $"#x{v:X8}";
return $"#x{v:x8}";
}
public static string lower(Context ctx, string text)
{
return text.ToLower();
}
public static string upper(Context ctx, string text)
{
return text.ToUpper();
}
public static double GetLevel(Context ctx)
@ -26,7 +36,41 @@ namespace XNeedle.Transformation
level++;
return level;
}
public static double crc(Context ctx)
private static uint ComputeCrc32(byte[] data, uint poly, uint init, uint xorOut, bool reflected)
{
uint crc = init;
uint polyReflected = ReflectBits(poly, 32);
foreach (byte b in data)
{
if (reflected)
{
crc ^= b;
for (int i = 0; i < 8; i++)
crc = (crc & 1) != 0 ? (crc >> 1) ^ polyReflected : crc >> 1;
}
else
{
crc ^= (uint)b << 24;
for (int i = 0; i < 8; i++)
crc = (crc & 0x80000000u) != 0 ? (crc << 1) ^ poly : crc << 1;
}
}
return crc ^ xorOut;
}
private static uint ReflectBits(uint value, int bits)
{
uint reflected = 0;
for (int i = 0; i < bits; i++)
{
if ((value & (1u << i)) != 0)
reflected |= 1u << (bits - 1 - i);
}
return reflected;
}
public static int crc32(Context ctx, int poly = 0x04C11DB7, int init = 0, int xorOut = 0, bool reflected = false)
{
if (ctx.Node is null)
return 0;
@ -36,17 +80,15 @@ namespace XNeedle.Transformation
ctx.Node.WriteTo(writer);
writer.Flush();
string xml = buffer.ToString();
// Step 2: Extract all text after the opening tag through and including the closing tag
string tagName = ctx.Node.Name.LocalName;
string data = XmlFragParser.ChecksumData(tagName).Parse(xml);
//byte[] fragBytes = ctx.SLoc.Encoding.GetBytes(data);
byte[] fragBytes = Encoding.UTF8.GetBytes(data);
// Step 3: Digest into CRC32 using the source encoding
var crc32 = new System.IO.Hashing.Crc32();
crc32.Append(ctx.SLoc.Encoding.GetBytes(data));
uint result = BitConverter.ToUInt32(crc32.GetCurrentHash());
return (double)result;
// Step 3: Digest into CRC32 using parameterized CRC32 - by default MPEG-2
return (int)ComputeCrc32(fragBytes, (uint)poly, (uint)init, (uint)xorOut, reflected);
}
public static string text(Context ctx)
@ -187,5 +229,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 "";
}
}
}

View File

@ -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; }
}
}

View File

@ -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();