diff --git a/libs/UIX.Asm/Lexer.Directives.cs b/libs/UIX.Asm/Lexer.Directives.cs index f2ff0e2..f815f3b 100644 --- a/libs/UIX.Asm/Lexer.Directives.cs +++ b/libs/UIX.Asm/Lexer.Directives.cs @@ -87,45 +87,59 @@ partial class Lexer if (!typeNameResult.WasSuccessful) return Result.Failure(input, "Invalid constant directive", ["Expected qualified name of type to construct"]); + Markup.MarkupConstantPersistMode? persistMode = null; + string content = ""; + 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 - }; + // Support defining constants from string literals + var stringLiteralResult = ExpressionInBraces(StringLiteral)(input); + input = stringLiteralResult.Remainder; + if (!stringLiteralResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected '.', followed by the persist mode."]); - var openBracketResult = Parse.Char('(').Token()(input); - input = openBracketResult.Remainder; - if (!openBracketResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected '('"]); + persistMode = Markup.MarkupConstantPersistMode.FromString; + content = stringLiteralResult.Value[1..^1].Unescape(); + } + else + { + 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."]); - var contentResult = Parse.CharExcept(')').AtLeastOnce().Text().Token()(input); - input = contentResult.Remainder; - if (!contentResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected constant value"]); + persistMode = persistModeResult.Value.ToLowerInvariant() switch + { + "bin" => Markup.MarkupConstantPersistMode.Binary, + "str" => Markup.MarkupConstantPersistMode.FromString, + "can" => Markup.MarkupConstantPersistMode.Canonical, + _ => null + }; - var closeBracketResult = Parse.Char(')').Token()(input); - input = closeBracketResult.Remainder; - if (!closeBracketResult.WasSuccessful) - return Result.Failure(input, "Invalid constant directive", ["Expected ')'"]); + var openBracketResult = Parse.Char('(').Token()(input); + input = openBracketResult.Remainder; + if (!openBracketResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected '('"]); + + var contentResult = Parse.AnyChar.Until(StatementEnd).Text().Token()(input); + input = contentResult.Remainder; + if (!contentResult.WasSuccessful) + return Result.Failure(input, "Invalid constant directive", ["Expected constant value"]); + + content = contentResult.Value; + if (content.Length <= 1 || content[^1] != ')') + return Result.Failure(input, "Invalid constant directive", ["Expected ')'"]); + content = content[..^1]; + } var constantName = constNameResult.Value; var typeName = typeNameResult.Value; if (persistMode == Markup.MarkupConstantPersistMode.FromString) { - directive = new StringEncodedConstantDirective(constantName, typeName, contentResult.Value) + directive = new StringEncodedConstantDirective(constantName, typeName, content) { Line = line, Column = column, @@ -133,7 +147,7 @@ partial class Lexer } else if (persistMode == Markup.MarkupConstantPersistMode.Canonical) { - directive = new CanonicalInstanceConstantDirective(constantName, typeName, contentResult.Value) + directive = new CanonicalInstanceConstantDirective(constantName, typeName, content) { Line = line, Column = column, @@ -142,7 +156,7 @@ partial class Lexer else if (persistMode == Markup.MarkupConstantPersistMode.Binary) { byte[] constantBytes; - var byteParts = contentResult.Value.Split(','); + var byteParts = content.Split(','); if (byteParts.Length == 1) { var constantStr = byteParts[0].Trim(); @@ -167,7 +181,7 @@ partial class Lexer } else { - return Result.Failure(input, "Invalid constant directive", [$"Expected a 1, 2, or 4 hex number, or a list of bytes."]); + return Result.Failure(input, "Invalid constant directive", [$"Expected a 1-, 2-, or 4-digit hex number, or a list of bytes"]); } } else @@ -185,7 +199,7 @@ partial class Lexer } else { - return Result.Failure(input, "Invalid constant directive", [$"'{persistModeResult.Value}' is not a valid persist mode."]); + return Result.Failure(input, "Invalid constant directive", [$"Invalid persist mode"]); } } break; diff --git a/libs/UIX.Asm/Lexer.cs b/libs/UIX.Asm/Lexer.cs index a845b40..07a236f 100644 --- a/libs/UIX.Asm/Lexer.cs +++ b/libs/UIX.Asm/Lexer.cs @@ -16,6 +16,16 @@ public static partial class Lexer public static readonly Parser Uri = Parse.LetterOrDigit.Or(Parse.Chars(":/!._-")).AtLeastOnce().Text(); + // https://github.com/apexsharp/apexparser/blob/4eb5983c657b0e6c49ed47bc42a0346e80f9e26d/ApexSharp.ApexParser/Parser/ApexGrammar.cs#L360C9-L367C64 + public static readonly Parser StringLiteral = + from leading in Parse.WhiteSpace.Many() + from openQuote in Parse.Char('"') + from fragments in Parse.Char('\\').Then(_ => Parse.AnyChar.Select(c => $"\\{c}")) + .Or(Parse.CharExcept("\\\"").Many().Text()).Many() + from closeQuote in Parse.Char('"') + from trailing in Parse.WhiteSpace.Many() + select $"\"{string.Join(string.Empty, fragments)}\""; + public static readonly Parser StatementEnd = Parse.Char(';').Return(";").Or(Parse.LineTerminator); public static readonly Parser QualifiedTypeName = ParseQualifiedTypeName; @@ -70,4 +80,10 @@ public static partial class Lexer return Result.Success(qualifiedName, input); } + + private static Parser ExpressionInBraces(Parser parser, char open = '(', char close = ')') => + from openBrace in Parse.Char(open).Token() + from expression in parser.Optional() + from closeBrace in Parse.Char(close).Token() + select expression.GetOrElse(string.Empty).Trim(); } diff --git a/libs/UIX.Asm/Models/ConstantDirectives.cs b/libs/UIX.Asm/Models/ConstantDirectives.cs index fa7114c..42bcbc6 100644 --- a/libs/UIX.Asm/Models/ConstantDirectives.cs +++ b/libs/UIX.Asm/Models/ConstantDirectives.cs @@ -24,7 +24,7 @@ public record ConstantDirective : Directive public record StringEncodedConstantDirective : ConstantDirective { public StringEncodedConstantDirective(string name, QualifiedTypeName typeName, string content) - : base(name, typeName, $"{typeName}.str({content})") + : base(name, typeName, $"{typeName}(\"{content.Escape()}\")") { Content = content; PersistMode = MarkupConstantPersistMode.FromString; diff --git a/libs/UIX.Test/Assembly.cs b/libs/UIX.Test/Assembly.cs index b26f331..e08cfe0 100644 --- a/libs/UIX.Test/Assembly.cs +++ b/libs/UIX.Test/Assembly.cs @@ -45,6 +45,8 @@ public class Assembly .constant const3 = Color.str(255, 0, 0, 255) .constant const4 = Font.str(JetBrains Mono) .constant const5 = String.str(This is some blue text) +.constant const3255 = String.str(({0:X8})) +.constant const3256 = String("Content\nOn a new line") .section object Default_cont: @@ -77,7 +79,7 @@ Alt_locl: _output.WriteLine(ast.ToString()); Assert.NotNull(ast); - Assert.Equal(20, ast.Directives.Count()); + Assert.Equal(22, ast.Directives.Count()); Assert.Equal(24, ast.Code.Count()); }