From 3192685f822c9fbab8f1bf5af80c8567b72caff1 Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Sun, 7 Jul 2024 15:34:27 -0500 Subject: [PATCH] Add canonical instance encoding for constants --- libs/UIX.Asm/Disassembler.cs | 135 ++++++++++++---------- libs/UIX.Asm/Lexer.Directives.cs | 36 +++++- libs/UIX.Asm/Models/ConstantDirectives.cs | 79 +++++++++++++ libs/UIX.Asm/Models/Directives.cs | 57 --------- libs/UIX.Test/Assembly.cs | 12 +- 5 files changed, 190 insertions(+), 129 deletions(-) create mode 100644 libs/UIX.Asm/Models/ConstantDirectives.cs diff --git a/libs/UIX.Asm/Disassembler.cs b/libs/UIX.Asm/Disassembler.cs index 097d1a2..a4b4882 100644 --- a/libs/UIX.Asm/Disassembler.cs +++ b/libs/UIX.Asm/Disassembler.cs @@ -14,6 +14,7 @@ public class Disassembler private readonly MarkupLoadResult _loadResult; private readonly Dictionary _importedUris; private readonly Dictionary> _offsetLabelMap = new(); + private static readonly TypeSchema _stringTypeSchema = UIXTypes.MapIDToType(UIXTypeID.String); private Disassembler(MarkupLoadResult loadResult) { @@ -224,70 +225,10 @@ public class Disassembler } } - var stringTypeSchema = UIXTypes.MapIDToType(UIXTypeID.String); - foreach (var (c, typeSchema, constantValue) in constants) { var constantName = $"const{c:D}"; - QualifiedTypeName qualifiedTypeName = GetQualifiedName(typeSchema); - - if (constantValue is IStringEncodable encodable) - { - var encodedValue = encodable.EncodeString(); - yield return new StringEncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); - } - else if (constantValue is Layout.ILayout constantLayout && Layout.PredefinedLayouts.TryConvertToString(constantLayout, out var constantLayoutString)) - { - qualifiedTypeName = GetQualifiedName(UIXTypes.MapIDToType(UIXTypeID.Layout)); - yield return new StringEncodedConstantDirective(constantName, qualifiedTypeName, constantLayoutString); - } - else if (typeSchema.SupportsTypeConversion(stringTypeSchema)) - { - var encodedValue = constantValue.ToString(); - yield return new StringEncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); - } - else if (typeSchema.SupportsBinaryEncoding) - { - ByteCodeWriter writer = new(); - typeSchema.EncodeBinary(writer, constantValue); - - var reader = writer.CreateReader(); - byte[] encodedBytes = new byte[reader.Size]; - Marshal.Copy(reader.GetAddress(0), encodedBytes, 0, (int)reader.Size); - - yield return new BinaryEncodedConstantDirective(constantName, qualifiedTypeName, encodedBytes); - } - else - { - XName constElemName = qualifiedTypeName.NamespacePrefix is null - ? qualifiedTypeName.TypeName - : XName.Get(qualifiedTypeName.TypeName, qualifiedTypeName.NamespacePrefix); - XElement constElem = new(constElemName); - - var defaultConstantValue = typeSchema.ConstructDefault(); - - foreach (var prop in typeSchema.Properties) - { - // No need to serialize properties that can't be set - if (!prop.CanWrite) - continue; - - var defaultPropValue = prop.GetValue(defaultConstantValue); - var propValue = prop.GetValue(constantValue); - - var encodedPropValue = EncodeSimpleConstant(propValue); - var encodedDefaultPropValue = EncodeSimpleConstant(defaultPropValue); - - // No need to serialize properties that are at their default value - if (encodedPropValue == encodedDefaultPropValue) - continue; - - constElem.SetAttributeValue(prop.Name, encodedPropValue); - } - - var constructor = constElem.ToString(SaveOptions.DisableFormatting); - yield return new ConstantDirective(constantName, qualifiedTypeName, constructor); - } + yield return EncodeConstant(constantValue, constantName, typeSchema); } } @@ -376,6 +317,78 @@ public class Disassembler labels.Add(new(labelName)); } + private ConstantDirective EncodeConstant(object constantValue, string constantName, TypeSchema typeSchema) + { + var qualifiedTypeName = GetQualifiedName(typeSchema); + + // String encodable + if (constantValue is IStringEncodable encodable) + { + var encodedValue = encodable.EncodeString(); + return new StringEncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); + } + + // Custom handling for ILayout + if (constantValue is Layout.ILayout constantLayout && Layout.PredefinedLayouts.TryConvertToString(constantLayout, out var constantLayoutString)) + { + qualifiedTypeName = GetQualifiedName(UIXTypes.MapIDToType(UIXTypeID.Layout)); + return new CanonicalInstanceConstantDirective(constantName, qualifiedTypeName, constantLayoutString); + } + + if (typeSchema.SupportsTypeConversion(_stringTypeSchema)) + { + var encodedValue = constantValue.ToString(); + return new StringEncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); + } + + // Binary encodable + if (typeSchema.SupportsBinaryEncoding) + { + ByteCodeWriter writer = new(); + typeSchema.EncodeBinary(writer, constantValue); + + var reader = writer.CreateReader(); + byte[] encodedBytes = new byte[reader.Size]; + Marshal.Copy(reader.GetAddress(0), encodedBytes, 0, (int)reader.Size); + reader.Dispose(null); + + return new BinaryEncodedConstantDirective(constantName, qualifiedTypeName, encodedBytes); + } + +#if DEBUG + throw new NotSupportedException($"Unable to encode constant value '{constantValue}' of type '{qualifiedTypeName}'"); +#else + XName constElemName = qualifiedTypeName.NamespacePrefix is null + ? qualifiedTypeName.TypeName + : XName.Get(qualifiedTypeName.TypeName, qualifiedTypeName.NamespacePrefix); + XElement constElem = new(constElemName); + + var defaultConstantValue = typeSchema.ConstructDefault(); + + foreach (var prop in typeSchema.Properties) + { + // No need to serialize properties that can't be set + if (!prop.CanWrite) + continue; + + var defaultPropValue = prop.GetValue(defaultConstantValue); + var propValue = prop.GetValue(constantValue); + + var encodedPropValue = EncodeSimpleConstant(propValue); + var encodedDefaultPropValue = EncodeSimpleConstant(defaultPropValue); + + // No need to serialize properties that are at their default value + if (encodedPropValue == encodedDefaultPropValue) + continue; + + constElem.SetAttributeValue(prop.Name, encodedPropValue); + } + + var constructor = constElem.ToString(SaveOptions.DisableFormatting); + return new ConstantDirective(constantName, qualifiedTypeName, constructor); +#endif + } + private static string EncodeSimpleConstant(object value) { return value is IStringEncodable encodable diff --git a/libs/UIX.Asm/Lexer.Directives.cs b/libs/UIX.Asm/Lexer.Directives.cs index 93478bd..5248283 100644 --- a/libs/UIX.Asm/Lexer.Directives.cs +++ b/libs/UIX.Asm/Lexer.Directives.cs @@ -87,9 +87,23 @@ partial class Lexer if (!typeNameResult.WasSuccessful) return Result.Failure(input, "Invalid constant directive", ["Expected qualified name of type to construct"]); - var binaryEncodingMarkerResult = Parse.String(".bin")(input); - input = binaryEncodingMarkerResult.Remainder; - var binaryEncoded = binaryEncodingMarkerResult.WasSuccessful; + var encodingMarkerResult = Parse.Char('.')(input); + input = encodingMarkerResult.Remainder; + if (!encodingMarkerResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected '.', followed by the persist mode."]); + + var persistModeResult = Parse.CharExcept('(').AtLeastOnce().Text().Token()(input); + input = persistModeResult.Remainder; + if (!persistModeResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["No persist mode was specified."]); + + Markup.MarkupConstantPersistMode? persistMode = persistModeResult.Value.ToLowerInvariant() switch + { + "bin" => Markup.MarkupConstantPersistMode.Binary, + "str" => Markup.MarkupConstantPersistMode.FromString, + "can" => Markup.MarkupConstantPersistMode.Canonical, + _ => null + }; var openBracketResult = Parse.Char('(').Token()(input); input = openBracketResult.Remainder; @@ -109,7 +123,7 @@ partial class Lexer var constantName = constNameResult.Value; var typeName = typeNameResult.Value; - if (!binaryEncoded) + if (persistMode == Markup.MarkupConstantPersistMode.FromString) { directive = new StringEncodedConstantDirective(constantName, typeName, contentResult.Value) { @@ -117,7 +131,15 @@ partial class Lexer Column = column, }; } - else + else if (persistMode == Markup.MarkupConstantPersistMode.Canonical) + { + directive = new CanonicalInstanceConstantDirective(constantName, typeName, contentResult.Value) + { + Line = line, + Column = column, + }; + } + else if (persistMode == Markup.MarkupConstantPersistMode.Binary) { byte[] constantBytes; var byteParts = contentResult.Value.Split(','); @@ -156,6 +178,10 @@ partial class Lexer Column = column, }; } + else + { + return Result.Failure(input, "Invalid constant directive", [$"'{persistModeResult.Value}' is not a valid persist mode."]); + } } break; diff --git a/libs/UIX.Asm/Models/ConstantDirectives.cs b/libs/UIX.Asm/Models/ConstantDirectives.cs new file mode 100644 index 0000000..fa7114c --- /dev/null +++ b/libs/UIX.Asm/Models/ConstantDirectives.cs @@ -0,0 +1,79 @@ +using Microsoft.Iris.Markup; +using System; +using System.Linq; + +namespace Microsoft.Iris.Asm.Models; + +public record ConstantDirective : Directive +{ + public ConstantDirective(string name, QualifiedTypeName typeName, string constructor) : base("constant") + { + Name = name; + TypeName = typeName; + Constructor = constructor; + } + + public string Name { get; } + public QualifiedTypeName TypeName { get; } + public string Constructor { get; } + public MarkupConstantPersistMode PersistMode { get; init; } + + public override string ToString() => $"{base.ToString()} {Name} = {Constructor}"; +} + +public record StringEncodedConstantDirective : ConstantDirective +{ + public StringEncodedConstantDirective(string name, QualifiedTypeName typeName, string content) + : base(name, typeName, $"{typeName}.str({content})") + { + Content = content; + PersistMode = MarkupConstantPersistMode.FromString; + } + + public string Content { get; } + + public override string ToString() => base.ToString(); +} + +public record BinaryEncodedConstantDirective : ConstantDirective +{ + public BinaryEncodedConstantDirective(string name, QualifiedTypeName typeName, byte[] content) + : base(name, typeName, GetConstructor(typeName, content)) + { + Content = content; + PersistMode = MarkupConstantPersistMode.Binary; + } + + public byte[] Content { get; } + + public override string ToString() => base.ToString(); + + private static string GetConstructor(QualifiedTypeName typeName, byte[] content) + { + string contentStr; + if (content.Length == 1) + contentStr = $"0x{content[0]:X2}"; + else if (content.Length == 2) + contentStr = $"0x{BitConverter.ToUInt16(content, 0):X4}"; + else if (content.Length == 4) + contentStr = $"0x{BitConverter.ToUInt32(content, 0):X8}"; + else + contentStr = string.Join(", ", content.Select(b => $"0x{b:X2}")); + + return $"{typeName}.bin({contentStr})"; + } +} + +public record CanonicalInstanceConstantDirective : ConstantDirective +{ + public CanonicalInstanceConstantDirective(string name, QualifiedTypeName typeName, string canonicalName) + : base(name, typeName, $"{typeName}.can({canonicalName})") + { + CanonicalName = canonicalName; + PersistMode = MarkupConstantPersistMode.Canonical; + } + + public string CanonicalName { get; } + + public override string ToString() => base.ToString(); +} diff --git a/libs/UIX.Asm/Models/Directives.cs b/libs/UIX.Asm/Models/Directives.cs index 1283114..5778963 100644 --- a/libs/UIX.Asm/Models/Directives.cs +++ b/libs/UIX.Asm/Models/Directives.cs @@ -15,63 +15,6 @@ public record SectionDirective : Directive public override string ToString() => $"{base.ToString()} {Name}"; } -public record ConstantDirective : Directive -{ - public ConstantDirective(string name, QualifiedTypeName typeName, string constructor) : base("constant") - { - Name = name; - TypeName = typeName; - Constructor = constructor; - } - - public string Name { get; } - public QualifiedTypeName TypeName { get; } - public string Constructor { get; } - - public override string ToString() => $"{base.ToString()} {Name} = {Constructor}"; -} - -public record StringEncodedConstantDirective : ConstantDirective -{ - public StringEncodedConstantDirective(string name, QualifiedTypeName typeName, string content) - : base(name, typeName, $"{typeName}({content})") - { - Content = content; - } - - public string Content { get; } - - public override string ToString() => base.ToString(); -} - -public record BinaryEncodedConstantDirective : ConstantDirective -{ - public BinaryEncodedConstantDirective(string name, QualifiedTypeName typeName, byte[] content) - : base(name, typeName, GetConstructor(typeName, content)) - { - Content = content; - } - - public byte[] Content { get; } - - public override string ToString() => base.ToString(); - - private static string GetConstructor(QualifiedTypeName typeName, byte[] content) - { - string contentStr; - if (content.Length == 1) - contentStr = $"0x{content[0]:X2}"; - else if (content.Length == 2) - contentStr = $"0x{BitConverter.ToUInt16(content, 0):X4}"; - else if (content.Length == 4) - contentStr = $"0x{BitConverter.ToUInt32(content, 0):X8}"; - else - contentStr = string.Join(", ", content.Select(b => $"0x{b:X2}")); - - return $"{typeName}.bin({contentStr})"; - } -} - public record ExportDirective : Directive { public ExportDirective(string labelPrefix, uint listenerCount, string baseTypeName) : base("export") diff --git a/libs/UIX.Test/Assembly.cs b/libs/UIX.Test/Assembly.cs index 18af4dc..8d32e16 100644 --- a/libs/UIX.Test/Assembly.cs +++ b/libs/UIX.Test/Assembly.cs @@ -32,12 +32,12 @@ public class Assembly(ITestOutputHelper output) .import-mbrs UI{Locals, Content} .import-mbrs Text{Color, Content, Font} -.constant const0 = Color(255, 255, 0, 0) -.constant const1 = String(Howdy from Microsoft.Iris!) -.constant const2 = String(SelectCommand) -.constant const3 = Color(255, 0, 0, 255) -.constant const4 = Font(JetBrains Mono) -.constant const5 = String(This is some blue text) +.constant const0 = Color.str(255, 255, 0, 0) +.constant const1 = String.str(Howdy from Microsoft.Iris!) +.constant const2 = String.str(SelectCommand) +.constant const3 = Color.str(255, 0, 0, 255) +.constant const4 = Font.str(JetBrains Mono) +.constant const5 = String.str(This is some blue text) .section object Default_cont: