From 03859bec6e48b6450f71b1bcad2c9367d73471e8 Mon Sep 17 00:00:00 2001 From: Yoshi Askharoun Date: Mon, 12 Feb 2024 20:56:51 -0600 Subject: [PATCH] Add inline XML to assembler and disassembler --- libs/UIX.Asm/AsmMarkupLoader.cs | 17 ++++++-- libs/UIX.Asm/Disassembler.cs | 59 ++++++++++++++++++++++--- libs/UIX.Asm/Lexer.Directives.cs | 71 ++++++++++++++++++++----------- libs/UIX.Asm/Models/Directives.cs | 19 +++++++-- libs/UIX.Test/Assembly.cs | 8 ++-- 5 files changed, 135 insertions(+), 39 deletions(-) diff --git a/libs/UIX.Asm/AsmMarkupLoader.cs b/libs/UIX.Asm/AsmMarkupLoader.cs index 8724cdb..cf9d38b 100644 --- a/libs/UIX.Asm/AsmMarkupLoader.cs +++ b/libs/UIX.Asm/AsmMarkupLoader.cs @@ -209,17 +209,28 @@ internal class AsmMarkupLoader object constantValue; var constantTypeSchema = ResolveTypeFromQualifiedName(constant.TypeName); - if (constantTypeSchema.SupportsTypeConversion(stringTypeSchema)) + if (constant is EncodedConstantDirective encodedConstant) { - var parseResult = constantTypeSchema.TypeConverter(constant.Content, stringTypeSchema, out constantValue); + var parseResult = constantTypeSchema.TypeConverter(encodedConstant.Content, stringTypeSchema, out constantValue); if (parseResult.Failed) { - ReportError($"Failed to create an instance of '{constant.TypeName}' from '{constant.Content}'", constant); + ReportError($"Failed to create an instance of '{constant.TypeName}' from '{encodedConstant.Content}'", constant); continue; } } else { + var xmlElem = System.Xml.Linq.XElement.Parse(constant.Constructor); + + constantValue = constantTypeSchema.ConstructDefault(); + + foreach (var attr in xmlElem.Attributes()) + { + var propName = attr.Name.LocalName; + var prop = constantTypeSchema.FindProperty(propName); + prop.SetValue(ref constantValue, attr.Value); + } + ReportError($"'{constant.TypeName}' cannot be constructed from a string, and UIXA does not yet support XML construction.", constant); continue; } diff --git a/libs/UIX.Asm/Disassembler.cs b/libs/UIX.Asm/Disassembler.cs index daea712..56602ab 100644 --- a/libs/UIX.Asm/Disassembler.cs +++ b/libs/UIX.Asm/Disassembler.cs @@ -4,6 +4,7 @@ using Microsoft.Iris.Markup; using System; using System.Collections.Generic; using System.Linq; +using System.Xml.Linq; namespace Microsoft.Iris.Asm; @@ -148,7 +149,7 @@ public class Disassembler var persistedConstant = persistedList[c]; var typeSchema = persistedConstant.Type; - constants.Add((c, typeSchema, persistedConstant)); + constants.Add((c, typeSchema, persistedConstant.Data)); } } else @@ -173,15 +174,54 @@ public class Disassembler } } + var stringTypeSchema = UIXTypes.MapIDToType(UIXTypeID.String); + foreach (var (c, typeSchema, constantValue) in constants) { - string encodedValue = constantValue is IStringEncodable encodable - ? encodable.EncodeString() - : constantValue.ToString(); - + var constantName = $"const{c:D}"; QualifiedTypeName qualifiedTypeName = GetQualifiedName(typeSchema); + + if (constantValue is IStringEncodable encodable) + { + var encodedValue = encodable.EncodeString(); + yield return new EncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); + } + else if (typeSchema.SupportsTypeConversion(stringTypeSchema)) + { + var encodedValue = constantValue.ToString(); + yield return new EncodedConstantDirective(constantName, qualifiedTypeName, encodedValue); + } + else + { + XName constElemName = qualifiedTypeName.NamespacePrefix is null + ? qualifiedTypeName.TypeName + : XName.Get(qualifiedTypeName.TypeName, qualifiedTypeName.NamespacePrefix); + XElement constElem = new(constElemName); - yield return new ConstantDirective($"const{c:D}", qualifiedTypeName, encodedValue); + 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); + } } } @@ -272,4 +312,11 @@ public class Disassembler labels = _offsetLabelMap[offset] = new(1); labels.Add(new(labelName)); } + + private static string EncodeSimpleConstant(object value) + { + return value is IStringEncodable encodable + ? encodable.EncodeString() + : value.ToString(); + } } diff --git a/libs/UIX.Asm/Lexer.Directives.cs b/libs/UIX.Asm/Lexer.Directives.cs index 8cd12af..4837541 100644 --- a/libs/UIX.Asm/Lexer.Directives.cs +++ b/libs/UIX.Asm/Lexer.Directives.cs @@ -55,31 +55,54 @@ partial class Lexer input = Parse.Char('=').Token()(input).Remainder; - var typeNameResult = QualifiedTypeName.Token()(input); - input = typeNameResult.Remainder; - if (!typeNameResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected qualified name of type to construct"]); - - var openBracketResult = Parse.Char('(').Token()(input); - input = openBracketResult.Remainder; - if (!openBracketResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected '('"]); - - var contentResult = Parse.CharExcept(')').AtLeastOnce().Text().Token()(input); - input = contentResult.Remainder; - if (!contentResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected constant value"]); - - var closeBracketResult = Parse.Char(')').Token()(input); - input = closeBracketResult.Remainder; - if (!closeBracketResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected ')'"]); - - directive = new ConstantDirective(constNameResult.Value, typeNameResult.Value, contentResult.Value) + // Types that can't be encoded as simple strings, such as FlowLayout, + // are defined inline using XML elements. + bool isInlineXml = Parse.Char('<')(input).WasSuccessful; + if (isInlineXml) { - Line = line, - Column = column, - }; + // Find the end of the XML tag + var xmlPartResult = Parse.AnyChar.Until(Parse.String("/>")).Text()(input); + input = xmlPartResult.Remainder; + if (!xmlPartResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected valid XML"]); + + var xmlElem = System.Xml.Linq.XElement.Parse($"{xmlPartResult.Value}/>"); + QualifiedTypeName typeName = new(xmlElem.Name.NamespaceName, xmlElem.Name.LocalName); + var content = xmlElem.ToString(System.Xml.Linq.SaveOptions.DisableFormatting); + + directive = new ConstantDirective(constNameResult.Value, typeName, content) + { + Line = line, + }; + } + else + { + var typeNameResult = QualifiedTypeName.Token()(input); + input = typeNameResult.Remainder; + if (!typeNameResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected qualified name of type to construct"]); + + var openBracketResult = Parse.Char('(').Token()(input); + input = openBracketResult.Remainder; + if (!openBracketResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected '('"]); + + var contentResult = Parse.CharExcept(')').AtLeastOnce().Text().Token()(input); + input = contentResult.Remainder; + if (!contentResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected constant value"]); + + var closeBracketResult = Parse.Char(')').Token()(input); + input = closeBracketResult.Remainder; + if (!closeBracketResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected ')'"]); + + directive = new EncodedConstantDirective(constNameResult.Value, typeNameResult.Value, contentResult.Value) + { + Line = line, + Column = column, + }; + } break; case "EXPORT": diff --git a/libs/UIX.Asm/Models/Directives.cs b/libs/UIX.Asm/Models/Directives.cs index 1ab206f..2a57ae8 100644 --- a/libs/UIX.Asm/Models/Directives.cs +++ b/libs/UIX.Asm/Models/Directives.cs @@ -14,18 +14,31 @@ public record SectionDirective : Directive public record ConstantDirective : Directive { - public ConstantDirective(string name, QualifiedTypeName typeName, string content) : base("constant") + public ConstantDirective(string name, QualifiedTypeName typeName, string constructor) : base("constant") { Name = name; TypeName = typeName; - Content = content; + Constructor = constructor; } public string Name { get; } public QualifiedTypeName TypeName { get; } + public string Constructor { get; } + + public override string ToString() => $"{base.ToString()} {Name} = {Constructor}"; +} + +public record EncodedConstantDirective : ConstantDirective +{ + public EncodedConstantDirective(string name, QualifiedTypeName typeName, string content) + : base(name, typeName, $"{typeName}({content})") + { + Content = content; + } + public string Content { get; } - public override string ToString() => $"{base.ToString()} {Name} = {TypeName}({Content})"; + public override string ToString() => base.ToString(); } public record ExportDirective : Directive diff --git a/libs/UIX.Test/Assembly.cs b/libs/UIX.Test/Assembly.cs index c07dfe2..4e4f617 100644 --- a/libs/UIX.Test/Assembly.cs +++ b/libs/UIX.Test/Assembly.cs @@ -29,12 +29,14 @@ public class Assembly(ITestOutputHelper output) .import-type Color .import-type Font -.constant const0 = Color(A=255, R=255, G=0, B=0) +.constant const0 = Color(255, 255, 0, 0) .constant const1 = String(Howdy from Microsoft.Iris!) .constant const2 = String(SelectCommand) -.constant const3 = Color(A=255, R=0, G=0, B=255) +.constant const3 = Color(255, 0, 0, 255) .constant const4 = Font(JetBrains Mono) .constant const5 = String(This is some blue text) +.constant xmlTest = + .section object Default_cont: @@ -67,7 +69,7 @@ Alt_locl: output.WriteLine(ast.ToString()); Assert.NotNull(ast); - Assert.Equal(18, ast.Directives.Count()); + Assert.Equal(19, ast.Directives.Count()); Assert.Equal(24, ast.Code.Count()); }